From 1c8d1e8f7edc8e2f8e217f34393af4ac3e40a448 Mon Sep 17 00:00:00 2001 From: handlerug Date: Sun, 13 Jun 2021 23:14:18 +0700 Subject: [PATCH] Shortcuts reference It's invoked by pressing `?`, or Ctrl/Cmd + `/` in the editor. Resolves #64. --- assets/devconfig.ini | 5 - static/default.css | 79 +++++++++++++++ static/icon/x.svg | 1 + static/shortcuts.js | 234 ++++++++++++++++++++++++++++++++++++++----- 4 files changed, 291 insertions(+), 28 deletions(-) create mode 100644 static/icon/x.svg diff --git a/assets/devconfig.ini b/assets/devconfig.ini index 8321181..4a26789 100644 --- a/assets/devconfig.ini +++ b/assets/devconfig.ini @@ -17,8 +17,3 @@ FixedAuthCredentialsPath = mycocredentials.json UseRegistration = true RegistrationCredentialsPath = mycoregistration.json LimitRegistration = 3 - -[CustomScripts] -OmnipresentScripts = https://lesarbr.es/do-the-roll.js -ViewScripts = https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/components/prism-core.min.js,https://cdnjs.cloudflare.com/ajax/libs/prism/1.23.0/plugins/autoloader/prism-autoloader.min.js -EditScripts = https://example.org \ No newline at end of file diff --git a/static/default.css b/static/default.css index 8ab0fed..f01f6e8 100644 --- a/static/default.css +++ b/static/default.css @@ -303,4 +303,83 @@ mark { background: rgba(130, 80, 30, 5); color: inherit; } } } +/* handlerug: sorry but I can't write in that unique and very special way */ +/* i have to resort to the BORING way of writing CSS */ +.kbd-key { + display: inline-block; + min-width: 1.5ch; + text-align: center; +} +.dialog-wrap { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.3); + overflow-y: auto; + + padding: 0 16px; +} + +.dialog { + position: relative; + + width: 100%; + max-width: 700px; + margin: 96px auto; + padding: 24px; + + background-color: #fff; + border-radius: 4px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2); +} + +.dialog__title { + margin: 0; + font-size: 1.5em; +} + +.dialog__close-button { + position: absolute; + display: block; + top: 0; + right: 0; + margin: 16px; + padding: 8px; + border: none; + background: url(/static/icon/x.svg) no-repeat 8px 8px / 16px 16px; + width: 32px; + height: 32px; + cursor: pointer; +} + +.dialog__close-button:active { + opacity: .7; +} + +.shortcuts-group-heading { + margin: 1em 0 0.5em; + font-size: 1.2em; +} + +.shortcuts-group { + margin: 0; + padding: 0; +} + +.shortcuts-group + .shortcuts-group { + margin-top: 1.5em; +} + +.shortcut-row { + display: flex; + margin: 0.5em 0; + padding: 0; + list-style: none; +} + +.shortcut-row__description { + flex: 1; +} diff --git a/static/icon/x.svg b/static/icon/x.svg new file mode 100644 index 0000000..126c58f --- /dev/null +++ b/static/icon/x.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/static/shortcuts.js b/static/shortcuts.js index e0a4eb3..6ed468b 100644 --- a/static/shortcuts.js +++ b/static/shortcuts.js @@ -2,6 +2,8 @@ const $ = document.querySelector.bind(document); const $$ = (...args) => Array.prototype.slice.call(document.querySelectorAll(...args)); + const isMac = /Macintosh/.test(window.navigator.userAgent); + function keyEventToShortcut(event) { let elideShift = event.key.toUpperCase() === event.key && event.shiftKey; return (event.ctrlKey ? 'Ctrl+' : '') + @@ -11,6 +13,55 @@ (event.key === ',' ? 'Comma' : event.key === ' ' ? 'Space' : event.key); } + function prettifyShortcut(shortcut) { + let keys = shortcut.split('+'); + + if (isMac) { + let cmdIdx = keys.indexOf('Meta'); + if (cmdIdx !== -1 && keys.length - cmdIdx > 2) { + let tmp = keys[cmdIdx + 1]; + keys[cmdIdx + 1] = 'Meta'; + keys[cmdIdx] = tmp; + } + } + + let lastKey = keys[keys.length - 1]; + if (!keys.includes('Shift') && lastKey.toUpperCase() === lastKey && lastKey.toLowerCase() !== lastKey) { + keys.splice(keys.length - 1, 0, 'Shift'); + } + + for (let i = 0; i < keys.length; i++) { + if (isMac) { + switch (keys[i]) { + case 'Ctrl': keys[i] = '⌃'; break; + case 'Alt': keys[i] = '⌥'; break; + case 'Shift': keys[i] = '⇧'; break; + case 'Meta': keys[i] = '⌘'; break; + } + } + + switch (keys[i]) { + case 'ArrowLeft': keys[i] = '←'; break; + case 'ArrowRight': keys[i] = '→'; break; + case 'ArrowTop': keys[i] = '↑'; break; + case 'ArrowBottom': keys[i] = '↓'; break; + case 'Comma': keys[i] = ','; break; + } + + if (i === keys.length - 1 && i > 0) { + keys[i] = keys[i].toUpperCase(); + } + + switch (keys[i]) { + case ' ': keys[i] = 'Space'; break; + } + + keys[i] = `${keys[i]}`; + } + + return keys.join(isMac ? '' : ' + '); + } + function isTextField(element) { let name = element.nodeName.toLowerCase(); return name === 'textarea' || @@ -19,6 +70,11 @@ element.isContentEditable; } + let notTextField = event => !(event.target instanceof Node && isTextField(event.target)); + + let allShortcuts = []; + let shortcutsGroup = null; + class ShortcutHandler { constructor(element, filter = () => true) { this.element = element; @@ -39,6 +95,14 @@ add(text, action, description = null) { let shortcuts = text.split(',').map(shortcut => shortcut.trim().split(' ')); + if (shortcutsGroup) { + shortcutsGroup.push({ + action, + shortcut: text, + description, + }) + } + for (let shortcut of shortcuts) { let node = this.map; for (let key of shortcut) { @@ -59,6 +123,23 @@ } } + groupStart() { + shortcutsGroup = []; + } + + groupEnd() { + if (shortcutsGroup && shortcutsGroup.length) allShortcuts.push(shortcutsGroup); + shortcutsGroup = null; + } + + fakeItem(shortcut, description = null) { + let list = shortcutsGroup || allShortcuts; + list.push({ + shortcut: description ? shortcut : null, + description: description || shortcut, + }); + } + handleKeyDown(event) { if (event.defaultPrevented) return; if (['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) return; @@ -110,9 +191,104 @@ return (shortcut, link, ...other) => handler.add(shortcut, () => window.location.href = link, ...other); } + let prevActiveElement = null; + let shortcutsListDialog = null; + + function openShortcutsReference() { + if (!shortcutsListDialog) { + let wrap = document.createElement('div'); + wrap.className = 'dialog-wrap'; + shortcutsListDialog = wrap; + + let dialog = document.createElement('div'); + dialog.className = 'dialog shortcuts-modal'; + dialog.tabIndex = 0; + wrap.appendChild(dialog); + + let dialogHeader = document.createElement('div'); + dialogHeader.className = 'dialog__header'; + dialog.appendChild(dialogHeader); + + let title = document.createElement('h1'); + title.className = 'dialog__title'; + title.textContent = 'List of shortcuts'; + dialogHeader.appendChild(title); + + let closeButton = document.createElement('button'); + closeButton.className = 'dialog__close-button'; + closeButton.setAttribute('aria-label', 'Close this dialog'); + dialogHeader.appendChild(closeButton); + + for (let item of allShortcuts) { + if (item.description && !item.shortcut) { + let heading = document.createElement('h2'); + heading.className = 'shortcuts-group-heading'; + heading.textContent = item.description; + dialog.appendChild(heading); + + } else { + let list = document.createElement('ul'); + list.className = 'shortcuts-group'; + + for (let shortcut of item) { + let listItem = document.createElement('li'); + listItem.className = 'shortcut-row'; + list.appendChild(listItem); + + let descriptionColumn = document.createElement('div') + descriptionColumn.className = 'shortcut-row__description'; + descriptionColumn.textContent = shortcut.description; + listItem.appendChild(descriptionColumn); + + let shortcutColumn = document.createElement('div'); + shortcutColumn.className = 'shortcut-row__keys'; + shortcutColumn.innerHTML = shortcut.shortcut.split(',') + .map(shortcuts => shortcuts.trim().split(' ').map(prettifyShortcut).join(' ')) + .join(' or '); + listItem.appendChild(shortcutColumn); + } + + dialog.appendChild(list); + } + } + + let handleClose = (event) => { + event.preventDefault(); + event.stopPropagation(); + closeShortcutsReference(); + }; + + let dialogShortcuts = new ShortcutHandler(dialog, notTextField); + + dialogShortcuts.add('Escape', handleClose); + closeButton.addEventListener('click', handleClose); + wrap.addEventListener('click', handleClose); + + dialog.addEventListener('click', event => event.stopPropagation()); + + document.body.appendChild(wrap); + } + + document.body.overflow = 'hidden'; + shortcutsListDialog.hidden = false; + prevActiveElement = document.activeElement; + shortcutsListDialog.children[0].focus(); + } + + function closeShortcutsReference() { + if (shortcutsListDialog) { + document.body.overflow = ''; + shortcutsListDialog.hidden = true; + + if (prevActiveElement) { + prevActiveElement.focus(); + prevActiveElement = null; + } + } + } + window.addEventListener('load', () => { - let notFormField = event => !(event.target instanceof Node && isTextField(event.target)); - let globalShortcuts = new ShortcutHandler(document, notFormField); + let globalShortcuts = new ShortcutHandler(document, notTextField); // Global shortcuts @@ -120,29 +296,37 @@ let bindLink = bindLinkFactory(globalShortcuts); // * Common shortcuts + globalShortcuts.fakeItem('Common'); - 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 profile′s hypha') + globalShortcuts.groupStart(); + globalShortcuts.fakeItem('g 1 – 9', 'First 9 header links'); + 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 profile′s hypha'); + globalShortcuts.groupEnd(); 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 + globalShortcuts.groupStart(); + globalShortcuts.fakeItem('1 – 9', 'First 9 hypha′s links'); + 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'); + globalShortcuts.groupEnd(); let hyphaLinks = $$('article .wikilink'); for (let i = 1; i <= hyphaLinks.length && i < 10; i++) { bindElement(i.toString(), hyphaLinks[i-1], `Hypha link #${i}`); } + // * Meta shortcuts + + globalShortcuts.add('?', openShortcutsReference); + // Hypha editor shortcuts if (typeof editTextarea !== 'undefined') { @@ -153,20 +337,21 @@ // 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'], + ['Ctrl+b', 'Meta+b', wrapBold, 'Format: Bold'], + ['Ctrl+i', 'Meta+i', wrapItalic, 'Format: Italic'], + ['Ctrl+M', 'Meta+Shift+m', wrapMonospace, 'Format: Monospaced'], + ['Ctrl+I', 'Meta+Shift+i', wrapHighlighted, 'Format: Highlight'], + ['Ctrl+.', 'Meta+.', wrapLifted, 'Format: Superscript'], + ['Ctrl+Comma', 'Meta+Comma', wrapLowered, 'Format: 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'], + // I'm probably the only Mycorrhiza user who uses 1Password. -handlerug + ['Ctrl+X', 'Meta+Shift+x', wrapStrikethrough, 'Format: Strikethrough'], + ['Ctrl+k', 'Meta+k', wrapLink, 'Format: Link'], ]; - let isMac = /Macintosh/.test(window.navigator.userAgent); + editorShortcuts.fakeItem('Editor'); + editorShortcuts.groupStart(); for (let shortcut of shortcuts) { if (isMac) { editorShortcuts.add(shortcut[1], ...shortcut.slice(2)) @@ -174,6 +359,9 @@ editorShortcuts.add(shortcut[0], ...shortcut.slice(2)) } } + editorShortcuts.groupEnd(); + + editorShortcuts.add(isMac ? 'Meta+/' : 'Ctrl+/', openShortcutsReference); } }); -})(); \ No newline at end of file +})();