1 | import Parchment from 'parchment';
|
2 | import clone from 'clone';
|
3 | import equal from 'deep-equal';
|
4 | import Emitter from './emitter';
|
5 | import logger from './logger';
|
6 |
|
7 | let debug = logger('quill:selection');
|
8 |
|
9 |
|
10 | class Range {
|
11 | constructor(index, length = 0) {
|
12 | this.index = index;
|
13 | this.length = length;
|
14 | }
|
15 | }
|
16 |
|
17 |
|
18 | class Selection {
|
19 | constructor(scroll, emitter) {
|
20 | this.emitter = emitter;
|
21 | this.scroll = scroll;
|
22 | this.composing = false;
|
23 | this.mouseDown = false;
|
24 | this.root = this.scroll.domNode;
|
25 | this.cursor = Parchment.create('cursor', this);
|
26 |
|
27 | this.lastRange = this.savedRange = new Range(0, 0);
|
28 | this.handleComposition();
|
29 | this.handleDragging();
|
30 | this.emitter.listenDOM('selectionchange', document, () => {
|
31 | if (!this.mouseDown) {
|
32 | setTimeout(this.update.bind(this, Emitter.sources.USER), 1);
|
33 | }
|
34 | });
|
35 | this.emitter.on(Emitter.events.EDITOR_CHANGE, (type, delta) => {
|
36 | if (type === Emitter.events.TEXT_CHANGE && delta.length() > 0) {
|
37 | this.update(Emitter.sources.SILENT);
|
38 | }
|
39 | });
|
40 | this.emitter.on(Emitter.events.SCROLL_BEFORE_UPDATE, () => {
|
41 | if (!this.hasFocus()) return;
|
42 | let native = this.getNativeRange();
|
43 | if (native == null) return;
|
44 | if (native.start.node === this.cursor.textNode) return;
|
45 |
|
46 | this.emitter.once(Emitter.events.SCROLL_UPDATE, () => {
|
47 | try {
|
48 | this.setNativeRange(native.start.node, native.start.offset, native.end.node, native.end.offset);
|
49 | } catch (ignored) {}
|
50 | });
|
51 | });
|
52 | this.emitter.on(Emitter.events.SCROLL_OPTIMIZE, (mutations, context) => {
|
53 | if (context.range) {
|
54 | const { startNode, startOffset, endNode, endOffset } = context.range;
|
55 | this.setNativeRange(startNode, startOffset, endNode, endOffset);
|
56 | }
|
57 | });
|
58 | this.update(Emitter.sources.SILENT);
|
59 | }
|
60 |
|
61 | handleComposition() {
|
62 | this.root.addEventListener('compositionstart', () => {
|
63 | this.composing = true;
|
64 | });
|
65 | this.root.addEventListener('compositionend', () => {
|
66 | this.composing = false;
|
67 | if (this.cursor.parent) {
|
68 | const range = this.cursor.restore();
|
69 | if (!range) return;
|
70 | setTimeout(() => {
|
71 | this.setNativeRange(range.startNode, range.startOffset, range.endNode, range.endOffset);
|
72 | }, 1);
|
73 | }
|
74 | });
|
75 | }
|
76 |
|
77 | handleDragging() {
|
78 | this.emitter.listenDOM('mousedown', document.body, () => {
|
79 | this.mouseDown = true;
|
80 | });
|
81 | this.emitter.listenDOM('mouseup', document.body, () => {
|
82 | this.mouseDown = false;
|
83 | this.update(Emitter.sources.USER);
|
84 | });
|
85 | }
|
86 |
|
87 | focus() {
|
88 | if (this.hasFocus()) return;
|
89 | this.root.focus();
|
90 | this.setRange(this.savedRange);
|
91 | }
|
92 |
|
93 | format(format, value) {
|
94 | if (this.scroll.whitelist != null && !this.scroll.whitelist[format]) return;
|
95 | this.scroll.update();
|
96 | let nativeRange = this.getNativeRange();
|
97 | if (nativeRange == null || !nativeRange.native.collapsed || Parchment.query(format, Parchment.Scope.BLOCK)) return;
|
98 | if (nativeRange.start.node !== this.cursor.textNode) {
|
99 | let blot = Parchment.find(nativeRange.start.node, false);
|
100 | if (blot == null) return;
|
101 |
|
102 | if (blot instanceof Parchment.Leaf) {
|
103 | let after = blot.split(nativeRange.start.offset);
|
104 | blot.parent.insertBefore(this.cursor, after);
|
105 | } else {
|
106 | blot.insertBefore(this.cursor, nativeRange.start.node);
|
107 | }
|
108 | this.cursor.attach();
|
109 | }
|
110 | this.cursor.format(format, value);
|
111 | this.scroll.optimize();
|
112 | this.setNativeRange(this.cursor.textNode, this.cursor.textNode.data.length);
|
113 | this.update();
|
114 | }
|
115 |
|
116 | getBounds(index, length = 0) {
|
117 | let scrollLength = this.scroll.length();
|
118 | index = Math.min(index, scrollLength - 1);
|
119 | length = Math.min(index + length, scrollLength - 1) - index;
|
120 | let node, [leaf, offset] = this.scroll.leaf(index);
|
121 | if (leaf == null) return null;
|
122 | [node, offset] = leaf.position(offset, true);
|
123 | let range = document.createRange();
|
124 | if (length > 0) {
|
125 | range.setStart(node, offset);
|
126 | [leaf, offset] = this.scroll.leaf(index + length);
|
127 | if (leaf == null) return null;
|
128 | [node, offset] = leaf.position(offset, true);
|
129 | range.setEnd(node, offset);
|
130 | return range.getBoundingClientRect();
|
131 | } else {
|
132 | let side = 'left';
|
133 | let rect;
|
134 | if (node instanceof Text) {
|
135 | if (offset < node.data.length) {
|
136 | range.setStart(node, offset);
|
137 | range.setEnd(node, offset + 1);
|
138 | } else {
|
139 | range.setStart(node, offset - 1);
|
140 | range.setEnd(node, offset);
|
141 | side = 'right';
|
142 | }
|
143 | rect = range.getBoundingClientRect();
|
144 | } else {
|
145 | rect = leaf.domNode.getBoundingClientRect();
|
146 | if (offset > 0) side = 'right';
|
147 | }
|
148 | return {
|
149 | bottom: rect.top + rect.height,
|
150 | height: rect.height,
|
151 | left: rect[side],
|
152 | right: rect[side],
|
153 | top: rect.top,
|
154 | width: 0
|
155 | };
|
156 | }
|
157 | }
|
158 |
|
159 | getNativeRange() {
|
160 | let selection = document.getSelection();
|
161 | if (selection == null || selection.rangeCount <= 0) return null;
|
162 | let nativeRange = selection.getRangeAt(0);
|
163 | if (nativeRange == null) return null;
|
164 | let range = this.normalizeNative(nativeRange);
|
165 | debug.info('getNativeRange', range);
|
166 | return range;
|
167 | }
|
168 |
|
169 | getRange() {
|
170 | let normalized = this.getNativeRange();
|
171 | if (normalized == null) return [null, null];
|
172 | let range = this.normalizedToRange(normalized);
|
173 | return [range, normalized];
|
174 | }
|
175 |
|
176 | hasFocus() {
|
177 | return document.activeElement === this.root;
|
178 | }
|
179 |
|
180 | normalizedToRange(range) {
|
181 | let positions = [[range.start.node, range.start.offset]];
|
182 | if (!range.native.collapsed) {
|
183 | positions.push([range.end.node, range.end.offset]);
|
184 | }
|
185 | let indexes = positions.map((position) => {
|
186 | let [node, offset] = position;
|
187 | let blot = Parchment.find(node, true);
|
188 | let index = blot.offset(this.scroll);
|
189 | if (offset === 0) {
|
190 | return index;
|
191 | } else if (blot instanceof Parchment.Container) {
|
192 | return index + blot.length();
|
193 | } else {
|
194 | return index + blot.index(node, offset);
|
195 | }
|
196 | });
|
197 | let end = Math.min(Math.max(...indexes), this.scroll.length() - 1);
|
198 | let start = Math.min(end, ...indexes);
|
199 | return new Range(start, end-start);
|
200 | }
|
201 |
|
202 | normalizeNative(nativeRange) {
|
203 | if (!contains(this.root, nativeRange.startContainer) ||
|
204 | (!nativeRange.collapsed && !contains(this.root, nativeRange.endContainer))) {
|
205 | return null;
|
206 | }
|
207 | let range = {
|
208 | start: { node: nativeRange.startContainer, offset: nativeRange.startOffset },
|
209 | end: { node: nativeRange.endContainer, offset: nativeRange.endOffset },
|
210 | native: nativeRange
|
211 | };
|
212 | [range.start, range.end].forEach(function(position) {
|
213 | let node = position.node, offset = position.offset;
|
214 | while (!(node instanceof Text) && node.childNodes.length > 0) {
|
215 | if (node.childNodes.length > offset) {
|
216 | node = node.childNodes[offset];
|
217 | offset = 0;
|
218 | } else if (node.childNodes.length === offset) {
|
219 | node = node.lastChild;
|
220 | offset = node instanceof Text ? node.data.length : node.childNodes.length + 1;
|
221 | } else {
|
222 | break;
|
223 | }
|
224 | }
|
225 | position.node = node, position.offset = offset;
|
226 | });
|
227 | return range;
|
228 | }
|
229 |
|
230 | rangeToNative(range) {
|
231 | let indexes = range.collapsed ? [range.index] : [range.index, range.index + range.length];
|
232 | let args = [];
|
233 | let scrollLength = this.scroll.length();
|
234 | indexes.forEach((index, i) => {
|
235 | index = Math.min(scrollLength - 1, index);
|
236 | let node, [leaf, offset] = this.scroll.leaf(index);
|
237 | [node, offset] = leaf.position(offset, i !== 0);
|
238 | args.push(node, offset);
|
239 | });
|
240 | if (args.length < 2) {
|
241 | args = args.concat(args);
|
242 | }
|
243 | return args;
|
244 | }
|
245 |
|
246 | scrollIntoView(scrollingContainer) {
|
247 | let range = this.lastRange;
|
248 | if (range == null) return;
|
249 | let bounds = this.getBounds(range.index, range.length);
|
250 | if (bounds == null) return;
|
251 | let limit = this.scroll.length()-1;
|
252 | let [first, ] = this.scroll.line(Math.min(range.index, limit));
|
253 | let last = first;
|
254 | if (range.length > 0) {
|
255 | [last, ] = this.scroll.line(Math.min(range.index + range.length, limit));
|
256 | }
|
257 | if (first == null || last == null) return;
|
258 | let scrollBounds = scrollingContainer.getBoundingClientRect();
|
259 | if (bounds.top < scrollBounds.top) {
|
260 | scrollingContainer.scrollTop -= (scrollBounds.top - bounds.top);
|
261 | } else if (bounds.bottom > scrollBounds.bottom) {
|
262 | scrollingContainer.scrollTop += (bounds.bottom - scrollBounds.bottom);
|
263 | }
|
264 | }
|
265 |
|
266 | setNativeRange(startNode, startOffset, endNode = startNode, endOffset = startOffset, force = false) {
|
267 | debug.info('setNativeRange', startNode, startOffset, endNode, endOffset);
|
268 | if (startNode != null && (this.root.parentNode == null || startNode.parentNode == null || endNode.parentNode == null)) {
|
269 | return;
|
270 | }
|
271 | let selection = document.getSelection();
|
272 | if (selection == null) return;
|
273 | if (startNode != null) {
|
274 | if (!this.hasFocus()) this.root.focus();
|
275 | let native = (this.getNativeRange() || {}).native;
|
276 | if (native == null || force ||
|
277 | startNode !== native.startContainer ||
|
278 | startOffset !== native.startOffset ||
|
279 | endNode !== native.endContainer ||
|
280 | endOffset !== native.endOffset) {
|
281 |
|
282 | if (startNode.tagName == "BR") {
|
283 | startOffset = [].indexOf.call(startNode.parentNode.childNodes, startNode);
|
284 | startNode = startNode.parentNode;
|
285 | }
|
286 | if (endNode.tagName == "BR") {
|
287 | endOffset = [].indexOf.call(endNode.parentNode.childNodes, endNode);
|
288 | endNode = endNode.parentNode;
|
289 | }
|
290 | let range = document.createRange();
|
291 | range.setStart(startNode, startOffset);
|
292 | range.setEnd(endNode, endOffset);
|
293 | selection.removeAllRanges();
|
294 | selection.addRange(range);
|
295 | }
|
296 | } else {
|
297 | selection.removeAllRanges();
|
298 | this.root.blur();
|
299 | document.body.focus();
|
300 | }
|
301 | }
|
302 |
|
303 | setRange(range, force = false, source = Emitter.sources.API) {
|
304 | if (typeof force === 'string') {
|
305 | source = force;
|
306 | force = false;
|
307 | }
|
308 | debug.info('setRange', range);
|
309 | if (range != null) {
|
310 | let args = this.rangeToNative(range);
|
311 | this.setNativeRange(...args, force);
|
312 | } else {
|
313 | this.setNativeRange(null);
|
314 | }
|
315 | this.update(source);
|
316 | }
|
317 |
|
318 | update(source = Emitter.sources.USER) {
|
319 | let oldRange = this.lastRange;
|
320 | let [lastRange, nativeRange] = this.getRange();
|
321 | this.lastRange = lastRange;
|
322 | if (this.lastRange != null) {
|
323 | this.savedRange = this.lastRange;
|
324 | }
|
325 | if (!equal(oldRange, this.lastRange)) {
|
326 | if (!this.composing && nativeRange != null && nativeRange.native.collapsed && nativeRange.start.node !== this.cursor.textNode) {
|
327 | this.cursor.restore();
|
328 | }
|
329 | let args = [Emitter.events.SELECTION_CHANGE, clone(this.lastRange), clone(oldRange), source];
|
330 | this.emitter.emit(Emitter.events.EDITOR_CHANGE, ...args);
|
331 | if (source !== Emitter.sources.SILENT) {
|
332 | this.emitter.emit(...args);
|
333 | }
|
334 | }
|
335 | }
|
336 | }
|
337 |
|
338 |
|
339 | function contains(parent, descendant) {
|
340 | try {
|
341 |
|
342 | descendant.parentNode;
|
343 | } catch (e) {
|
344 | return false;
|
345 | }
|
346 |
|
347 |
|
348 | if (descendant instanceof Text) {
|
349 | descendant = descendant.parentNode;
|
350 | }
|
351 | return parent.contains(descendant);
|
352 | }
|
353 |
|
354 |
|
355 | export { Range, Selection as default };
|