From f49ba6bd4d38e4026bd88630014126a6e0086351 Mon Sep 17 00:00:00 2001 From: Mo Date: Fri, 18 Nov 2022 09:01:48 -0600 Subject: [PATCH] feat: keyboard shortcuts for primary actions (#2030) --- packages/ui-services/src/IO/IOService.ts | 212 ------------------ .../src/Keyboard/KeyboardCommandHandler.ts | 12 + .../src/Keyboard/KeyboardCommands.ts | 26 +++ .../ui-services/src/Keyboard/KeyboardKey.ts | 12 + .../src/Keyboard/KeyboardKeyEvent.ts | 4 + .../src/Keyboard/KeyboardModifier.ts | 7 + .../src/Keyboard/KeyboardService.ts | 203 +++++++++++++++++ .../src/Keyboard/KeyboardShortcut.ts | 20 ++ .../Keyboard/eventMatchesKeyAndModifiers.ts | 27 +++ .../src/Keyboard/getKeyboardShortcuts.ts | 136 +++++++++++ .../Keyboard/keyboardCharacterForModifier.ts | 22 ++ .../src/Keyboard/keyboardStringForShortcut.ts | 29 +++ .../src/Keyboard/modifiersForEvent.ts | 18 ++ .../ui-services/src/Keyboard/platformCheck.ts | 9 + packages/ui-services/src/index.ts | 9 +- .../index.css | 2 +- .../javascripts/Application/Application.ts | 38 ++-- .../javascripts/Application/WebServices.ts | 4 +- .../ApplicationView/ApplicationView.tsx | 152 +++++++------ .../ApplicationView/CommandProvider.tsx | 36 +++ .../Components/Button/RoundIconButton.tsx | 4 +- .../ChangeEditor/ChangeEditorButton.tsx | 19 +- .../ComponentView/ComponentView.tsx | 4 +- .../ContentListView/ContentList.tsx | 2 +- .../ContentListView/ContentListView.tsx | 140 ++++++------ .../Types/AbstractListItemProps.ts | 2 +- .../KeyboardShortcutIndicator.tsx | 38 ++++ .../LinkedItems/ItemLinkAutocompleteInput.tsx | 11 +- .../LinkedItemBubblesContainer.tsx | 27 ++- .../javascripts/Components/Menu/MenuItem.tsx | 10 +- .../MultipleSelectedNotes.tsx | 2 +- .../NoteView/NoteStatusIndicator.tsx | 2 +- .../Components/NoteView/NoteView.test.ts | 2 +- .../Components/NoteView/NoteView.tsx | 9 +- .../NoteView/PlainEditor/PlainEditor.tsx | 6 +- .../NotesContextMenu/NotesContextMenu.tsx | 2 +- .../Components/NotesOptions/AddTagOption.tsx | 2 +- .../NotesOptions/ChangeEditorOption.tsx | 15 +- .../Components/NotesOptions/NotesOptions.tsx | 65 +++++- .../NotesOptions/NotesOptionsPanel.tsx | 2 +- .../NotesOptions/NotesOptionsProps.ts | 2 +- .../PinNoteButton/PinNoteButton.tsx | 34 ++- .../Preferences/PreferencesView.tsx | 7 +- .../QuickSettingsMenu/FocusModeSwitch.tsx | 16 +- .../PanelSettingsSection.tsx | 76 ++----- .../QuickSettingsMenu/QuickSettingsMenu.tsx | 23 +- .../ResponsivePane/ResponsivePaneProvider.tsx | 21 +- .../HistoryModalContentPane.tsx | 2 +- .../RevisionHistoryModalProps.tsx | 2 +- .../SelectedRevisionContent.tsx | 2 +- .../Components/SearchBar/SearchBar.tsx | 2 + .../Components/Tags/TagsListItem.tsx | 2 +- .../Components/Tags/TagsSectionAddButton.tsx | 12 +- .../src/javascripts/Constants/ElementIDs.ts | 1 + .../Controllers/FilesController.ts | 2 +- .../ItemList/ItemListController.spec.ts | 2 +- .../ItemList/ItemListController.ts | 7 +- .../Navigation/NavigationController.ts | 11 +- .../NoteHistory/HistoryModalController.ts | 14 +- .../{ => NotesController}/NotesController.ts | 52 ++++- .../NotesControllerInterface.ts | 5 + .../javascripts/Controllers/PaneController.ts | 98 +++++++- .../Controllers/QuickSettingsController.ts | 24 +- .../Controllers/SelectedItemsController.ts | 12 +- .../Controllers/ViewControllerManager.ts | 10 +- .../src/javascripts/Utils/toggleFocusMode.tsx | 20 ++ packages/web/src/stylesheets/_focused.scss | 50 ++++- 67 files changed, 1296 insertions(+), 555 deletions(-) delete mode 100644 packages/ui-services/src/IO/IOService.ts create mode 100644 packages/ui-services/src/Keyboard/KeyboardCommandHandler.ts create mode 100644 packages/ui-services/src/Keyboard/KeyboardCommands.ts create mode 100644 packages/ui-services/src/Keyboard/KeyboardKey.ts create mode 100644 packages/ui-services/src/Keyboard/KeyboardKeyEvent.ts create mode 100644 packages/ui-services/src/Keyboard/KeyboardModifier.ts create mode 100644 packages/ui-services/src/Keyboard/KeyboardService.ts create mode 100644 packages/ui-services/src/Keyboard/KeyboardShortcut.ts create mode 100644 packages/ui-services/src/Keyboard/eventMatchesKeyAndModifiers.ts create mode 100644 packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts create mode 100644 packages/ui-services/src/Keyboard/keyboardCharacterForModifier.ts create mode 100644 packages/ui-services/src/Keyboard/keyboardStringForShortcut.ts create mode 100644 packages/ui-services/src/Keyboard/modifiersForEvent.ts create mode 100644 packages/ui-services/src/Keyboard/platformCheck.ts create mode 100644 packages/web/src/javascripts/Components/ApplicationView/CommandProvider.tsx create mode 100644 packages/web/src/javascripts/Components/KeyboardShortcutIndicator/KeyboardShortcutIndicator.tsx rename packages/web/src/javascripts/Controllers/{ => NotesController}/NotesController.ts (87%) create mode 100644 packages/web/src/javascripts/Controllers/NotesController/NotesControllerInterface.ts create mode 100644 packages/web/src/javascripts/Utils/toggleFocusMode.tsx diff --git a/packages/ui-services/src/IO/IOService.ts b/packages/ui-services/src/IO/IOService.ts deleted file mode 100644 index 3065c3e6f..000000000 --- a/packages/ui-services/src/IO/IOService.ts +++ /dev/null @@ -1,212 +0,0 @@ -import { removeFromArray } from '@standardnotes/utils' - -export enum KeyboardKey { - Tab = 'Tab', - Backspace = 'Backspace', - Up = 'ArrowUp', - Down = 'ArrowDown', - Left = 'ArrowLeft', - Right = 'ArrowRight', - Enter = 'Enter', - Escape = 'Escape', - Home = 'Home', - End = 'End', -} - -export enum KeyboardModifier { - Shift = 'Shift', - Ctrl = 'Control', - /** ⌘ key on Mac, ⊞ key on Windows */ - Meta = 'Meta', - Alt = 'Alt', -} - -enum KeyboardKeyEvent { - Down = 'KeyEventDown', - Up = 'KeyEventUp', -} - -type KeyboardObserver = { - key?: KeyboardKey | string - modifiers?: KeyboardModifier[] - onKeyDown?: (event: KeyboardEvent) => void - onKeyUp?: (event: KeyboardEvent) => void - element?: HTMLElement - elements?: HTMLElement[] - notElement?: HTMLElement - notElementIds?: string[] - notTags?: string[] -} - -export class IOService { - readonly activeModifiers = new Set() - private observers: KeyboardObserver[] = [] - - constructor(private isMac: boolean) { - window.addEventListener('keydown', this.handleKeyDown) - window.addEventListener('keyup', this.handleKeyUp) - window.addEventListener('blur', this.handleWindowBlur) - } - - public deinit() { - this.observers.length = 0 - window.removeEventListener('keydown', this.handleKeyDown) - window.removeEventListener('keyup', this.handleKeyUp) - window.removeEventListener('blur', this.handleWindowBlur) - ;(this.handleKeyDown as unknown) = undefined - ;(this.handleKeyUp as unknown) = undefined - ;(this.handleWindowBlur as unknown) = undefined - } - - private addActiveModifier = (modifier: KeyboardModifier | undefined): void => { - if (!modifier) { - return - } - - switch (modifier) { - case KeyboardModifier.Meta: { - if (this.isMac) { - this.activeModifiers.add(modifier) - } - break - } - case KeyboardModifier.Ctrl: { - if (!this.isMac) { - this.activeModifiers.add(modifier) - } - break - } - default: { - this.activeModifiers.add(modifier) - break - } - } - } - - private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => { - if (!modifier) { - return - } - - this.activeModifiers.delete(modifier) - } - - public cancelAllKeyboardModifiers = (): void => { - this.activeModifiers.clear() - } - - public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => { - this.addActiveModifier(modifier) - } - - public handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => { - this.removeActiveModifier(modifier) - } - - private handleKeyDown = (event: KeyboardEvent): void => { - this.updateAllModifiersFromEvent(event) - this.notifyObserver(event, KeyboardKeyEvent.Down) - } - - private handleKeyUp = (event: KeyboardEvent): void => { - this.updateAllModifiersFromEvent(event) - this.notifyObserver(event, KeyboardKeyEvent.Up) - } - - private updateAllModifiersFromEvent(event: KeyboardEvent): void { - for (const modifier of Object.values(KeyboardModifier)) { - if (event.getModifierState(modifier)) { - this.addActiveModifier(modifier) - } else { - this.removeActiveModifier(modifier) - } - } - } - - handleWindowBlur = (): void => { - for (const modifier of this.activeModifiers) { - this.activeModifiers.delete(modifier) - } - } - - modifiersForEvent(event: KeyboardEvent): KeyboardModifier[] { - const allModifiers = Object.values(KeyboardModifier) - const eventModifiers = allModifiers.filter((modifier) => { - // For a modifier like ctrlKey, must check both event.ctrlKey and event.key. - // That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true. - const matches = - ((event.ctrlKey || event.key === KeyboardModifier.Ctrl) && modifier === KeyboardModifier.Ctrl) || - ((event.metaKey || event.key === KeyboardModifier.Meta) && modifier === KeyboardModifier.Meta) || - ((event.altKey || event.key === KeyboardModifier.Alt) && modifier === KeyboardModifier.Alt) || - ((event.shiftKey || event.key === KeyboardModifier.Shift) && modifier === KeyboardModifier.Shift) - - return matches - }) - - return eventModifiers - } - - private eventMatchesKeyAndModifiers( - event: KeyboardEvent, - key: KeyboardKey | string | undefined, - modifiers: KeyboardModifier[] = [], - ): boolean { - const eventModifiers = this.modifiersForEvent(event) - if (eventModifiers.length !== modifiers.length) { - return false - } - for (const modifier of modifiers) { - if (!eventModifiers.includes(modifier)) { - return false - } - } - // Modifers match, check key - if (!key) { - return true - } - // In the browser, shift + f results in key 'f', but in Electron, shift + f results in 'F' - // In our case we don't differentiate between the two. - return key.toLowerCase() === event.key.toLowerCase() - } - - notifyObserver(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void { - const target = event.target as HTMLElement - for (const observer of this.observers) { - if (observer.element && event.target !== observer.element) { - continue - } - - if (observer.elements && !observer.elements.includes(target)) { - continue - } - - if (observer.notElement && observer.notElement === event.target) { - continue - } - - if (observer.notElementIds && observer.notElementIds.includes(target.id)) { - continue - } - - if (observer.notTags && observer.notTags.includes(target.tagName)) { - continue - } - - if (this.eventMatchesKeyAndModifiers(event, observer.key, observer.modifiers)) { - const callback = keyEvent === KeyboardKeyEvent.Down ? observer.onKeyDown : observer.onKeyUp - if (callback) { - callback(event) - } - } - } - } - - addKeyObserver(observer: KeyboardObserver): () => void { - this.observers.push(observer) - - const thislessObservers = this.observers - return () => { - removeFromArray(thislessObservers, observer) - } - } -} diff --git a/packages/ui-services/src/Keyboard/KeyboardCommandHandler.ts b/packages/ui-services/src/Keyboard/KeyboardCommandHandler.ts new file mode 100644 index 000000000..a865eeac5 --- /dev/null +++ b/packages/ui-services/src/Keyboard/KeyboardCommandHandler.ts @@ -0,0 +1,12 @@ +import { KeyboardCommand } from './KeyboardCommands' + +export type KeyboardCommandHandler = { + command: KeyboardCommand + onKeyDown?: (event: KeyboardEvent) => boolean | void + onKeyUp?: (event: KeyboardEvent) => boolean | void + element?: HTMLElement + elements?: HTMLElement[] + notElement?: HTMLElement + notElementIds?: string[] + notTags?: string[] +} diff --git a/packages/ui-services/src/Keyboard/KeyboardCommands.ts b/packages/ui-services/src/Keyboard/KeyboardCommands.ts new file mode 100644 index 000000000..68e7ded0d --- /dev/null +++ b/packages/ui-services/src/Keyboard/KeyboardCommands.ts @@ -0,0 +1,26 @@ +export type KeyboardCommand = symbol + +function createKeyboardCommand(type: string): KeyboardCommand { + return Symbol(type) +} + +export const TOGGLE_LIST_PANE_KEYBOARD_COMMAND = createKeyboardCommand('TOGGLE_LIST_PANE_KEYBOARD_COMMAND') +export const TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND = createKeyboardCommand('TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND') +export const CREATE_NEW_NOTE_KEYBOARD_COMMAND = createKeyboardCommand('CREATE_NEW_NOTE_KEYBOARD_COMMAND') +export const NEXT_LIST_ITEM_KEYBOARD_COMMAND = createKeyboardCommand('NEXT_LIST_ITEM_KEYBOARD_COMMAND') +export const PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND = createKeyboardCommand('PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND') +export const SEARCH_KEYBOARD_COMMAND = createKeyboardCommand('SEARCH_KEYBOARD_COMMAND') +export const CANCEL_SEARCH_COMMAND = createKeyboardCommand('CANCEL_SEARCH_COMMAND') +export const SELECT_ALL_ITEMS_KEYBOARD_COMMAND = createKeyboardCommand('SELECT_ALL_ITEMS_KEYBOARD_COMMAND') +export const SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND = createKeyboardCommand('SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND') +export const DELETE_NOTE_KEYBOARD_COMMAND = createKeyboardCommand('DELETE_NOTE_KEYBOARD_COMMAND') +export const TAB_COMMAND = createKeyboardCommand('PLAIN_EDITOR_INSERT_TAB_KEYBOARD_COMMAND') +export const ESCAPE_COMMAND = createKeyboardCommand('ESCAPE_COMMAND') +export const TOGGLE_FOCUS_MODE_COMMAND = createKeyboardCommand('TOGGLE_FOCUS_MODE_COMMAND') +export const CHANGE_EDITOR_COMMAND = createKeyboardCommand('CHANGE_EDITOR_COMMAND') +export const FOCUS_TAGS_INPUT_COMMAND = createKeyboardCommand('FOCUS_TAGS_INPUT_COMMAND') +export const CREATE_NEW_TAG_COMMAND = createKeyboardCommand('CREATE_NEW_TAG_COMMAND') +export const OPEN_NOTE_HISTORY_COMMAND = createKeyboardCommand('OPEN_NOTE_HISTORY_COMMAND') +export const CAPTURE_SAVE_COMMAND = createKeyboardCommand('CAPTURE_SAVE_COMMAND') +export const STAR_NOTE_COMMAND = createKeyboardCommand('STAR_NOTE_COMMAND') +export const PIN_NOTE_COMMAND = createKeyboardCommand('PIN_NOTE_COMMAND') diff --git a/packages/ui-services/src/Keyboard/KeyboardKey.ts b/packages/ui-services/src/Keyboard/KeyboardKey.ts new file mode 100644 index 000000000..ba026027d --- /dev/null +++ b/packages/ui-services/src/Keyboard/KeyboardKey.ts @@ -0,0 +1,12 @@ +export enum KeyboardKey { + Tab = 'Tab', + Backspace = 'Backspace', + Up = 'ArrowUp', + Down = 'ArrowDown', + Left = 'ArrowLeft', + Right = 'ArrowRight', + Enter = 'Enter', + Escape = 'Escape', + Home = 'Home', + End = 'End', +} diff --git a/packages/ui-services/src/Keyboard/KeyboardKeyEvent.ts b/packages/ui-services/src/Keyboard/KeyboardKeyEvent.ts new file mode 100644 index 000000000..4fea36829 --- /dev/null +++ b/packages/ui-services/src/Keyboard/KeyboardKeyEvent.ts @@ -0,0 +1,4 @@ +export enum KeyboardKeyEvent { + Down = 'KeyEventDown', + Up = 'KeyEventUp', +} diff --git a/packages/ui-services/src/Keyboard/KeyboardModifier.ts b/packages/ui-services/src/Keyboard/KeyboardModifier.ts new file mode 100644 index 000000000..1390e0504 --- /dev/null +++ b/packages/ui-services/src/Keyboard/KeyboardModifier.ts @@ -0,0 +1,7 @@ +export enum KeyboardModifier { + Shift = 'Shift', + Ctrl = 'Control', + /** ⌘ key on Mac, ⊞ key on Windows */ + Meta = 'Meta', + Alt = 'Alt', +} diff --git a/packages/ui-services/src/Keyboard/KeyboardService.ts b/packages/ui-services/src/Keyboard/KeyboardService.ts new file mode 100644 index 000000000..3ca5c36f4 --- /dev/null +++ b/packages/ui-services/src/Keyboard/KeyboardService.ts @@ -0,0 +1,203 @@ +import { Environment, Platform } from '@standardnotes/snjs' +import { removeFromArray } from '@standardnotes/utils' +import { eventMatchesKeyAndModifiers } from './eventMatchesKeyAndModifiers' +import { KeyboardCommand } from './KeyboardCommands' +import { KeyboardKeyEvent } from './KeyboardKeyEvent' +import { KeyboardModifier } from './KeyboardModifier' +import { KeyboardCommandHandler } from './KeyboardCommandHandler' +import { KeyboardShortcut, PlatformedKeyboardShortcut } from './KeyboardShortcut' +import { getKeyboardShortcuts } from './getKeyboardShortcuts' + +export class KeyboardService { + readonly activeModifiers = new Set() + private commandHandlers: KeyboardCommandHandler[] = [] + private commandMap = new Map() + + constructor(private platform: Platform, environment: Environment) { + window.addEventListener('keydown', this.handleKeyDown) + window.addEventListener('keyup', this.handleKeyUp) + window.addEventListener('blur', this.handleWindowBlur) + + const shortcuts = getKeyboardShortcuts(platform, environment) + for (const shortcut of shortcuts) { + this.registerShortcut(shortcut) + } + } + + get isMac() { + return this.platform === Platform.MacDesktop || this.platform === Platform.MacWeb + } + + public deinit() { + this.commandHandlers.length = 0 + window.removeEventListener('keydown', this.handleKeyDown) + window.removeEventListener('keyup', this.handleKeyUp) + window.removeEventListener('blur', this.handleWindowBlur) + ;(this.handleKeyDown as unknown) = undefined + ;(this.handleKeyUp as unknown) = undefined + ;(this.handleWindowBlur as unknown) = undefined + } + + private addActiveModifier = (modifier: KeyboardModifier | undefined): void => { + if (!modifier) { + return + } + + switch (modifier) { + case KeyboardModifier.Meta: { + if (this.isMac) { + this.activeModifiers.add(modifier) + } + break + } + case KeyboardModifier.Ctrl: { + if (!this.isMac) { + this.activeModifiers.add(modifier) + } + break + } + default: { + this.activeModifiers.add(modifier) + break + } + } + } + + private removeActiveModifier = (modifier: KeyboardModifier | undefined): void => { + if (!modifier) { + return + } + + this.activeModifiers.delete(modifier) + } + + public cancelAllKeyboardModifiers = (): void => { + this.activeModifiers.clear() + } + + public handleComponentKeyDown = (modifier: KeyboardModifier | undefined): void => { + this.addActiveModifier(modifier) + } + + public handleComponentKeyUp = (modifier: KeyboardModifier | undefined): void => { + this.removeActiveModifier(modifier) + } + + private handleKeyDown = (event: KeyboardEvent): void => { + this.updateAllModifiersFromEvent(event) + + this.handleKeyboardEvent(event, KeyboardKeyEvent.Down) + } + + private handleKeyUp = (event: KeyboardEvent): void => { + this.updateAllModifiersFromEvent(event) + + this.handleKeyboardEvent(event, KeyboardKeyEvent.Up) + } + + private updateAllModifiersFromEvent(event: KeyboardEvent): void { + for (const modifier of Object.values(KeyboardModifier)) { + if (event.getModifierState(modifier)) { + this.addActiveModifier(modifier) + } else { + this.removeActiveModifier(modifier) + } + } + } + + handleWindowBlur = (): void => { + for (const modifier of this.activeModifiers) { + this.activeModifiers.delete(modifier) + } + } + + private handleKeyboardEvent(event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void { + for (const command of this.commandMap.keys()) { + const shortcut = this.commandMap.get(command) + if (!shortcut) { + continue + } + + if (eventMatchesKeyAndModifiers(event, shortcut)) { + if (shortcut.preventDefault) { + event.preventDefault() + } + this.handleCommand(command, event, keyEvent) + } + } + } + + private handleCommand(command: KeyboardCommand, event: KeyboardEvent, keyEvent: KeyboardKeyEvent): void { + const target = event.target as HTMLElement + + for (const observer of this.commandHandlers) { + if (observer.command !== command) { + continue + } + + if (observer.element && event.target !== observer.element) { + continue + } + + if (observer.elements && !observer.elements.includes(target)) { + continue + } + + if (observer.notElement && observer.notElement === event.target) { + continue + } + + if (observer.notElementIds && observer.notElementIds.includes(target.id)) { + continue + } + + if (observer.notTags && observer.notTags.includes(target.tagName)) { + continue + } + + const callback = keyEvent === KeyboardKeyEvent.Down ? observer.onKeyDown : observer.onKeyUp + if (callback) { + const exclusive = callback(event) + if (exclusive) { + return + } + } + } + } + + registerShortcut(shortcut: KeyboardShortcut): void { + this.commandMap.set(shortcut.command, shortcut) + } + + addCommandHandler(observer: KeyboardCommandHandler): () => void { + this.commandHandlers.push(observer) + + const thislessObservers = this.commandHandlers + return () => { + observer.onKeyDown = undefined + observer.onKeyDown = undefined + removeFromArray(thislessObservers, observer) + } + } + + addCommandHandlers(handlers: KeyboardCommandHandler[]): () => void { + const disposers = handlers.map((handler) => this.addCommandHandler(handler)) + return () => { + for (const disposer of disposers) { + disposer() + } + } + } + + keyboardShortcutForCommand(command: KeyboardCommand): PlatformedKeyboardShortcut | undefined { + const shortcut = this.commandMap.get(command) + if (!shortcut) { + return undefined + } + + return { + platform: this.platform, + ...shortcut, + } + } +} diff --git a/packages/ui-services/src/Keyboard/KeyboardShortcut.ts b/packages/ui-services/src/Keyboard/KeyboardShortcut.ts new file mode 100644 index 000000000..392a1f788 --- /dev/null +++ b/packages/ui-services/src/Keyboard/KeyboardShortcut.ts @@ -0,0 +1,20 @@ +import { Platform } from '@standardnotes/snjs' +import { KeyboardCommand } from './KeyboardCommands' +import { KeyboardKey } from './KeyboardKey' +import { KeyboardModifier } from './KeyboardModifier' + +export type KeyboardShortcut = { + command: KeyboardCommand + modifiers?: KeyboardModifier[] + key?: KeyboardKey | string + /** + * Alternative to using key, if the key can be affected by alt + shift. For example, if you want alt + shift + n, + * use code 'KeyN' instead of key 'n', as the modifiers would turn n into '˜' on Mac. + */ + code?: string + preventDefault?: boolean +} + +export type PlatformedKeyboardShortcut = KeyboardShortcut & { + platform: Platform +} diff --git a/packages/ui-services/src/Keyboard/eventMatchesKeyAndModifiers.ts b/packages/ui-services/src/Keyboard/eventMatchesKeyAndModifiers.ts new file mode 100644 index 000000000..96c337595 --- /dev/null +++ b/packages/ui-services/src/Keyboard/eventMatchesKeyAndModifiers.ts @@ -0,0 +1,27 @@ +import { KeyboardShortcut } from './KeyboardShortcut' +import { modifiersForEvent } from './modifiersForEvent' + +export function eventMatchesKeyAndModifiers(event: KeyboardEvent, shortcut: KeyboardShortcut): boolean { + const eventModifiers = modifiersForEvent(event) + const shortcutModifiers = shortcut.modifiers ?? [] + + if (eventModifiers.length !== shortcutModifiers.length) { + return false + } + + for (const modifier of shortcutModifiers) { + if (!eventModifiers.includes(modifier)) { + return false + } + } + + if (!shortcut.key && !shortcut.code) { + return true + } + + if (shortcut.key) { + return shortcut.key.toLowerCase() === event.key.toLowerCase() + } else { + return shortcut.code === event.code + } +} diff --git a/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts new file mode 100644 index 000000000..cc69d5b8a --- /dev/null +++ b/packages/ui-services/src/Keyboard/getKeyboardShortcuts.ts @@ -0,0 +1,136 @@ +import { Environment, Platform } from '@standardnotes/snjs' +import { isMacPlatform } from './platformCheck' +import { + CREATE_NEW_NOTE_KEYBOARD_COMMAND, + TOGGLE_LIST_PANE_KEYBOARD_COMMAND, + TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND, + NEXT_LIST_ITEM_KEYBOARD_COMMAND, + PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND, + SEARCH_KEYBOARD_COMMAND, + SELECT_ALL_ITEMS_KEYBOARD_COMMAND, + SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND, + DELETE_NOTE_KEYBOARD_COMMAND, + TAB_COMMAND, + ESCAPE_COMMAND, + CANCEL_SEARCH_COMMAND, + TOGGLE_FOCUS_MODE_COMMAND, + CHANGE_EDITOR_COMMAND, + FOCUS_TAGS_INPUT_COMMAND, + CREATE_NEW_TAG_COMMAND, + OPEN_NOTE_HISTORY_COMMAND, + CAPTURE_SAVE_COMMAND, + STAR_NOTE_COMMAND, + PIN_NOTE_COMMAND, +} from './KeyboardCommands' +import { KeyboardKey } from './KeyboardKey' +import { KeyboardModifier } from './KeyboardModifier' +import { KeyboardShortcut } from './KeyboardShortcut' + +export function getKeyboardShortcuts(platform: Platform, _environment: Environment): KeyboardShortcut[] { + const isMac = isMacPlatform(platform) + + const primaryModifier = isMac ? KeyboardModifier.Meta : KeyboardModifier.Ctrl + + return [ + { + command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND, + key: 'l', + modifiers: [primaryModifier, KeyboardModifier.Shift], + }, + { + command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND, + key: 'e', + modifiers: [primaryModifier, KeyboardModifier.Shift], + }, + { + command: CREATE_NEW_NOTE_KEYBOARD_COMMAND, + code: 'KeyN', + modifiers: [KeyboardModifier.Alt, KeyboardModifier.Shift], + }, + { + command: NEXT_LIST_ITEM_KEYBOARD_COMMAND, + key: KeyboardKey.Down, + }, + { + command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND, + key: KeyboardKey.Up, + }, + { + command: SEARCH_KEYBOARD_COMMAND, + code: 'KeyF', + modifiers: [KeyboardModifier.Alt, KeyboardModifier.Shift], + }, + { + command: CANCEL_SEARCH_COMMAND, + key: KeyboardKey.Escape, + }, + { + command: SELECT_ALL_ITEMS_KEYBOARD_COMMAND, + key: 'a', + modifiers: [primaryModifier], + }, + { + command: SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND, + modifiers: [KeyboardModifier.Alt], + }, + { + command: DELETE_NOTE_KEYBOARD_COMMAND, + key: KeyboardKey.Backspace, + modifiers: [primaryModifier], + }, + { + command: TAB_COMMAND, + key: KeyboardKey.Tab, + }, + { + command: ESCAPE_COMMAND, + key: KeyboardKey.Escape, + }, + { + command: TOGGLE_FOCUS_MODE_COMMAND, + key: 'f', + modifiers: [primaryModifier, KeyboardModifier.Shift], + }, + { + command: CHANGE_EDITOR_COMMAND, + key: '/', + modifiers: [primaryModifier, KeyboardModifier.Shift], + preventDefault: true, + }, + { + command: FOCUS_TAGS_INPUT_COMMAND, + code: 'KeyT', + modifiers: [primaryModifier, KeyboardModifier.Alt], + preventDefault: true, + }, + { + command: CREATE_NEW_TAG_COMMAND, + code: 'KeyN', + modifiers: [primaryModifier, KeyboardModifier.Alt], + }, + { + command: OPEN_NOTE_HISTORY_COMMAND, + key: 'h', + modifiers: [primaryModifier, KeyboardModifier.Shift], + preventDefault: true, + }, + { + command: CAPTURE_SAVE_COMMAND, + key: 's', + modifiers: [primaryModifier], + preventDefault: true, + }, + { + command: STAR_NOTE_COMMAND, + key: 's', + modifiers: [primaryModifier, KeyboardModifier.Shift], + preventDefault: true, + }, + { + command: PIN_NOTE_COMMAND, + key: 'p', + modifiers: [primaryModifier, KeyboardModifier.Shift], + preventDefault: true, + }, + ] +} diff --git a/packages/ui-services/src/Keyboard/keyboardCharacterForModifier.ts b/packages/ui-services/src/Keyboard/keyboardCharacterForModifier.ts new file mode 100644 index 000000000..c1825f418 --- /dev/null +++ b/packages/ui-services/src/Keyboard/keyboardCharacterForModifier.ts @@ -0,0 +1,22 @@ +import { Platform } from '@standardnotes/snjs' +import { KeyboardModifier } from './KeyboardModifier' + +function isMacPlatform(platform: Platform) { + return platform === Platform.MacDesktop || platform === Platform.MacWeb +} + +export function keyboardCharacterForModifier(modifier: KeyboardModifier, platform: Platform) { + const isMac = isMacPlatform(platform) + + if (modifier === KeyboardModifier.Meta) { + return isMac ? '⌘' : '⊞' + } else if (modifier === KeyboardModifier.Ctrl) { + return isMac ? '⌃' : 'Ctrl' + } else if (modifier === KeyboardModifier.Alt) { + return isMac ? '⌥' : 'Alt' + } else if (modifier === KeyboardModifier.Shift) { + return isMac ? '⇧' : 'Shift' + } else { + return KeyboardModifier[modifier] + } +} diff --git a/packages/ui-services/src/Keyboard/keyboardStringForShortcut.ts b/packages/ui-services/src/Keyboard/keyboardStringForShortcut.ts new file mode 100644 index 000000000..ba3bf297a --- /dev/null +++ b/packages/ui-services/src/Keyboard/keyboardStringForShortcut.ts @@ -0,0 +1,29 @@ +import { isMacPlatform } from '@standardnotes/ui-services' +import { keyboardCharacterForModifier } from './keyboardCharacterForModifier' +import { PlatformedKeyboardShortcut } from './KeyboardShortcut' + +function stringForCode(code = ''): string { + return code.replace('Key', '').replace('Digit', '') +} + +export function keyboardStringForShortcut(shortcut: PlatformedKeyboardShortcut | undefined) { + if (!shortcut) { + return '' + } + + const key = shortcut.key?.toUpperCase() || stringForCode(shortcut.code) + + if (!shortcut.modifiers || shortcut.modifiers.length === 0) { + return key + } + + const modifierCharacters = shortcut.modifiers.map((modifier) => + keyboardCharacterForModifier(modifier, shortcut.platform), + ) + + if (isMacPlatform(shortcut.platform)) { + return `${modifierCharacters.join('')}${key}` + } else { + return `${modifierCharacters.join('+')}+${key}` + } +} diff --git a/packages/ui-services/src/Keyboard/modifiersForEvent.ts b/packages/ui-services/src/Keyboard/modifiersForEvent.ts new file mode 100644 index 000000000..622e44f55 --- /dev/null +++ b/packages/ui-services/src/Keyboard/modifiersForEvent.ts @@ -0,0 +1,18 @@ +import { KeyboardModifier } from './KeyboardModifier' + +export function modifiersForEvent(event: KeyboardEvent): KeyboardModifier[] { + const allModifiers = Object.values(KeyboardModifier) + const eventModifiers = allModifiers.filter((modifier) => { + // For a modifier like ctrlKey, must check both event.ctrlKey and event.key. + // That's because on keyup, event.ctrlKey would be false, but event.key == Control would be true. + const matches = + ((event.ctrlKey || event.key === KeyboardModifier.Ctrl) && modifier === KeyboardModifier.Ctrl) || + ((event.metaKey || event.key === KeyboardModifier.Meta) && modifier === KeyboardModifier.Meta) || + ((event.altKey || event.key === KeyboardModifier.Alt) && modifier === KeyboardModifier.Alt) || + ((event.shiftKey || event.key === KeyboardModifier.Shift) && modifier === KeyboardModifier.Shift) + + return matches + }) + + return eventModifiers +} diff --git a/packages/ui-services/src/Keyboard/platformCheck.ts b/packages/ui-services/src/Keyboard/platformCheck.ts new file mode 100644 index 000000000..aefcb5c5f --- /dev/null +++ b/packages/ui-services/src/Keyboard/platformCheck.ts @@ -0,0 +1,9 @@ +import { Platform } from '@standardnotes/snjs' + +export function isMacPlatform(platform: Platform) { + return platform === Platform.MacDesktop || platform === Platform.MacWeb +} + +export function isWindowsPlatform(platform: Platform) { + return platform === Platform.WindowsDesktop || platform === Platform.WindowsWeb +} diff --git a/packages/ui-services/src/index.ts b/packages/ui-services/src/index.ts index 50ed3a9e0..8526cc7c0 100644 --- a/packages/ui-services/src/index.ts +++ b/packages/ui-services/src/index.ts @@ -1,7 +1,14 @@ export * from './Alert/Functions' export * from './Alert/WebAlertService' export * from './Archive/ArchiveManager' -export * from './IO/IOService' +export * from './Keyboard/KeyboardService' +export * from './Keyboard/KeyboardShortcut' +export * from './Keyboard/KeyboardCommands' +export * from './Keyboard/platformCheck' +export * from './Keyboard/KeyboardKey' +export * from './Keyboard/KeyboardModifier' +export * from './Keyboard/keyboardCharacterForModifier' +export * from './Keyboard/keyboardStringForShortcut' export * from './Preferences/PreferenceId' export * from './Route/Params/DemoParams' export * from './Route/Params/OnboardingParams' diff --git a/packages/web/src/components/assets/org.standardnotes.theme-autobiography/index.css b/packages/web/src/components/assets/org.standardnotes.theme-autobiography/index.css index fead15fe8..f44bb6d0a 100644 --- a/packages/web/src/components/assets/org.standardnotes.theme-autobiography/index.css +++ b/packages/web/src/components/assets/org.standardnotes.theme-autobiography/index.css @@ -24,7 +24,7 @@ --sn-stylekit-accessory-tint-color-1: #941648; - --sn-stylekit-shadow-color: var(--background-2); + --sn-stylekit-shadow-color: #c4b89f; --sn-stylekit-background-color: var(--background-1); --sn-stylekit-foreground-color: var(--foreground-color); --sn-stylekit-border-color: var(--border-color); diff --git a/packages/web/src/javascripts/Application/Application.ts b/packages/web/src/javascripts/Application/Application.ts index 7413e5545..267afe706 100644 --- a/packages/web/src/javascripts/Application/Application.ts +++ b/packages/web/src/javascripts/Application/Application.ts @@ -29,7 +29,7 @@ import { DesktopManager } from './Device/DesktopManager' import { ArchiveManager, AutolockService, - IOService, + KeyboardService, RouteService, RouteServiceInterface, ThemeManager, @@ -85,19 +85,17 @@ export class WebApplication extends SNApplication implements WebApplicationInter this.itemControllerGroup = new ItemGroupController(this) this.routeService = new RouteService(this, internalEventBus) - const viewControllerManager = new ViewControllerManager(this, deviceInterface) - const archiveService = new ArchiveManager(this) - const io = new IOService(platform === Platform.MacWeb || platform === Platform.MacDesktop) - const themeService = new ThemeManager(this, internalEventBus) - - this.setWebServices({ - viewControllerManager, - archiveService, - desktopService: isDesktopDevice(deviceInterface) ? new DesktopManager(this, deviceInterface) : undefined, - io, - autolockService: this.isNativeMobileWeb() ? undefined : new AutolockService(this, internalEventBus), - themeService, - }) + this.webServices = {} as WebServices + this.webServices.keyboardService = new KeyboardService(platform, this.environment) + this.webServices.archiveService = new ArchiveManager(this) + this.webServices.themeService = new ThemeManager(this, internalEventBus) + this.webServices.autolockService = this.isNativeMobileWeb() + ? undefined + : new AutolockService(this, internalEventBus) + this.webServices.desktopService = isDesktopDevice(deviceInterface) + ? new DesktopManager(this, deviceInterface) + : undefined + this.webServices.viewControllerManager = new ViewControllerManager(this, deviceInterface) if (this.isNativeMobileWeb()) { this.mobileWebReceiver = new MobileWebReceiver(this) @@ -152,10 +150,6 @@ export class WebApplication extends SNApplication implements WebApplicationInter } } - setWebServices(services: WebServices): void { - this.webServices = services - } - public addWebEventObserver(observer: WebEventObserver): () => void { this.webEventObservers.push(observer) @@ -194,6 +188,10 @@ export class WebApplication extends SNApplication implements WebApplicationInter return this.webServices.archiveService } + public get paneController() { + return this.webServices.viewControllerManager.paneController + } + public get desktopDevice(): DesktopDeviceInterface | undefined { if (isDesktopDevice(this.deviceInterface)) { return this.deviceInterface @@ -221,8 +219,8 @@ export class WebApplication extends SNApplication implements WebApplicationInter return this.webServices.themeService } - public get io() { - return this.webServices.io + public get keyboardService() { + return this.webServices.keyboardService } async checkForSecurityUpdate() { diff --git a/packages/web/src/javascripts/Application/WebServices.ts b/packages/web/src/javascripts/Application/WebServices.ts index 8e7fa5906..727160ff8 100644 --- a/packages/web/src/javascripts/Application/WebServices.ts +++ b/packages/web/src/javascripts/Application/WebServices.ts @@ -1,6 +1,6 @@ import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { DesktopManager } from './Device/DesktopManager' -import { ArchiveManager, AutolockService, IOService, ThemeManager } from '@standardnotes/ui-services' +import { ArchiveManager, AutolockService, KeyboardService, ThemeManager } from '@standardnotes/ui-services' export type WebServices = { viewControllerManager: ViewControllerManager @@ -8,5 +8,5 @@ export type WebServices = { autolockService?: AutolockService archiveService: ArchiveManager themeService: ThemeManager - io: IOService + keyboardService: KeyboardService } diff --git a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx index 5b36f2275..7ab931310 100644 --- a/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx +++ b/packages/web/src/javascripts/Components/ApplicationView/ApplicationView.tsx @@ -30,6 +30,7 @@ import ConfirmDeleteAccountContainer from '@/Components/ConfirmDeleteAccountModa import DarkModeHandler from '../DarkModeHandler/DarkModeHandler' import ApplicationProvider from './ApplicationProvider' import { ErrorBoundary } from '@/Utils/ErrorBoundary' +import CommandProvider from './CommandProvider' type Props = { application: WebApplication @@ -196,85 +197,90 @@ const ApplicationView: FunctionComponent = ({ application, mainApplicatio return ( - - - - -
-
- - - + + + + +
+
+ + + + + + + +
+ + <> +
+ + + - - - - + + + {renderChallenges()} + + <> + + + + + + + + + +
- - <> -
- - - - - - {renderChallenges()} - - <> - - - - - - - - - - -
- - - + + + + ) } diff --git a/packages/web/src/javascripts/Components/ApplicationView/CommandProvider.tsx b/packages/web/src/javascripts/Components/ApplicationView/CommandProvider.tsx new file mode 100644 index 000000000..6cf7eba34 --- /dev/null +++ b/packages/web/src/javascripts/Components/ApplicationView/CommandProvider.tsx @@ -0,0 +1,36 @@ +import { ReactNode, createContext, useContext, memo } from 'react' + +import { observer } from 'mobx-react-lite' +import { KeyboardService } from '@standardnotes/ui-services' + +const CommandServiceContext = createContext(undefined) + +export const useCommandService = () => { + const value = useContext(CommandServiceContext) + + if (!value) { + throw new Error('Component must be a child of ') + } + + return value +} + +type ChildrenProps = { + children: ReactNode +} + +type ProviderProps = { + service: KeyboardService +} & ChildrenProps + +const MemoizedChildren = memo(({ children }: ChildrenProps) => <>{children}) + +const CommandServiceProvider = ({ service, children }: ProviderProps) => { + return ( + + + + ) +} + +export default observer(CommandServiceProvider) diff --git a/packages/web/src/javascripts/Components/Button/RoundIconButton.tsx b/packages/web/src/javascripts/Components/Button/RoundIconButton.tsx index abafba18c..eacdd2b0f 100644 --- a/packages/web/src/javascripts/Components/Button/RoundIconButton.tsx +++ b/packages/web/src/javascripts/Components/Button/RoundIconButton.tsx @@ -13,7 +13,7 @@ type Props = { } const RoundIconButton = forwardRef( - ({ onClick, className, icon: iconType, iconClassName, id }: Props, ref: ForwardedRef) => { + ({ onClick, className, icon: iconType, iconClassName, id, label }: Props, ref: ForwardedRef) => { const click: MouseEventHandler = (e) => { e.preventDefault() onClick() @@ -29,6 +29,8 @@ const RoundIconButton = forwardRef( onClick={click} ref={ref} id={id} + title={label} + aria-label={label} > diff --git a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx index f9d8a46a5..42dd7f8ed 100644 --- a/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx +++ b/packages/web/src/javascripts/Components/ChangeEditor/ChangeEditorButton.tsx @@ -1,11 +1,12 @@ import { WebApplication } from '@/Application/Application' import { ViewControllerManager } from '@/Controllers/ViewControllerManager' import { observer } from 'mobx-react-lite' -import { FunctionComponent, useCallback, useRef, useState } from 'react' +import { FunctionComponent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import ChangeEditorMenu from './ChangeEditorMenu' import Popover from '../Popover/Popover' import RoundIconButton from '../Button/RoundIconButton' import { getIconAndTintForNoteType } from '@/Utils/Items/Icons/getIconAndTintForNoteType' +import { CHANGE_EDITOR_COMMAND, keyboardStringForShortcut } from '@standardnotes/ui-services' type Props = { application: WebApplication @@ -40,10 +41,24 @@ const ChangeEditorButton: FunctionComponent = ({ setIsClickOutsideDisabled(true) }, []) + useEffect(() => { + return application.keyboardService.addCommandHandler({ + command: CHANGE_EDITOR_COMMAND, + onKeyDown: () => { + void toggleMenu() + }, + }) + }, [application, toggleMenu]) + + const shortcut = useMemo( + () => application.keyboardService.keyboardShortcutForCommand(CHANGE_EDITOR_COMMAND), + [application], + ) + return (
= ({ application, onLoad, compone const removeActionObserver = componentViewer.addActionObserver((action, data) => { switch (action) { case ComponentAction.KeyDown: - application.io.handleComponentKeyDown(data.keyboardModifier) + application.keyboardService.handleComponentKeyDown(data.keyboardModifier) break case ComponentAction.KeyUp: - application.io.handleComponentKeyUp(data.keyboardModifier) + application.keyboardService.handleComponentKeyUp(data.keyboardModifier) break case ComponentAction.Click: application.getViewControllerManager().notesController.setContextMenuOpen(false) diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx index b53002907..eceac56be 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentList.tsx @@ -9,7 +9,7 @@ import { ItemListController } from '@/Controllers/ItemList/ItemListController' import { FilesController } from '@/Controllers/FilesController' import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { NotesController } from '@/Controllers/NotesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' import { ElementIds } from '@/Constants/ElementIDs' import { classNames } from '@/Utils/ConcatenateClassNames' import { ContentType, SNTag } from '@standardnotes/snjs' diff --git a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx index af7d77570..1dd175139 100644 --- a/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx +++ b/packages/web/src/javascripts/Components/ContentListView/ContentListView.tsx @@ -1,4 +1,12 @@ -import { KeyboardKey, KeyboardModifier } from '@standardnotes/ui-services' +import { + CANCEL_SEARCH_COMMAND, + CREATE_NEW_NOTE_KEYBOARD_COMMAND, + keyboardStringForShortcut, + NEXT_LIST_ITEM_KEYBOARD_COMMAND, + PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND, + SEARCH_KEYBOARD_COMMAND, + SELECT_ALL_ITEMS_KEYBOARD_COMMAND, +} from '@standardnotes/ui-services' import { WebApplication } from '@/Application/Application' import { PANEL_NAME_NOTES } from '@/Constants/Constants' import { FileItem, PrefKey } from '@standardnotes/snjs' @@ -12,7 +20,7 @@ import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { FilesController } from '@/Controllers/FilesController' import { NoAccountWarningController } from '@/Controllers/NoAccountWarningController' -import { NotesController } from '@/Controllers/NotesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' import { AccountMenuController } from '@/Controllers/AccountMenu/AccountMenuController' import { ElementIds } from '@/Constants/ElementIDs' import ContentListHeader from './Header/ContentListHeader' @@ -110,7 +118,6 @@ const ContentListView: FunctionComponent = ({ panelWidth, renderedItems, items, - searchBarElement, isCurrentNoteTemplate, } = itemListController @@ -142,80 +149,69 @@ const ContentListView: FunctionComponent = ({ }, [isFilesSmartView, filesController, createNewNote, toggleAppPane, application]) useEffect(() => { + const searchBarElement = document.getElementById(ElementIds.SearchBar) /** * In the browser we're not allowed to override cmd/ctrl + n, so we have to * use Control modifier as well. These rules don't apply to desktop, but * probably better to be consistent. */ - const disposeNewNoteKeyObserver = application.io.addKeyObserver({ - key: 'n', - modifiers: [KeyboardModifier.Meta, KeyboardModifier.Ctrl], - onKeyDown: (event) => { - event.preventDefault() - void addNewItem() + return application.keyboardService.addCommandHandlers([ + { + command: CREATE_NEW_NOTE_KEYBOARD_COMMAND, + onKeyDown: (event) => { + event.preventDefault() + void addNewItem() + }, }, - }) - - const disposeNextNoteKeyObserver = application.io.addKeyObserver({ - key: KeyboardKey.Down, - elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])], - onKeyDown: () => { - if (searchBarElement === document.activeElement) { - searchBarElement?.blur() - } - selectNextItem() + { + command: NEXT_LIST_ITEM_KEYBOARD_COMMAND, + elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])], + onKeyDown: () => { + if (searchBarElement === document.activeElement) { + searchBarElement?.blur() + } + selectNextItem() + }, }, - }) - - const disposePreviousNoteKeyObserver = application.io.addKeyObserver({ - key: KeyboardKey.Up, - element: document.body, - onKeyDown: () => { - selectPreviousItem() + { + command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND, + element: document.body, + onKeyDown: () => { + selectPreviousItem() + }, }, - }) - - const disposeSearchKeyObserver = application.io.addKeyObserver({ - key: 'f', - modifiers: [KeyboardModifier.Meta, KeyboardModifier.Shift], - onKeyDown: () => { - if (searchBarElement) { - searchBarElement.focus() - } + { + command: SEARCH_KEYBOARD_COMMAND, + onKeyDown: (event) => { + if (searchBarElement) { + event.preventDefault() + searchBarElement.focus() + } + }, }, - }) - - const disposeSelectAllKeyObserver = application.io.addKeyObserver({ - key: 'a', - modifiers: [KeyboardModifier.Ctrl], - onKeyDown: (event) => { - const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`) - - if (!isTargetInsideContentList) { - return - } - - event.preventDefault() - selectionController.selectAll() + { + command: CANCEL_SEARCH_COMMAND, + onKeyDown: () => { + if (searchBarElement) { + searchBarElement.blur() + } + }, }, - }) + { + command: SELECT_ALL_ITEMS_KEYBOARD_COMMAND, + onKeyDown: (event) => { + const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`) - return () => { - disposeNewNoteKeyObserver() - disposeNextNoteKeyObserver() - disposePreviousNoteKeyObserver() - disposeSearchKeyObserver() - disposeSelectAllKeyObserver() - } - }, [ - addNewItem, - application.io, - createNewNote, - searchBarElement, - selectNextItem, - selectPreviousItem, - selectionController, - ]) + if (!isTargetInsideContentList) { + return + } + + event.preventDefault() + selectionController.selectAll() + }, + }, + ]) + }, [addNewItem, application.keyboardService, createNewNote, selectNextItem, selectPreviousItem, selectionController]) const panelResizeFinishCallback: ResizeFinishCallback = useCallback( (width, _lastLeft, _isMaxWidth, isCollapsed) => { @@ -229,11 +225,17 @@ const ContentListView: FunctionComponent = ({ [application, selectedAsTag, navigationController], ) - const addButtonLabel = useMemo( - () => (isFilesSmartView ? 'Upload file' : 'Create a new note in the selected tag'), - [isFilesSmartView], + const shortcutForCreate = useMemo( + () => application.keyboardService.keyboardShortcutForCommand(CREATE_NEW_NOTE_KEYBOARD_COMMAND), + [application], ) + const addButtonLabel = useMemo(() => { + return isFilesSmartView + ? 'Upload file' + : `Create a new note in the selected tag (${shortcutForCreate && keyboardStringForShortcut(shortcutForCreate)})` + }, [isFilesSmartView, shortcutForCreate]) + const matchesMediumBreakpoint = useMediaQuery(MediaQueryBreakpoints.md) const matchesXLBreakpoint = useMediaQuery(MediaQueryBreakpoints.xl) const isTabletScreenSize = matchesMediumBreakpoint && !matchesXLBreakpoint diff --git a/packages/web/src/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts b/packages/web/src/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts index cf6646ece..8a0df8437 100644 --- a/packages/web/src/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts +++ b/packages/web/src/javascripts/Components/ContentListView/Types/AbstractListItemProps.ts @@ -1,6 +1,6 @@ import { WebApplication } from '@/Application/Application' import { FilesController } from '@/Controllers/FilesController' -import { NotesController } from '@/Controllers/NotesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' import { SortableItem, SNTag, Uuids } from '@standardnotes/snjs' import { ListableContentItem } from './ListableContentItem' diff --git a/packages/web/src/javascripts/Components/KeyboardShortcutIndicator/KeyboardShortcutIndicator.tsx b/packages/web/src/javascripts/Components/KeyboardShortcutIndicator/KeyboardShortcutIndicator.tsx new file mode 100644 index 000000000..43c67153e --- /dev/null +++ b/packages/web/src/javascripts/Components/KeyboardShortcutIndicator/KeyboardShortcutIndicator.tsx @@ -0,0 +1,38 @@ +import { PlatformedKeyboardShortcut, keyboardCharacterForModifier, isMacPlatform } from '@standardnotes/ui-services' + +type Props = { + shortcut: PlatformedKeyboardShortcut + className?: string +} + +export const KeyboardShortcutIndicator = ({ shortcut, className }: Props) => { + const modifiers = shortcut.modifiers || [] + const primaryKey = (shortcut.key || '').toUpperCase() + const addPluses = !isMacPlatform(shortcut.platform) + const spacingClass = addPluses ? '' : 'ml-0.5' + + const keys: string[] = [] + modifiers.forEach((modifier, index) => { + keys.push(keyboardCharacterForModifier(modifier, shortcut.platform)) + + if (addPluses && (primaryKey || index !== modifiers.length - 1)) { + keys.push('+') + } + }) + + if (primaryKey) { + keys.push(primaryKey) + } + + return ( +
+ {keys.map((key, index) => { + return ( +
+ {key} +
+ ) + })} +
+ ) +} diff --git a/packages/web/src/javascripts/Components/LinkedItems/ItemLinkAutocompleteInput.tsx b/packages/web/src/javascripts/Components/LinkedItems/ItemLinkAutocompleteInput.tsx index a205b70c7..d97a158b5 100644 --- a/packages/web/src/javascripts/Components/LinkedItems/ItemLinkAutocompleteInput.tsx +++ b/packages/web/src/javascripts/Components/LinkedItems/ItemLinkAutocompleteInput.tsx @@ -26,9 +26,16 @@ type Props = { focusPreviousItem: () => void focusedId: string | undefined setFocusedId: (id: string) => void + hoverLabel?: string } -const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focusedId, setFocusedId }: Props) => { +const ItemLinkAutocompleteInput = ({ + linkingController, + focusPreviousItem, + focusedId, + setFocusedId, + hoverLabel, +}: Props) => { const application = useApplication() const { tags, linkItemToSelectedItem, createAndAddNewTag, isEntitledToNoteLinking, activeItem } = linkingController @@ -128,6 +135,8 @@ const ItemLinkAutocompleteInput = ({ linkingController, focusPreviousItem, focus onKeyDown={onKeyDown} id={ElementIds.ItemLinkAutocompleteInput} autoComplete="off" + title={hoverLabel} + aria-label={hoverLabel} /> {areSearchResultsVisible && ( { const { toggleAppPane } = useResponsiveAppPane() + + const commandService = useCommandService() + const { allItemLinks, notesLinkingToActiveItem, @@ -24,6 +29,23 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => { activateItem, } = linkingController + useEffect(() => { + return commandService.addCommandHandler({ + command: FOCUS_TAGS_INPUT_COMMAND, + onKeyDown: () => { + const input = document.getElementById(ElementIds.ItemLinkAutocompleteInput) + if (input) { + input.focus() + } + }, + }) + }, [commandService]) + + const shortcut = useMemo( + () => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(FOCUS_TAGS_INPUT_COMMAND)), + [commandService], + ) + const [focusedId, setFocusedId] = useState() const focusableIds = allItemLinks .map((link) => link.id) @@ -75,7 +97,7 @@ const LinkedItemBubblesContainer = ({ linkingController }: Props) => { return ( ) diff --git a/packages/web/src/javascripts/Components/Menu/MenuItem.tsx b/packages/web/src/javascripts/Components/Menu/MenuItem.tsx index 9258a5333..482444dfd 100644 --- a/packages/web/src/javascripts/Components/Menu/MenuItem.tsx +++ b/packages/web/src/javascripts/Components/Menu/MenuItem.tsx @@ -7,6 +7,8 @@ import { FOCUSABLE_BUT_NOT_TABBABLE } from '@/Constants/Constants' import { MenuItemType } from './MenuItemType' import RadioIndicator from '../Radio/RadioIndicator' import { classNames } from '@/Utils/ConcatenateClassNames' +import { PlatformedKeyboardShortcut } from '@standardnotes/ui-services' +import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator' type MenuItemProps = { children: ReactNode @@ -20,6 +22,7 @@ type MenuItemProps = { iconClassName?: string tabIndex?: number disabled?: boolean + shortcut?: PlatformedKeyboardShortcut } const MenuItem = forwardRef( @@ -36,6 +39,7 @@ const MenuItem = forwardRef( iconClassName, tabIndex, disabled, + shortcut, }: MenuItemProps, ref: Ref, ) => { @@ -58,7 +62,10 @@ const MenuItem = forwardRef( aria-checked={checked} > {children} - +
+ {shortcut && } + +
) : ( @@ -78,6 +85,7 @@ const MenuItem = forwardRef( onBlur={onBlur} {...(type === MenuItemType.RadioButton ? { 'aria-checked': checked } : {})} > + {shortcut && } {type === MenuItemType.IconButton && icon ? : null} {type === MenuItemType.RadioButton && typeof checked === 'boolean' ? ( diff --git a/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx b/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx index 9dcde7f9d..65bd64840 100644 --- a/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx +++ b/packages/web/src/javascripts/Components/MultipleSelectedNotes/MultipleSelectedNotes.tsx @@ -6,7 +6,7 @@ import PinNoteButton from '@/Components/PinNoteButton/PinNoteButton' import Button from '../Button/Button' import { useCallback } from 'react' import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { NotesController } from '@/Controllers/NotesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' import { LinkingController } from '@/Controllers/LinkingController' diff --git a/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx b/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx index 6571a0590..dfddabf23 100644 --- a/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx +++ b/packages/web/src/javascripts/Components/NoteView/NoteStatusIndicator.tsx @@ -29,7 +29,7 @@ const IndicatorWithTooltip = ({ children: ReactNode animateIcon?: boolean }) => ( -
+
{renderHeaderOptions && ( -
+
( return } - tabObserverDisposer.current = application.io.addKeyObserver({ + tabObserverDisposer.current = application.keyboardService.addCommandHandler({ element: editor, - key: KeyboardKey.Tab, + command: TAB_COMMAND, onKeyDown: (event) => { if (document.hidden || note.current.locked || event.shiftKey) { return diff --git a/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx b/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx index a7f41580f..1e607ed3e 100644 --- a/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx +++ b/packages/web/src/javascripts/Components/NotesContextMenu/NotesContextMenu.tsx @@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite' import NotesOptions from '@/Components/NotesOptions/NotesOptions' import { useRef } from 'react' import { WebApplication } from '@/Application/Application' -import { NotesController } from '@/Controllers/NotesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' import { NavigationController } from '@/Controllers/Navigation/NavigationController' import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' import Popover from '../Popover/Popover' diff --git a/packages/web/src/javascripts/Components/NotesOptions/AddTagOption.tsx b/packages/web/src/javascripts/Components/NotesOptions/AddTagOption.tsx index 4e57af6a8..c47038a0e 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/AddTagOption.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/AddTagOption.tsx @@ -2,7 +2,7 @@ import { observer } from 'mobx-react-lite' import { FunctionComponent, useCallback, useRef, useState } from 'react' import Icon from '@/Components/Icon/Icon' import { NavigationController } from '@/Controllers/Navigation/NavigationController' -import { NotesController } from '@/Controllers/NotesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' import { KeyboardKey } from '@standardnotes/ui-services' import Popover from '../Popover/Popover' import { IconType } from '@standardnotes/snjs' diff --git a/packages/web/src/javascripts/Components/NotesOptions/ChangeEditorOption.tsx b/packages/web/src/javascripts/Components/NotesOptions/ChangeEditorOption.tsx index c196221cf..7f8134385 100644 --- a/packages/web/src/javascripts/Components/NotesOptions/ChangeEditorOption.tsx +++ b/packages/web/src/javascripts/Components/NotesOptions/ChangeEditorOption.tsx @@ -1,10 +1,11 @@ -import { KeyboardKey } from '@standardnotes/ui-services' +import { CHANGE_EDITOR_COMMAND, KeyboardKey } from '@standardnotes/ui-services' import { WebApplication } from '@/Application/Application' import { SNNote } from '@standardnotes/snjs' -import { FunctionComponent, useCallback, useRef, useState } from 'react' +import { FunctionComponent, useCallback, useMemo, useRef, useState } from 'react' import Icon from '@/Components/Icon/Icon' import ChangeEditorMenu from '@/Components/ChangeEditor/ChangeEditorMenu' import Popover from '../Popover/Popover' +import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator' type ChangeEditorOptionProps = { application: WebApplication @@ -27,6 +28,11 @@ const ChangeEditorOption: FunctionComponent = ({ setIsOpen((isOpen) => !isOpen) }, []) + const shortcut = useMemo( + () => application.keyboardService.keyboardShortcutForCommand(CHANGE_EDITOR_COMMAND), + [application], + ) + return (
- +
+ {shortcut && } + +
void @@ -216,8 +222,8 @@ const NotesOptions = ({ ) useEffect(() => { - const removeAltKeyObserver = application.io.addKeyObserver({ - modifiers: [KeyboardModifier.Alt], + const removeAltKeyObserver = application.keyboardService.addCommandHandler({ + command: SHOW_HIDDEN_OPTIONS_KEYBOARD_COMMAND, onKeyDown: () => { setAltKeyDown(true) }, @@ -274,6 +280,21 @@ const NotesOptions = ({ historyModalController.openModal(notesController.firstSelectedNote) }, [historyModalController, notesController.firstSelectedNote]) + const historyShortcut = useMemo( + () => application.keyboardService.keyboardShortcutForCommand(OPEN_NOTE_HISTORY_COMMAND), + [application], + ) + + const pinShortcut = useMemo( + () => application.keyboardService.keyboardShortcutForCommand(PIN_NOTE_COMMAND), + [application], + ) + + const starShortcut = useMemo( + () => application.keyboardService.keyboardShortcutForCommand(STAR_NOTE_COMMAND), + [application], + ) + const unauthorized = notes.some((note) => !application.isAuthorizedToRenderItem(note)) if (unauthorized) { return @@ -295,8 +316,13 @@ const NotesOptions = ({ {notes.length === 1 && ( <> @@ -364,8 +390,13 @@ const NotesOptions = ({ notesController.setStarSelectedNotes(!starred) }} > - - {starred ? 'Unstar' : 'Star'} +
+ + + {starred ? 'Unstar' : 'Star'} + + {starShortcut && } +
{unpinned && ( @@ -375,8 +406,13 @@ const NotesOptions = ({ notesController.setPinSelectedNotes(true) }} > - - Pin to top +
+ + + Pin to top + + {pinShortcut && } +
)} {pinned && ( @@ -386,8 +422,13 @@ const NotesOptions = ({ notesController.setPinSelectedNotes(false) }} > - - Unpin +
+ + + Unpin + + {pinShortcut && } +
)} ) } diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/PanelSettingsSection.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/PanelSettingsSection.tsx index 9a5f6e1c1..60a13fc7e 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/PanelSettingsSection.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/PanelSettingsSection.tsx @@ -1,76 +1,48 @@ -import { WebApplication } from '@/Application/Application' -import { memo, useEffect, useState } from 'react' -import { ApplicationEvent, PrefKey } from '@standardnotes/snjs' +import { TOGGLE_LIST_PANE_KEYBOARD_COMMAND, TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND } from '@standardnotes/ui-services' +import { useMemo } from 'react' import MenuItem from '../Menu/MenuItem' import { MenuItemType } from '../Menu/MenuItemType' -import { PANEL_NAME_NAVIGATION, PANEL_NAME_NOTES } from '@/Constants/Constants' -import { PrefDefaults } from '@/Constants/PrefDefaults' +import { observer } from 'mobx-react-lite' +import { useResponsiveAppPane } from '../ResponsivePane/ResponsivePaneProvider' +import { useCommandService } from '../ApplicationView/CommandProvider' -type Props = { - application: WebApplication -} +const PanelSettingsSection = () => { + const { isListPaneCollapsed, isNavigationPaneCollapsed, toggleListPane, toggleNavigationPane } = + useResponsiveAppPane() -const WidthForCollapsedPanel = 5 -const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth] -const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth] + const commandService = useCommandService() -const PanelSettingsSection = ({ application }: Props) => { - const [currentNavPanelWidth, setCurrentNavPanelWidth] = useState( - application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth), + const navigationShortcut = useMemo( + () => commandService.keyboardShortcutForCommand(TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND), + [commandService], ) - const [currentItemsPanelWidth, setCurrentItemsPanelWidth] = useState( - application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth), + const listShortcut = useMemo( + () => commandService.keyboardShortcutForCommand(TOGGLE_LIST_PANE_KEYBOARD_COMMAND), + [commandService], ) - const toggleNavigationPanel = () => { - const isCollapsed = currentNavPanelWidth <= WidthForCollapsedPanel - if (isCollapsed) { - void application.setPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth) - } else { - void application.setPreference(PrefKey.TagsPanelWidth, WidthForCollapsedPanel) - } - application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, !isCollapsed) - } - - const toggleItemsListPanel = () => { - const isCollapsed = currentItemsPanelWidth <= WidthForCollapsedPanel - if (isCollapsed) { - void application.setPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth) - } else { - void application.setPreference(PrefKey.NotesPanelWidth, WidthForCollapsedPanel) - } - application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, !isCollapsed) - } - - useEffect(() => { - const removeObserver = application.addEventObserver(async () => { - setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)) - setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)) - }, ApplicationEvent.PreferencesChanged) - - return removeObserver - }, [application]) - return (
WidthForCollapsedPanel} - onChange={toggleNavigationPanel} + checked={isNavigationPaneCollapsed} + onChange={toggleNavigationPane} + shortcut={navigationShortcut} > - Show navigation panel + Show Tags Panel WidthForCollapsedPanel} - onChange={toggleItemsListPanel} + checked={isListPaneCollapsed} + onChange={toggleListPane} + shortcut={listShortcut} > - Show list panel + Show Notes Panel
) } -export default memo(PanelSettingsSection) +export default observer(PanelSettingsSection) diff --git a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx index b53b12a6a..07ad14c93 100644 --- a/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx +++ b/packages/web/src/javascripts/Components/QuickSettingsMenu/QuickSettingsMenu.tsx @@ -23,27 +23,13 @@ import PanelSettingsSection from './PanelSettingsSection' import { PrefDefaults } from '@/Constants/PrefDefaults' import { classNames } from '@/Utils/ConcatenateClassNames' -const focusModeAnimationDuration = 1255 +export const focusModeAnimationDuration = 1255 type MenuProps = { quickSettingsMenuController: QuickSettingsController application: WebApplication } -const toggleFocusMode = (enabled: boolean) => { - if (enabled) { - document.body.classList.add('focus-mode') - } else { - if (document.body.classList.contains('focus-mode')) { - document.body.classList.add('disable-focus-mode') - document.body.classList.remove('focus-mode') - setTimeout(() => { - document.body.classList.remove('disable-focus-mode') - }, focusModeAnimationDuration) - } - } -} - const QuickSettingsMenu: FunctionComponent = ({ application, quickSettingsMenuController }) => { const { closeQuickSettingsMenu, focusModeEnabled, setFocusModeEnabled } = quickSettingsMenuController const [themes, setThemes] = useState([]) @@ -71,10 +57,6 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet const prefsButtonRef = useRef(null) const defaultThemeButtonRef = useRef(null) - useEffect(() => { - toggleFocusMode(focusModeEnabled) - }, [focusModeEnabled]) - const reloadThemes = useCallback(() => { const themes = application.items .getDisplayableComponents() @@ -210,13 +192,14 @@ const QuickSettingsMenu: FunctionComponent = ({ application, quickSet {themes.map((theme) => ( ))} + - +
) } diff --git a/packages/web/src/javascripts/Components/ResponsivePane/ResponsivePaneProvider.tsx b/packages/web/src/javascripts/Components/ResponsivePane/ResponsivePaneProvider.tsx index 3ac84b957..3ec7b7903 100644 --- a/packages/web/src/javascripts/Components/ResponsivePane/ResponsivePaneProvider.tsx +++ b/packages/web/src/javascripts/Components/ResponsivePane/ResponsivePaneProvider.tsx @@ -20,8 +20,12 @@ import { observer } from 'mobx-react-lite' type ResponsivePaneData = { selectedPane: AppPaneId toggleAppPane: (paneId: AppPaneId) => void - isNotesListVisibleOnTablets: boolean toggleNotesListOnTablets: () => void + toggleListPane: () => void + toggleNavigationPane: () => void + isNotesListVisibleOnTablets: boolean + isListPaneCollapsed: boolean + isNavigationPaneCollapsed: boolean } const ResponsivePaneContext = createContext(undefined) @@ -112,8 +116,21 @@ const ResponsivePaneProvider = ({ paneController, children }: ProviderProps) => toggleAppPane, isNotesListVisibleOnTablets, toggleNotesListOnTablets, + isListPaneCollapsed: paneController.isListPaneCollapsed, + isNavigationPaneCollapsed: paneController.isNavigationPaneCollapsed, + toggleListPane: paneController.toggleListPane, + toggleNavigationPane: paneController.toggleNavigationPane, }), - [currentSelectedPane, isNotesListVisibleOnTablets, toggleAppPane, toggleNotesListOnTablets], + [ + currentSelectedPane, + isNotesListVisibleOnTablets, + toggleAppPane, + toggleNotesListOnTablets, + paneController.toggleListPane, + paneController.toggleNavigationPane, + paneController.isListPaneCollapsed, + paneController.isNavigationPaneCollapsed, + ], ) return ( diff --git a/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalContentPane.tsx b/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalContentPane.tsx index 554c40e70..91828e5c9 100644 --- a/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalContentPane.tsx +++ b/packages/web/src/javascripts/Components/RevisionHistoryModal/HistoryModalContentPane.tsx @@ -2,7 +2,7 @@ import RevisionContentLocked from './RevisionContentLocked' import SelectedRevisionContent from './SelectedRevisionContent' import { observer } from 'mobx-react-lite' import { WebApplication } from '@/Application/Application' -import { NotesController } from '@/Controllers/NotesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController' import { NoteHistoryController, RevisionContentState } from '@/Controllers/NoteHistory/NoteHistoryController' import Spinner from '@/Components/Spinner/Spinner' diff --git a/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalProps.tsx b/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalProps.tsx index 04109e522..c66cce320 100644 --- a/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalProps.tsx +++ b/packages/web/src/javascripts/Components/RevisionHistoryModal/RevisionHistoryModalProps.tsx @@ -1,6 +1,6 @@ import { WebApplication } from '@/Application/Application' import { HistoryModalController } from '@/Controllers/NoteHistory/HistoryModalController' -import { NotesController } from '@/Controllers/NotesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' import { SelectedItemsController } from '@/Controllers/SelectedItemsController' import { SubscriptionController } from '@/Controllers/Subscription/SubscriptionController' import { SNNote } from '@standardnotes/snjs' diff --git a/packages/web/src/javascripts/Components/RevisionHistoryModal/SelectedRevisionContent.tsx b/packages/web/src/javascripts/Components/RevisionHistoryModal/SelectedRevisionContent.tsx index ff2ca24dc..b42efde8f 100644 --- a/packages/web/src/javascripts/Components/RevisionHistoryModal/SelectedRevisionContent.tsx +++ b/packages/web/src/javascripts/Components/RevisionHistoryModal/SelectedRevisionContent.tsx @@ -3,7 +3,7 @@ import { ContentType, SNNote } from '@standardnotes/snjs' import { observer } from 'mobx-react-lite' import { FunctionComponent, useEffect, useMemo } from 'react' import ComponentView from '@/Components/ComponentView/ComponentView' -import { NotesController } from '@/Controllers/NotesController' +import { NotesController } from '@/Controllers/NotesController/NotesController' import { NoteHistoryController } from '@/Controllers/NoteHistory/NoteHistoryController' const ABSOLUTE_CENTER_CLASSNAME = 'absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2' diff --git a/packages/web/src/javascripts/Components/SearchBar/SearchBar.tsx b/packages/web/src/javascripts/Components/SearchBar/SearchBar.tsx index 72df9a10d..b48df9151 100644 --- a/packages/web/src/javascripts/Components/SearchBar/SearchBar.tsx +++ b/packages/web/src/javascripts/Components/SearchBar/SearchBar.tsx @@ -7,6 +7,7 @@ import Icon from '../Icon/Icon' import DecoratedInput from '../Input/DecoratedInput' import { observer } from 'mobx-react-lite' import ClearInputButton from '../ClearInputButton/ClearInputButton' +import { ElementIds } from '@/Constants/ElementIDs' type Props = { itemListController: ItemListController @@ -48,6 +49,7 @@ const SearchBar = ({ itemListController, searchOptionsController }: Props) => {
= observer( {isEditing && ( = ({ tags }) => { + const commandService = useCommandService() + + const shortcut = useMemo( + () => keyboardStringForShortcut(commandService.keyboardShortcutForCommand(CREATE_NEW_TAG_COMMAND)), + [commandService], + ) return ( tags.createNewTemplate()} /> diff --git a/packages/web/src/javascripts/Constants/ElementIDs.ts b/packages/web/src/javascripts/Constants/ElementIDs.ts index 7cf8b7089..81247e863 100644 --- a/packages/web/src/javascripts/Constants/ElementIDs.ts +++ b/packages/web/src/javascripts/Constants/ElementIDs.ts @@ -11,4 +11,5 @@ export const ElementIds = { RootId: 'app-group-root', NoteStatusTooltip: 'note-status-tooltip', ItemLinkAutocompleteInput: 'item-link-autocomplete-input', + SearchBar: 'search-bar', } as const diff --git a/packages/web/src/javascripts/Controllers/FilesController.ts b/packages/web/src/javascripts/Controllers/FilesController.ts index 685b85b12..90a7139f3 100644 --- a/packages/web/src/javascripts/Controllers/FilesController.ts +++ b/packages/web/src/javascripts/Controllers/FilesController.ts @@ -26,7 +26,7 @@ import { addToast, dismissToast, ToastType, updateToast } from '@standardnotes/t import { action, makeObservable, observable, reaction } from 'mobx' import { WebApplication } from '../Application/Application' import { AbstractViewController } from './Abstract/AbstractViewController' -import { NotesController } from './NotesController' +import { NotesController } from './NotesController/NotesController' import { downloadOrShareBlobBasedOnPlatform } from '@/Utils/DownloadOrShareBasedOnPlatform' const UnprotectedFileActions = [PopoverFileItemActionType.ToggleFileProtection] diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.spec.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.spec.ts index c20619996..8e3b13c9d 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.spec.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.spec.ts @@ -4,7 +4,7 @@ import { InternalEventBus } from '@standardnotes/services' import { WebApplication } from '@/Application/Application' import { LinkingController } from '../LinkingController' import { NavigationController } from '../Navigation/NavigationController' -import { NotesController } from '../NotesController' +import { NotesController } from '../NotesController/NotesController' import { SearchOptionsController } from '../SearchOptionsController' import { SelectedItemsController } from '../SelectedItemsController' import { ItemListController } from './ItemListController' diff --git a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts index eea5dcd43..79d082544 100644 --- a/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts +++ b/packages/web/src/javascripts/Controllers/ItemList/ItemListController.ts @@ -28,7 +28,7 @@ import { NavigationController } from '../Navigation/NavigationController' import { CrossControllerEvent } from '../CrossControllerEvent' import { SearchOptionsController } from '../SearchOptionsController' import { SelectedItemsController } from '../SelectedItemsController' -import { NotesController } from '../NotesController' +import { NotesController } from '../NotesController/NotesController' import { formatDateAndTimeForNote } from '@/Utils/DateUtils' import { PrefDefaults } from '@/Constants/PrefDefaults' import dayjs from 'dayjs' @@ -42,7 +42,6 @@ import { ItemsReloadSource } from './ItemsReloadSource' const MinNoteCellHeight = 51.0 const DefaultListNumNotes = 20 -const ElementIdSearchBar = 'search-bar' const ElementIdScrollContainer = 'notes-scrollable' export class ItemListController extends AbstractViewController implements InternalEventHandlerInterface { @@ -277,10 +276,6 @@ export class ItemListController extends AbstractViewController implements Intern this.showDisplayOptionsMenu = enabled } - get searchBarElement() { - return document.getElementById(ElementIdSearchBar) - } - get isFiltering(): boolean { return !!this.noteFilterText && this.noteFilterText.length > 0 } diff --git a/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts b/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts index 5066fa91b..b549ad560 100644 --- a/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts +++ b/packages/web/src/javascripts/Controllers/Navigation/NavigationController.ts @@ -1,4 +1,4 @@ -import { confirmDialog, NavigationControllerPersistableValue } from '@standardnotes/ui-services' +import { confirmDialog, CREATE_NEW_TAG_COMMAND, NavigationControllerPersistableValue } from '@standardnotes/ui-services' import { STRING_DELETE_TAG } from '@/Constants/Strings' import { MAX_MENU_SIZE_MULTIPLIER, MENU_MARGIN_FROM_APP_BORDER, SMART_TAGS_FEATURE_NAME } from '@/Constants/Constants' import { @@ -155,6 +155,15 @@ export class NavigationController }, ), ) + + this.disposers.push( + this.application.keyboardService.addCommandHandler({ + command: CREATE_NEW_TAG_COMMAND, + onKeyDown: () => { + this.createNewTemplate() + }, + }), + ) } private findAndSetTag = (uuid: UuidString) => { diff --git a/packages/web/src/javascripts/Controllers/NoteHistory/HistoryModalController.ts b/packages/web/src/javascripts/Controllers/NoteHistory/HistoryModalController.ts index 6f918f4d0..945277c29 100644 --- a/packages/web/src/javascripts/Controllers/NoteHistory/HistoryModalController.ts +++ b/packages/web/src/javascripts/Controllers/NoteHistory/HistoryModalController.ts @@ -1,7 +1,9 @@ import { WebApplication } from '@/Application/Application' import { InternalEventBus, SNNote } from '@standardnotes/snjs' +import { OPEN_NOTE_HISTORY_COMMAND } from '@standardnotes/ui-services' import { action, makeObservable, observable } from 'mobx' import { AbstractViewController } from '../Abstract/AbstractViewController' +import { NotesControllerInterface } from '../NotesController/NotesControllerInterface' export class HistoryModalController extends AbstractViewController { note?: SNNote = undefined @@ -11,13 +13,23 @@ export class HistoryModalController extends AbstractViewController { this.note = undefined } - constructor(application: WebApplication, eventBus: InternalEventBus) { + constructor(application: WebApplication, eventBus: InternalEventBus, notesController: NotesControllerInterface) { super(application, eventBus) makeObservable(this, { note: observable, setNote: action, }) + + this.disposers.push( + application.keyboardService.addCommandHandler({ + command: OPEN_NOTE_HISTORY_COMMAND, + onKeyDown: () => { + this.openModal(notesController.firstSelectedNote) + return true + }, + }), + ) } setNote = (note: SNNote | undefined) => { diff --git a/packages/web/src/javascripts/Controllers/NotesController.ts b/packages/web/src/javascripts/Controllers/NotesController/NotesController.ts similarity index 87% rename from packages/web/src/javascripts/Controllers/NotesController.ts rename to packages/web/src/javascripts/Controllers/NotesController/NotesController.ts index 131c50056..230c4378f 100644 --- a/packages/web/src/javascripts/Controllers/NotesController.ts +++ b/packages/web/src/javascripts/Controllers/NotesController/NotesController.ts @@ -1,16 +1,17 @@ import { destroyAllObjectProperties } from '@/Utils' -import { confirmDialog } from '@standardnotes/ui-services' +import { confirmDialog, PIN_NOTE_COMMAND, STAR_NOTE_COMMAND } from '@standardnotes/ui-services' import { StringEmptyTrash, Strings, StringUtils } from '@/Constants/Strings' import { MENU_MARGIN_FROM_APP_BORDER } from '@/Constants/Constants' import { SNNote, NoteMutator, ContentType, SNTag, TagMutator, InternalEventBus } from '@standardnotes/snjs' import { makeObservable, observable, action, computed, runInAction } from 'mobx' -import { WebApplication } from '../Application/Application' -import { AbstractViewController } from './Abstract/AbstractViewController' -import { SelectedItemsController } from './SelectedItemsController' -import { ItemListController } from './ItemList/ItemListController' -import { NavigationController } from './Navigation/NavigationController' +import { WebApplication } from '../../Application/Application' +import { AbstractViewController } from '../Abstract/AbstractViewController' +import { SelectedItemsController } from '../SelectedItemsController' +import { ItemListController } from '../ItemList/ItemListController' +import { NavigationController } from '../Navigation/NavigationController' +import { NotesControllerInterface } from './NotesControllerInterface' -export class NotesController extends AbstractViewController { +export class NotesController extends AbstractViewController implements NotesControllerInterface { lastSelectedNote: SNNote | undefined contextMenuOpen = false contextMenuPosition: { top?: number; left: number; bottom?: number } = { @@ -57,6 +58,21 @@ export class NotesController extends AbstractViewController { setShowProtectedWarning: action, unselectNotes: action, }) + + this.disposers.push( + this.application.keyboardService.addCommandHandler({ + command: PIN_NOTE_COMMAND, + onKeyDown: () => { + this.togglePinSelectedNotes() + }, + }), + this.application.keyboardService.addCommandHandler({ + command: STAR_NOTE_COMMAND, + onKeyDown: () => { + this.toggleStarSelectedNotes() + }, + }), + ) } public setServicesPostConstruction(itemListController: ItemListController) { @@ -239,6 +255,28 @@ export class NotesController extends AbstractViewController { return false } + togglePinSelectedNotes(): void { + const notes = this.selectedNotes + const pinned = notes.some((note) => note.pinned) + + if (!pinned) { + this.setPinSelectedNotes(true) + } else { + this.setPinSelectedNotes(false) + } + } + + toggleStarSelectedNotes(): void { + const notes = this.selectedNotes + const starred = notes.some((note) => note.starred) + + if (!starred) { + this.setStarSelectedNotes(true) + } else { + this.setStarSelectedNotes(false) + } + } + setPinSelectedNotes(pinned: boolean): void { this.changeSelectedNotes((mutator) => { mutator.pinned = pinned diff --git a/packages/web/src/javascripts/Controllers/NotesController/NotesControllerInterface.ts b/packages/web/src/javascripts/Controllers/NotesController/NotesControllerInterface.ts new file mode 100644 index 000000000..83ae438d6 --- /dev/null +++ b/packages/web/src/javascripts/Controllers/NotesController/NotesControllerInterface.ts @@ -0,0 +1,5 @@ +import { SNNote } from '@standardnotes/models' + +export interface NotesControllerInterface { + get firstSelectedNote(): SNNote | undefined +} diff --git a/packages/web/src/javascripts/Controllers/PaneController.ts b/packages/web/src/javascripts/Controllers/PaneController.ts index b2842efe6..d9ce4b8c6 100644 --- a/packages/web/src/javascripts/Controllers/PaneController.ts +++ b/packages/web/src/javascripts/Controllers/PaneController.ts @@ -1,35 +1,93 @@ +import { TOGGLE_LIST_PANE_KEYBOARD_COMMAND, TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND } from '@standardnotes/ui-services' +import { ApplicationEvent, InternalEventBus, PrefKey } from '@standardnotes/snjs' import { AppPaneId } from './../Components/ResponsivePane/AppPaneMetadata' import { isMobileScreen } from '@/Utils' -import { makeObservable, observable, action } from 'mobx' +import { makeObservable, observable, action, computed } from 'mobx' import { Disposer } from '@/Types/Disposer' import { MediaQueryBreakpoints } from '@/Hooks/useMediaQuery' +import { WebApplication } from '@/Application/Application' +import { AbstractViewController } from './Abstract/AbstractViewController' +import { PrefDefaults } from '@/Constants/PrefDefaults' +import { PANEL_NAME_NAVIGATION, PANEL_NAME_NOTES } from '@/Constants/Constants' -export class PaneController { +const WidthForCollapsedPanel = 5 +const MinimumNavPanelWidth = PrefDefaults[PrefKey.TagsPanelWidth] +const MinimumNotesPanelWidth = PrefDefaults[PrefKey.NotesPanelWidth] + +export class PaneController extends AbstractViewController { currentPane: AppPaneId = isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor previousPane: AppPaneId = isMobileScreen() ? AppPaneId.Items : AppPaneId.Editor isInMobileView = isMobileScreen() protected disposers: Disposer[] = [] - constructor() { + currentNavPanelWidth = 0 + currentItemsPanelWidth = 0 + + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) + makeObservable(this, { currentPane: observable, previousPane: observable, isInMobileView: observable, + currentNavPanelWidth: observable, + currentItemsPanelWidth: observable, + + isListPaneCollapsed: computed, + isNavigationPaneCollapsed: computed, setCurrentPane: action, setPreviousPane: action, setIsInMobileView: action, + toggleListPane: action, + toggleNavigationPane: action, + setCurrentItemsPanelWidth: action, + setCurrentNavPanelWidth: action, }) + this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)) + this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)) + const mediaQuery = window.matchMedia(MediaQueryBreakpoints.md) if (mediaQuery?.addEventListener != undefined) { mediaQuery.addEventListener('change', this.mediumScreenMQHandler) } else { mediaQuery.addListener(this.mediumScreenMQHandler) } + + this.disposers.push( + application.addEventObserver(async () => { + this.setCurrentNavPanelWidth(application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth)) + this.setCurrentItemsPanelWidth(application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth)) + }, ApplicationEvent.PreferencesChanged), + + application.keyboardService.addCommandHandler({ + command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND, + onKeyDown: (event) => { + event.preventDefault() + this.toggleListPane() + }, + }), + application.keyboardService.addCommandHandler({ + command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND, + onKeyDown: (event) => { + event.preventDefault() + this.toggleNavigationPane() + }, + }), + ) + } + + setCurrentNavPanelWidth(width: number) { + this.currentNavPanelWidth = width + } + + setCurrentItemsPanelWidth(width: number) { + this.currentItemsPanelWidth = width } deinit() { + super.deinit() const mq = window.matchMedia(MediaQueryBreakpoints.md) if (mq?.removeEventListener != undefined) { mq.removeEventListener('change', this.mediumScreenMQHandler) @@ -57,4 +115,38 @@ export class PaneController { setIsInMobileView(isInMobileView: boolean) { this.isInMobileView = isInMobileView } + + toggleListPane = () => { + const currentItemsPanelWidth = this.application.getPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth) + + const isCollapsed = currentItemsPanelWidth <= WidthForCollapsedPanel + if (isCollapsed) { + void this.application.setPreference(PrefKey.NotesPanelWidth, MinimumNotesPanelWidth) + } else { + void this.application.setPreference(PrefKey.NotesPanelWidth, WidthForCollapsedPanel) + } + + this.application.publishPanelDidResizeEvent(PANEL_NAME_NOTES, !isCollapsed) + } + + toggleNavigationPane = () => { + const currentNavPanelWidth = this.application.getPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth) + + const isCollapsed = currentNavPanelWidth <= WidthForCollapsedPanel + if (isCollapsed) { + void this.application.setPreference(PrefKey.TagsPanelWidth, MinimumNavPanelWidth) + } else { + void this.application.setPreference(PrefKey.TagsPanelWidth, WidthForCollapsedPanel) + } + + this.application.publishPanelDidResizeEvent(PANEL_NAME_NAVIGATION, !isCollapsed) + } + + get isListPaneCollapsed() { + return this.currentItemsPanelWidth > WidthForCollapsedPanel + } + + get isNavigationPaneCollapsed() { + return this.currentNavPanelWidth > WidthForCollapsedPanel + } } diff --git a/packages/web/src/javascripts/Controllers/QuickSettingsController.ts b/packages/web/src/javascripts/Controllers/QuickSettingsController.ts index 16d4149c9..d3d3a5964 100644 --- a/packages/web/src/javascripts/Controllers/QuickSettingsController.ts +++ b/packages/web/src/javascripts/Controllers/QuickSettingsController.ts @@ -1,11 +1,18 @@ +import { InternalEventBus } from '@standardnotes/snjs' +import { WebApplication } from '@/Application/Application' import { action, makeObservable, observable } from 'mobx' +import { AbstractViewController } from './Abstract/AbstractViewController' +import { TOGGLE_FOCUS_MODE_COMMAND } from '@standardnotes/ui-services' +import { toggleFocusMode } from '@/Utils/toggleFocusMode' -export class QuickSettingsController { +export class QuickSettingsController extends AbstractViewController { open = false shouldAnimateCloseMenu = false focusModeEnabled = false - constructor() { + constructor(application: WebApplication, eventBus: InternalEventBus) { + super(application, eventBus) + makeObservable(this, { open: observable, shouldAnimateCloseMenu: observable, @@ -17,6 +24,17 @@ export class QuickSettingsController { toggle: action, closeQuickSettingsMenu: action, }) + + this.disposers.push( + application.keyboardService.addCommandHandler({ + command: TOGGLE_FOCUS_MODE_COMMAND, + onKeyDown: (event) => { + event.preventDefault() + this.setFocusModeEnabled(!this.focusModeEnabled) + return true + }, + }), + ) } setOpen = (open: boolean): void => { @@ -29,6 +47,8 @@ export class QuickSettingsController { setFocusModeEnabled = (enabled: boolean): void => { this.focusModeEnabled = enabled + + toggleFocusMode(enabled) } toggle = (): void => { diff --git a/packages/web/src/javascripts/Controllers/SelectedItemsController.ts b/packages/web/src/javascripts/Controllers/SelectedItemsController.ts index 604501a82..a97f1c5db 100644 --- a/packages/web/src/javascripts/Controllers/SelectedItemsController.ts +++ b/packages/web/src/javascripts/Controllers/SelectedItemsController.ts @@ -103,8 +103,8 @@ export class SelectedItemsController ) } - private get io() { - return this.application.io + private get keyboardService() { + return this.application.keyboardService } get selectedItemsCount(): number { @@ -196,7 +196,7 @@ export class SelectedItemsController } cancelMultipleSelection = () => { - this.io.cancelAllKeyboardModifiers() + this.keyboardService.cancelAllKeyboardModifiers() const firstSelectedItem = this.firstSelectedItem @@ -256,9 +256,9 @@ export class SelectedItemsController log(LoggingDomain.Selection, 'selectItem', item.uuid) - const hasMeta = this.io.activeModifiers.has(KeyboardModifier.Meta) - const hasCtrl = this.io.activeModifiers.has(KeyboardModifier.Ctrl) - const hasShift = this.io.activeModifiers.has(KeyboardModifier.Shift) + const hasMeta = this.keyboardService.activeModifiers.has(KeyboardModifier.Meta) + const hasCtrl = this.keyboardService.activeModifiers.has(KeyboardModifier.Ctrl) + const hasShift = this.keyboardService.activeModifiers.has(KeyboardModifier.Shift) const hasMoreThanOneSelected = this.selectedItemsCount > 1 const isAuthorizedForAccess = await this.application.protections.authorizeItemAccess(item) diff --git a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts b/packages/web/src/javascripts/Controllers/ViewControllerManager.ts index 411c03388..b7d55bee7 100644 --- a/packages/web/src/javascripts/Controllers/ViewControllerManager.ts +++ b/packages/web/src/javascripts/Controllers/ViewControllerManager.ts @@ -24,7 +24,7 @@ import { action, makeObservable, observable } from 'mobx' import { ActionsMenuController } from './ActionsMenuController' import { FeaturesController } from './FeaturesController' import { FilesController } from './FilesController' -import { NotesController } from './NotesController' +import { NotesController } from './NotesController/NotesController' import { ItemListController } from './ItemList/ItemListController' import { NoAccountWarningController } from './NoAccountWarningController' import { PreferencesController } from './PreferencesController' @@ -60,7 +60,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface { readonly itemListController: ItemListController readonly preferencesController: PreferencesController readonly purchaseFlowController: PurchaseFlowController - readonly quickSettingsMenuController = new QuickSettingsController() + readonly quickSettingsMenuController: QuickSettingsController readonly searchOptionsController: SearchOptionsController readonly subscriptionController: SubscriptionController readonly syncStatusController = new SyncStatusController() @@ -92,7 +92,9 @@ export class ViewControllerManager implements InternalEventHandlerInterface { this.subscriptionManager = application.subscriptions - this.paneController = new PaneController() + this.quickSettingsMenuController = new QuickSettingsController(application, this.eventBus) + + this.paneController = new PaneController(application, this.eventBus) this.preferencesController = new PreferencesController(application, this.eventBus) @@ -152,7 +154,7 @@ export class ViewControllerManager implements InternalEventHandlerInterface { this.subscriptionController, ) - this.historyModalController = new HistoryModalController(this.application, this.eventBus) + this.historyModalController = new HistoryModalController(this.application, this.eventBus, this.notesController) this.toastService = new ToastService() diff --git a/packages/web/src/javascripts/Utils/toggleFocusMode.tsx b/packages/web/src/javascripts/Utils/toggleFocusMode.tsx new file mode 100644 index 000000000..d80003870 --- /dev/null +++ b/packages/web/src/javascripts/Utils/toggleFocusMode.tsx @@ -0,0 +1,20 @@ +import { focusModeAnimationDuration } from '../Components/QuickSettingsMenu/QuickSettingsMenu' + +export const FOCUS_MODE_CLASS_NAME = 'focus-mode' +export const DISABLING_FOCUS_MODE_CLASS_NAME = 'disable-focus-mode' + +export const toggleFocusMode = (enabled: boolean) => { + if (enabled) { + document.body.classList.add(FOCUS_MODE_CLASS_NAME) + return + } + + if (document.body.classList.contains(FOCUS_MODE_CLASS_NAME)) { + document.body.classList.add(DISABLING_FOCUS_MODE_CLASS_NAME) + document.body.classList.remove(FOCUS_MODE_CLASS_NAME) + + setTimeout(() => { + document.body.classList.remove(DISABLING_FOCUS_MODE_CLASS_NAME) + }, focusModeAnimationDuration) + } +} diff --git a/packages/web/src/stylesheets/_focused.scss b/packages/web/src/stylesheets/_focused.scss index 3d1e65ee3..13b46be13 100644 --- a/packages/web/src/stylesheets/_focused.scss +++ b/packages/web/src/stylesheets/_focused.scss @@ -15,8 +15,41 @@ -webkit-app-region: drag; } - #editor-title-bar { - display: none; + #editor-column { + padding: 25px 10% 0px 10%; + + @media screen and (min-width: 992px) { + padding: 25px 15% 0px 15%; + } + + @media screen and (min-width: 1200px) { + padding: 25px 20% 0px 20%; + } + + background-color: var(--sn-stylekit-contrast-background-color); + + .content { + box-shadow: 0 0 4px 1px var(--sn-stylekit-shadow-color); + overflow: hidden; + } + + .note-view-linking-container { + display: none; + } + + #editor-title-bar:hover .note-view-linking-container { + display: flex; + } + + .note-view-options-buttons, + .note-status-tooltip-container { + opacity: 0; + } + + #editor-title-bar:hover .note-view-options-buttons, + #editor-title-bar:hover .note-status-tooltip-container { + opacity: 1; + } } #editor-menu-bar { @@ -27,20 +60,21 @@ display: none; } - #footer-bar { + #footer-bar .left, + #footer-bar .right { opacity: 0.08; transition: opacity 0.25s; } - #footer-bar:hover { + #footer-bar *:hover { opacity: 1; } #navigation, #items-column { will-change: opacity; - animation: fade-out 1.25s forwards; - transition: width 1.25s; + animation: fade-out 0.5s forwards; + transition: width 0.5s; transition-delay: 0s; width: 0px !important; flex: none !important; @@ -61,9 +95,9 @@ .disable-focus-mode { #navigation, #items-column { - transition: width 1.25s; + transition: width 0.5s; will-change: opacity; - animation: fade-in 1.25s forwards; + animation: fade-in 0.5s forwards; } }