0
Fork 0
mirror of https://github.com/fastmail/Squire.git synced 2024-12-31 11:54:03 -05:00
Squire/source/keyboard/KeyHandlers.ts
Neil Jenkins 46bc36861e Fix Shift-number shortcuts don't work on windows
The perennial issue of should keyboard shortcuts be about the character
or the key!
2024-03-04 10:52:07 +11:00

222 lines
6.5 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'] = keyHandlers[ctrlKey + 'Shift-z'] = (
self: Squire,
event: KeyboardEvent,
): void => {
event.preventDefault();
self.redo();
};
export { _onKey, keyHandlers };