1
0
mirror of https://github.com/osmarks/mycorrhiza.git synced 2024-12-04 18:19:54 +00:00

Refactor shortcuts

A selection of great commit messages:

- "Safari is a great browser!" -- No one

  If in doubt, just stick overflow: hidden onto it. No, seriously, I
  just added it in Safari devtools and it worked. You'd think Apple has
  a competent team, right?

- Bully Safari into not triggering the Show status bar menu item

  If in doubt, just bully Safari. What is Safari gonna do? Tell Steve
  Jobs?
This commit is contained in:
Umar Getagazov 2022-08-20 22:57:34 +03:00
parent 4831e4c7af
commit 738948f752
5 changed files with 299 additions and 324 deletions

27
static/common.js Normal file
View File

@ -0,0 +1,27 @@
const $ = document.querySelector.bind(document)
const $$ = (...args) => Array.prototype.slice.call(document.querySelectorAll(...args))
const isMac = /Macintosh/.test(window.navigator.userAgent)
const arrToStr = a => Array.isArray(a) ? a.join('') : a
const strToArr = a => Array.isArray(a) ? a : [a]
const rrh = {
html(s, ...parts) {
s = s.reduce((acc, cur, i) => (`${acc}${cur}${parts[i] ? arrToStr(parts[i]) : ''}`), '')
const wrapper = document.createElement('div')
wrapper.innerHTML = s
return wrapper.children[0]
},
l10nMap: {},
l10n(text, translations) {
// Choose the translation on load to be consistent with the
// server-rendered interface.
if (translations) {
translations.en = text
this.l10nMap[text] = translations[navigator.languages
.map(lang => lang.split('-')[0])
.find(lang => translations[lang])] || text
}
return this.l10nMap[text] || text
},
}

View File

@ -309,7 +309,7 @@ kbd {
top: 0; top: 0;
left: 50%; left: 50%;
width: 100%; width: 100%;
max-width: 700px; max-width: 800px;
margin: 96px auto; margin: 96px auto;
padding: 24px; padding: 24px;
transform: translate(-50%, 0); transform: translate(-50%, 0);
@ -357,9 +357,16 @@ kbd {
} }
.shortcuts-help .dialog__content { .shortcuts-help .dialog__content {
display: grid; columns: 300px 2;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); column-gap: 32px;
grid-column-gap: 32px; overflow: hidden;
}
.shortcuts-group {
/* Don't break a shortcut group between columns */
break-inside: avoid;
page-break-inside: avoid;
overflow: hidden;
} }
.shortcuts-group-heading { .shortcuts-group-heading {

View File

@ -1,369 +1,321 @@
const $ = document.querySelector.bind(document); rrh.l10n('List of shortcuts', { ru: 'Горячие клавиши' })
const $$ = (...args) => Array.prototype.slice.call(document.querySelectorAll(...args)); rrh.l10n('Close this dialog', { ru: 'Закрыть диалог' })
const isMac = /Macintosh/.test(window.navigator.userAgent); rrh.l10n('Common', { ru: 'Общее' })
rrh.l10n('Home', { ru: 'Главная' })
rrh.l10n('Hypha', { ru: 'Гифа' })
rrh.l10n('Editor', { ru: 'Редактор' })
rrh.l10n('Format', { ru: 'Форматирование' })
rrh.shortcuts = {
// map is the whole shortcut graph. active points to the node of the
// shortcut graph that's currently "selected". When the user presses a
// key, the code finds the key node in the active subgraph and sets
// active to the found node. On each key press we get a narrower view
// until we reach a leaf and execute the action. View this property in
// the JavaScript console of your browser for easier understanding.
map: {},
groups: [],
addGroup(group) {
this.groups.push(group)
},
addBindingToGroup(name, binding) {
let group = this.groups.find(group => group.name === name)
if (!group) {
console.warn('Shortcut group', name, 'not found')
return
}
group.bind(binding)
},
_register(shortcuts, action, other = {}) {
// shortcuts looks like this: ['g r', 'Ctrl+r']
// Every item of shortcuts is a sequential key chord.
for (let chord of strToArr(shortcuts)) {
let leaf = this.map
let keys = chord.trim().split(' ')
for (let key of keys) {
// If there's no existing edge, create one
if (!leaf[key]) leaf[key] = {}
leaf = leaf[key]
if (leaf.action) throw new Error(`Shortcut ${chord} already exists`)
}
// Now we've traversed to the leaf. Bind the shortcut
leaf.action = action
Object.assign(leaf, other)
}
},
_handleKeyDown(event) {
if (event.defaultPrevented) return
if (['Control', 'Alt', 'Shift', 'Meta'].includes(event.key)) return
if ((!event.ctrlKey && !event.metaKey && !event.shiftKey && !event.altKey) &&
event.target instanceof Node && isTextField(event.target)) return
let shortcut = keyEventToShortcut(event)
if (!this.active[shortcut]) {
this._resetActive()
return
}
this.active = this.active[shortcut]
if (this.active.action && (!this.active.element || event.target === this.active.element)) {
event.stopPropagation()
if (this.active.force) event.preventDefault()
this.active.action(event)
this._resetActive()
return
}
if (this.timeout) clearTimeout(this.timeout)
this.timeout = window.setTimeout(() => this._resetActive(), 1500)
},
_resetActive() {
this.active = this.map
if (this.timeout) {
clearTimeout(this.timeout)
this.timeout = null
}
},
}
window.addEventListener('keydown', event => rrh.shortcuts._handleKeyDown(event))
rrh.shortcuts._resetActive()
// Convert a KeyboardEvent into a shortcut string for matching by
// ShortcutHandler.
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+' : '') +
(event.altKey ? 'Alt+' : '') + (event.altKey ? 'Alt+' : '') +
(event.metaKey ? 'Meta+' : '') + (event.metaKey ? 'Meta+' : '') +
(!elideShift && event.shiftKey ? 'Shift+' : '') + (!elideShift && event.shiftKey ? 'Shift+' : '') +
(event.key === ',' ? 'Comma' : event.key === ' ' ? 'Space' : event.key); (event.key === ',' ? 'Comma' : event.key === ' ' ? 'Space' : event.key)
} }
// Prettify the shortcut string by replacing modifiers and arrow codes with
// Unicode symbol for presentation to the user.
function prettifyShortcut(shortcut) { function prettifyShortcut(shortcut) {
let keys = shortcut.split('+'); let keys = shortcut.split('+')
if (isMac) { if (isMac) {
let cmdIdx = keys.indexOf('Meta'); let cmdIdx = keys.indexOf('Meta')
if (cmdIdx !== -1 && keys.length - cmdIdx > 2) { if (cmdIdx !== -1 && keys.length - cmdIdx > 2) {
let tmp = keys[cmdIdx + 1]; let tmp = keys[cmdIdx + 1]
keys[cmdIdx + 1] = 'Meta'; keys[cmdIdx + 1] = 'Meta'
keys[cmdIdx] = tmp; keys[cmdIdx] = tmp
} }
} }
let lastKey = keys[keys.length - 1]; // Add Shift into shortcut strings like Ctrl+L
let lastKey = keys[keys.length - 1]
if (!keys.includes('Shift') && lastKey.toUpperCase() === lastKey && lastKey.toLowerCase() !== lastKey) { if (!keys.includes('Shift') && lastKey.toUpperCase() === lastKey && lastKey.toLowerCase() !== lastKey) {
keys.splice(keys.length - 1, 0, 'Shift'); keys.splice(keys.length - 1, 0, 'Shift')
} }
for (let i = 0; i < keys.length; i++) { for (let i = 0; i < keys.length; i++) {
if (isMac) { if (isMac) {
switch (keys[i]) { if (keys[i] === 'Ctrl') keys[i] = '⌃'
case 'Ctrl': keys[i] = '⌃'; break; if (keys[i] === 'Alt') keys[i] = '⌥'
case 'Alt': keys[i] = '⌥'; break; if (keys[i] === 'Shift') keys[i] = '⇧'
case 'Shift': keys[i] = '⇧'; break; if (keys[i] === 'Meta') keys[i] = '⌘'
case 'Meta': keys[i] = '⌘'; break; } else {
} if (keys[i] === 'Meta') keys[i] = 'Win'
} }
if (i === keys.length - 1 && i > 0 && keys[i].length === 1) { if (i === keys.length - 1 && i > 0 && keys[i].length === 1) {
keys[i] = keys[i].toUpperCase(); // Make every key uppercase. This does not introduce any ambiguous
// cases because we insert a Shift modifier for upper-case keys
// earlier.
keys[i] = keys[i].toUpperCase()
} }
switch (keys[i]) { if (keys[i] === 'ArrowLeft') keys[i] = '←'
case 'ArrowLeft': keys[i] = '←'; break; if (keys[i] === 'ArrowRight') keys[i] = '→'
case 'ArrowRight': keys[i] = '→'; break; if (keys[i] === 'ArrowUp') keys[i] = '↑'
case 'ArrowUp': keys[i] = '↑'; break; if (keys[i] === 'ArrowDown') keys[i] = '↓'
case 'ArrowDown': keys[i] = '↓'; break; if (keys[i] === 'Comma') keys[i] = ','
case 'Comma': keys[i] = ','; break; if (keys[i] === 'Enter') keys[i] = '↩'
case 'Enter': keys[i] = '↩'; break; if (keys[i] === ' ') keys[i] = 'Space'
case ' ': keys[i] = 'Space'; break; keys[i] = `<kbd>${keys[i]}</kbd>`
}
keys[i] = `<kbd>${keys[i]}</kbd>`;
} }
return keys.join(isMac ? '' : ' + '); 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' ||
name === 'select' || name === 'select' ||
(name === 'input' && !['submit', 'reset', 'checkbox', 'radio'].includes(element.type)) || (name === 'input' && !['submit', 'reset', 'checkbox', 'radio'].includes(element.type)) ||
element.isContentEditable; element.isContentEditable
} }
let notTextField = event => !(event.target instanceof Node && isTextField(event.target)); class Shortcut {
constructor(shortcuts, target, description, other) {
this.shortcuts = shortcuts
this.target = target
this.description = rrh.l10n(description)
Object.assign(this, other)
}
}
let allShortcuts = []; class ShortcutGroup {
let shortcutsGroup = null; constructor(name, element = null, bindings = []) {
this.name = rrh.l10n(name)
class ShortcutHandler { this.shortcuts = []
constructor(element, override, filter = () => true) { this.element = element
this.element = element; bindings.forEach(binding => this.bind(binding))
this.map = {};
this.active = this.map;
this.override = override;
this.filter = filter;
this.timeout = null;
this.handleKeyDown = this.handleKeyDown.bind(this);
this.resetActive = this.resetActive.bind(this);
this.addEventListeners();
} }
addEventListeners() { bind({ shortcuts, target, description, ...other }) {
this.element.addEventListener('keydown', this.handleKeyDown); shortcuts = strToArr(shortcuts).map(s => s.trim())
} if (!other.element) other.element = this.element
if (target instanceof Function) {
add(text, action, description = null, shownInHelp = true) { this.shortcuts.push({ shortcuts, description })
let shortcuts = text.trim().split(',').map(shortcut => shortcut.trim().split(' ')); rrh.shortcuts._register(shortcuts, target, other)
} else if (target instanceof Node) {
if (shortcutsGroup && shownInHelp) { this.bind({
shortcutsGroup.push({ shortcuts,
action, target: () => {
shortcut: text, if (isTextField(target)) target.focus()
target.click()
},
description, ...other,
})
} else if (Array.isArray(target) && (target.length === 0 || target[0] instanceof Node)) {
this.shortcuts.push({
shortcuts: shortcuts.map(s => `${s} 1 — 9`),
description, description,
}) })
} for (let i = 0; i < target.length && i < 9; i++) {
let element = target[i]
for (let shortcut of shortcuts) { rrh.shortcuts._register(shortcuts.map(s => `${s} ${i + 1}`), () => {
let node = this.map; if (isTextField(element)) element.focus()
for (let key of shortcut) { else element.click()
if (!node[key]) { }, other)
node[key] = {};
}
node = node[key];
if (node.action) {
delete node.action;
delete node.shortcut;
delete node.description;
}
} }
} else if (typeof target === 'string') {
node.action = action; this.bind({
node.shortcut = shortcut; shortcuts,
node.description = description; target: () => window.location.href = target,
} description,
} ...other,
})
group(...args) { } else if (target !== undefined && target !== null) {
if (typeof args[0] === 'string') this.fakeItem(args.shift()); throw new Error('Invalid target type')
shortcutsGroup = [];
args[0].bind(this)();
if (shortcutsGroup && shortcutsGroup.length) allShortcuts.push(shortcutsGroup);
shortcutsGroup = null;
}
bindElement(shortcut, element, ...other) {
element = typeof element === 'string' ? $(element) : element;
if (!element) return;
this.add(shortcut, () => {
if (isTextField(element)) {
element.focus();
} else {
element.click();
}
}, ...other);
}
bindLink(shortcut, link, ...other) {
this.add(shortcut, () => window.location.href = link, ...other);
}
bindCollection(prefix, elements, collectionDescription, itemDescription) {
this.fakeItem(prefix + ' 1 9', collectionDescription);
if (typeof elements === 'string') {
elements = $$(elements);
} else if (Array.isArray(elements)) {
elements = elements.map(el => typeof el === 'string' ? $(el) : el);
}
for (let i = 1; i <= elements.length && i < 10; i++) {
this.bindElement(`${prefix} ${i}`, elements[i - 1], `${itemDescription} #${i}`, false);
}
}
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;
if (!this.filter(event)) return;
let shortcut = keyEventToShortcut(event);
if (!this.active[shortcut]) {
this.resetActive();
return;
}
this.active = this.active[shortcut];
if (this.active.action) {
event.stopPropagation();
this.active.action(event);
if (this.override) event.preventDefault();
this.resetActive();
return;
}
if (this.timeout) clearTimeout(this.timeout);
this.timeout = window.setTimeout(this.resetActive, 1500);
}
resetActive() {
this.active = this.map;
if (this.timeout) {
clearTimeout(this.timeout)
this.timeout = null;
} }
} }
} }
class ShortcutsHelpDialog { function openHelp() {
constructor() { if ($('.shortcuts-help')) return
let template = $('#dialog-template');
let clonedTemplate = template.content.cloneNode(true);
this.backdrop = clonedTemplate.children[0];
this.dialog = clonedTemplate.children[1];
this.dialog.classList.add('shortcuts-help'); document.body.overflow = 'hidden'
this.dialog.hidden = true; let prevActiveElement = document.activeElement
this.backdrop.hidden = true;
document.body.appendChild(this.backdrop); let backdrop = rrh.html`<div class="dialog-backdrop"></div>`
document.body.appendChild(this.dialog); backdrop.onclick = close
document.body.appendChild(backdrop)
this.close = this.close.bind(this); let dialog = rrh.html`
<div class="dialog shortcuts-help" tabindex="0">
<div class="dialog__header">
<h1 class="dialog__title">${rrh.l10n('List of shortcuts')}</h1>
<button class="dialog__close-button" aria-label="${rrh.l10n('Close this dialog')}"></button>
</div>
<div class="dialog__content"></div>
</div>
`
dialog.querySelector('.dialog__close-button').onclick = close
dialog.onkeydown = event => {
if (event.key === 'Escape') close()
}
document.body.appendChild(dialog)
dialog.focus()
this.dialog.querySelector('.dialog__title').textContent = 'List of shortcuts'; function close() {
this.dialog.querySelector('.dialog__close-button').addEventListener('click', this.close); document.body.overflow = ''
this.backdrop.addEventListener('click', this.close); document.body.removeChild(backdrop)
document.body.removeChild(dialog)
this.shortcuts = new ShortcutHandler(this.dialog, false); if (prevActiveElement) prevActiveElement.focus()
this.shortcuts.add('Escape', this.close, null, false);
let shortcutsGroup;
let shortcutsGroupTemplate = document.createElement('div');
shortcutsGroupTemplate.className = 'shortcuts-group';
for (let item of allShortcuts) {
if (item.description && !item.shortcut) {
shortcutsGroup = shortcutsGroupTemplate.cloneNode();
this.dialog.querySelector('.dialog__content').appendChild(shortcutsGroup);
let heading = document.createElement('h2');
heading.className = 'shortcuts-group-heading';
heading.textContent = item.description;
shortcutsGroup.appendChild(heading);
} else {
let list = document.createElement('ul');
list.className = 'shortcuts-list';
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(' <span class="kbd-or">or</span> ');
listItem.appendChild(shortcutColumn);
}
if (shortcutsGroup) {
shortcutsGroup.appendChild(list);
}
}
}
} }
open() { function formatShortcuts(shortcuts) {
this.prevActiveElement = document.activeElement; return shortcuts.map(s => s.split(' ')
.map(prettifyShortcut).join(' '))
document.body.overflow = 'hidden'; .join(' <span class="kbd-or">or</span> ')
this.backdrop.hidden = false;
this.dialog.hidden = false;
this.dialog.focus();
} }
close() { for (let group of rrh.shortcuts.groups) {
document.body.overflow = ''; if (group.shortcuts.length === 0) continue
this.backdrop.hidden = true; dialog.querySelector('.dialog__content').appendChild(rrh.html`
this.dialog.hidden = true; <div class="shortcuts-group">
<h2 class="shortcuts-group-heading">${group.name}</h2>
if (this.prevActiveElement) { <ul class="shortcuts-list">
this.prevActiveElement.focus(); ${group.shortcuts.map(({ description, shortcuts }) => `
this.prevActiveElement = null; <li class="shortcut-row">
} <div class="shortcut-row__description">${description}</div>
<div class="shortcut-row__keys">
${formatShortcuts(shortcuts)}
</div>
</li>
`)}
</ul>
</div>
`)
} }
} }
window.addEventListener('load', () => { rrh.shortcuts.addGroup(new ShortcutGroup('Common', null, [
let helpDialog = null; new Shortcut('g', $$('.top-bar__highlight-link'), 'First 9 header links'),
let openHelp = () => { new Shortcut('g h', '/', 'Home'),
if (!helpDialog) helpDialog = new ShortcutsHelpDialog(); new Shortcut('g l', '/list/', 'List of hyphae'),
helpDialog.open(); new Shortcut('g r', '/recent-changes/', 'Recent changes'),
}; new Shortcut('g u', $('.auth-links__user-link'), 'Your profiles hypha'),
new Shortcut(['?', isMac ? 'Meta+/' : 'Ctrl+/'], openHelp, 'Shortcut help', { force: true }),
]))
let onEditPage = typeof editTextarea !== 'undefined'; if (document.body.dataset.rrhAddr.startsWith('/hypha')) {
rrh.shortcuts.addGroup(new ShortcutGroup('Hypha', null, [
new Shortcut('', $$('article .wikilink'), 'First 9 hyphas links'),
new Shortcut(['p', 'Alt+ArrowLeft', 'Ctrl+Alt+ArrowLeft'], $('.prevnext__prev'), 'Next hypha'),
new Shortcut(['n', 'Alt+ArrowRight', 'Ctrl+Alt+ArrowRight'], $('.prevnext__next'), 'Previous hypha'),
new Shortcut(['s', 'Alt+ArrowUp', 'Ctrl+Alt+ArrowUp'], $$('.navi-title a').slice(1, -1).slice(-1)[0], 'Parent hypha'),
new Shortcut(['c', 'Alt+ArrowDown', 'Ctrl+Alt+ArrowDown'], $('.subhyphae__link'), 'First child hypha'),
new Shortcut(['e', isMac ? 'Meta+Enter' : 'Ctrl+Enter'], $('.btn__link_navititle[href^="/edit/"]'), 'Edit this hypha'),
new Shortcut('v', $('.hypha-info__link[href^="/hypha/"]'), 'Go to hyphas page'),
new Shortcut('a', $('.hypha-info__link[href^="/media/"]'), 'Go to media management'),
new Shortcut('h', $('.hypha-info__link[href^="/history/"]'), 'Go to history'),
new Shortcut('r', $('.hypha-info__link[href^="/rename/"]'), 'Rename this hypha'),
new Shortcut('b', $('.hypha-info__link[href^="/backlinks/"]'), 'Backlinks'),
]))
}
// Global shortcuts work everywhere. if (document.body.dataset.rrhAddr.startsWith('/edit')) {
let globalShortcuts = new ShortcutHandler(document, false); rrh.shortcuts.addGroup(new ShortcutGroup('Editor', null, [
globalShortcuts.add(isMac ? 'Meta+/' : 'Ctrl+/', openHelp); new Shortcut(isMac ? 'Meta+Enter' : 'Ctrl+Enter', $('.edit-form__save'), 'Save changes'),
new Shortcut(isMac ? 'Meta+Shift+Enter' : 'Ctrl+Shift+Enter', $('.edit-form__preview'), 'Preview changes'),
]))
// Page shortcuts work everywhere except on text fields. if (editTextarea) {
let pageShortcuts = new ShortcutHandler(document, false, notTextField); rrh.shortcuts.addGroup(new ShortcutGroup('Format', null, [
pageShortcuts.add('?', openHelp, null, false); new Shortcut(isMac ? 'Meta+b' : 'Ctrl+b', wrapBold, 'Bold', { force: true }),
new Shortcut(isMac ? 'Meta+i' : 'Ctrl+i', wrapItalic, 'Italic', { force: true }),
// Common shortcuts new Shortcut(isMac ? 'Meta+Shift+m' : 'Ctrl+M', wrapMonospace, 'Monospaced', { force: true }),
pageShortcuts.group('Common', function () { new Shortcut(isMac ? 'Meta+Shift+i' : 'Ctrl+I', wrapHighlighted, 'Highlight', { force: true }),
this.bindCollection('g', '.top-bar__highlight-link', 'First 9 header links', 'Header link'); new Shortcut(isMac ? 'Meta+.' : 'Ctrl+.', wrapLifted, 'Superscript', { force: true }),
this.bindLink('g h', '/', 'Home'); new Shortcut(isMac ? 'Meta+Comma' : 'Ctrl+Comma', wrapLowered, 'Subscript', { force: true }),
this.bindLink('g l', '/list/', 'List of hyphae'); new Shortcut(isMac ? 'Meta+Shift+x' : 'Ctrl+X', wrapStrikethrough, 'Strikethrough', { force: true }),
this.bindLink('g r', '/recent-changes/', 'Recent changes'); new Shortcut(isMac ? 'Meta+k' : 'Ctrl+k', wrapLink, 'Inline link', { force: true }),
this.bindElement('g u', '.auth-links__user-link', 'Your profiles hypha');
});
if (!onEditPage) {
// Hypha shortcuts
pageShortcuts.group('Hypha', function () {
this.bindCollection('', 'article .wikilink', 'First 9 hyphas links', 'Hypha link');
this.bindElement('p, Alt+ArrowLeft, Ctrl+Alt+ArrowLeft', '.prevnext__prev', 'Next hypha');
this.bindElement('n, Alt+ArrowRight, Ctrl+Alt+ArrowRight', '.prevnext__next', 'Previous hypha');
this.bindElement('s, Alt+ArrowUp, Ctrl+Alt+ArrowUp', $$('.navi-title a').slice(1, -1).slice(-1)[0], 'Parent hypha');
this.bindElement('c, Alt+ArrowDown, Ctrl+Alt+ArrowDown', '.subhyphae__link', 'First child hypha');
this.bindElement('e, ' + (isMac ? "Meta+Enter" : "Ctrl+Enter"), '.btn__link_navititle[href^="/edit/"]', 'Edit this hypha');
this.bindElement('v', '.hypha-info__link[href^="/hypha/"]', 'Go to hyphas page');
this.bindElement('a', '.hypha-info__link[href^="/media/"]', 'Go to media management');
this.bindElement('h', '.hypha-info__link[href^="/history/"]', 'Go to history');
this.bindElement('r', '.hypha-info__link[href^="/rename/"]', 'Rename this hypha');
this.bindElement('b', '.hypha-info__link[href^="/backlinks/"]', 'Backlinks');
});
} else {
// Hypha editor shortcuts. These work only on editor's text area.
let editorShortcuts = new ShortcutHandler(editTextarea, true);
let shortcuts = [
// Win+Linux Mac Action Description
['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'],
['Ctrl+X', 'Meta+Shift+x', wrapStrikethrough, 'Format: Strikethrough'],
['Ctrl+k', 'Meta+k', wrapLink, 'Format: Inline link'],
// Apparently, ⌘; conflicts with a Safari's hotkey. Whatever. // Apparently, ⌘; conflicts with a Safari's hotkey. Whatever.
['Ctrl+;', 'Meta+;', insertDate, 'Insert date UTC'], new Shortcut(isMac ? 'Meta+;' : 'Ctrl+;', insertDate, 'Insert date UTC', { force: true }),
]; ]))
editorShortcuts.group('Editor', function () {
for (let shortcut of shortcuts) {
if (isMac) {
this.add(shortcut[1], ...shortcut.slice(2))
} else {
this.add(shortcut[0], ...shortcut.slice(2))
}
}
});
editorShortcuts.group(function () {
this.bindElement(isMac ? 'Meta+Enter' : 'Ctrl+Enter', $('.edit-form__save'), 'Save changes');
this.bindElement(isMac ? 'Meta+Shift+Enter' : 'Ctrl+Shift+Enter', $('.edit-form__preview'), 'Preview changes');
});
} }
}); }

View File

@ -12,7 +12,6 @@
<title>{{block "title" .}}{{end}}</title> <title>{{block "title" .}}{{end}}</title>
<link rel="icon" href="/static/favicon.ico"> <link rel="icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/style.css"> <link rel="stylesheet" href="/static/style.css">
<script src="/static/shortcuts.js"></script>
{{range .HeadElements}}{{.}}{{end}} {{range .HeadElements}}{{.}}{{end}}
</head> </head>
<body data-rrh-addr="{{if .Addr}}{{.Addr}}{{else}}{{.Meta.Addr}}{{end}}"{{range $key, $value := .BodyAttributes}} data-rrh-{{$key}}="{{$value}}"{{end}}> <body data-rrh-addr="{{if .Addr}}{{.Addr}}{{else}}{{.Meta.Addr}}{{end}}"{{range $key, $value := .BodyAttributes}} data-rrh-{{$key}}="{{$value}}"{{end}}>
@ -46,17 +45,8 @@
</nav> </nav>
</header> </header>
{{block "body" .}}{{end}} {{block "body" .}}{{end}}
<template id="dialog-template"> <script src="/static/common.js"></script>
<div class="dialog-backdrop"></div> <script src="/static/shortcuts.js"></script>
<div class="dialog" tabindex="0">
<div class="dialog__header">
<h1 class="dialog__title"></h1>
<button class="dialog__close-button" aria-label="{{block `close this dialog` .}}{{end}}"></button>
</div>
<div class="dialog__content"></div>
</div>
</template>
<script src="/static/view.js"></script> <script src="/static/view.js"></script>
{{range .CommonScripts}} {{range .CommonScripts}}
<script src="{{.}}"></script> <script src="{{.}}"></script>

View File

@ -23,7 +23,6 @@ var (
const ruText = ` const ruText = `
{{define "search by title"}}Поиск по названию{{end}} {{define "search by title"}}Поиск по названию{{end}}
{{define "close this dialog"}}Закрыть этот диалог{{end}}
{{define "login"}}Войти{{end}} {{define "login"}}Войти{{end}}
{{define "register"}}Регистрация{{end}} {{define "register"}}Регистрация{{end}}
{{define "confirm"}}Подтвердить{{end}} {{define "confirm"}}Подтвердить{{end}}