1
0
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:
handlerug 2021-06-13 23:14:18 +07:00
parent 83574105f5
commit 1c8d1e8f7e
No known key found for this signature in database
GPG Key ID: 38009F0605051491
4 changed files with 291 additions and 28 deletions

View File

@ -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

View File

@ -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
View 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

View File

@ -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 profiles 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 profiles 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 hyphas 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);
}
});
})();
})();