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:
parent
43799dc57d
commit
b679ff0b3e
7 changed files with 40 additions and 40 deletions
4
package-lock.json
generated
4
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) &&
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue