diff --git a/app/scripts/comp/browser/shortcuts.ts b/app/scripts/comp/browser/shortcuts.ts index 0135ff7a..702439a1 100644 --- a/app/scripts/comp/browser/shortcuts.ts +++ b/app/scripts/comp/browser/shortcuts.ts @@ -31,10 +31,9 @@ const AllGlobalShortcuts = { restoreApp: undefined }; -const GlobalShortcutAppSettingsFields: Record< - keyof typeof AllGlobalShortcuts, - AppSettingsFieldName -> = { +export type GlobalShortcutType = keyof typeof AllGlobalShortcuts; + +const GlobalShortcutAppSettingsFields: Record = { copyPassword: 'globalShortcutCopyPassword', copyUser: 'globalShortcutCopyUser', copyUrl: 'globalShortcutCopyUrl', @@ -108,11 +107,11 @@ const Shortcuts = { return formatting ? `${shortcut} + ` : `${shortcut}+`; }, - globalShortcutText(type: keyof typeof AllGlobalShortcuts, formatting?: boolean): string { + globalShortcutText(type: GlobalShortcutType, formatting?: boolean): string { return this.presentShortcut(this.globalShortcut(type), formatting); }, - globalShortcut(type: keyof typeof AllGlobalShortcuts): string | undefined { + globalShortcut(type: GlobalShortcutType): string | undefined { const setting = GlobalShortcutAppSettingsFields[type]; const appSettingsShortcut = AppSettings[setting]; if (typeof appSettingsShortcut === 'string') { @@ -128,7 +127,7 @@ const Shortcuts = { return undefined; }, - setGlobalShortcut(type: keyof typeof AllGlobalShortcuts, value: string): void { + setGlobalShortcut(type: GlobalShortcutType, value: string | null): void { if (!AllGlobalShortcuts[type]) { throw new Error('Bad shortcut: ' + type); } diff --git a/app/scripts/ui-old/settings/settings-shortcuts-view.js b/app/scripts/ui-old/settings/settings-shortcuts-view.js deleted file mode 100644 index d196aacd..00000000 --- a/app/scripts/ui-old/settings/settings-shortcuts-view.js +++ /dev/null @@ -1,108 +0,0 @@ -import { View } from 'framework/views/view'; -import { Shortcuts } from 'comp/app/shortcuts'; -import { Launcher } from 'comp/launcher'; -import { Keys } from 'const/keys'; -import { Features } from 'util/features'; -import { Locale } from 'util/locale'; -import template from 'templates/settings/settings-shortcuts.hbs'; - -class SettingsShortcutsView extends View { - template = template; - - systemShortcuts = [ - 'Meta+A', - 'Alt+A', - 'Alt+C', - 'Alt+D', - 'Meta+F', - 'Meta+C', - 'Meta+B', - 'Meta+U', - 'Meta+T', - 'Alt+N', - 'Meta+O', - 'Meta+S', - 'Meta+G', - 'Meta+,', - 'Meta+L' - ]; - - events = { - 'click button.shortcut': 'shortcutClick' - }; - - render() { - super.render({ - cmd: Shortcuts.actionShortcutSymbol(true), - alt: Shortcuts.altShortcutSymbol(true), - globalIsLarge: !Features.isMac, - autoTypeSupported: !!Launcher, - globalShortcuts: Launcher - ? { - autoType: Shortcuts.globalShortcutText('autoType', true), - copyPassword: Shortcuts.globalShortcutText('copyPassword', true), - copyUser: Shortcuts.globalShortcutText('copyUser', true), - copyUrl: Shortcuts.globalShortcutText('copyUrl', true), - copyOtp: Shortcuts.globalShortcutText('copyOtp', true), - restoreApp: Shortcuts.globalShortcutText('restoreApp', true) - } - : undefined - }); - } - - shortcutClick(e) { - const globalShortcutType = e.target.dataset.shortcut; - - const existing = $(`.shortcut__editor[data-shortcut=${globalShortcutType}]`); - if (existing.length) { - existing.remove(); - return; - } - - const shortcutEditor = $('
') - .addClass('shortcut__editor') - .attr('data-shortcut', globalShortcutType); - $('
').text(Locale.setShEdit).appendTo(shortcutEditor); - const shortcutEditorInput = $('') - .addClass('shortcut__editor-input') - .val(Shortcuts.globalShortcutText(globalShortcutType)) - .appendTo(shortcutEditor); - if (!Features.isMac) { - shortcutEditorInput.addClass('shortcut__editor-input--large'); - } - - shortcutEditor.insertAfter($(e.target).parent()); - shortcutEditorInput.focus(); - shortcutEditorInput.on('keypress', (e) => e.preventDefault()); - shortcutEditorInput.on('keydown', (e) => { - e.preventDefault(); - e.stopImmediatePropagation(); - - if (e.which === Keys.DOM_VK_DELETE || e.which === Keys.DOM_VK_BACK_SPACE) { - Shortcuts.setGlobalShortcut(globalShortcutType, undefined); - this.render(); - return; - } - if (e.which === Keys.DOM_VK_ESCAPE) { - shortcutEditorInput.blur(); - return; - } - - const shortcut = Shortcuts.keyEventToShortcut(e); - const presentableShortcutText = Shortcuts.presentShortcut(shortcut.value); - - shortcutEditorInput.val(presentableShortcutText); - - const exists = this.systemShortcuts.includes(shortcut.text); - shortcutEditorInput.toggleClass('input--error', exists); - - const isValid = shortcut.valid && !exists; - if (isValid) { - Shortcuts.setGlobalShortcut(globalShortcutType, shortcut.value); - this.render(); - } - }); - } -} - -export { SettingsShortcutsView }; diff --git a/app/scripts/ui/settings/settings-shortcuts.ts b/app/scripts/ui/settings/settings-shortcuts.ts new file mode 100644 index 00000000..969f124b --- /dev/null +++ b/app/scripts/ui/settings/settings-shortcuts.ts @@ -0,0 +1,76 @@ +import { FunctionComponent, h } from 'preact'; +import { GlobalShortcutDef, SettingsShortcutsView } from 'views/settings/settings-shortcuts-view'; +import { Features } from 'util/features'; +import { Launcher } from 'comp/launcher'; +import { GlobalShortcutType, Shortcuts } from 'comp/browser/shortcuts'; +import { Locale } from 'util/locale'; +import { useModelWatcher } from 'util/ui/hooks'; +import { AppSettings } from 'models/app-settings'; +import { useState } from 'preact/hooks'; + +function getGlobalShortcuts(): GlobalShortcutDef[] { + return [ + { + id: 'autoType', + title: Locale.setShAutoTypeGlobal, + value: Shortcuts.globalShortcutText('autoType', true) + }, + { + id: 'copyPassword', + title: Locale.setShCopyPassOnly, + value: Shortcuts.globalShortcutText('copyPassword', true) + }, + { + id: 'copyUser', + title: Locale.setShCopyUser, + value: Shortcuts.globalShortcutText('copyUser', true) + }, + { + id: 'copyUrl', + title: Locale.setShCopyUrl, + value: Shortcuts.globalShortcutText('copyUrl', true) + }, + { + id: 'copyOtp', + title: Locale.setShCopyOtp, + value: Shortcuts.globalShortcutText('copyOtp', true) + }, + { + id: 'restoreApp', + title: Locale.setShRestoreApp.with('KeeWeb'), + value: Shortcuts.globalShortcutText('restoreApp', true) + } + ]; +} + +export const SettingsShortcuts: FunctionComponent = () => { + useModelWatcher(AppSettings); + const [editedShortcut, setEditedShortcut] = useState(''); + + const shortcutClicked = (id: GlobalShortcutType) => { + setEditedShortcut(editedShortcut === id ? '' : id); + }; + + const shortcutChanged = (id: GlobalShortcutType, shortcut: string) => { + Shortcuts.setGlobalShortcut(id, shortcut); + setEditedShortcut(''); + }; + + const shortcutReset = (id: GlobalShortcutType) => { + Shortcuts.setGlobalShortcut(id, null); + setEditedShortcut(''); + }; + + return h(SettingsShortcutsView, { + cmd: Shortcuts.actionShortcutSymbol(true), + alt: Shortcuts.altShortcutSymbol(true), + globalIsLarge: !Features.isMac, + autoTypeSupported: !!Launcher, + globalShortcuts: Launcher ? getGlobalShortcuts() : undefined, + editedShortcut, + + shortcutClicked, + shortcutChanged, + shortcutReset + }); +}; diff --git a/app/scripts/views/components/shortcut-editor.tsx b/app/scripts/views/components/shortcut-editor.tsx new file mode 100644 index 00000000..06f09b85 --- /dev/null +++ b/app/scripts/views/components/shortcut-editor.tsx @@ -0,0 +1,84 @@ +import { FunctionComponent } from 'preact'; +import { Locale } from 'util/locale'; +import { classes } from 'util/ui/classes'; +import { useLayoutEffect, useRef, useState } from 'preact/hooks'; +import { Keys } from 'const/keys'; +import { Shortcuts } from 'comp/browser/shortcuts'; + +const SystemShortcuts = [ + 'Meta+A', + 'Alt+A', + 'Alt+C', + 'Alt+D', + 'Meta+F', + 'Meta+C', + 'Meta+B', + 'Meta+U', + 'Meta+T', + 'Alt+N', + 'Meta+O', + 'Meta+S', + 'Meta+G', + 'Meta+,', + 'Meta+L' +]; + +export const ShortcutEditor: FunctionComponent<{ + shortcut: string; + large: boolean; + + onSet: (shortcut: string) => void; + onClear: () => void; +}> = ({ shortcut, large, onSet, onClear }) => { + const shortcutInput = useRef(); + + const [shortcutExists, setShortcutExists] = useState(false); + + useLayoutEffect(() => { + shortcutInput.current.focus(); + }, []); + + const inputKeyDown = (e: KeyboardEvent) => { + e.preventDefault(); + e.stopImmediatePropagation(); + + if (e.which === Keys.DOM_VK_DELETE || e.which === Keys.DOM_VK_BACK_SPACE) { + onClear(); + return; + } + if (e.which === Keys.DOM_VK_ESCAPE) { + shortcutInput.current.blur(); + return; + } + + const shortcut = Shortcuts.keyEventToShortcut(e); + const presentableShortcutText = Shortcuts.presentShortcut(shortcut.value); + + shortcutInput.current.value = presentableShortcutText; + + const exists = SystemShortcuts.includes(shortcut.value); + setShortcutExists(exists); + + const isValid = shortcut.valid && !exists; + if (isValid) { + onSet(shortcut.value); + } + }; + + return ( +
+
{Locale.setShEdit}
+ e.preventDefault()} + onKeyDown={inputKeyDown} + /> +
+ ); +}; diff --git a/app/scripts/views/settings/settings-shortcuts-view.tsx b/app/scripts/views/settings/settings-shortcuts-view.tsx new file mode 100644 index 00000000..5a7dced6 --- /dev/null +++ b/app/scripts/views/settings/settings-shortcuts-view.tsx @@ -0,0 +1,129 @@ +import { FunctionComponent } from 'preact'; +import { Locale } from 'util/locale'; +import { classes } from 'util/ui/classes'; +import { ShortcutEditor } from 'views/components/shortcut-editor'; +import { GlobalShortcutType } from 'comp/browser/shortcuts'; + +export interface GlobalShortcutDef { + id: GlobalShortcutType; + value: string; + title: string; +} + +export const SettingsShortcutsView: FunctionComponent<{ + cmd: string; + alt: string; + autoTypeSupported: boolean; + globalShortcuts?: GlobalShortcutDef[]; + globalIsLarge: boolean; + editedShortcut?: string; + + shortcutClicked: (id: GlobalShortcutType) => void; + shortcutChanged: (id: GlobalShortcutType, shortcut: string) => void; + shortcutReset: (id: GlobalShortcutType) => void; +}> = ({ + cmd, + alt, + autoTypeSupported, + globalShortcuts, + globalIsLarge, + editedShortcut, + + shortcutClicked, + shortcutChanged, + shortcutReset +}) => { + return ( +
+

+ {Locale.setShTitle} +

+
+ {cmd}A {Locale.or}{' '} + {alt}A {Locale.setShShowAll} +
+
+ {alt}C {Locale.setShColors} +
+
+ {alt}D {Locale.setShTrash} +
+
+ {cmd}F {Locale.setShFind} +
+
+ esc {Locale.setShClearSearch} +
+
+ {cmd}C {Locale.setShCopyPass} +
+
+ {cmd}B {Locale.setShCopyUser} +
+
+ {cmd}U {Locale.setShCopyUrl} +
+
+ {alt}2 {Locale.setShCopyOtp} +
+ {autoTypeSupported ? ( +
+ {cmd}T {Locale.setShAutoType} +
+ ) : null} +
+ {Locale.setShPrev} +
+
+ {Locale.setShNext} +
+
+ {alt}N {Locale.setShCreateEntry} +
+
+ {cmd}O {Locale.setShOpen} +
+
+ {cmd}S {Locale.setShSave} +
+
+ {cmd}G {Locale.setShGen} +
+
+ {cmd}, {Locale.setShSet} +
+
+ {cmd}L {Locale.setShLock} +
+ + {globalShortcuts ? ( + <> +

{Locale.setShGlobal}

+ {globalShortcuts.map((sh) => ( +
+ {' '} + {sh.title} + {sh.id === editedShortcut ? ( + shortcutChanged(sh.id, shortcut)} + onClear={() => shortcutReset(sh.id)} + /> + ) : null} +
+ ))} + + ) : null} +
+ ); +}; diff --git a/app/scripts/views/settings/settings-view.tsx b/app/scripts/views/settings/settings-view.tsx index 2939aa4b..dd7474c1 100644 --- a/app/scripts/views/settings/settings-view.tsx +++ b/app/scripts/views/settings/settings-view.tsx @@ -3,6 +3,7 @@ import { SettingsPage } from 'models/workspace'; import { Scrollable } from 'views/components/scrollable'; import { Locale } from 'util/locale'; import { SettingsGeneral } from 'ui/settings/settings-general'; +import { SettingsShortcuts } from 'ui/settings/settings-shortcuts'; import { SettingsAbout } from 'ui/settings/settings-about'; import { SettingsHelp } from 'ui/settings/settings-help'; @@ -19,6 +20,7 @@ export const SettingsView: FunctionComponent<{
{page === 'general' ? : null} + {page === 'shortcuts' ? : null} {page === 'about' ? : null} {page === 'help' ? : null} diff --git a/app/templates/settings/settings-shortcuts.hbs b/app/templates/settings/settings-shortcuts.hbs deleted file mode 100644 index 6f12453d..00000000 --- a/app/templates/settings/settings-shortcuts.hbs +++ /dev/null @@ -1,38 +0,0 @@ -
-

{{res 'setShTitle'}}

-
{{cmd}}A {{res 'or'}} {{alt}}A {{res 'setShShowAll'}}
-
{{alt}}C {{res 'setShColors'}}
-
{{alt}}D {{res 'setShTrash'}}
-
{{cmd}}F {{res 'setShFind'}}
-
esc {{res 'setShClearSearch'}}
-
{{cmd}}C {{res 'setShCopyPass'}}
-
{{cmd}}B {{res 'setShCopyUser'}}
-
{{cmd}}U {{res 'setShCopyUrl'}}
-
{{alt}}2 {{res 'setShCopyOtp'}}
- {{#if autoTypeSupported}} -
{{cmd}}T {{res 'setShAutoType'}}
- {{/if}} -
{{res 'setShPrev'}}
-
{{res 'setShNext'}}
-
{{alt}}N {{res 'setShCreateEntry'}}
-
{{cmd}}O {{res 'setShOpen'}}
-
{{cmd}}S {{res 'setShSave'}}
-
{{cmd}}G {{res 'setShGen'}}
-
{{cmd}}, {{res 'setShSet'}}
-
{{cmd}}L {{res 'setShLock'}}
- {{#if globalShortcuts}} -

{{res 'setShGlobal'}}

-
{{res 'setShAutoTypeGlobal'}}
-
{{res 'setShCopyPassOnly'}}
-
{{res 'setShCopyUser'}}
-
{{res 'setShCopyUrl'}}
-
{{res 'setShCopyOtp'}}
-
{{#res 'setShRestoreApp'}}KeeWeb{{/res}}
- {{/if}} -