import clone from 'clone';
|
|
import equal from 'deep-equal';
|
|
import extend from 'extend';
|
|
import Delta, { AttributeMap } from 'quill-delta';
|
|
import { EmbedBlot, Scope, TextBlot } from 'parchment';
|
|
import Quill from '../core/quill';
|
|
import logger from '../core/logger';
|
|
import Module from '../core/module';
|
|
|
|
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 = {};
|
|
Object.keys(this.options.bindings).forEach(name => {
|
|
if (this.options.bindings[name]) {
|
|
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, context = {}, handler = {}) {
|
|
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 = extend({}, 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) 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;
|
|
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,
|
|
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 => {
|
|
if (binding.format[name] === true)
|
|
return curContext.format[name] != null;
|
|
if (binding.format[name] === false)
|
|
return curContext.format[name] == null;
|
|
return equal(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;
|
|
}
|
|
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 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()
|
|
.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);
|
|
if (context.offset >= line.length() - 1) {
|
|
const [next] = this.quill.getLine(range.index + 1);
|
|
if (next) {
|
|
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) {
|
|
const lines = this.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) || {};
|
|
}
|
|
this.quill.deleteText(range, Quill.sources.USER);
|
|
if (Object.keys(formats).length > 0) {
|
|
this.quill.formatLine(range.index, 1, formats, Quill.sources.USER);
|
|
}
|
|
this.quill.setSelection(range.index, Quill.sources.SILENT);
|
|
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();
|
|
|
|
Object.keys(context.format).forEach(name => {
|
|
if (lineFormats[name] != null) return;
|
|
if (Array.isArray(context.format[name])) return;
|
|
if (name === 'code' || name === 'link') return;
|
|
this.quill.format(name, context.format[name], Quill.sources.USER);
|
|
});
|
|
}
|
|
}
|
|
|
|
Keyboard.DEFAULTS = {
|
|
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 = extend({}, line.formats(), { list: 'checked' });
|
|
const delta = new Delta()
|
|
.retain(range.index)
|
|
.insert('\n', formats)
|
|
.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.scrollIntoView();
|
|
},
|
|
},
|
|
'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)
|
|
.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.scrollIntoView();
|
|
},
|
|
},
|
|
'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) {
|
|
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: {
|
|
list: false,
|
|
'code-block': false,
|
|
blockquote: false,
|
|
header: 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)
|
|
.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']
|
|
) {
|
|
cur = cur.prev;
|
|
numLines -= 1;
|
|
// Requisite prev lines are empty
|
|
if (numLines <= 0) {
|
|
const delta = new Delta()
|
|
.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),
|
|
},
|
|
};
|
|
|
|
function makeCodeBlockHandler(indent) {
|
|
return {
|
|
key: 'Tab',
|
|
shiftKey: !indent,
|
|
format: { 'code-block': true },
|
|
handler(range) {
|
|
const CodeBlock = this.quill.scroll.query('code-block');
|
|
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, CodeBlock.TAB);
|
|
if (i === 0) {
|
|
index += CodeBlock.TAB.length;
|
|
} else {
|
|
length += CodeBlock.TAB.length;
|
|
}
|
|
} else if (line.domNode.textContent.startsWith(CodeBlock.TAB)) {
|
|
line.deleteAt(0, CodeBlock.TAB.length);
|
|
if (i === 0) {
|
|
index -= CodeBlock.TAB.length;
|
|
} else {
|
|
length -= CodeBlock.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') {
|
|
let targetCell = targetRow.children.head;
|
|
let cur = cell;
|
|
while (cur.prev != null) {
|
|
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 {
|
|
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 = clone(binding, false);
|
|
} else {
|
|
return null;
|
|
}
|
|
if (binding.shortKey) {
|
|
binding[SHORTKEY] = binding.shortKey;
|
|
delete binding.shortKey;
|
|
}
|
|
return binding;
|
|
}
|
|
|
|
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 };
|