keyboard.js 18 KB

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