2023-01-22 19:35:12 -05:00
|
|
|
|
import {
|
|
|
|
|
isMac,
|
|
|
|
|
isWin,
|
|
|
|
|
isIOS,
|
|
|
|
|
ctrlKey,
|
|
|
|
|
supportsInputEvents,
|
|
|
|
|
} from '../Constants';
|
|
|
|
|
import { deleteContentsOfRange } from '../range/InsertDelete';
|
|
|
|
|
import type { Squire } from '../Editor';
|
|
|
|
|
import { Enter } from './Enter';
|
|
|
|
|
import { Backspace } from './Backspace';
|
|
|
|
|
import { Delete } from './Delete';
|
|
|
|
|
import { ShiftTab, Tab } from './Tab';
|
|
|
|
|
import { Space } from './Space';
|
2023-02-21 19:48:45 -05:00
|
|
|
|
import { rangeDoesEndAtBlockBoundary } from '../range/Block';
|
|
|
|
|
import { moveRangeBoundariesDownTree } from '../range/Boundaries';
|
2023-01-22 19:35:12 -05:00
|
|
|
|
|
|
|
|
|
// ---
|
|
|
|
|
|
|
|
|
|
const _onKey = function (this: Squire, event: KeyboardEvent): void {
|
2024-02-01 05:24:11 -05:00
|
|
|
|
// Ignore key events where event.isComposing, to stop us from blatting
|
|
|
|
|
// Kana-Kanji conversion
|
|
|
|
|
if (event.defaultPrevented || event.isComposing) {
|
2023-01-22 19:35:12 -05:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We need to apply the Backspace/delete handlers regardless of
|
|
|
|
|
// control key modifiers.
|
2023-12-19 18:52:42 -05:00
|
|
|
|
let key = event.key;
|
|
|
|
|
let modifiers = '';
|
2023-01-22 19:35:12 -05:00
|
|
|
|
if (key !== 'Backspace' && key !== 'Delete') {
|
|
|
|
|
if (event.altKey) {
|
|
|
|
|
modifiers += 'Alt-';
|
|
|
|
|
}
|
|
|
|
|
if (event.ctrlKey) {
|
|
|
|
|
modifiers += 'Ctrl-';
|
|
|
|
|
}
|
|
|
|
|
if (event.metaKey) {
|
|
|
|
|
modifiers += 'Meta-';
|
|
|
|
|
}
|
|
|
|
|
if (event.shiftKey) {
|
|
|
|
|
modifiers += 'Shift-';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// However, on Windows, Shift-Delete is apparently "cut" (WTF right?), so
|
|
|
|
|
// we want to let the browser handle Shift-Delete in this situation.
|
|
|
|
|
if (isWin && event.shiftKey && key === 'Delete') {
|
|
|
|
|
modifiers += 'Shift-';
|
|
|
|
|
}
|
|
|
|
|
key = modifiers + key;
|
|
|
|
|
|
2023-12-19 18:52:42 -05:00
|
|
|
|
const range: Range = this.getSelection();
|
2023-01-22 19:35:12 -05:00
|
|
|
|
if (this._keyHandlers[key]) {
|
|
|
|
|
this._keyHandlers[key](this, event, range);
|
|
|
|
|
} else if (
|
|
|
|
|
!range.collapsed &&
|
|
|
|
|
!event.ctrlKey &&
|
|
|
|
|
!event.metaKey &&
|
2023-12-19 18:52:42 -05:00
|
|
|
|
key.length === 1
|
2023-01-22 19:35:12 -05:00
|
|
|
|
) {
|
|
|
|
|
// Record undo checkpoint.
|
|
|
|
|
this.saveUndoState(range);
|
|
|
|
|
// Delete the selection
|
|
|
|
|
deleteContentsOfRange(range, this._root);
|
|
|
|
|
this._ensureBottomLine();
|
|
|
|
|
this.setSelection(range);
|
|
|
|
|
this._updatePath(range, true);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ---
|
|
|
|
|
|
|
|
|
|
type KeyHandler = (self: Squire, event: KeyboardEvent, range: Range) => void;
|
|
|
|
|
|
|
|
|
|
const keyHandlers: Record<string, KeyHandler> = {
|
|
|
|
|
'Backspace': Backspace,
|
|
|
|
|
'Delete': Delete,
|
|
|
|
|
'Tab': Tab,
|
|
|
|
|
'Shift-Tab': ShiftTab,
|
2023-12-19 18:52:42 -05:00
|
|
|
|
' ': Space,
|
2023-01-22 19:35:12 -05:00
|
|
|
|
'ArrowLeft'(self: Squire): void {
|
|
|
|
|
self._removeZWS();
|
|
|
|
|
},
|
2023-02-21 19:48:45 -05:00
|
|
|
|
'ArrowRight'(self: Squire, event: KeyboardEvent, range: Range): void {
|
2023-01-22 19:35:12 -05:00
|
|
|
|
self._removeZWS();
|
2023-02-21 19:48:45 -05:00
|
|
|
|
// Allow right arrow to always break out of <code> block.
|
|
|
|
|
const root = self.getRoot();
|
|
|
|
|
if (rangeDoesEndAtBlockBoundary(range, root)) {
|
|
|
|
|
moveRangeBoundariesDownTree(range);
|
|
|
|
|
let node: Node | null = range.endContainer;
|
|
|
|
|
do {
|
|
|
|
|
if (node.nodeName === 'CODE') {
|
|
|
|
|
let next = node.nextSibling;
|
|
|
|
|
if (!(next instanceof Text)) {
|
|
|
|
|
const textNode = document.createTextNode(' '); // nbsp
|
|
|
|
|
node.parentNode!.insertBefore(textNode, next);
|
|
|
|
|
next = textNode;
|
|
|
|
|
}
|
|
|
|
|
range.setStart(next, 1);
|
|
|
|
|
self.setSelection(range);
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
} while (
|
|
|
|
|
!node.nextSibling &&
|
|
|
|
|
(node = node.parentNode) &&
|
|
|
|
|
node !== root
|
|
|
|
|
);
|
|
|
|
|
}
|
2023-01-22 19:35:12 -05:00
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (!supportsInputEvents) {
|
|
|
|
|
keyHandlers.Enter = Enter;
|
|
|
|
|
keyHandlers['Shift-Enter'] = Enter;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// System standard for page up/down on Mac/iOS is to just scroll, not move the
|
|
|
|
|
// cursor. On Linux/Windows, it should move the cursor, but some browsers don't
|
|
|
|
|
// implement this natively. Override to support it.
|
|
|
|
|
if (!isMac && !isIOS) {
|
|
|
|
|
keyHandlers.PageUp = (self: Squire) => {
|
|
|
|
|
self.moveCursorToStart();
|
|
|
|
|
};
|
|
|
|
|
keyHandlers.PageDown = (self: Squire) => {
|
|
|
|
|
self.moveCursorToEnd();
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ---
|
|
|
|
|
|
|
|
|
|
const mapKeyToFormat = (
|
|
|
|
|
tag: string,
|
|
|
|
|
remove?: { tag: string } | null,
|
|
|
|
|
): KeyHandler => {
|
|
|
|
|
remove = remove || null;
|
|
|
|
|
return (self: Squire, event: Event) => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const range = self.getSelection();
|
|
|
|
|
if (self.hasFormat(tag, null, range)) {
|
|
|
|
|
self.changeFormat(null, { tag }, range);
|
|
|
|
|
} else {
|
|
|
|
|
self.changeFormat({ tag }, remove, range);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
keyHandlers[ctrlKey + 'b'] = mapKeyToFormat('B');
|
|
|
|
|
keyHandlers[ctrlKey + 'i'] = mapKeyToFormat('I');
|
|
|
|
|
keyHandlers[ctrlKey + 'u'] = mapKeyToFormat('U');
|
|
|
|
|
keyHandlers[ctrlKey + 'Shift-7'] = mapKeyToFormat('S');
|
|
|
|
|
keyHandlers[ctrlKey + 'Shift-5'] = mapKeyToFormat('SUB', { tag: 'SUP' });
|
|
|
|
|
keyHandlers[ctrlKey + 'Shift-6'] = mapKeyToFormat('SUP', { tag: 'SUB' });
|
|
|
|
|
|
|
|
|
|
keyHandlers[ctrlKey + 'Shift-8'] = (
|
|
|
|
|
self: Squire,
|
|
|
|
|
event: KeyboardEvent,
|
|
|
|
|
): void => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const path = self.getPath();
|
|
|
|
|
if (!/(?:^|>)UL/.test(path)) {
|
|
|
|
|
self.makeUnorderedList();
|
|
|
|
|
} else {
|
|
|
|
|
self.removeList();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
keyHandlers[ctrlKey + 'Shift-9'] = (
|
|
|
|
|
self: Squire,
|
|
|
|
|
event: KeyboardEvent,
|
|
|
|
|
): void => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const path = self.getPath();
|
|
|
|
|
if (!/(?:^|>)OL/.test(path)) {
|
|
|
|
|
self.makeOrderedList();
|
|
|
|
|
} else {
|
|
|
|
|
self.removeList();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
keyHandlers[ctrlKey + '['] = (self: Squire, event: KeyboardEvent): void => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const path = self.getPath();
|
|
|
|
|
if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) {
|
|
|
|
|
self.decreaseQuoteLevel();
|
|
|
|
|
} else {
|
|
|
|
|
self.decreaseListLevel();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
keyHandlers[ctrlKey + ']'] = (self: Squire, event: KeyboardEvent): void => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
const path = self.getPath();
|
|
|
|
|
if (/(?:^|>)BLOCKQUOTE/.test(path) || !/(?:^|>)[OU]L/.test(path)) {
|
|
|
|
|
self.increaseQuoteLevel();
|
|
|
|
|
} else {
|
|
|
|
|
self.increaseListLevel();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
keyHandlers[ctrlKey + 'd'] = (self: Squire, event: KeyboardEvent): void => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
self.toggleCode();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
keyHandlers[ctrlKey + 'z'] = (self: Squire, event: KeyboardEvent): void => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
self.undo();
|
|
|
|
|
};
|
|
|
|
|
keyHandlers[ctrlKey + 'y'] = keyHandlers[ctrlKey + 'Shift-z'] = (
|
|
|
|
|
self: Squire,
|
|
|
|
|
event: KeyboardEvent,
|
|
|
|
|
): void => {
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
self.redo();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export { _onKey, keyHandlers };
|