0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-22 15:23:29 -05:00
Squire/source/keyboard/KeyHandlers.ts
Neil Jenkins d2e211796b Fix undo shortcut sometimes not working
Same deal - does the Shift modifier also modify the character? Just
allow both.

Fixes #457.
2024-03-04 11:22:10 +11:00

225 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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';
import { rangeDoesEndAtBlockBoundary } from '../range/Block';
import { moveRangeBoundariesDownTree } from '../range/Boundaries';
// ---
const _onKey = function (this: Squire, event: KeyboardEvent): void {
// Ignore key events where event.isComposing, to stop us from blatting
// Kana-Kanji conversion
if (event.defaultPrevented || event.isComposing) {
return;
}
// We need to apply the Backspace/delete handlers regardless of
// control key modifiers.
let key = event.key;
let modifiers = '';
const code = event.code;
// If pressing a number key + Shift, make sure we handle it as the number
// key and not whatever different character the shift might turn it into.
if (/^Digit\d$/.test(code)) {
key = code.slice(-1);
}
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;
const range: Range = this.getSelection();
if (this._keyHandlers[key]) {
this._keyHandlers[key](this, event, range);
} else if (
!range.collapsed &&
!event.ctrlKey &&
!event.metaKey &&
key.length === 1
) {
// 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,
' ': Space,
'ArrowLeft'(self: Squire): void {
self._removeZWS();
},
'ArrowRight'(self: Squire, event: KeyboardEvent, range: Range): void {
self._removeZWS();
// 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
);
}
},
};
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'] =
// Depending on platform, the Shift may cause the key to come through as
// upper case, but sometimes not. Just add both as shortcuts — the browser
// will only ever fire one or the other.
keyHandlers[ctrlKey + 'Shift-z'] =
keyHandlers[ctrlKey + 'Shift-Z'] =
(self: Squire, event: KeyboardEvent): void => {
event.preventDefault();
self.redo();
};
export { _onKey, keyHandlers };