UNPKG

5.74 kBJavaScriptView Raw
1import { EmbedBlot, Scope } from 'parchment';
2import TextBlot from './text.js';
3class Cursor extends EmbedBlot {
4 static blotName = 'cursor';
5 static className = 'ql-cursor';
6 static tagName = 'span';
7 static CONTENTS = '\uFEFF'; // Zero width no break space
8
9 static value() {
10 return undefined;
11 }
12 constructor(scroll, domNode, selection) {
13 super(scroll, domNode);
14 this.selection = selection;
15 this.textNode = document.createTextNode(Cursor.CONTENTS);
16 this.domNode.appendChild(this.textNode);
17 this.savedLength = 0;
18 }
19 detach() {
20 // super.detach() will also clear domNode.__blot
21 if (this.parent != null) this.parent.removeChild(this);
22 }
23 format(name, value) {
24 if (this.savedLength !== 0) {
25 super.format(name, value);
26 return;
27 }
28 // TODO: Fix this next time the file is edited.
29 // eslint-disable-next-line @typescript-eslint/no-this-alias
30 let target = this;
31 let index = 0;
32 while (target != null && target.statics.scope !== Scope.BLOCK_BLOT) {
33 index += target.offset(target.parent);
34 target = target.parent;
35 }
36 if (target != null) {
37 this.savedLength = Cursor.CONTENTS.length;
38 // @ts-expect-error TODO: allow empty context in Parchment
39 target.optimize();
40 target.formatAt(index, Cursor.CONTENTS.length, name, value);
41 this.savedLength = 0;
42 }
43 }
44 index(node, offset) {
45 if (node === this.textNode) return 0;
46 return super.index(node, offset);
47 }
48 length() {
49 return this.savedLength;
50 }
51 position() {
52 return [this.textNode, this.textNode.data.length];
53 }
54 remove() {
55 super.remove();
56 // @ts-expect-error Fix me later
57 this.parent = null;
58 }
59 restore() {
60 if (this.selection.composing || this.parent == null) return null;
61 const range = this.selection.getNativeRange();
62 // Browser may push down styles/nodes inside the cursor blot.
63 // https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#push-down-values
64 while (this.domNode.lastChild != null && this.domNode.lastChild !== this.textNode) {
65 // @ts-expect-error Fix me later
66 this.domNode.parentNode.insertBefore(this.domNode.lastChild, this.domNode);
67 }
68 const prevTextBlot = this.prev instanceof TextBlot ? this.prev : null;
69 const prevTextLength = prevTextBlot ? prevTextBlot.length() : 0;
70 const nextTextBlot = this.next instanceof TextBlot ? this.next : null;
71 // @ts-expect-error TODO: make TextBlot.text public
72 const nextText = nextTextBlot ? nextTextBlot.text : '';
73 const {
74 textNode
75 } = this;
76 // take text from inside this blot and reset it
77 const newText = textNode.data.split(Cursor.CONTENTS).join('');
78 textNode.data = Cursor.CONTENTS;
79
80 // proactively merge TextBlots around cursor so that optimization
81 // doesn't lose the cursor. the reason we are here in cursor.restore
82 // could be that the user clicked in prevTextBlot or nextTextBlot, or
83 // the user typed something.
84 let mergedTextBlot;
85 if (prevTextBlot) {
86 mergedTextBlot = prevTextBlot;
87 if (newText || nextTextBlot) {
88 prevTextBlot.insertAt(prevTextBlot.length(), newText + nextText);
89 if (nextTextBlot) {
90 nextTextBlot.remove();
91 }
92 }
93 } else if (nextTextBlot) {
94 mergedTextBlot = nextTextBlot;
95 nextTextBlot.insertAt(0, newText);
96 } else {
97 const newTextNode = document.createTextNode(newText);
98 mergedTextBlot = this.scroll.create(newTextNode);
99 this.parent.insertBefore(mergedTextBlot, this);
100 }
101 this.remove();
102 if (range) {
103 // calculate selection to restore
104 const remapOffset = (node, offset) => {
105 if (prevTextBlot && node === prevTextBlot.domNode) {
106 return offset;
107 }
108 if (node === textNode) {
109 return prevTextLength + offset - 1;
110 }
111 if (nextTextBlot && node === nextTextBlot.domNode) {
112 return prevTextLength + newText.length + offset;
113 }
114 return null;
115 };
116 const start = remapOffset(range.start.node, range.start.offset);
117 const end = remapOffset(range.end.node, range.end.offset);
118 if (start !== null && end !== null) {
119 return {
120 startNode: mergedTextBlot.domNode,
121 startOffset: start,
122 endNode: mergedTextBlot.domNode,
123 endOffset: end
124 };
125 }
126 }
127 return null;
128 }
129 update(mutations, context) {
130 if (mutations.some(mutation => {
131 return mutation.type === 'characterData' && mutation.target === this.textNode;
132 })) {
133 const range = this.restore();
134 if (range) context.range = range;
135 }
136 }
137
138 // Avoid .ql-cursor being a descendant of `<a/>`.
139 // The reason is Safari pushes down `<a/>` on text insertion.
140 // That will cause DOM nodes not sync with the model.
141 //
142 // For example ({I} is the caret), given the markup:
143 // <a><span class="ql-cursor">\uFEFF{I}</span></a>
144 // When typing a char "x", `<a/>` will be pushed down inside the `<span>` first:
145 // <span class="ql-cursor"><a>\uFEFF{I}</a></span>
146 // And then "x" will be inserted after `<a/>`:
147 // <span class="ql-cursor"><a>\uFEFF</a>d{I}</span>
148 optimize(context) {
149 // @ts-expect-error Fix me later
150 super.optimize(context);
151 let {
152 parent
153 } = this;
154 while (parent) {
155 if (parent.domNode.tagName === 'A') {
156 this.savedLength = Cursor.CONTENTS.length;
157 // @ts-expect-error TODO: make isolate generic
158 parent.isolate(this.offset(parent), this.length()).unwrap();
159 this.savedLength = 0;
160 break;
161 }
162 parent = parent.parent;
163 }
164 }
165 value() {
166 return '';
167 }
168}
169export default Cursor;
170//# sourceMappingURL=cursor.js.map
\No newline at end of file