123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170 |
- 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 `<a/>`.
- // The reason is Safari pushes down `<a/>` on text insertion.
- // That will cause DOM nodes not sync with the model.
- //
- // For example ({I} is the caret), given the markup:
- // <a><span class="ql-cursor">\uFEFF{I}</span></a>
- // When typing a char "x", `<a/>` will be pushed down inside the `<span>` first:
- // <span class="ql-cursor"><a>\uFEFF{I}</a></span>
- // And then "x" will be inserted after `<a/>`:
- // <span class="ql-cursor"><a>\uFEFF</a>d{I}</span>
- 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
|