import { EmbedBlot, Scope } from 'parchment'; import TextBlot from './text'; class Cursor extends EmbedBlot { 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; } 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; 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(); this.parent = null; } restore() { if (this.selection.composing || this.parent == null) return null; const range = this.selection.getNativeRange(); // Link format will insert text outside of anchor tag while ( this.domNode.lastChild != null && this.domNode.lastChild !== this.textNode ) { 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; 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; } } value() { return ''; } } Cursor.blotName = 'cursor'; Cursor.className = 'ql-cursor'; Cursor.tagName = 'span'; Cursor.CONTENTS = '\uFEFF'; // Zero width no break space export default Cursor;