mycorrhiza/static/shortcuts.js
2021-06-13 15:15:28 +07:00

179 lines
6.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
const $ = document.querySelector.bind(document);
const $$ = (...args) => Array.prototype.slice.call(document.querySelectorAll(...args));
function keyEventToShortcut(event) {
let elideShift = event.key.toUpperCase() === event.key && event.shiftKey;
return (event.ctrlKey ? 'Ctrl+' : '') +
(event.altKey ? 'Alt+' : '') +
(event.metaKey ? 'Meta+' : '') +
(!elideShift && event.shiftKey ? 'Shift+' : '') +
(event.key === ',' ? 'Comma' : event.key === ' ' ? 'Space' : event.key);
}
function isTextField(element) {
let name = element.nodeName.toLowerCase();
return name === 'textarea' ||
name === 'select' ||
(name === 'input' && !['submit', 'reset', 'checkbox', 'radio'].includes(element.type)) ||
element.isContentEditable;
}
class ShortcutHandler {
constructor(element, filter = () => true) {
this.element = element;
this.map = {};
this.active = this.map;
this.filter = filter;
this.timeout = null;
this.handleKeyDown = this.handleKeyDown.bind(this);
this.resetActive = this.resetActive.bind(this);
this.addEventListeners();
}
addEventListeners() {
this.element.addEventListener('keydown', this.handleKeyDown);
}
add(text, action, description = null) {
let shortcuts = text.split(',').map(shortcut => shortcut.trim().split(' '));
for (let shortcut of shortcuts) {
let node = this.map;
for (let key of shortcut) {
if (!node[key]) {
node[key] = {};
}
node = node[key];
if (node.action) {
delete node.action;
delete node.shortcut;
delete node.description;
}
}
node.action = action;
node.shortcut = shortcut;
node.description = description;
}
}
handleKeyDown(event) {
if (event.defaultPrevented) return;
if (['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) return;
if (!this.filter(event)) return;
let shortcut = keyEventToShortcut(event);
if (!this.active[shortcut]) {
this.resetActive();
return;
}
this.active = this.active[shortcut];
if (this.active.action) {
this.active.action(event);
event.preventDefault();
this.resetActive();
return;
}
if (this.timeout) clearTimeout(this.timeout);
this.timeout = window.setTimeout(this.resetActive, 1500);
}
resetActive() {
this.active = this.map;
if (this.timeout) {
clearTimeout(this.timeout)
this.timeout = null;
}
}
}
function bindElementFactory(handler) {
return (shortcut, element, ...other) => {
element = typeof element === 'string' ? $(element) : element;
if (!element) return;
handler.add(shortcut, () => {
if (isTextField(element)) {
element.focus();
} else {
element.click();
}
}, ...other);
};
}
function bindLinkFactory(handler) {
return (shortcut, link, ...other) => handler.add(shortcut, () => window.location.href = link, ...other);
}
window.addEventListener('load', () => {
let notFormField = event => !(event.target instanceof Node && isTextField(event.target));
let globalShortcuts = new ShortcutHandler(document, notFormField);
// Global shortcuts
let bindElement = bindElementFactory(globalShortcuts);
let bindLink = bindLinkFactory(globalShortcuts);
// * Common shortcuts
bindElement('p, Alt+ArrowLeft', '.prevnext__prev', 'Next hypha');
bindElement('n, Alt+ArrowRight', '.prevnext__next', 'Previous hypha');
bindElement('s, Alt+ArrowTop', $$('.navi-title a').slice(1, -1).slice(-1)[0], 'Parent hypha');
bindLink('g h', '/', 'Home');
bindLink('g l', '/list/', 'List of hyphae');
bindLink('g r', '/recent-changes/', 'Recent changes');
bindElement('g u', '.header-links__entry_user .header-links__link', 'Your profiles hypha')
let headerLinks = $$('.header-links__link');
for (let i = 1; i <= headerLinks.length && i < 10; i++) {
bindElement(`g ${i}`, headerLinks[i-1], `Header link #${i}`);
}
// * Hypha shortcuts
let hyphaLinks = $$('article .wikilink');
for (let i = 1; i <= hyphaLinks.length && i < 10; i++) {
bindElement(i.toString(), hyphaLinks[i-1], `Hypha link #${i}`);
}
// Hypha editor shortcuts
if (typeof editTextarea !== 'undefined') {
let editorShortcuts = new ShortcutHandler(editTextarea);
let shortcuts = [
// Inspired by MS Word, Pages, Google Docs and Telegram desktop clients.
// And by myself, too.
// Win+Linux Mac Action Description
['Ctrl+b', 'Meta+b', wrapBold, 'Editor: Bold'],
['Ctrl+i', 'Meta+i', wrapItalic, 'Editor: Italic'],
['Ctrl+M', 'Meta+Shift+m', wrapMonospace, 'Editor: Monospaced'],
['Ctrl+I', 'Meta+Shift+i', wrapHighlighted, 'Editor: Highlight'],
['Ctrl+.', 'Meta+.', wrapLifted, 'Editor: Superscript'],
['Ctrl+Comma', 'Meta+Comma', wrapLowered, 'Editor: Subscript'],
// Strikethrough conflicts with 1Password on my machine but
// I'm probably the only Mycorrhiza user who uses 1Password.
['Ctrl+X', 'Meta+Shift+x', wrapStrikethrough, 'Editor: Strikethrough'],
['Ctrl+k', 'Meta+k', wrapLink, 'Editor: Link'],
];
let isMac = /Macintosh/.test(window.navigator.userAgent);
for (let shortcut of shortcuts) {
if (isMac) {
editorShortcuts.add(shortcut[1], ...shortcut.slice(2))
} else {
editorShortcuts.add(shortcut[0], ...shortcut.slice(2))
}
}
}
});
})();