keyboard.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730
  1. import cloneDeep from 'lodash.clonedeep';
  2. import isEqual from 'lodash.isequal';
  3. import Delta, { AttributeMap } from 'quill-delta';
  4. import { EmbedBlot, Scope, TextBlot } from 'parchment';
  5. import Quill from '../core/quill';
  6. import logger from '../core/logger';
  7. import Module from '../core/module';
  8. const debug = logger('quill:keyboard');
  9. const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey';
  10. class Keyboard extends Module {
  11. static match(evt, binding) {
  12. if (
  13. ['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].some(key => {
  14. return !!binding[key] !== evt[key] && binding[key] !== null;
  15. })
  16. ) {
  17. return false;
  18. }
  19. return binding.key === evt.key || binding.key === evt.which;
  20. }
  21. constructor(quill, options) {
  22. super(quill, options);
  23. this.bindings = {};
  24. Object.keys(this.options.bindings).forEach(name => {
  25. if (this.options.bindings[name]) {
  26. this.addBinding(this.options.bindings[name]);
  27. }
  28. });
  29. this.addBinding({ key: 'Enter', shiftKey: null }, this.handleEnter);
  30. this.addBinding(
  31. { key: 'Enter', metaKey: null, ctrlKey: null, altKey: null },
  32. () => {},
  33. );
  34. if (/Firefox/i.test(navigator.userAgent)) {
  35. // Need to handle delete and backspace for Firefox in the general case #1171
  36. this.addBinding(
  37. { key: 'Backspace' },
  38. { collapsed: true },
  39. this.handleBackspace,
  40. );
  41. this.addBinding(
  42. { key: 'Delete' },
  43. { collapsed: true },
  44. this.handleDelete,
  45. );
  46. } else {
  47. this.addBinding(
  48. { key: 'Backspace' },
  49. { collapsed: true, prefix: /^.?$/ },
  50. this.handleBackspace,
  51. );
  52. this.addBinding(
  53. { key: 'Delete' },
  54. { collapsed: true, suffix: /^.?$/ },
  55. this.handleDelete,
  56. );
  57. }
  58. this.addBinding(
  59. { key: 'Backspace' },
  60. { collapsed: false },
  61. this.handleDeleteRange,
  62. );
  63. this.addBinding(
  64. { key: 'Delete' },
  65. { collapsed: false },
  66. this.handleDeleteRange,
  67. );
  68. this.addBinding(
  69. {
  70. key: 'Backspace',
  71. altKey: null,
  72. ctrlKey: null,
  73. metaKey: null,
  74. shiftKey: null,
  75. },
  76. { collapsed: true, offset: 0 },
  77. this.handleBackspace,
  78. );
  79. this.listen();
  80. }
  81. addBinding(keyBinding, context = {}, handler = {}) {
  82. const binding = normalize(keyBinding);
  83. if (binding == null) {
  84. debug.warn('Attempted to add invalid keyboard binding', binding);
  85. return;
  86. }
  87. if (typeof context === 'function') {
  88. context = { handler: context };
  89. }
  90. if (typeof handler === 'function') {
  91. handler = { handler };
  92. }
  93. const keys = Array.isArray(binding.key) ? binding.key : [binding.key];
  94. keys.forEach(key => {
  95. const singleBinding = {
  96. ...binding,
  97. key,
  98. ...context,
  99. ...handler,
  100. };
  101. this.bindings[singleBinding.key] = this.bindings[singleBinding.key] || [];
  102. this.bindings[singleBinding.key].push(singleBinding);
  103. });
  104. }
  105. listen() {
  106. this.quill.root.addEventListener('keydown', evt => {
  107. if (evt.defaultPrevented || evt.isComposing) return;
  108. const bindings = (this.bindings[evt.key] || []).concat(
  109. this.bindings[evt.which] || [],
  110. );
  111. const matches = bindings.filter(binding => Keyboard.match(evt, binding));
  112. if (matches.length === 0) return;
  113. const range = this.quill.getSelection();
  114. if (range == null || !this.quill.hasFocus()) return;
  115. const [line, offset] = this.quill.getLine(range.index);
  116. const [leafStart, offsetStart] = this.quill.getLeaf(range.index);
  117. const [leafEnd, offsetEnd] =
  118. range.length === 0
  119. ? [leafStart, offsetStart]
  120. : this.quill.getLeaf(range.index + range.length);
  121. const prefixText =
  122. leafStart instanceof TextBlot
  123. ? leafStart.value().slice(0, offsetStart)
  124. : '';
  125. const suffixText =
  126. leafEnd instanceof TextBlot ? leafEnd.value().slice(offsetEnd) : '';
  127. const curContext = {
  128. collapsed: range.length === 0,
  129. empty: range.length === 0 && line.length() <= 1,
  130. format: this.quill.getFormat(range),
  131. line,
  132. offset,
  133. prefix: prefixText,
  134. suffix: suffixText,
  135. event: evt,
  136. };
  137. const prevented = matches.some(binding => {
  138. if (
  139. binding.collapsed != null &&
  140. binding.collapsed !== curContext.collapsed
  141. ) {
  142. return false;
  143. }
  144. if (binding.empty != null && binding.empty !== curContext.empty) {
  145. return false;
  146. }
  147. if (binding.offset != null && binding.offset !== curContext.offset) {
  148. return false;
  149. }
  150. if (Array.isArray(binding.format)) {
  151. // any format is present
  152. if (binding.format.every(name => curContext.format[name] == null)) {
  153. return false;
  154. }
  155. } else if (typeof binding.format === 'object') {
  156. // all formats must match
  157. if (
  158. !Object.keys(binding.format).every(name => {
  159. if (binding.format[name] === true)
  160. return curContext.format[name] != null;
  161. if (binding.format[name] === false)
  162. return curContext.format[name] == null;
  163. return isEqual(binding.format[name], curContext.format[name]);
  164. })
  165. ) {
  166. return false;
  167. }
  168. }
  169. if (binding.prefix != null && !binding.prefix.test(curContext.prefix)) {
  170. return false;
  171. }
  172. if (binding.suffix != null && !binding.suffix.test(curContext.suffix)) {
  173. return false;
  174. }
  175. return binding.handler.call(this, range, curContext, binding) !== true;
  176. });
  177. if (prevented) {
  178. evt.preventDefault();
  179. }
  180. });
  181. }
  182. handleBackspace(range, context) {
  183. // Check for astral symbols
  184. const length = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(context.prefix)
  185. ? 2
  186. : 1;
  187. if (range.index === 0 || this.quill.getLength() <= 1) return;
  188. let formats = {};
  189. const [line] = this.quill.getLine(range.index);
  190. let delta = new Delta().retain(range.index - length).delete(length);
  191. if (context.offset === 0) {
  192. // Always deleting newline here, length always 1
  193. const [prev] = this.quill.getLine(range.index - 1);
  194. if (prev) {
  195. const isPrevLineEmpty =
  196. prev.statics.blotName === 'block' && prev.length() <= 1;
  197. if (!isPrevLineEmpty) {
  198. const curFormats = line.formats();
  199. const prevFormats = this.quill.getFormat(range.index - 1, 1);
  200. formats = AttributeMap.diff(curFormats, prevFormats) || {};
  201. if (Object.keys(formats).length > 0) {
  202. // line.length() - 1 targets \n in line, another -1 for newline being deleted
  203. const formatDelta = new Delta()
  204. .retain(range.index + line.length() - 2)
  205. .retain(1, formats);
  206. delta = delta.compose(formatDelta);
  207. }
  208. }
  209. }
  210. }
  211. this.quill.updateContents(delta, Quill.sources.USER);
  212. this.quill.focus();
  213. }
  214. handleDelete(range, context) {
  215. // Check for astral symbols
  216. const length = /^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(context.suffix)
  217. ? 2
  218. : 1;
  219. if (range.index >= this.quill.getLength() - length) return;
  220. let formats = {};
  221. const [line] = this.quill.getLine(range.index);
  222. let delta = new Delta().retain(range.index).delete(length);
  223. if (context.offset >= line.length() - 1) {
  224. const [next] = this.quill.getLine(range.index + 1);
  225. if (next) {
  226. const curFormats = line.formats();
  227. const nextFormats = this.quill.getFormat(range.index, 1);
  228. formats = AttributeMap.diff(curFormats, nextFormats) || {};
  229. if (Object.keys(formats).length > 0) {
  230. delta = delta.retain(next.length() - 1).retain(1, formats);
  231. }
  232. }
  233. }
  234. this.quill.updateContents(delta, Quill.sources.USER);
  235. this.quill.focus();
  236. }
  237. handleDeleteRange(range) {
  238. const lines = this.quill.getLines(range);
  239. let formats = {};
  240. if (lines.length > 1) {
  241. const firstFormats = lines[0].formats();
  242. const lastFormats = lines[lines.length - 1].formats();
  243. formats = AttributeMap.diff(lastFormats, firstFormats) || {};
  244. }
  245. this.quill.deleteText(range, Quill.sources.USER);
  246. if (Object.keys(formats).length > 0) {
  247. this.quill.formatLine(range.index, 1, formats, Quill.sources.USER);
  248. }
  249. this.quill.setSelection(range.index, Quill.sources.SILENT);
  250. this.quill.focus();
  251. }
  252. handleEnter(range, context) {
  253. const lineFormats = Object.keys(context.format).reduce(
  254. (formats, format) => {
  255. if (
  256. this.quill.scroll.query(format, Scope.BLOCK) &&
  257. !Array.isArray(context.format[format])
  258. ) {
  259. formats[format] = context.format[format];
  260. }
  261. return formats;
  262. },
  263. {},
  264. );
  265. const delta = new Delta()
  266. .retain(range.index)
  267. .delete(range.length)
  268. .insert('\n', lineFormats);
  269. this.quill.updateContents(delta, Quill.sources.USER);
  270. this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
  271. this.quill.focus();
  272. Object.keys(context.format).forEach(name => {
  273. if (lineFormats[name] != null) return;
  274. if (Array.isArray(context.format[name])) return;
  275. if (name === 'code' || name === 'link') return;
  276. this.quill.format(name, context.format[name], Quill.sources.USER);
  277. });
  278. }
  279. }
  280. Keyboard.DEFAULTS = {
  281. bindings: {
  282. bold: makeFormatHandler('bold'),
  283. italic: makeFormatHandler('italic'),
  284. underline: makeFormatHandler('underline'),
  285. indent: {
  286. // highlight tab or tab at beginning of list, indent or blockquote
  287. key: 'Tab',
  288. format: ['blockquote', 'indent', 'list'],
  289. handler(range, context) {
  290. if (context.collapsed && context.offset !== 0) return true;
  291. this.quill.format('indent', '+1', Quill.sources.USER);
  292. return false;
  293. },
  294. },
  295. outdent: {
  296. key: 'Tab',
  297. shiftKey: true,
  298. format: ['blockquote', 'indent', 'list'],
  299. // highlight tab or tab at beginning of list, indent or blockquote
  300. handler(range, context) {
  301. if (context.collapsed && context.offset !== 0) return true;
  302. this.quill.format('indent', '-1', Quill.sources.USER);
  303. return false;
  304. },
  305. },
  306. 'outdent backspace': {
  307. key: 'Backspace',
  308. collapsed: true,
  309. shiftKey: null,
  310. metaKey: null,
  311. ctrlKey: null,
  312. altKey: null,
  313. format: ['indent', 'list'],
  314. offset: 0,
  315. handler(range, context) {
  316. if (context.format.indent != null) {
  317. this.quill.format('indent', '-1', Quill.sources.USER);
  318. } else if (context.format.list != null) {
  319. this.quill.format('list', false, Quill.sources.USER);
  320. }
  321. },
  322. },
  323. 'indent code-block': makeCodeBlockHandler(true),
  324. 'outdent code-block': makeCodeBlockHandler(false),
  325. 'remove tab': {
  326. key: 'Tab',
  327. shiftKey: true,
  328. collapsed: true,
  329. prefix: /\t$/,
  330. handler(range) {
  331. this.quill.deleteText(range.index - 1, 1, Quill.sources.USER);
  332. },
  333. },
  334. tab: {
  335. key: 'Tab',
  336. handler(range, context) {
  337. if (context.format.table) return true;
  338. this.quill.history.cutoff();
  339. const delta = new Delta()
  340. .retain(range.index)
  341. .delete(range.length)
  342. .insert('\t');
  343. this.quill.updateContents(delta, Quill.sources.USER);
  344. this.quill.history.cutoff();
  345. this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
  346. return false;
  347. },
  348. },
  349. 'blockquote empty enter': {
  350. key: 'Enter',
  351. collapsed: true,
  352. format: ['blockquote'],
  353. empty: true,
  354. handler() {
  355. this.quill.format('blockquote', false, Quill.sources.USER);
  356. },
  357. },
  358. 'list empty enter': {
  359. key: 'Enter',
  360. collapsed: true,
  361. format: ['list'],
  362. empty: true,
  363. handler(range, context) {
  364. const formats = { list: false };
  365. if (context.format.indent) {
  366. formats.indent = false;
  367. }
  368. this.quill.formatLine(
  369. range.index,
  370. range.length,
  371. formats,
  372. Quill.sources.USER,
  373. );
  374. },
  375. },
  376. 'checklist enter': {
  377. key: 'Enter',
  378. collapsed: true,
  379. format: { list: 'checked' },
  380. handler(range) {
  381. const [line, offset] = this.quill.getLine(range.index);
  382. const formats = {
  383. ...line.formats(),
  384. list: 'checked',
  385. };
  386. const delta = new Delta()
  387. .retain(range.index)
  388. .insert('\n', formats)
  389. .retain(line.length() - offset - 1)
  390. .retain(1, { list: 'unchecked' });
  391. this.quill.updateContents(delta, Quill.sources.USER);
  392. this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
  393. this.quill.scrollIntoView();
  394. },
  395. },
  396. 'header enter': {
  397. key: 'Enter',
  398. collapsed: true,
  399. format: ['header'],
  400. suffix: /^$/,
  401. handler(range, context) {
  402. const [line, offset] = this.quill.getLine(range.index);
  403. const delta = new Delta()
  404. .retain(range.index)
  405. .insert('\n', context.format)
  406. .retain(line.length() - offset - 1)
  407. .retain(1, { header: null });
  408. this.quill.updateContents(delta, Quill.sources.USER);
  409. this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
  410. this.quill.scrollIntoView();
  411. },
  412. },
  413. 'table backspace': {
  414. key: 'Backspace',
  415. format: ['table'],
  416. collapsed: true,
  417. offset: 0,
  418. handler() {},
  419. },
  420. 'table delete': {
  421. key: 'Delete',
  422. format: ['table'],
  423. collapsed: true,
  424. suffix: /^$/,
  425. handler() {},
  426. },
  427. 'table enter': {
  428. key: 'Enter',
  429. shiftKey: null,
  430. format: ['table'],
  431. handler(range) {
  432. const module = this.quill.getModule('table');
  433. if (module) {
  434. const [table, row, cell, offset] = module.getTable(range);
  435. const shift = tableSide(table, row, cell, offset);
  436. if (shift == null) return;
  437. let index = table.offset();
  438. if (shift < 0) {
  439. const delta = new Delta().retain(index).insert('\n');
  440. this.quill.updateContents(delta, Quill.sources.USER);
  441. this.quill.setSelection(
  442. range.index + 1,
  443. range.length,
  444. Quill.sources.SILENT,
  445. );
  446. } else if (shift > 0) {
  447. index += table.length();
  448. const delta = new Delta().retain(index).insert('\n');
  449. this.quill.updateContents(delta, Quill.sources.USER);
  450. this.quill.setSelection(index, Quill.sources.USER);
  451. }
  452. }
  453. },
  454. },
  455. 'table tab': {
  456. key: 'Tab',
  457. shiftKey: null,
  458. format: ['table'],
  459. handler(range, context) {
  460. const { event, line: cell } = context;
  461. const offset = cell.offset(this.quill.scroll);
  462. if (event.shiftKey) {
  463. this.quill.setSelection(offset - 1, Quill.sources.USER);
  464. } else {
  465. this.quill.setSelection(offset + cell.length(), Quill.sources.USER);
  466. }
  467. },
  468. },
  469. 'list autofill': {
  470. key: ' ',
  471. shiftKey: null,
  472. collapsed: true,
  473. format: {
  474. list: false,
  475. 'code-block': false,
  476. blockquote: false,
  477. header: false,
  478. table: false,
  479. },
  480. prefix: /^\s*?(\d+\.|-|\*|\[ ?\]|\[x\])$/,
  481. handler(range, context) {
  482. if (this.quill.scroll.query('list') == null) return true;
  483. const { length } = context.prefix;
  484. const [line, offset] = this.quill.getLine(range.index);
  485. if (offset > length) return true;
  486. let value;
  487. switch (context.prefix.trim()) {
  488. case '[]':
  489. case '[ ]':
  490. value = 'unchecked';
  491. break;
  492. case '[x]':
  493. value = 'checked';
  494. break;
  495. case '-':
  496. case '*':
  497. value = 'bullet';
  498. break;
  499. default:
  500. value = 'ordered';
  501. }
  502. this.quill.insertText(range.index, ' ', Quill.sources.USER);
  503. this.quill.history.cutoff();
  504. const delta = new Delta()
  505. .retain(range.index - offset)
  506. .delete(length + 1)
  507. .retain(line.length() - 2 - offset)
  508. .retain(1, { list: value });
  509. this.quill.updateContents(delta, Quill.sources.USER);
  510. this.quill.history.cutoff();
  511. this.quill.setSelection(range.index - length, Quill.sources.SILENT);
  512. return false;
  513. },
  514. },
  515. 'code exit': {
  516. key: 'Enter',
  517. collapsed: true,
  518. format: ['code-block'],
  519. prefix: /^$/,
  520. suffix: /^\s*$/,
  521. handler(range) {
  522. const [line, offset] = this.quill.getLine(range.index);
  523. let numLines = 2;
  524. let cur = line;
  525. while (
  526. cur != null &&
  527. cur.length() <= 1 &&
  528. cur.formats()['code-block']
  529. ) {
  530. cur = cur.prev;
  531. numLines -= 1;
  532. // Requisite prev lines are empty
  533. if (numLines <= 0) {
  534. const delta = new Delta()
  535. .retain(range.index + line.length() - offset - 2)
  536. .retain(1, { 'code-block': null })
  537. .delete(1);
  538. this.quill.updateContents(delta, Quill.sources.USER);
  539. this.quill.setSelection(range.index - 1, Quill.sources.SILENT);
  540. return false;
  541. }
  542. }
  543. return true;
  544. },
  545. },
  546. 'embed left': makeEmbedArrowHandler('ArrowLeft', false),
  547. 'embed left shift': makeEmbedArrowHandler('ArrowLeft', true),
  548. 'embed right': makeEmbedArrowHandler('ArrowRight', false),
  549. 'embed right shift': makeEmbedArrowHandler('ArrowRight', true),
  550. 'table down': makeTableArrowHandler(false),
  551. 'table up': makeTableArrowHandler(true),
  552. },
  553. };
  554. function makeCodeBlockHandler(indent) {
  555. return {
  556. key: 'Tab',
  557. shiftKey: !indent,
  558. format: { 'code-block': true },
  559. handler(range) {
  560. const CodeBlock = this.quill.scroll.query('code-block');
  561. const lines =
  562. range.length === 0
  563. ? this.quill.getLines(range.index, 1)
  564. : this.quill.getLines(range);
  565. let { index, length } = range;
  566. lines.forEach((line, i) => {
  567. if (indent) {
  568. line.insertAt(0, CodeBlock.TAB);
  569. if (i === 0) {
  570. index += CodeBlock.TAB.length;
  571. } else {
  572. length += CodeBlock.TAB.length;
  573. }
  574. } else if (line.domNode.textContent.startsWith(CodeBlock.TAB)) {
  575. line.deleteAt(0, CodeBlock.TAB.length);
  576. if (i === 0) {
  577. index -= CodeBlock.TAB.length;
  578. } else {
  579. length -= CodeBlock.TAB.length;
  580. }
  581. }
  582. });
  583. this.quill.update(Quill.sources.USER);
  584. this.quill.setSelection(index, length, Quill.sources.SILENT);
  585. },
  586. };
  587. }
  588. function makeEmbedArrowHandler(key, shiftKey) {
  589. const where = key === 'ArrowLeft' ? 'prefix' : 'suffix';
  590. return {
  591. key,
  592. shiftKey,
  593. altKey: null,
  594. [where]: /^$/,
  595. handler(range) {
  596. let { index } = range;
  597. if (key === 'ArrowRight') {
  598. index += range.length + 1;
  599. }
  600. const [leaf] = this.quill.getLeaf(index);
  601. if (!(leaf instanceof EmbedBlot)) return true;
  602. if (key === 'ArrowLeft') {
  603. if (shiftKey) {
  604. this.quill.setSelection(
  605. range.index - 1,
  606. range.length + 1,
  607. Quill.sources.USER,
  608. );
  609. } else {
  610. this.quill.setSelection(range.index - 1, Quill.sources.USER);
  611. }
  612. } else if (shiftKey) {
  613. this.quill.setSelection(
  614. range.index,
  615. range.length + 1,
  616. Quill.sources.USER,
  617. );
  618. } else {
  619. this.quill.setSelection(
  620. range.index + range.length + 1,
  621. Quill.sources.USER,
  622. );
  623. }
  624. return false;
  625. },
  626. };
  627. }
  628. function makeFormatHandler(format) {
  629. return {
  630. key: format[0],
  631. shortKey: true,
  632. handler(range, context) {
  633. this.quill.format(format, !context.format[format], Quill.sources.USER);
  634. },
  635. };
  636. }
  637. function makeTableArrowHandler(up) {
  638. return {
  639. key: up ? 'ArrowUp' : 'ArrowDown',
  640. collapsed: true,
  641. format: ['table'],
  642. handler(range, context) {
  643. // TODO move to table module
  644. const key = up ? 'prev' : 'next';
  645. const cell = context.line;
  646. const targetRow = cell.parent[key];
  647. if (targetRow != null) {
  648. if (targetRow.statics.blotName === 'table-row') {
  649. let targetCell = targetRow.children.head;
  650. let cur = cell;
  651. while (cur.prev != null) {
  652. cur = cur.prev;
  653. targetCell = targetCell.next;
  654. }
  655. const index =
  656. targetCell.offset(this.quill.scroll) +
  657. Math.min(context.offset, targetCell.length() - 1);
  658. this.quill.setSelection(index, 0, Quill.sources.USER);
  659. }
  660. } else {
  661. const targetLine = cell.table()[key];
  662. if (targetLine != null) {
  663. if (up) {
  664. this.quill.setSelection(
  665. targetLine.offset(this.quill.scroll) + targetLine.length() - 1,
  666. 0,
  667. Quill.sources.USER,
  668. );
  669. } else {
  670. this.quill.setSelection(
  671. targetLine.offset(this.quill.scroll),
  672. 0,
  673. Quill.sources.USER,
  674. );
  675. }
  676. }
  677. }
  678. return false;
  679. },
  680. };
  681. }
  682. function normalize(binding) {
  683. if (typeof binding === 'string' || typeof binding === 'number') {
  684. binding = { key: binding };
  685. } else if (typeof binding === 'object') {
  686. binding = cloneDeep(binding);
  687. } else {
  688. return null;
  689. }
  690. if (binding.shortKey) {
  691. binding[SHORTKEY] = binding.shortKey;
  692. delete binding.shortKey;
  693. }
  694. return binding;
  695. }
  696. function tableSide(table, row, cell, offset) {
  697. if (row.prev == null && row.next == null) {
  698. if (cell.prev == null && cell.next == null) {
  699. return offset === 0 ? -1 : 1;
  700. }
  701. return cell.prev == null ? -1 : 1;
  702. }
  703. if (row.prev == null) {
  704. return -1;
  705. }
  706. if (row.next == null) {
  707. return 1;
  708. }
  709. return null;
  710. }
  711. export { Keyboard as default, SHORTKEY, normalize };