1 | import clone from 'clone';
|
2 | import equal from 'deep-equal';
|
3 | import extend from 'extend';
|
4 | import Delta from 'quill-delta';
|
5 | import DeltaOp from 'quill-delta/lib/op';
|
6 | import Parchment from 'parchment';
|
7 | import Embed from '../blots/embed';
|
8 | import Quill from '../core/quill';
|
9 | import logger from '../core/logger';
|
10 | import Module from '../core/module';
|
11 |
|
12 | let debug = logger('quill:keyboard');
|
13 |
|
14 | const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey';
|
15 |
|
16 |
|
17 | class Keyboard extends Module {
|
18 | static match(evt, binding) {
|
19 | binding = normalize(binding);
|
20 | if (['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].some(function(key) {
|
21 | return (!!binding[key] !== evt[key] && binding[key] !== null);
|
22 | })) {
|
23 | return false;
|
24 | }
|
25 | return binding.key === (evt.which || evt.keyCode);
|
26 | }
|
27 |
|
28 | constructor(quill, options) {
|
29 | super(quill, options);
|
30 | this.bindings = {};
|
31 | Object.keys(this.options.bindings).forEach((name) => {
|
32 | if (name === 'list autofill' &&
|
33 | quill.scroll.whitelist != null &&
|
34 | !quill.scroll.whitelist['list']) {
|
35 | return;
|
36 | }
|
37 | if (this.options.bindings[name]) {
|
38 | this.addBinding(this.options.bindings[name]);
|
39 | }
|
40 | });
|
41 | this.addBinding({ key: Keyboard.keys.ENTER, shiftKey: null }, handleEnter);
|
42 | this.addBinding({ key: Keyboard.keys.ENTER, metaKey: null, ctrlKey: null, altKey: null }, function() {});
|
43 | if (/Firefox/i.test(navigator.userAgent)) {
|
44 |
|
45 | this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true }, handleBackspace);
|
46 | this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true }, handleDelete);
|
47 | } else {
|
48 | this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: true, prefix: /^.?$/ }, handleBackspace);
|
49 | this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: true, suffix: /^.?$/ }, handleDelete);
|
50 | }
|
51 | this.addBinding({ key: Keyboard.keys.BACKSPACE }, { collapsed: false }, handleDeleteRange);
|
52 | this.addBinding({ key: Keyboard.keys.DELETE }, { collapsed: false }, handleDeleteRange);
|
53 | this.addBinding({ key: Keyboard.keys.BACKSPACE, altKey: null, ctrlKey: null, metaKey: null, shiftKey: null },
|
54 | { collapsed: true, offset: 0 },
|
55 | handleBackspace);
|
56 | this.listen();
|
57 | }
|
58 |
|
59 | addBinding(key, context = {}, handler = {}) {
|
60 | let binding = normalize(key);
|
61 | if (binding == null || binding.key == null) {
|
62 | return debug.warn('Attempted to add invalid keyboard binding', binding);
|
63 | }
|
64 | if (typeof context === 'function') {
|
65 | context = { handler: context };
|
66 | }
|
67 | if (typeof handler === 'function') {
|
68 | handler = { handler: handler };
|
69 | }
|
70 | binding = extend(binding, context, handler);
|
71 | this.bindings[binding.key] = this.bindings[binding.key] || [];
|
72 | this.bindings[binding.key].push(binding);
|
73 | }
|
74 |
|
75 | listen() {
|
76 | this.quill.root.addEventListener('keydown', (evt) => {
|
77 | if (evt.defaultPrevented) return;
|
78 | let which = evt.which || evt.keyCode;
|
79 | let bindings = (this.bindings[which] || []).filter(function(binding) {
|
80 | return Keyboard.match(evt, binding);
|
81 | });
|
82 | if (bindings.length === 0) return;
|
83 | let range = this.quill.getSelection();
|
84 | if (range == null || !this.quill.hasFocus()) return;
|
85 | let [line, offset] = this.quill.getLine(range.index);
|
86 | let [leafStart, offsetStart] = this.quill.getLeaf(range.index);
|
87 | let [leafEnd, offsetEnd] = range.length === 0 ? [leafStart, offsetStart] : this.quill.getLeaf(range.index + range.length);
|
88 | let prefixText = leafStart instanceof Parchment.Text ? leafStart.value().slice(0, offsetStart) : '';
|
89 | let suffixText = leafEnd instanceof Parchment.Text ? leafEnd.value().slice(offsetEnd) : '';
|
90 | let curContext = {
|
91 | collapsed: range.length === 0,
|
92 | empty: range.length === 0 && line.length() <= 1,
|
93 | format: this.quill.getFormat(range),
|
94 | offset: offset,
|
95 | prefix: prefixText,
|
96 | suffix: suffixText
|
97 | };
|
98 | let prevented = bindings.some((binding) => {
|
99 | if (binding.collapsed != null && binding.collapsed !== curContext.collapsed) return false;
|
100 | if (binding.empty != null && binding.empty !== curContext.empty) return false;
|
101 | if (binding.offset != null && binding.offset !== curContext.offset) return false;
|
102 | if (Array.isArray(binding.format)) {
|
103 |
|
104 | if (binding.format.every(function(name) {
|
105 | return curContext.format[name] == null;
|
106 | })) {
|
107 | return false;
|
108 | }
|
109 | } else if (typeof binding.format === 'object') {
|
110 |
|
111 | if (!Object.keys(binding.format).every(function(name) {
|
112 | if (binding.format[name] === true) return curContext.format[name] != null;
|
113 | if (binding.format[name] === false) return curContext.format[name] == null;
|
114 | return equal(binding.format[name], curContext.format[name]);
|
115 | })) {
|
116 | return false;
|
117 | }
|
118 | }
|
119 | if (binding.prefix != null && !binding.prefix.test(curContext.prefix)) return false;
|
120 | if (binding.suffix != null && !binding.suffix.test(curContext.suffix)) return false;
|
121 | return binding.handler.call(this, range, curContext) !== true;
|
122 | });
|
123 | if (prevented) {
|
124 | evt.preventDefault();
|
125 | }
|
126 | });
|
127 | }
|
128 | }
|
129 |
|
130 | Keyboard.keys = {
|
131 | BACKSPACE: 8,
|
132 | TAB: 9,
|
133 | ENTER: 13,
|
134 | ESCAPE: 27,
|
135 | LEFT: 37,
|
136 | UP: 38,
|
137 | RIGHT: 39,
|
138 | DOWN: 40,
|
139 | DELETE: 46
|
140 | };
|
141 |
|
142 | Keyboard.DEFAULTS = {
|
143 | bindings: {
|
144 | 'bold' : makeFormatHandler('bold'),
|
145 | 'italic' : makeFormatHandler('italic'),
|
146 | 'underline' : makeFormatHandler('underline'),
|
147 | 'indent': {
|
148 |
|
149 | key: Keyboard.keys.TAB,
|
150 | format: ['blockquote', 'indent', 'list'],
|
151 | handler: function(range, context) {
|
152 | if (context.collapsed && context.offset !== 0) return true;
|
153 | this.quill.format('indent', '+1', Quill.sources.USER);
|
154 | }
|
155 | },
|
156 | 'outdent': {
|
157 | key: Keyboard.keys.TAB,
|
158 | shiftKey: true,
|
159 | format: ['blockquote', 'indent', 'list'],
|
160 |
|
161 | handler: function(range, context) {
|
162 | if (context.collapsed && context.offset !== 0) return true;
|
163 | this.quill.format('indent', '-1', Quill.sources.USER);
|
164 | }
|
165 | },
|
166 | 'outdent backspace': {
|
167 | key: Keyboard.keys.BACKSPACE,
|
168 | collapsed: true,
|
169 | shiftKey: null,
|
170 | metaKey: null,
|
171 | ctrlKey: null,
|
172 | altKey: null,
|
173 | format: ['indent', 'list'],
|
174 | offset: 0,
|
175 | handler: function(range, context) {
|
176 | if (context.format.indent != null) {
|
177 | this.quill.format('indent', '-1', Quill.sources.USER);
|
178 | } else if (context.format.list != null) {
|
179 | this.quill.format('list', false, Quill.sources.USER);
|
180 | }
|
181 | }
|
182 | },
|
183 | 'indent code-block': makeCodeBlockHandler(true),
|
184 | 'outdent code-block': makeCodeBlockHandler(false),
|
185 | 'remove tab': {
|
186 | key: Keyboard.keys.TAB,
|
187 | shiftKey: true,
|
188 | collapsed: true,
|
189 | prefix: /\t$/,
|
190 | handler: function(range) {
|
191 | this.quill.deleteText(range.index - 1, 1, Quill.sources.USER);
|
192 | }
|
193 | },
|
194 | 'tab': {
|
195 | key: Keyboard.keys.TAB,
|
196 | handler: function(range) {
|
197 | this.quill.history.cutoff();
|
198 | let delta = new Delta().retain(range.index)
|
199 | .delete(range.length)
|
200 | .insert('\t');
|
201 | this.quill.updateContents(delta, Quill.sources.USER);
|
202 | this.quill.history.cutoff();
|
203 | this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
|
204 | }
|
205 | },
|
206 | 'list empty enter': {
|
207 | key: Keyboard.keys.ENTER,
|
208 | collapsed: true,
|
209 | format: ['list'],
|
210 | empty: true,
|
211 | handler: function(range, context) {
|
212 | this.quill.format('list', false, Quill.sources.USER);
|
213 | if (context.format.indent) {
|
214 | this.quill.format('indent', false, Quill.sources.USER);
|
215 | }
|
216 | }
|
217 | },
|
218 | 'checklist enter': {
|
219 | key: Keyboard.keys.ENTER,
|
220 | collapsed: true,
|
221 | format: { list: 'checked' },
|
222 | handler: function(range) {
|
223 | let [line, offset] = this.quill.getLine(range.index);
|
224 | let delta = new Delta().retain(range.index)
|
225 | .insert('\n', { list: 'checked' })
|
226 | .retain(line.length() - offset - 1)
|
227 | .retain(1, { list: 'unchecked' });
|
228 | this.quill.updateContents(delta, Quill.sources.USER);
|
229 | this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
|
230 | this.quill.scrollIntoView();
|
231 | }
|
232 | },
|
233 | 'header enter': {
|
234 | key: Keyboard.keys.ENTER,
|
235 | collapsed: true,
|
236 | format: ['header'],
|
237 | suffix: /^$/,
|
238 | handler: function(range, context) {
|
239 | let [line, offset] = this.quill.getLine(range.index);
|
240 | let delta = new Delta().retain(range.index)
|
241 | .insert('\n', context.format)
|
242 | .retain(line.length() - offset - 1)
|
243 | .retain(1, { header: null });
|
244 | this.quill.updateContents(delta, Quill.sources.USER);
|
245 | this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
|
246 | this.quill.scrollIntoView();
|
247 | }
|
248 | },
|
249 | 'list autofill': {
|
250 | key: ' ',
|
251 | collapsed: true,
|
252 | format: { list: false },
|
253 | prefix: /^\s*?(1\.|-|\[ ?\]|\[x\])$/,
|
254 | handler: function(range, context) {
|
255 | let length = context.prefix.length;
|
256 | let [line, offset] = this.quill.getLine(range.index);
|
257 | if (offset > length) return true;
|
258 | let value;
|
259 | switch (context.prefix.trim()) {
|
260 | case '[]': case '[ ]':
|
261 | value = 'unchecked';
|
262 | break;
|
263 | case '[x]':
|
264 | value = 'checked';
|
265 | break;
|
266 | case '-':
|
267 | value = 'bullet';
|
268 | break;
|
269 | default:
|
270 | value = 'ordered';
|
271 | }
|
272 | this.quill.insertText(range.index, ' ', Quill.sources.USER);
|
273 | this.quill.history.cutoff();
|
274 | let delta = new Delta().retain(range.index - offset)
|
275 | .delete(length + 1)
|
276 | .retain(line.length() - 2 - offset)
|
277 | .retain(1, { list: value });
|
278 | this.quill.updateContents(delta, Quill.sources.USER);
|
279 | this.quill.history.cutoff();
|
280 | this.quill.setSelection(range.index - length, Quill.sources.SILENT);
|
281 | }
|
282 | },
|
283 | 'code exit': {
|
284 | key: Keyboard.keys.ENTER,
|
285 | collapsed: true,
|
286 | format: ['code-block'],
|
287 | prefix: /\n\n$/,
|
288 | suffix: /^\s+$/,
|
289 | handler: function(range) {
|
290 | const [line, offset] = this.quill.getLine(range.index);
|
291 | const delta = new Delta()
|
292 | .retain(range.index + line.length() - offset - 2)
|
293 | .retain(1, { 'code-block': null })
|
294 | .delete(1);
|
295 | this.quill.updateContents(delta, Quill.sources.USER);
|
296 | }
|
297 | },
|
298 | 'embed left': makeEmbedArrowHandler(Keyboard.keys.LEFT, false),
|
299 | 'embed left shift': makeEmbedArrowHandler(Keyboard.keys.LEFT, true),
|
300 | 'embed right': makeEmbedArrowHandler(Keyboard.keys.RIGHT, false),
|
301 | 'embed right shift': makeEmbedArrowHandler(Keyboard.keys.RIGHT, true)
|
302 | }
|
303 | };
|
304 |
|
305 | function makeEmbedArrowHandler(key, shiftKey) {
|
306 | const where = key === Keyboard.keys.LEFT ? 'prefix' : 'suffix';
|
307 | return {
|
308 | key,
|
309 | shiftKey,
|
310 | [where]: /^$/,
|
311 | handler: function(range) {
|
312 | let index = range.index;
|
313 | if (key === Keyboard.keys.RIGHT) {
|
314 | index += (range.length + 1);
|
315 | }
|
316 | const [leaf, ] = this.quill.getLeaf(index);
|
317 | if (!(leaf instanceof Embed)) return true;
|
318 | if (key === Keyboard.keys.LEFT) {
|
319 | if (shiftKey) {
|
320 | this.quill.setSelection(range.index - 1, range.length + 1, Quill.sources.USER);
|
321 | } else {
|
322 | this.quill.setSelection(range.index - 1, Quill.sources.USER);
|
323 | }
|
324 | } else {
|
325 | if (shiftKey) {
|
326 | this.quill.setSelection(range.index, range.length + 1, Quill.sources.USER);
|
327 | } else {
|
328 | this.quill.setSelection(range.index + range.length + 1, Quill.sources.USER);
|
329 | }
|
330 | }
|
331 | return false;
|
332 | }
|
333 | };
|
334 | }
|
335 |
|
336 |
|
337 | function handleBackspace(range, context) {
|
338 | if (range.index === 0 || this.quill.getLength() <= 1) return;
|
339 | let [line, ] = this.quill.getLine(range.index);
|
340 | let formats = {};
|
341 | if (context.offset === 0) {
|
342 | let [prev, ] = this.quill.getLine(range.index - 1);
|
343 | if (prev != null && prev.length() > 1) {
|
344 | let curFormats = line.formats();
|
345 | let prevFormats = this.quill.getFormat(range.index-1, 1);
|
346 | formats = DeltaOp.attributes.diff(curFormats, prevFormats) || {};
|
347 | }
|
348 | }
|
349 |
|
350 | let length = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(context.prefix) ? 2 : 1;
|
351 | this.quill.deleteText(range.index-length, length, Quill.sources.USER);
|
352 | if (Object.keys(formats).length > 0) {
|
353 | this.quill.formatLine(range.index-length, length, formats, Quill.sources.USER);
|
354 | }
|
355 | this.quill.focus();
|
356 | }
|
357 |
|
358 | function handleDelete(range, context) {
|
359 |
|
360 | let length = /^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(context.suffix) ? 2 : 1;
|
361 | if (range.index >= this.quill.getLength() - length) return;
|
362 | let formats = {}, nextLength = 0;
|
363 | let [line, ] = this.quill.getLine(range.index);
|
364 | if (context.offset >= line.length() - 1) {
|
365 | let [next, ] = this.quill.getLine(range.index + 1);
|
366 | if (next) {
|
367 | let curFormats = line.formats();
|
368 | let nextFormats = this.quill.getFormat(range.index, 1);
|
369 | formats = DeltaOp.attributes.diff(curFormats, nextFormats) || {};
|
370 | nextLength = next.length();
|
371 | }
|
372 | }
|
373 | this.quill.deleteText(range.index, length, Quill.sources.USER);
|
374 | if (Object.keys(formats).length > 0) {
|
375 | this.quill.formatLine(range.index + nextLength - 1, length, formats, Quill.sources.USER);
|
376 | }
|
377 | }
|
378 |
|
379 | function handleDeleteRange(range) {
|
380 | let lines = this.quill.getLines(range);
|
381 | let formats = {};
|
382 | if (lines.length > 1) {
|
383 | let firstFormats = lines[0].formats();
|
384 | let lastFormats = lines[lines.length - 1].formats();
|
385 | formats = DeltaOp.attributes.diff(lastFormats, firstFormats) || {};
|
386 | }
|
387 | this.quill.deleteText(range, Quill.sources.USER);
|
388 | if (Object.keys(formats).length > 0) {
|
389 | this.quill.formatLine(range.index, 1, formats, Quill.sources.USER);
|
390 | }
|
391 | this.quill.setSelection(range.index, Quill.sources.SILENT);
|
392 | this.quill.focus();
|
393 | }
|
394 |
|
395 | function handleEnter(range, context) {
|
396 | if (range.length > 0) {
|
397 | this.quill.scroll.deleteAt(range.index, range.length);
|
398 | }
|
399 | let lineFormats = Object.keys(context.format).reduce(function(lineFormats, format) {
|
400 | if (Parchment.query(format, Parchment.Scope.BLOCK) && !Array.isArray(context.format[format])) {
|
401 | lineFormats[format] = context.format[format];
|
402 | }
|
403 | return lineFormats;
|
404 | }, {});
|
405 | this.quill.insertText(range.index, '\n', lineFormats, Quill.sources.USER);
|
406 |
|
407 |
|
408 | this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
|
409 | this.quill.focus();
|
410 | Object.keys(context.format).forEach((name) => {
|
411 | if (lineFormats[name] != null) return;
|
412 | if (Array.isArray(context.format[name])) return;
|
413 | if (name === 'link') return;
|
414 | this.quill.format(name, context.format[name], Quill.sources.USER);
|
415 | });
|
416 | }
|
417 |
|
418 | function makeCodeBlockHandler(indent) {
|
419 | return {
|
420 | key: Keyboard.keys.TAB,
|
421 | shiftKey: !indent,
|
422 | format: {'code-block': true },
|
423 | handler: function(range) {
|
424 | let CodeBlock = Parchment.query('code-block');
|
425 | let index = range.index, length = range.length;
|
426 | let [block, offset] = this.quill.scroll.descendant(CodeBlock, index);
|
427 | if (block == null) return;
|
428 | let scrollIndex = this.quill.getIndex(block);
|
429 | let start = block.newlineIndex(offset, true) + 1;
|
430 | let end = block.newlineIndex(scrollIndex + offset + length);
|
431 | let lines = block.domNode.textContent.slice(start, end).split('\n');
|
432 | offset = 0;
|
433 | lines.forEach((line, i) => {
|
434 | if (indent) {
|
435 | block.insertAt(start + offset, CodeBlock.TAB);
|
436 | offset += CodeBlock.TAB.length;
|
437 | if (i === 0) {
|
438 | index += CodeBlock.TAB.length;
|
439 | } else {
|
440 | length += CodeBlock.TAB.length;
|
441 | }
|
442 | } else if (line.startsWith(CodeBlock.TAB)) {
|
443 | block.deleteAt(start + offset, CodeBlock.TAB.length);
|
444 | offset -= CodeBlock.TAB.length;
|
445 | if (i === 0) {
|
446 | index -= CodeBlock.TAB.length;
|
447 | } else {
|
448 | length -= CodeBlock.TAB.length;
|
449 | }
|
450 | }
|
451 | offset += line.length + 1;
|
452 | });
|
453 | this.quill.update(Quill.sources.USER);
|
454 | this.quill.setSelection(index, length, Quill.sources.SILENT);
|
455 | }
|
456 | };
|
457 | }
|
458 |
|
459 | function makeFormatHandler(format) {
|
460 | return {
|
461 | key: format[0].toUpperCase(),
|
462 | shortKey: true,
|
463 | handler: function(range, context) {
|
464 | this.quill.format(format, !context.format[format], Quill.sources.USER);
|
465 | }
|
466 | };
|
467 | }
|
468 |
|
469 | function normalize(binding) {
|
470 | if (typeof binding === 'string' || typeof binding === 'number') {
|
471 | return normalize({ key: binding });
|
472 | }
|
473 | if (typeof binding === 'object') {
|
474 | binding = clone(binding, false);
|
475 | }
|
476 | if (typeof binding.key === 'string') {
|
477 | if (Keyboard.keys[binding.key.toUpperCase()] != null) {
|
478 | binding.key = Keyboard.keys[binding.key.toUpperCase()];
|
479 | } else if (binding.key.length === 1) {
|
480 | binding.key = binding.key.toUpperCase().charCodeAt(0);
|
481 | } else {
|
482 | return null;
|
483 | }
|
484 | }
|
485 | if (binding.shortKey) {
|
486 | binding[SHORTKEY] = binding.shortKey;
|
487 | delete binding.shortKey;
|
488 | }
|
489 | return binding;
|
490 | }
|
491 |
|
492 |
|
493 | export { Keyboard as default, SHORTKEY };
|