mirror of
https://github.com/osmarks/mycorrhiza.git
synced 2025-04-27 21:13:10 +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
|
UseRegistration = true
|
||||||
RegistrationCredentialsPath = mycoregistration.json
|
RegistrationCredentialsPath = mycoregistration.json
|
||||||
LimitRegistration = 3
|
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 $ = document.querySelector.bind(document);
|
||||||
const $$ = (...args) => Array.prototype.slice.call(document.querySelectorAll(...args));
|
const $$ = (...args) => Array.prototype.slice.call(document.querySelectorAll(...args));
|
||||||
|
|
||||||
|
const isMac = /Macintosh/.test(window.navigator.userAgent);
|
||||||
|
|
||||||
function keyEventToShortcut(event) {
|
function keyEventToShortcut(event) {
|
||||||
let elideShift = event.key.toUpperCase() === event.key && event.shiftKey;
|
let elideShift = event.key.toUpperCase() === event.key && event.shiftKey;
|
||||||
return (event.ctrlKey ? 'Ctrl+' : '') +
|
return (event.ctrlKey ? 'Ctrl+' : '') +
|
||||||
@ -11,6 +13,55 @@
|
|||||||
(event.key === ',' ? 'Comma' : event.key === ' ' ? 'Space' : event.key);
|
(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) {
|
function isTextField(element) {
|
||||||
let name = element.nodeName.toLowerCase();
|
let name = element.nodeName.toLowerCase();
|
||||||
return name === 'textarea' ||
|
return name === 'textarea' ||
|
||||||
@ -19,6 +70,11 @@
|
|||||||
element.isContentEditable;
|
element.isContentEditable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let notTextField = event => !(event.target instanceof Node && isTextField(event.target));
|
||||||
|
|
||||||
|
let allShortcuts = [];
|
||||||
|
let shortcutsGroup = null;
|
||||||
|
|
||||||
class ShortcutHandler {
|
class ShortcutHandler {
|
||||||
constructor(element, filter = () => true) {
|
constructor(element, filter = () => true) {
|
||||||
this.element = element;
|
this.element = element;
|
||||||
@ -39,6 +95,14 @@
|
|||||||
add(text, action, description = null) {
|
add(text, action, description = null) {
|
||||||
let shortcuts = text.split(',').map(shortcut => shortcut.trim().split(' '));
|
let shortcuts = text.split(',').map(shortcut => shortcut.trim().split(' '));
|
||||||
|
|
||||||
|
if (shortcutsGroup) {
|
||||||
|
shortcutsGroup.push({
|
||||||
|
action,
|
||||||
|
shortcut: text,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
for (let shortcut of shortcuts) {
|
for (let shortcut of shortcuts) {
|
||||||
let node = this.map;
|
let node = this.map;
|
||||||
for (let key of shortcut) {
|
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) {
|
handleKeyDown(event) {
|
||||||
if (event.defaultPrevented) return;
|
if (event.defaultPrevented) return;
|
||||||
if (['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) 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);
|
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', () => {
|
window.addEventListener('load', () => {
|
||||||
let notFormField = event => !(event.target instanceof Node && isTextField(event.target));
|
let globalShortcuts = new ShortcutHandler(document, notTextField);
|
||||||
let globalShortcuts = new ShortcutHandler(document, notFormField);
|
|
||||||
|
|
||||||
// Global shortcuts
|
// Global shortcuts
|
||||||
|
|
||||||
@ -120,29 +296,37 @@
|
|||||||
let bindLink = bindLinkFactory(globalShortcuts);
|
let bindLink = bindLinkFactory(globalShortcuts);
|
||||||
|
|
||||||
// * Common shortcuts
|
// * Common shortcuts
|
||||||
|
globalShortcuts.fakeItem('Common');
|
||||||
|
|
||||||
bindElement('p, Alt+ArrowLeft', '.prevnext__prev', 'Next hypha');
|
globalShortcuts.groupStart();
|
||||||
bindElement('n, Alt+ArrowRight', '.prevnext__next', 'Previous hypha');
|
globalShortcuts.fakeItem('g 1 – 9', 'First 9 header links');
|
||||||
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 h', '/', 'Home');
|
bindLink('g r', '/recent-changes/', 'Recent changes');
|
||||||
bindLink('g l', '/list/', 'List of hyphae');
|
bindElement('g u', '.header-links__entry_user .header-links__link', 'Your profile′s hypha');
|
||||||
bindLink('g r', '/recent-changes/', 'Recent changes');
|
globalShortcuts.groupEnd();
|
||||||
|
|
||||||
bindElement('g u', '.header-links__entry_user .header-links__link', 'Your profile′s hypha')
|
|
||||||
|
|
||||||
let headerLinks = $$('.header-links__link');
|
let headerLinks = $$('.header-links__link');
|
||||||
for (let i = 1; i <= headerLinks.length && i < 10; i++) {
|
for (let i = 1; i <= headerLinks.length && i < 10; i++) {
|
||||||
bindElement(`g ${i}`, headerLinks[i-1], `Header link #${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');
|
let hyphaLinks = $$('article .wikilink');
|
||||||
for (let i = 1; i <= hyphaLinks.length && i < 10; i++) {
|
for (let i = 1; i <= hyphaLinks.length && i < 10; i++) {
|
||||||
bindElement(i.toString(), hyphaLinks[i-1], `Hypha link #${i}`);
|
bindElement(i.toString(), hyphaLinks[i-1], `Hypha link #${i}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// * Meta shortcuts
|
||||||
|
|
||||||
|
globalShortcuts.add('?', openShortcutsReference);
|
||||||
|
|
||||||
// Hypha editor shortcuts
|
// Hypha editor shortcuts
|
||||||
|
|
||||||
if (typeof editTextarea !== 'undefined') {
|
if (typeof editTextarea !== 'undefined') {
|
||||||
@ -153,20 +337,21 @@
|
|||||||
// And by myself, too.
|
// And by myself, too.
|
||||||
|
|
||||||
// Win+Linux Mac Action Description
|
// Win+Linux Mac Action Description
|
||||||
['Ctrl+b', 'Meta+b', wrapBold, 'Editor: Bold'],
|
['Ctrl+b', 'Meta+b', wrapBold, 'Format: Bold'],
|
||||||
['Ctrl+i', 'Meta+i', wrapItalic, 'Editor: Italic'],
|
['Ctrl+i', 'Meta+i', wrapItalic, 'Format: Italic'],
|
||||||
['Ctrl+M', 'Meta+Shift+m', wrapMonospace, 'Editor: Monospaced'],
|
['Ctrl+M', 'Meta+Shift+m', wrapMonospace, 'Format: Monospaced'],
|
||||||
['Ctrl+I', 'Meta+Shift+i', wrapHighlighted, 'Editor: Highlight'],
|
['Ctrl+I', 'Meta+Shift+i', wrapHighlighted, 'Format: Highlight'],
|
||||||
['Ctrl+.', 'Meta+.', wrapLifted, 'Editor: Superscript'],
|
['Ctrl+.', 'Meta+.', wrapLifted, 'Format: Superscript'],
|
||||||
['Ctrl+Comma', 'Meta+Comma', wrapLowered, 'Editor: Subscript'],
|
['Ctrl+Comma', 'Meta+Comma', wrapLowered, 'Format: Subscript'],
|
||||||
// Strikethrough conflicts with 1Password on my machine but
|
// Strikethrough conflicts with 1Password on my machine but
|
||||||
// I'm probably the only Mycorrhiza user who uses 1Password.
|
// I'm probably the only Mycorrhiza user who uses 1Password. -handlerug
|
||||||
['Ctrl+X', 'Meta+Shift+x', wrapStrikethrough, 'Editor: Strikethrough'],
|
['Ctrl+X', 'Meta+Shift+x', wrapStrikethrough, 'Format: Strikethrough'],
|
||||||
['Ctrl+k', 'Meta+k', wrapLink, 'Editor: Link'],
|
['Ctrl+k', 'Meta+k', wrapLink, 'Format: Link'],
|
||||||
];
|
];
|
||||||
|
|
||||||
let isMac = /Macintosh/.test(window.navigator.userAgent);
|
editorShortcuts.fakeItem('Editor');
|
||||||
|
|
||||||
|
editorShortcuts.groupStart();
|
||||||
for (let shortcut of shortcuts) {
|
for (let shortcut of shortcuts) {
|
||||||
if (isMac) {
|
if (isMac) {
|
||||||
editorShortcuts.add(shortcut[1], ...shortcut.slice(2))
|
editorShortcuts.add(shortcut[1], ...shortcut.slice(2))
|
||||||
@ -174,6 +359,9 @@
|
|||||||
editorShortcuts.add(shortcut[0], ...shortcut.slice(2))
|
editorShortcuts.add(shortcut[0], ...shortcut.slice(2))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
editorShortcuts.groupEnd();
|
||||||
|
|
||||||
|
editorShortcuts.add(isMac ? 'Meta+/' : 'Ctrl+/', openShortcutsReference);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
})();
|
})();
|
Loading…
x
Reference in New Issue
Block a user