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
+})();