123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713 |
- import { cloneDeep, isEqual } from 'lodash-es';
- import Delta, { AttributeMap } from 'quill-delta';
- import { EmbedBlot, Scope, TextBlot } from 'parchment';
- import Quill from '../core/quill.js';
- import logger from '../core/logger.js';
- import Module from '../core/module.js';
- const debug = logger('quill:keyboard');
- const SHORTKEY = /Mac/i.test(navigator.platform) ? 'metaKey' : 'ctrlKey';
- class Keyboard extends Module {
- static match(evt, binding) {
- if (['altKey', 'ctrlKey', 'metaKey', 'shiftKey'].some(key => {
- return !!binding[key] !== evt[key] && binding[key] !== null;
- })) {
- return false;
- }
- return binding.key === evt.key || binding.key === evt.which;
- }
- constructor(quill, options) {
- super(quill, options);
- this.bindings = {};
- // @ts-expect-error Fix me later
- Object.keys(this.options.bindings).forEach(name => {
- // @ts-expect-error Fix me later
- if (this.options.bindings[name]) {
- // @ts-expect-error Fix me later
- this.addBinding(this.options.bindings[name]);
- }
- });
- this.addBinding({
- key: 'Enter',
- shiftKey: null
- }, this.handleEnter);
- this.addBinding({
- key: 'Enter',
- metaKey: null,
- ctrlKey: null,
- altKey: null
- }, () => {});
- if (/Firefox/i.test(navigator.userAgent)) {
- // Need to handle delete and backspace for Firefox in the general case #1171
- this.addBinding({
- key: 'Backspace'
- }, {
- collapsed: true
- }, this.handleBackspace);
- this.addBinding({
- key: 'Delete'
- }, {
- collapsed: true
- }, this.handleDelete);
- } else {
- this.addBinding({
- key: 'Backspace'
- }, {
- collapsed: true,
- prefix: /^.?$/
- }, this.handleBackspace);
- this.addBinding({
- key: 'Delete'
- }, {
- collapsed: true,
- suffix: /^.?$/
- }, this.handleDelete);
- }
- this.addBinding({
- key: 'Backspace'
- }, {
- collapsed: false
- }, this.handleDeleteRange);
- this.addBinding({
- key: 'Delete'
- }, {
- collapsed: false
- }, this.handleDeleteRange);
- this.addBinding({
- key: 'Backspace',
- altKey: null,
- ctrlKey: null,
- metaKey: null,
- shiftKey: null
- }, {
- collapsed: true,
- offset: 0
- }, this.handleBackspace);
- this.listen();
- }
- addBinding(keyBinding) {
- let context = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
- let handler = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
- const binding = normalize(keyBinding);
- if (binding == null) {
- debug.warn('Attempted to add invalid keyboard binding', binding);
- return;
- }
- if (typeof context === 'function') {
- context = {
- handler: context
- };
- }
- if (typeof handler === 'function') {
- handler = {
- handler
- };
- }
- const keys = Array.isArray(binding.key) ? binding.key : [binding.key];
- keys.forEach(key => {
- const singleBinding = {
- ...binding,
- key,
- ...context,
- ...handler
- };
- this.bindings[singleBinding.key] = this.bindings[singleBinding.key] || [];
- this.bindings[singleBinding.key].push(singleBinding);
- });
- }
- listen() {
- this.quill.root.addEventListener('keydown', evt => {
- if (evt.defaultPrevented || evt.isComposing) return;
- // evt.isComposing is false when pressing Enter/Backspace when composing in Safari
- // https://bugs.webkit.org/show_bug.cgi?id=165004
- const isComposing = evt.keyCode === 229 && (evt.key === 'Enter' || evt.key === 'Backspace');
- if (isComposing) return;
- const bindings = (this.bindings[evt.key] || []).concat(this.bindings[evt.which] || []);
- const matches = bindings.filter(binding => Keyboard.match(evt, binding));
- if (matches.length === 0) return;
- // @ts-expect-error
- const blot = Quill.find(evt.target, true);
- if (blot && blot.scroll !== this.quill.scroll) return;
- const range = this.quill.getSelection();
- if (range == null || !this.quill.hasFocus()) return;
- const [line, offset] = this.quill.getLine(range.index);
- const [leafStart, offsetStart] = this.quill.getLeaf(range.index);
- const [leafEnd, offsetEnd] = range.length === 0 ? [leafStart, offsetStart] : this.quill.getLeaf(range.index + range.length);
- const prefixText = leafStart instanceof TextBlot ? leafStart.value().slice(0, offsetStart) : '';
- const suffixText = leafEnd instanceof TextBlot ? leafEnd.value().slice(offsetEnd) : '';
- const curContext = {
- collapsed: range.length === 0,
- // @ts-expect-error Fix me later
- empty: range.length === 0 && line.length() <= 1,
- format: this.quill.getFormat(range),
- line,
- offset,
- prefix: prefixText,
- suffix: suffixText,
- event: evt
- };
- const prevented = matches.some(binding => {
- if (binding.collapsed != null && binding.collapsed !== curContext.collapsed) {
- return false;
- }
- if (binding.empty != null && binding.empty !== curContext.empty) {
- return false;
- }
- if (binding.offset != null && binding.offset !== curContext.offset) {
- return false;
- }
- if (Array.isArray(binding.format)) {
- // any format is present
- if (binding.format.every(name => curContext.format[name] == null)) {
- return false;
- }
- } else if (typeof binding.format === 'object') {
- // all formats must match
- if (!Object.keys(binding.format).every(name => {
- // @ts-expect-error Fix me later
- if (binding.format[name] === true) return curContext.format[name] != null;
- // @ts-expect-error Fix me later
- if (binding.format[name] === false) return curContext.format[name] == null;
- // @ts-expect-error Fix me later
- return isEqual(binding.format[name], curContext.format[name]);
- })) {
- return false;
- }
- }
- if (binding.prefix != null && !binding.prefix.test(curContext.prefix)) {
- return false;
- }
- if (binding.suffix != null && !binding.suffix.test(curContext.suffix)) {
- return false;
- }
- // @ts-expect-error Fix me later
- return binding.handler.call(this, range, curContext, binding) !== true;
- });
- if (prevented) {
- evt.preventDefault();
- }
- });
- }
- handleBackspace(range, context) {
- // Check for astral symbols
- const length = /[\uD800-\uDBFF][\uDC00-\uDFFF]$/.test(context.prefix) ? 2 : 1;
- if (range.index === 0 || this.quill.getLength() <= 1) return;
- let formats = {};
- const [line] = this.quill.getLine(range.index);
- let delta = new Delta().retain(range.index - length).delete(length);
- if (context.offset === 0) {
- // Always deleting newline here, length always 1
- const [prev] = this.quill.getLine(range.index - 1);
- if (prev) {
- const isPrevLineEmpty = prev.statics.blotName === 'block' && prev.length() <= 1;
- if (!isPrevLineEmpty) {
- // @ts-expect-error Fix me later
- const curFormats = line.formats();
- const prevFormats = this.quill.getFormat(range.index - 1, 1);
- formats = AttributeMap.diff(curFormats, prevFormats) || {};
- if (Object.keys(formats).length > 0) {
- // line.length() - 1 targets \n in line, another -1 for newline being deleted
- const formatDelta = new Delta()
- // @ts-expect-error Fix me later
- .retain(range.index + line.length() - 2).retain(1, formats);
- delta = delta.compose(formatDelta);
- }
- }
- }
- }
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.focus();
- }
- handleDelete(range, context) {
- // Check for astral symbols
- const length = /^[\uD800-\uDBFF][\uDC00-\uDFFF]/.test(context.suffix) ? 2 : 1;
- if (range.index >= this.quill.getLength() - length) return;
- let formats = {};
- const [line] = this.quill.getLine(range.index);
- let delta = new Delta().retain(range.index).delete(length);
- // @ts-expect-error Fix me later
- if (context.offset >= line.length() - 1) {
- const [next] = this.quill.getLine(range.index + 1);
- if (next) {
- // @ts-expect-error Fix me later
- const curFormats = line.formats();
- const nextFormats = this.quill.getFormat(range.index, 1);
- formats = AttributeMap.diff(curFormats, nextFormats) || {};
- if (Object.keys(formats).length > 0) {
- delta = delta.retain(next.length() - 1).retain(1, formats);
- }
- }
- }
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.focus();
- }
- handleDeleteRange(range) {
- deleteRange({
- range,
- quill: this.quill
- });
- this.quill.focus();
- }
- handleEnter(range, context) {
- const lineFormats = Object.keys(context.format).reduce((formats, format) => {
- if (this.quill.scroll.query(format, Scope.BLOCK) && !Array.isArray(context.format[format])) {
- formats[format] = context.format[format];
- }
- return formats;
- }, {});
- const delta = new Delta().retain(range.index).delete(range.length).insert('\n', lineFormats);
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
- this.quill.focus();
- }
- }
- const defaultOptions = {
- bindings: {
- bold: makeFormatHandler('bold'),
- italic: makeFormatHandler('italic'),
- underline: makeFormatHandler('underline'),
- indent: {
- // highlight tab or tab at beginning of list, indent or blockquote
- key: 'Tab',
- format: ['blockquote', 'indent', 'list'],
- handler(range, context) {
- if (context.collapsed && context.offset !== 0) return true;
- this.quill.format('indent', '+1', Quill.sources.USER);
- return false;
- }
- },
- outdent: {
- key: 'Tab',
- shiftKey: true,
- format: ['blockquote', 'indent', 'list'],
- // highlight tab or tab at beginning of list, indent or blockquote
- handler(range, context) {
- if (context.collapsed && context.offset !== 0) return true;
- this.quill.format('indent', '-1', Quill.sources.USER);
- return false;
- }
- },
- 'outdent backspace': {
- key: 'Backspace',
- collapsed: true,
- shiftKey: null,
- metaKey: null,
- ctrlKey: null,
- altKey: null,
- format: ['indent', 'list'],
- offset: 0,
- handler(range, context) {
- if (context.format.indent != null) {
- this.quill.format('indent', '-1', Quill.sources.USER);
- } else if (context.format.list != null) {
- this.quill.format('list', false, Quill.sources.USER);
- }
- }
- },
- 'indent code-block': makeCodeBlockHandler(true),
- 'outdent code-block': makeCodeBlockHandler(false),
- 'remove tab': {
- key: 'Tab',
- shiftKey: true,
- collapsed: true,
- prefix: /\t$/,
- handler(range) {
- this.quill.deleteText(range.index - 1, 1, Quill.sources.USER);
- }
- },
- tab: {
- key: 'Tab',
- handler(range, context) {
- if (context.format.table) return true;
- this.quill.history.cutoff();
- const delta = new Delta().retain(range.index).delete(range.length).insert('\t');
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.history.cutoff();
- this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
- return false;
- }
- },
- 'blockquote empty enter': {
- key: 'Enter',
- collapsed: true,
- format: ['blockquote'],
- empty: true,
- handler() {
- this.quill.format('blockquote', false, Quill.sources.USER);
- }
- },
- 'list empty enter': {
- key: 'Enter',
- collapsed: true,
- format: ['list'],
- empty: true,
- handler(range, context) {
- const formats = {
- list: false
- };
- if (context.format.indent) {
- formats.indent = false;
- }
- this.quill.formatLine(range.index, range.length, formats, Quill.sources.USER);
- }
- },
- 'checklist enter': {
- key: 'Enter',
- collapsed: true,
- format: {
- list: 'checked'
- },
- handler(range) {
- const [line, offset] = this.quill.getLine(range.index);
- const formats = {
- // @ts-expect-error Fix me later
- ...line.formats(),
- list: 'checked'
- };
- const delta = new Delta().retain(range.index).insert('\n', formats)
- // @ts-expect-error Fix me later
- .retain(line.length() - offset - 1).retain(1, {
- list: 'unchecked'
- });
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
- this.quill.scrollSelectionIntoView();
- }
- },
- 'header enter': {
- key: 'Enter',
- collapsed: true,
- format: ['header'],
- suffix: /^$/,
- handler(range, context) {
- const [line, offset] = this.quill.getLine(range.index);
- const delta = new Delta().retain(range.index).insert('\n', context.format)
- // @ts-expect-error Fix me later
- .retain(line.length() - offset - 1).retain(1, {
- header: null
- });
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.setSelection(range.index + 1, Quill.sources.SILENT);
- this.quill.scrollSelectionIntoView();
- }
- },
- 'table backspace': {
- key: 'Backspace',
- format: ['table'],
- collapsed: true,
- offset: 0,
- handler() {}
- },
- 'table delete': {
- key: 'Delete',
- format: ['table'],
- collapsed: true,
- suffix: /^$/,
- handler() {}
- },
- 'table enter': {
- key: 'Enter',
- shiftKey: null,
- format: ['table'],
- handler(range) {
- const module = this.quill.getModule('table');
- if (module) {
- // @ts-expect-error
- const [table, row, cell, offset] = module.getTable(range);
- const shift = tableSide(table, row, cell, offset);
- if (shift == null) return;
- let index = table.offset();
- if (shift < 0) {
- const delta = new Delta().retain(index).insert('\n');
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.setSelection(range.index + 1, range.length, Quill.sources.SILENT);
- } else if (shift > 0) {
- index += table.length();
- const delta = new Delta().retain(index).insert('\n');
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.setSelection(index, Quill.sources.USER);
- }
- }
- }
- },
- 'table tab': {
- key: 'Tab',
- shiftKey: null,
- format: ['table'],
- handler(range, context) {
- const {
- event,
- line: cell
- } = context;
- const offset = cell.offset(this.quill.scroll);
- if (event.shiftKey) {
- this.quill.setSelection(offset - 1, Quill.sources.USER);
- } else {
- this.quill.setSelection(offset + cell.length(), Quill.sources.USER);
- }
- }
- },
- 'list autofill': {
- key: ' ',
- shiftKey: null,
- collapsed: true,
- format: {
- 'code-block': false,
- blockquote: false,
- table: false
- },
- prefix: /^\s*?(\d+\.|-|\*|\[ ?\]|\[x\])$/,
- handler(range, context) {
- if (this.quill.scroll.query('list') == null) return true;
- const {
- length
- } = context.prefix;
- const [line, offset] = this.quill.getLine(range.index);
- if (offset > length) return true;
- let value;
- switch (context.prefix.trim()) {
- case '[]':
- case '[ ]':
- value = 'unchecked';
- break;
- case '[x]':
- value = 'checked';
- break;
- case '-':
- case '*':
- value = 'bullet';
- break;
- default:
- value = 'ordered';
- }
- this.quill.insertText(range.index, ' ', Quill.sources.USER);
- this.quill.history.cutoff();
- const delta = new Delta().retain(range.index - offset).delete(length + 1)
- // @ts-expect-error Fix me later
- .retain(line.length() - 2 - offset).retain(1, {
- list: value
- });
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.history.cutoff();
- this.quill.setSelection(range.index - length, Quill.sources.SILENT);
- return false;
- }
- },
- 'code exit': {
- key: 'Enter',
- collapsed: true,
- format: ['code-block'],
- prefix: /^$/,
- suffix: /^\s*$/,
- handler(range) {
- const [line, offset] = this.quill.getLine(range.index);
- let numLines = 2;
- let cur = line;
- while (cur != null && cur.length() <= 1 && cur.formats()['code-block']) {
- // @ts-expect-error
- cur = cur.prev;
- numLines -= 1;
- // Requisite prev lines are empty
- if (numLines <= 0) {
- const delta = new Delta()
- // @ts-expect-error Fix me later
- .retain(range.index + line.length() - offset - 2).retain(1, {
- 'code-block': null
- }).delete(1);
- this.quill.updateContents(delta, Quill.sources.USER);
- this.quill.setSelection(range.index - 1, Quill.sources.SILENT);
- return false;
- }
- }
- return true;
- }
- },
- 'embed left': makeEmbedArrowHandler('ArrowLeft', false),
- 'embed left shift': makeEmbedArrowHandler('ArrowLeft', true),
- 'embed right': makeEmbedArrowHandler('ArrowRight', false),
- 'embed right shift': makeEmbedArrowHandler('ArrowRight', true),
- 'table down': makeTableArrowHandler(false),
- 'table up': makeTableArrowHandler(true)
- }
- };
- Keyboard.DEFAULTS = defaultOptions;
- function makeCodeBlockHandler(indent) {
- return {
- key: 'Tab',
- shiftKey: !indent,
- format: {
- 'code-block': true
- },
- handler(range, _ref) {
- let {
- event
- } = _ref;
- const CodeBlock = this.quill.scroll.query('code-block');
- // @ts-expect-error
- const {
- TAB
- } = CodeBlock;
- if (range.length === 0 && !event.shiftKey) {
- this.quill.insertText(range.index, TAB, Quill.sources.USER);
- this.quill.setSelection(range.index + TAB.length, Quill.sources.SILENT);
- return;
- }
- const lines = range.length === 0 ? this.quill.getLines(range.index, 1) : this.quill.getLines(range);
- let {
- index,
- length
- } = range;
- lines.forEach((line, i) => {
- if (indent) {
- line.insertAt(0, TAB);
- if (i === 0) {
- index += TAB.length;
- } else {
- length += TAB.length;
- }
- // @ts-expect-error Fix me later
- } else if (line.domNode.textContent.startsWith(TAB)) {
- line.deleteAt(0, TAB.length);
- if (i === 0) {
- index -= TAB.length;
- } else {
- length -= TAB.length;
- }
- }
- });
- this.quill.update(Quill.sources.USER);
- this.quill.setSelection(index, length, Quill.sources.SILENT);
- }
- };
- }
- function makeEmbedArrowHandler(key, shiftKey) {
- const where = key === 'ArrowLeft' ? 'prefix' : 'suffix';
- return {
- key,
- shiftKey,
- altKey: null,
- [where]: /^$/,
- handler(range) {
- let {
- index
- } = range;
- if (key === 'ArrowRight') {
- index += range.length + 1;
- }
- const [leaf] = this.quill.getLeaf(index);
- if (!(leaf instanceof EmbedBlot)) return true;
- if (key === 'ArrowLeft') {
- if (shiftKey) {
- this.quill.setSelection(range.index - 1, range.length + 1, Quill.sources.USER);
- } else {
- this.quill.setSelection(range.index - 1, Quill.sources.USER);
- }
- } else if (shiftKey) {
- this.quill.setSelection(range.index, range.length + 1, Quill.sources.USER);
- } else {
- this.quill.setSelection(range.index + range.length + 1, Quill.sources.USER);
- }
- return false;
- }
- };
- }
- function makeFormatHandler(format) {
- return {
- key: format[0],
- shortKey: true,
- handler(range, context) {
- this.quill.format(format, !context.format[format], Quill.sources.USER);
- }
- };
- }
- function makeTableArrowHandler(up) {
- return {
- key: up ? 'ArrowUp' : 'ArrowDown',
- collapsed: true,
- format: ['table'],
- handler(range, context) {
- // TODO move to table module
- const key = up ? 'prev' : 'next';
- const cell = context.line;
- const targetRow = cell.parent[key];
- if (targetRow != null) {
- if (targetRow.statics.blotName === 'table-row') {
- // @ts-expect-error
- let targetCell = targetRow.children.head;
- let cur = cell;
- while (cur.prev != null) {
- // @ts-expect-error
- cur = cur.prev;
- targetCell = targetCell.next;
- }
- const index = targetCell.offset(this.quill.scroll) + Math.min(context.offset, targetCell.length() - 1);
- this.quill.setSelection(index, 0, Quill.sources.USER);
- }
- } else {
- // @ts-expect-error
- const targetLine = cell.table()[key];
- if (targetLine != null) {
- if (up) {
- this.quill.setSelection(targetLine.offset(this.quill.scroll) + targetLine.length() - 1, 0, Quill.sources.USER);
- } else {
- this.quill.setSelection(targetLine.offset(this.quill.scroll), 0, Quill.sources.USER);
- }
- }
- }
- return false;
- }
- };
- }
- function normalize(binding) {
- if (typeof binding === 'string' || typeof binding === 'number') {
- binding = {
- key: binding
- };
- } else if (typeof binding === 'object') {
- binding = cloneDeep(binding);
- } else {
- return null;
- }
- if (binding.shortKey) {
- binding[SHORTKEY] = binding.shortKey;
- delete binding.shortKey;
- }
- return binding;
- }
- // TODO: Move into quill.ts or editor.ts
- function deleteRange(_ref2) {
- let {
- quill,
- range
- } = _ref2;
- const lines = quill.getLines(range);
- let formats = {};
- if (lines.length > 1) {
- const firstFormats = lines[0].formats();
- const lastFormats = lines[lines.length - 1].formats();
- formats = AttributeMap.diff(lastFormats, firstFormats) || {};
- }
- quill.deleteText(range, Quill.sources.USER);
- if (Object.keys(formats).length > 0) {
- quill.formatLine(range.index, 1, formats, Quill.sources.USER);
- }
- quill.setSelection(range.index, Quill.sources.SILENT);
- }
- function tableSide(_table, row, cell, offset) {
- if (row.prev == null && row.next == null) {
- if (cell.prev == null && cell.next == null) {
- return offset === 0 ? -1 : 1;
- }
- return cell.prev == null ? -1 : 1;
- }
- if (row.prev == null) {
- return -1;
- }
- if (row.next == null) {
- return 1;
- }
- return null;
- }
- export { Keyboard as default, SHORTKEY, normalize, deleteRange };
- //# sourceMappingURL=keyboard.js.map
|