-
Notifications
You must be signed in to change notification settings - Fork 339
Shortcuts streamline & help page the second #1612
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
fafc983
3ce3f2a
bc70908
3ba2933
7459407
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { tinykeys } from 'tinykeys'; | ||
| import { KeybindingsMap, tinykeys } from 'tinykeys'; | ||
| import selfoss from './selfoss-base'; | ||
| import { Direction } from './helpers/navigation'; | ||
| import { RefObject } from 'react'; | ||
|
|
||
| type KeyboardEventHandler = (event: KeyboardEvent) => void; | ||
|
|
||
|
|
@@ -10,157 +11,220 @@ type KeyboardEventHandler = (event: KeyboardEvent) => void; | |
| */ | ||
| function ignoreWhenInteracting( | ||
| handler: KeyboardEventHandler, | ||
| ignoreDialog: RefObject<HTMLDialogElement> = undefined, | ||
| ): KeyboardEventHandler { | ||
| return (event: KeyboardEvent): void => { | ||
| if (selfoss.lightboxActive.value) { | ||
| return; | ||
| } else if (ignoreDialog && ignoreDialog.current.open) { | ||
| return; | ||
| } | ||
|
|
||
| handler(event); | ||
| }; | ||
| } | ||
|
|
||
| interface IKeybinding { | ||
| readableName?: string; | ||
| description: string; | ||
| action: KeyboardEventHandler; | ||
| } | ||
|
|
||
| /** | ||
| * Set up shortcuts on document. | ||
| * A selfoss-side definition of all keybindings | ||
| * This is used to: | ||
| * - Limit boilerplate | ||
| * - Generate a tinykeys compatible KeybindingsMap (@see makeKeybindingsMap) | ||
| * - Generate a keybindings overview for the help page (see HelpShortcuts component) | ||
| */ | ||
| export default function makeShortcuts(): () => void { | ||
| return tinykeys(window, { | ||
| // 'space': next article | ||
| Space: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| export const KEYBINDINGS: { [keycombo: string]: IKeybinding } = { | ||
| Space: { | ||
| description: 'select and open next entry', | ||
| action: () => { | ||
| selfoss.entriesPage?.jumpToNext(); | ||
| }), | ||
|
|
||
| // 'n': next article | ||
| n: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| n: { | ||
| description: 'select next entry', | ||
| action: () => { | ||
| selfoss.entriesPage?.nextPrev(Direction.NEXT, false); | ||
| }), | ||
|
|
||
| // 'right cursor': next article | ||
| ArrowRight: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| Arrowright: { | ||
| readableName: '→', | ||
| description: 'select next entry (and open it when the current is open)', | ||
| action: () => { | ||
| selfoss.entriesPage?.entryNav(Direction.NEXT); | ||
| }), | ||
|
|
||
| // 'j': next article | ||
| j: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| j: { | ||
| description: 'select and open next entry', | ||
| action: () => { | ||
| selfoss.entriesPage?.nextPrev(Direction.NEXT, true); | ||
| }), | ||
|
|
||
| // 'shift+space': previous article | ||
| 'Shift+Space': ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| 'Shift+Space': { | ||
| description: 'select and open previous entry', | ||
| action: () => { | ||
| selfoss.entriesPage?.nextPrev(Direction.PREV, true); | ||
| }), | ||
|
|
||
| // 'p': previous article | ||
| p: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| p: { | ||
| description: 'select previous entry', | ||
| action: () => { | ||
| selfoss.entriesPage?.nextPrev(Direction.PREV, false); | ||
| }), | ||
|
|
||
| // 'left': previous article | ||
| ArrowLeft: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| ArrowLeft: { | ||
| readableName: '←', | ||
| description: | ||
| 'select previous entry (and open it when the current is open)', | ||
| action: () => { | ||
| selfoss.entriesPage?.entryNav(Direction.PREV); | ||
| }), | ||
|
|
||
| // 'k': previous article | ||
| k: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| k: { | ||
| description: 'select and open previous entry', | ||
| action: () => { | ||
| selfoss.entriesPage?.nextPrev(Direction.PREV, true); | ||
| }), | ||
|
|
||
| // 's': star/unstar | ||
| s: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| s: { | ||
| description: | ||
| 'mark and unmark current selected entry as starred/unstarred', | ||
| action: () => { | ||
| selfoss.entriesPage?.toggleSelectedStarred(); | ||
| }), | ||
|
|
||
| // 'm': mark/unmark | ||
| m: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| m: { | ||
| description: 'mark and unmark current selected entry as read/unread', | ||
| action: () => { | ||
| selfoss.entriesPage?.toggleSelectedRead(); | ||
| }), | ||
|
|
||
| // 'o': open/close entry | ||
| o: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| 'Control+m': { | ||
| description: 'mark all as read', | ||
| action: () => { | ||
| document.querySelector<HTMLButtonElement>('#nav-mark').click(); | ||
| }, | ||
| }, | ||
| o: { | ||
| description: 'open / close current entry', | ||
| action: () => { | ||
| selfoss.entriesPage?.toggleSelectedExpanded(); | ||
| }), | ||
|
|
||
| // 'Shift + o': close open entries | ||
| 'Shift+o': ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| 'Shift+o': { | ||
| description: 'close all open entries', | ||
| action: () => { | ||
| selfoss.entriesPage?.collapseAllEntries(); | ||
| }), | ||
|
|
||
| // 'v': open target | ||
| v: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| v: { | ||
| description: 'open url of current entry in new tab/window', | ||
| action: () => { | ||
| selfoss.entriesPage?.openSelectedTarget(); | ||
| }), | ||
|
|
||
| // 'Shift + v': open target and mark read | ||
| 'Shift+v': ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| 'Shift+v': { | ||
| description: | ||
| 'open url of current entry in new tab/window and mark read', | ||
| action: () => { | ||
| selfoss.entriesPage?.openSelectedTargetAndMarkRead(); | ||
| }), | ||
|
|
||
| // 'r': Reload the current view | ||
| r: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| r: { | ||
| description: 'reload the list', | ||
| action: () => { | ||
| selfoss.entriesPage?.reload(); | ||
| }), | ||
|
|
||
| // 'Shift + r': Refresh sources | ||
| 'Shift+r': ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| 'Shift+r': { | ||
| description: 'refresh sources', | ||
| action: () => { | ||
| document.querySelector<HTMLButtonElement>('#nav-refresh').click(); | ||
| }), | ||
|
|
||
| // 'Control+m': mark all as read | ||
| 'Control+m': ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| document.querySelector<HTMLButtonElement>('#nav-mark').click(); | ||
| }), | ||
|
|
||
| // 't': throw (mark as read & open next) | ||
| t: ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| t: { | ||
| description: 'throw current entry to next (mark as read & open next)', | ||
| action: () => { | ||
| selfoss.entriesPage?.throw(Direction.NEXT); | ||
| }), | ||
|
|
||
| // throw (mark as read & open previous) | ||
| 'Shift+t': ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| 'Shift+t': { | ||
| description: | ||
| 'throw current entry to previous (mark as read & open previous)', | ||
| action: () => { | ||
| selfoss.entriesPage?.throw(Direction.PREV); | ||
| }), | ||
|
|
||
| // 'Shift+n': switch to newest items overview / menu item | ||
| 'Shift+n': ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| 'Shift+n': { | ||
| description: 'open newest entries page', | ||
| action: () => { | ||
| document | ||
| .querySelector<HTMLAnchorElement>('#nav-filter-newest') | ||
| .click(); | ||
| }), | ||
|
|
||
| // 'Shift+u': switch to unread items overview / menu item | ||
| 'Shift+u': ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| 'Shift+u': { | ||
| description: 'open unread entries page', | ||
| action: () => { | ||
| document | ||
| .querySelector<HTMLAnchorElement>('#nav-filter-unread') | ||
| .click(); | ||
| }), | ||
|
|
||
| // 'Shift+s': switch to starred items overview / menu item | ||
| 'Shift+s': ignoreWhenInteracting((event: KeyboardEvent): void => { | ||
| event.preventDefault(); | ||
| }, | ||
| }, | ||
| 'Shift+s': { | ||
| description: 'open starred entries page', | ||
| action: () => { | ||
| document | ||
| .querySelector<HTMLAnchorElement>('#nav-filter-starred') | ||
| .click(); | ||
| }), | ||
| }); | ||
| }, | ||
| }, | ||
| '[Shift]+?': { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should also add it to the static docs.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! Although, perhaps finishing up the auto-generated docs table might be better to make sure nothing falls through the cracks properly? Shift+R is also undocumented in the docs as of now |
||
| readableName: '?', | ||
| description: 'show help', | ||
|
Denperidge marked this conversation as resolved.
|
||
| action: () => {}, | ||
| }, | ||
| }; | ||
|
|
||
| function makeKeybindingsMap( | ||
| shortcutsDialog: RefObject<HTMLDialogElement>, | ||
| ): KeybindingsMap { | ||
| const shortcuts = Object.entries(KEYBINDINGS); | ||
| const keybindingsMap: KeybindingsMap = {}; | ||
|
|
||
| for (const [keycombo, keybind] of shortcuts) { | ||
| if (keycombo === '[Shift]+?') { | ||
| keybindingsMap[keycombo] = ignoreWhenInteracting( | ||
| (event: KeyboardEvent) => { | ||
| event.preventDefault(); | ||
| if (shortcutsDialog.current.open) { | ||
| shortcutsDialog.current.close(); | ||
| } else { | ||
| shortcutsDialog.current.showModal(); | ||
| } | ||
| }, | ||
| ); | ||
| } else { | ||
| keybindingsMap[keycombo] = ignoreWhenInteracting( | ||
| (event: KeyboardEvent) => { | ||
| event.preventDefault(); | ||
| keybind.action(event); | ||
| }, | ||
| shortcutsDialog, | ||
| ); | ||
| } | ||
| } | ||
| return keybindingsMap; | ||
| } | ||
|
|
||
| /** | ||
| * Set up shortcuts on document. | ||
| */ | ||
| export default function makeShortcuts( | ||
| shortcutsDialog: RefObject<HTMLDialogElement>, | ||
| ): () => void { | ||
| return tinykeys(window, makeKeybindingsMap(shortcutsDialog)); | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.