mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-01-21 07:46:52 +00:00
Shortcuts reference
It's invoked by pressing `?`, or Ctrl/Cmd + `/` in the editor. Resolves #64.
This commit is contained in:
parent
83574105f5
commit
1c8d1e8f7e
@ -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
|
@ -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;
|
||||
}
|
||||
|
1
static/icon/x.svg
Normal file
1
static/icon/x.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke="#656565"><path d="M 1 1 L 15 15 M 15 1 L 1 15" stroke-width="2"/></svg>
|
After Width: | Height: | Size: 139 B |
@ -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] = `<span class="kbd-key">${keys[i]}</span>`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
});
|
||||
})();
|
||||
})();
|
||||
|
Loading…
Reference in New Issue
Block a user