UNPKG

11.9 kBJavaScriptView Raw
1import Parchment from 'parchment';
2import clone from 'clone';
3import equal from 'deep-equal';
4import Emitter from './emitter';
5import logger from './logger';
6
7let debug = logger('quill:selection');
8
9
10class Range {
11 constructor(index, length = 0) {
12 this.index = index;
13 this.length = length;
14 }
15}
16
17
18class 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 // savedRange is last non-null range
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; // cursor.restore() will handle
45 // TODO unclear if this has negative side effects
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 // TODO Give blot ability to not split
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); // Should never happen
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(); // root.blur() not enough on IE11+Travis+SauceLabs (but not local VMs)
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
339function contains(parent, descendant) {
340 try {
341 // Firefox inserts inaccessible nodes around video elements
342 descendant.parentNode;
343 } catch (e) {
344 return false;
345 }
346 // IE11 has bug with Text nodes
347 // https://connect.microsoft.com/IE/feedback/details/780874/node-contains-is-incorrect
348 if (descendant instanceof Text) {
349 descendant = descendant.parentNode;
350 }
351 return parent.contains(descendant);
352}
353
354
355export { Range, Selection as default };