import { EmbedBlot, Scope } from 'parchment'; import TextBlot from './text.js'; class Cursor extends EmbedBlot { static blotName = 'cursor'; static className = 'ql-cursor'; static tagName = 'span'; static CONTENTS = '\uFEFF'; // Zero width no break space static value() { return undefined; } constructor(scroll, domNode, selection) { super(scroll, domNode); this.selection = selection; this.textNode = document.createTextNode(Cursor.CONTENTS); this.domNode.appendChild(this.textNode); this.savedLength = 0; } detach() { // super.detach() will also clear domNode.__blot if (this.parent != null) this.parent.removeChild(this); } format(name, value) { if (this.savedLength !== 0) { super.format(name, value); return; } // TODO: Fix this next time the file is edited. // eslint-disable-next-line @typescript-eslint/no-this-alias let target = this; let index = 0; while (target != null && target.statics.scope !== Scope.BLOCK_BLOT) { index += target.offset(target.parent); target = target.parent; } if (target != null) { this.savedLength = Cursor.CONTENTS.length; // @ts-expect-error TODO: allow empty context in Parchment target.optimize(); target.formatAt(index, Cursor.CONTENTS.length, name, value); this.savedLength = 0; } } index(node, offset) { if (node === this.textNode) return 0; return super.index(node, offset); } length() { return this.savedLength; } position() { return [this.textNode, this.textNode.data.length]; } remove() { super.remove(); // @ts-expect-error Fix me later this.parent = null; } restore() { if (this.selection.composing || this.parent == null) return null; const range = this.selection.getNativeRange(); // Browser may push down styles/nodes inside the cursor blot. // https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#push-down-values while (this.domNode.lastChild != null && this.domNode.lastChild !== this.textNode) { // @ts-expect-error Fix me later this.domNode.parentNode.insertBefore(this.domNode.lastChild, this.domNode); } const prevTextBlot = this.prev instanceof TextBlot ? this.prev : null; const prevTextLength = prevTextBlot ? prevTextBlot.length() : 0; const nextTextBlot = this.next instanceof TextBlot ? this.next : null; // @ts-expect-error TODO: make TextBlot.text public const nextText = nextTextBlot ? nextTextBlot.text : ''; const { textNode } = this; // take text from inside this blot and reset it const newText = textNode.data.split(Cursor.CONTENTS).join(''); textNode.data = Cursor.CONTENTS; // proactively merge TextBlots around cursor so that optimization // doesn't lose the cursor. the reason we are here in cursor.restore // could be that the user clicked in prevTextBlot or nextTextBlot, or // the user typed something. let mergedTextBlot; if (prevTextBlot) { mergedTextBlot = prevTextBlot; if (newText || nextTextBlot) { prevTextBlot.insertAt(prevTextBlot.length(), newText + nextText); if (nextTextBlot) { nextTextBlot.remove(); } } } else if (nextTextBlot) { mergedTextBlot = nextTextBlot; nextTextBlot.insertAt(0, newText); } else { const newTextNode = document.createTextNode(newText); mergedTextBlot = this.scroll.create(newTextNode); this.parent.insertBefore(mergedTextBlot, this); } this.remove(); if (range) { // calculate selection to restore const remapOffset = (node, offset) => { if (prevTextBlot && node === prevTextBlot.domNode) { return offset; } if (node === textNode) { return prevTextLength + offset - 1; } if (nextTextBlot && node === nextTextBlot.domNode) { return prevTextLength + newText.length + offset; } return null; }; const start = remapOffset(range.start.node, range.start.offset); const end = remapOffset(range.end.node, range.end.offset); if (start !== null && end !== null) { return { startNode: mergedTextBlot.domNode, startOffset: start, endNode: mergedTextBlot.domNode, endOffset: end }; } } return null; } update(mutations, context) { if (mutations.some(mutation => { return mutation.type === 'characterData' && mutation.target === this.textNode; })) { const range = this.restore(); if (range) context.range = range; } } // Avoid .ql-cursor being a descendant of ``. // The reason is Safari pushes down `` on text insertion. // That will cause DOM nodes not sync with the model. // // For example ({I} is the caret), given the markup: // \uFEFF{I} // When typing a char "x", `` will be pushed down inside the `` first: // \uFEFF{I} // And then "x" will be inserted after ``: // \uFEFFd{I} optimize(context) { // @ts-expect-error Fix me later super.optimize(context); let { parent } = this; while (parent) { if (parent.domNode.tagName === 'A') { this.savedLength = Cursor.CONTENTS.length; // @ts-expect-error TODO: make isolate generic parent.isolate(this.offset(parent), this.length()).unwrap(); this.savedLength = 0; break; } parent = parent.parent; } } value() { return ''; } } export default Cursor; //# sourceMappingURL=cursor.js.map