0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2025-01-03 05:00:13 -05:00

feat: allow initializing editor in the context of a different document, window or shadow root

This commit is contained in:
Jakub Jagiełło 2024-03-01 12:22:20 +01:00
parent 43799dc57d
commit b679ff0b3e
7 changed files with 40 additions and 40 deletions

4
package-lock.json generated
View file

@ -1,12 +1,12 @@
{ {
"name": "squire-rte", "name": "squire-rte",
"version": "2.2.6", "version": "2.2.7",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "squire-rte", "name": "squire-rte",
"version": "2.2.6", "version": "2.2.7",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
"@babel/core": "^7.22.20", "@babel/core": "^7.22.20",

View file

@ -290,7 +290,7 @@ const _onPaste = function (this: Squire, event: ClipboardEvent): void {
// No interface. Includes all versions of IE :( // No interface. Includes all versions of IE :(
// -------------------------------------------- // --------------------------------------------
const body = document.body; const body = this._document.body;
const range = this.getSelection(); const range = this.getSelection();
const startContainer = range.startContainer; const startContainer = range.startContainer;
const startOffset = range.startOffset; const startOffset = range.startOffset;

View file

@ -97,6 +97,8 @@ interface SquireConfig {
class Squire { class Squire {
_root: HTMLElement; _root: HTMLElement;
_document: Document;
_window: Window;
_config: SquireConfig; _config: SquireConfig;
_isFocused: boolean; _isFocused: boolean;
@ -124,6 +126,8 @@ class Squire {
constructor(root: HTMLElement, config?: Partial<SquireConfig>) { constructor(root: HTMLElement, config?: Partial<SquireConfig>) {
this._root = root; this._root = root;
this._document = root.ownerDocument || document;
this._window = this._document.defaultView || window;
this._config = this._makeConfig(config); this._config = this._makeConfig(config);
@ -235,9 +239,10 @@ class Squire {
RETURN_DOM_FRAGMENT: true, RETURN_DOM_FRAGMENT: true,
FORCE_BODY: false, FORCE_BODY: false,
}); });
const doc = this._document;
return frag return frag
? document.importNode(frag, true) ? doc.importNode(frag, true)
: document.createDocumentFragment(); : doc.createDocumentFragment();
}, },
didError: (error: any): void => console.log(error), didError: (error: any): void => console.log(error),
}; };
@ -358,7 +363,8 @@ class Squire {
// an infinite loop. So we detect whether we're actually // an infinite loop. So we detect whether we're actually
// focused/blurred before firing. // focused/blurred before firing.
if (/^(?:focus|blur)/.test(type)) { if (/^(?:focus|blur)/.test(type)) {
const isFocused = this._root === document.activeElement; const shadow = this._root.getRootNode(); // either Document, ShadowRoot or orphan node itself
const isFocused = 'activeElement' in shadow && this._root === shadow.activeElement;
if (type === 'focus') { if (type === 'focus') {
if (!isFocused || this._isFocused) { if (!isFocused || this._isFocused) {
return this; return this;
@ -417,7 +423,7 @@ class Squire {
this._events.set(type, handlers); this._events.set(type, handlers);
if (!this.customEvents.has(type)) { if (!this.customEvents.has(type)) {
if (type === 'selectionchange') { if (type === 'selectionchange') {
target = document; target = this._document;
} }
target.addEventListener(type, this, true); target.addEventListener(type, this, true);
} }
@ -444,7 +450,7 @@ class Squire {
this._events.delete(type); this._events.delete(type);
if (!this.customEvents.has(type)) { if (!this.customEvents.has(type)) {
if (type === 'selectionchange') { if (type === 'selectionchange') {
target = document; target = this._document;
} }
target.removeEventListener(type, this, true); target.removeEventListener(type, this, true);
} }
@ -548,7 +554,7 @@ class Squire {
end.remove(); end.remove();
if (!range) { if (!range) {
range = document.createRange(); range = this._document.createRange();
} }
range.setStart(startContainer, startOffset); range.setStart(startContainer, startOffset);
range.setEnd(endContainer, endOffset); range.setEnd(endContainer, endOffset);
@ -580,7 +586,7 @@ class Squire {
} }
getSelection(): Range { getSelection(): Range {
const selection = window.getSelection(); const selection = this._window.getSelection();
const root = this._root; const root = this._root;
let range: Range | null = null; let range: Range | null = null;
// If not focused, always rely on cached selection; another function may // If not focused, always rely on cached selection; another function may
@ -603,7 +609,7 @@ class Squire {
range = this._lastSelection; range = this._lastSelection;
// Check the editor is in the live document; if not, the range has // Check the editor is in the live document; if not, the range has
// probably been rewritten by the browser and is bogus // probably been rewritten by the browser and is bogus
if (!document.contains(range.commonAncestorContainer)) { if (!root.getRootNode().contains(range.commonAncestorContainer)) {
range = null; range = null;
} }
} }
@ -621,7 +627,7 @@ class Squire {
if (!this._isFocused) { if (!this._isFocused) {
this._enableRestoreSelection(); this._enableRestoreSelection();
} else { } else {
const selection = window.getSelection(); const selection = this._window.getSelection();
if (selection) { if (selection) {
if ('setBaseAndExtent' in Selection.prototype) { if ('setBaseAndExtent' in Selection.prototype) {
selection.setBaseAndExtent( selection.setBaseAndExtent(
@ -1174,7 +1180,7 @@ class Squire {
let offset = range.startOffset; let offset = range.startOffset;
let textNode: Text; let textNode: Text;
if (!startContainer || !(startContainer instanceof Text)) { if (!startContainer || !(startContainer instanceof Text)) {
const text = document.createTextNode(''); const text = this._document.createTextNode('');
startContainer.insertBefore( startContainer.insertBefore(
text, text,
startContainer.childNodes[offset], startContainer.childNodes[offset],
@ -1521,11 +1527,9 @@ class Squire {
// formatted text. // formatted text.
let fixer: Node | Text | null | undefined; let fixer: Node | Text | null | undefined;
if (range.collapsed) { if (range.collapsed) {
if (cantFocusEmptyTextNodes) { fixer = this._document.createTextNode(
fixer = document.createTextNode(ZWS); cantFocusEmptyTextNodes ? ZWS : ''
} else { );
fixer = document.createTextNode('');
}
insertNodeInRange(range, fixer!); insertNodeInRange(range, fixer!);
} }
@ -1697,7 +1701,7 @@ class Squire {
} }
insertNodeInRange( insertNodeInRange(
range, range,
document.createTextNode(url.slice(protocolEnd)), this._document.createTextNode(url.slice(protocolEnd)),
); );
} }
attributes = Object.assign( attributes = Object.assign(
@ -1806,7 +1810,7 @@ class Squire {
const endIndex = index + match[0].length; const endIndex = index + match[0].length;
if (index) { if (index) {
parent.insertBefore( parent.insertBefore(
document.createTextNode(data.slice(0, index)), this._document.createTextNode(data.slice(0, index)),
node, node,
); );
} }
@ -1979,7 +1983,7 @@ class Squire {
node = range.startContainer; node = range.startContainer;
const offset = range.startOffset; const offset = range.startOffset;
if (!(node instanceof Text)) { if (!(node instanceof Text)) {
node = document.createTextNode(''); node = this._document.createTextNode('');
parent.insertBefore(node, parent.firstChild); parent.insertBefore(node, parent.firstChild);
} }
// If blank line: split and insert default block // If blank line: split and insert default block
@ -2111,7 +2115,7 @@ class Squire {
(!nodeAfterSplit.textContent || (!nodeAfterSplit.textContent ||
nodeAfterSplit.textContent === ZWS) nodeAfterSplit.textContent === ZWS)
) { ) {
child = document.createTextNode('') as Text; child = this._document.createTextNode('') as Text;
replaceWith(nodeAfterSplit, child); replaceWith(nodeAfterSplit, child);
nodeAfterSplit = child; nodeAfterSplit = child;
break; break;
@ -2540,7 +2544,7 @@ class Squire {
if (range.collapsed || isContainer(range.commonAncestorContainer)) { if (range.collapsed || isContainer(range.commonAncestorContainer)) {
this.modifyBlocks((frag) => { this.modifyBlocks((frag) => {
const root = this._root; const root = this._root;
const output = document.createDocumentFragment(); const output = this._document.createDocumentFragment();
const blockWalker = getBlockWalker(frag, root); const blockWalker = getBlockWalker(frag, root);
let node: Element | Text | null; let node: Element | Text | null;
// 1. Extract inline content; drop all blocks and contains. // 1. Extract inline content; drop all blocks and contains.
@ -2563,7 +2567,7 @@ class Squire {
if (!brBreaksLine[l]) { if (!brBreaksLine[l]) {
detach(br); detach(br);
} else { } else {
replaceWith(br, document.createTextNode('\n')); replaceWith(br, this._document.createTextNode('\n'));
} }
} }
// 3. Remove <code>; its format clashes with <pre> // 3. Remove <code>; its format clashes with <pre>
@ -2573,7 +2577,7 @@ class Squire {
replaceWith(nodes[l], empty(nodes[l])); replaceWith(nodes[l], empty(nodes[l]));
} }
if (output.childNodes.length) { if (output.childNodes.length) {
output.appendChild(document.createTextNode('\n')); output.appendChild(this._document.createTextNode('\n'));
} }
output.appendChild(empty(node)); output.appendChild(empty(node));
} }
@ -2620,11 +2624,11 @@ class Squire {
while ((node = walker.nextNode())) { while ((node = walker.nextNode())) {
let value = node.data; let value = node.data;
value = value.replace(/ (?= )/g, ' '); // sp -> nbsp value = value.replace(/ (?= )/g, ' '); // sp -> nbsp
const contents = document.createDocumentFragment(); const contents = this._document.createDocumentFragment();
let index: number; let index: number;
while ((index = value.indexOf('\n')) > -1) { while ((index = value.indexOf('\n')) > -1) {
contents.appendChild( contents.appendChild(
document.createTextNode(value.slice(0, index)), this._document.createTextNode(value.slice(0, index)),
); );
contents.appendChild(createElement('BR')); contents.appendChild(createElement('BR'));
value = value.slice(index + 1); value = value.slice(index + 1);
@ -2679,7 +2683,7 @@ class Squire {
this.createDefaultBlock([ this.createDefaultBlock([
this._removeFormatting( this._removeFormatting(
node as Element, node as Element,
document.createDocumentFragment(), this._document.createDocumentFragment(),
), ),
]), ]),
); );
@ -2726,8 +2730,8 @@ class Squire {
// Split end point first to avoid problems when end and start // Split end point first to avoid problems when end and start
// in same container. // in same container.
const formattedNodes = document.createDocumentFragment(); const formattedNodes = this._document.createDocumentFragment();
const cleanNodes = document.createDocumentFragment(); const cleanNodes = this._document.createDocumentFragment();
const nodeAfterSplit = split(endContainer, endOffset, stopNode, root); const nodeAfterSplit = split(endContainer, endOffset, stopNode, root);
let nodeInSplit = split(startContainer, startOffset, stopNode, root); let nodeInSplit = split(startContainer, startOffset, stopNode, root);
let nextNode: ChildNode | null; let nextNode: ChildNode | null;

View file

@ -92,7 +92,7 @@ const keyHandlers: Record<string, KeyHandler> = {
if (node.nodeName === 'CODE') { if (node.nodeName === 'CODE') {
let next = node.nextSibling; let next = node.nextSibling;
if (!(next instanceof Text)) { if (!(next instanceof Text)) {
const textNode = document.createTextNode(' '); // nbsp const textNode = self._document.createTextNode(' '); // nbsp
node.parentNode!.insertBefore(textNode, next); node.parentNode!.insertBefore(textNode, next);
next = textNode; next = textNode;
} }

View file

@ -31,11 +31,7 @@ const fixCursor = (node: Node): Node => {
} }
} }
if (!child) { if (!child) {
if (cantFocusEmptyTextNodes) { fixer = document.createTextNode(cantFocusEmptyTextNodes ? ZWS : '');
fixer = document.createTextNode(ZWS);
} else {
fixer = document.createTextNode('');
}
} }
} else if ( } else if (
(node instanceof Element || node instanceof DocumentFragment) && (node instanceof Element || node instanceof DocumentFragment) &&

View file

@ -15,7 +15,7 @@ const isNodeContainedInRange = (
node: Node, node: Node,
partial: boolean, partial: boolean,
): boolean => { ): boolean => {
const nodeRange = document.createRange(); const nodeRange = (node.ownerDocument || document).createRange();
nodeRange.selectNode(node); nodeRange.selectNode(node);
if (partial) { if (partial) {
// Node must not finish before range starts or start after range // Node must not finish before range starts or start after range

View file

@ -36,7 +36,7 @@ function createRange(
endContainer?: Node, endContainer?: Node,
endOffset?: number, endOffset?: number,
): Range { ): Range {
const range = document.createRange(); const range = (startContainer.ownerDocument || document).createRange();
range.setStart(startContainer, startOffset); range.setStart(startContainer, startOffset);
if (endContainer && typeof endOffset === 'number') { if (endContainer && typeof endOffset === 'number') {
range.setEnd(endContainer, endOffset); range.setEnd(endContainer, endOffset);
@ -108,7 +108,7 @@ const extractContentsOfRange = (
common: Node | null, common: Node | null,
root: Element, root: Element,
): DocumentFragment => { ): DocumentFragment => {
const frag = document.createDocumentFragment(); const frag = (root.ownerDocument || document).createDocumentFragment();
if (range.collapsed) { if (range.collapsed) {
return frag; return frag;
} }
@ -363,7 +363,7 @@ const insertTreeFragmentIntoRange = (
} }
if (/*isBlock( container ) && */ offset !== getLength(container)) { if (/*isBlock( container ) && */ offset !== getLength(container)) {
// Collect any inline contents of the block after the range point // Collect any inline contents of the block after the range point
blockContentsAfterSplit = document.createDocumentFragment(); blockContentsAfterSplit = (root.ownerDocument || document).createDocumentFragment();
while ((node = container.childNodes[offset])) { while ((node = container.childNodes[offset])) {
blockContentsAfterSplit.appendChild(node); blockContentsAfterSplit.appendChild(node);
} }