diff --git a/app/assets/javascripts/keyboard_tools.js b/app/assets/javascripts/keyboard_tools.js index ffc07998b..9d17de7bb 100644 --- a/app/assets/javascripts/keyboard_tools.js +++ b/app/assets/javascripts/keyboard_tools.js @@ -43,11 +43,20 @@ document.addEventListener('DOMContentLoaded', async () => { }); return return_obj; }, - dialog: function (msg) { + dialog: function (...elements) { this.dialogClose(); const d = document.createElement('div'); d.classList.add('__keyboard_help'); - d.innerText = msg; + d.append(...elements.map(e => { + if (typeof e == 'string') { + const p = document.createElement('p'); + p.innerText = e; + return p; + } + else { + return e + } + })); document.body.appendChild(d); }, dialogClose: function () { @@ -98,34 +107,34 @@ document.addEventListener('DOMContentLoaded', async () => { } }); - /** - * @param {number} len - * @returns {string} - */ - const delimitShortcutsGroup = (len) => { - return `${'='.repeat(len)}\n`; - }; - - /** - * @param {KeyboardShortcut} shortcut - * @param {number} [gap] - * @returns {string} - */ - const formatShortcut = ({ key, text }, gap = 4) => { - return `${key}${' '.repeat(gap - (key.length - 1))}${text}`; - }; - /** * @param {KeyboardShortcut[]} shortcuts - * @param {number} [gap] - * @returns {string} + * @param {string} caption + * @returns {HTMLTableElement} */ - const formatShortcuts = (shortcuts, gap = 4) => { - return shortcuts.filter((s) => s.if === void 0 || s.if) - .map((s) => formatShortcut(s, gap)) - .join('\n'); + const formatShortcuts = (caption, shortcuts) => { + const table = document.createElement('table'); + table.classList.add('table', 'is-full-width'); + const captionEl = document.createElement('caption'); + captionEl.innerText = caption; + table.append(captionEl); + for (const { key, text } of shortcuts.filter(s => s.if ?? true)) { + const tr = document.createElement('tr'); + const shortcode = document.createElement('td'); + const kbd = document.createElement('kbd'); + kbd.innerText = key; + shortcode.append(kbd); + const description = document.createElement('td'); + description.innerText = text; + tr.append(shortcode, description); + table.append(tr); + } + return table; }; + const categoryShortcuts = () => Object.keys(QPixel.Keyboard.categories()) + .map((name, i) => ({ key: (i + 1).toString(), text: name })); + const renderHelpMenu = () => { /** @type {KeyboardShortcut[]} */ const generalShortcuts = [ @@ -148,15 +157,9 @@ document.addEventListener('DOMContentLoaded', async () => { ]; QPixel.Keyboard.dialog( - 'Keyboard Shortcuts\n' + - delimitShortcutsGroup(33) + - formatShortcuts(generalShortcuts, 4) + - '\n\n' + - 'Selection shortcuts:\n\n' + - formatShortcuts(selectionShortcuts, 4) + - '\n\n' + - 'Selection shortcuts will select\n' + - 'first post, if none selected' + formatShortcuts('Keyboard Shortcuts', generalShortcuts), + formatShortcuts('Selection shortcuts', selectionShortcuts), + 'Selection shortcuts will select first post, if none selected' ); }; @@ -175,9 +178,8 @@ document.addEventListener('DOMContentLoaded', async () => { ]; QPixel.Keyboard.dialog( - 'Go to ...\n' + - delimitShortcutsGroup(26) + - formatShortcuts(shortcuts, 3) + 'Go to ...', + formatShortcuts('Pages', shortcuts) ); }; @@ -194,9 +196,8 @@ document.addEventListener('DOMContentLoaded', async () => { ]; QPixel.Keyboard.dialog( - 'Use tool ...\n' + - delimitShortcutsGroup(17) + - formatShortcuts(shortcuts, 3) + 'Use tool ...' + + formatShortcuts('Tools', shortcuts) ); }; @@ -207,11 +208,10 @@ document.addEventListener('DOMContentLoaded', async () => { { key: 'd', text: 'Down' }, { key: 'c', text: 'Close' } ]; - + QPixel.Keyboard.dialog( - 'Vote ...\n' + - delimitShortcutsGroup(9) + - formatShortcuts(shortcuts, 3) + 'Vote ...', + formatShortcuts('Vote', shortcuts) ); }; @@ -262,9 +262,11 @@ document.addEventListener('DOMContentLoaded', async () => { renderToolsMenu(); QPixel.Keyboard.state = 'tools'; } + } else if (e.key === 's') { + window.location.href = '/posts/search'; } else if (e.key === 'a') { - const cl = $('#answer_body_markdown'); - cl[0].scrollIntoView({ behavior: 'smooth' }); + const cl = /** @type {HTMLTextAreaElement} */(document.getElementById('post_body_markdown')); + cl.scrollIntoView({ behavior: 'smooth' }); cl.focus(); QPixel.Keyboard.dialogClose(); } else if (e.key === 'Enter') { @@ -296,48 +298,22 @@ document.addEventListener('DOMContentLoaded', async () => { } else if (e.key === 'f') { window.location.href = '/mod/flags'; } else if (e.key === 't') { - const data = Object.entries(QPixel.Keyboard.categories()); - let string_response = ''; - for (let i = 0; i < data.length; i++) { - string_response += formatShortcut({ - key: (i + 1).toString(), - text: data[i][0] - }, 3) + '\n'; - } QPixel.Keyboard.dialog( - 'Go to tags of ...\n' + - delimitShortcutsGroup(18) + - string_response.trim() + 'Go to tags of ...', + formatShortcuts("Categories", categoryShortcuts()) ); QPixel.Keyboard.state = 'goto/category-tags'; } else if (e.key === 'e') { - const data = Object.entries(QPixel.Keyboard.categories()); - let string_response = ''; - for (let i = 0; i < data.length; i++) { - string_response += formatShortcut({ - key: (i + 1).toString(), - text: data[i][0] - }, 3) + '\n'; - } QPixel.Keyboard.dialog( - 'Go to suggested edits of ...\n' + - delimitShortcutsGroup(28) + - string_response.trim() + 'Go to suggested edits of ...\n', + formatShortcuts("Categories", categoryShortcuts()) ); QPixel.Keyboard.state = 'goto/category-edits'; } else if (e.key === 'c') { - const data = Object.entries(QPixel.Keyboard.categories()); - let string_response = ''; - for (let i = 0; i < data.length; i++) { - string_response += formatShortcut({ - key: (i + 1).toString(), - text: data[i][0] - }, 3) + '\n'; - } + const data = Object.keys(QPixel.Keyboard.categories()); QPixel.Keyboard.dialog( - 'Go to category ...\n' + - delimitShortcutsGroup(18) + - string_response.trim() + 'Go to category ...\n', + formatShortcuts("Categories", categoryShortcuts()) ); QPixel.Keyboard.state = 'goto/category'; } diff --git a/app/assets/stylesheets/keyboard_tools.scss b/app/assets/stylesheets/keyboard_tools.scss index 08a9df1a4..71cd57393 100644 --- a/app/assets/stylesheets/keyboard_tools.scss +++ b/app/assets/stylesheets/keyboard_tools.scss @@ -1,6 +1,5 @@ .__keyboard_help { padding: 1rem; - font-family: monospace; white-space: pre-wrap; max-width: 350px; background-color: rgba(0,0,0,0.8); @@ -12,6 +11,12 @@ z-index: 2305843009213693951; /* This is the largest Mersenne-prime shown in full on Wikipedia and should be large enough for most practical use cases to ensure that the keyboard help is always above everything else. */ + table { + margin-top: 1em; + caption { + text-align: start; + } + } } .__keyboard_selected { outline: 0.25rem solid red; diff --git a/global.d.ts b/global.d.ts index 5ecaaa1d6..6ebe1f809 100644 --- a/global.d.ts +++ b/global.d.ts @@ -12,12 +12,12 @@ interface PostValidatorMessage { type PostValidator = (postText: string) => [boolean, PostValidatorMessage[]]; - interface ProcessedTag { +interface ProcessedTag { id: number | string text: string desc: string synonyms?: string | QPixelTagSynonym[] - } +} interface UserPreferences { community: Record; @@ -113,12 +113,12 @@ interface QPixelMD { * @param text the text with which to replace the selection */ replaceSelection?: ($field: JQuery, text: string) => void; - /** - * Inserts text at a given {@link idx} in a given {@link str} - * @param str text to insert into - * @param idx position to insert at - * @param insert text to insert - */ + /** + * Inserts text at a given {@link idx} in a given {@link str} + * @param str text to insert into + * @param idx position to insert at + * @param insert text to insert + */ stringInsert?: (str: string, idx: number, insert: string) => string; /** * See [strip_markdown](app/helpers/application_helper.rb) application helper @@ -182,7 +182,7 @@ interface QPixelKeyboard { user_id: number | null; categories: () => Record; - dialog: (message: string) => void; + dialog: (...elements: (HTMLElement | string)[]) => void; dialogClose: () => void; updateSelected: () => void; } @@ -203,7 +203,7 @@ type NotificationType = "warning" | "success" | "danger"; type QPixelPopupCallback = (ev: JQuery.ClickEvent, popup: QPixelPopup) => void -type QPixelPingablePopupCallback = (ev: JQuery.KeyUpEvent)=> Promise +type QPixelPingablePopupCallback = (ev: JQuery.KeyUpEvent) => Promise declare class QPixelPopup { static destroyAll: () => void; @@ -215,8 +215,8 @@ declare class QPixelPopup { static isSpecialKey: (keyCode: number) => boolean; constructor( - items: JQuery[], - field: HTMLInputElement | HTMLTextAreaElement, + items: JQuery[], + field: HTMLInputElement | HTMLTextAreaElement, callback: QPixelPopupCallback ); @@ -377,7 +377,7 @@ type QPixelPostType = { has_reactions: boolean answer_type_id: number | null has_only_specific_reactions: boolean - } +} interface QPixel { // constants @@ -598,8 +598,8 @@ interface QPixel { * @param onFinally callback to call for all requests */ handleJSONResponse?: (data: T, - onSuccess: (data: Extract) => void, - onFinally?: (data: T) => void) => boolean + onSuccess: (data: Extract) => void, + onFinally?: (data: T) => void) => boolean /** * Attempts to archive a comment thread