feat: Added "Keyboard shortcuts" help dialog. Can be opened by pressing Shift + ?

This commit is contained in:
Aman Harwara
2024-01-27 15:25:49 +05:30
parent 289849724a
commit ff3c45ba35
27 changed files with 312 additions and 27 deletions

View File

@@ -13,6 +13,8 @@ import Spinner from '@/Components/Spinner/Spinner'
import { MenuItemIconSize } from '@/Constants/TailwindClassNames'
import { useApplication } from '../ApplicationProvider'
import MenuSection from '../Menu/MenuSection'
import { TOGGLE_KEYBOARD_SHORTCUTS_MODAL, isMobilePlatform } from '@standardnotes/ui-services'
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
type Props = {
mainApplicationGroup: WebApplicationGroup
@@ -90,6 +92,10 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
const CREATE_ACCOUNT_INDEX = 1
const SWITCHER_INDEX = 0
const keyboardShortcutsHelpShortcut = useMemo(() => {
return application.keyboardService.keyboardShortcutForCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
}, [application.keyboardService])
return (
<>
<div className="mb-1 mt-1 hidden items-center justify-between px-4 md:flex md:px-3">
@@ -187,6 +193,19 @@ const GeneralAccountMenu: FunctionComponent<Props> = ({ setMenuPane, closeMenu,
</div>
<span className="text-neutral">v{application.version}</span>
</MenuItem>
{!isMobilePlatform(application.platform) && (
<MenuItem
onClick={() => {
application.keyboardService.triggerCommand(TOGGLE_KEYBOARD_SHORTCUTS_MODAL)
}}
>
<Icon type="keyboard" className={iconClassName} />
Keyboard shortcuts
{keyboardShortcutsHelpShortcut && (
<KeyboardShortcutIndicator shortcut={keyboardShortcutsHelpShortcut} className="ml-auto" />
)}
</MenuItem>
)}
</MenuSection>
{user ? (
<MenuSection>

View File

@@ -31,6 +31,7 @@ import ImportModal from '../ImportModal/ImportModal'
import IosKeyboardClose from '../IosKeyboardClose/IosKeyboardClose'
import EditorWidthSelectionModalWrapper from '../EditorWidthSelectionModal/EditorWidthSelectionModal'
import { ProtectionEvent } from '@standardnotes/services'
import KeyboardShortcutsModal from '../KeyboardShortcutsHelpModal/KeyboardShortcutsHelpModal'
type Props = {
application: WebApplication
@@ -267,6 +268,7 @@ const ApplicationView: FunctionComponent<Props> = ({ application, mainApplicatio
<EditorWidthSelectionModalWrapper />
<ConfirmDeleteAccountContainer application={application} />
<ImportModal importModalController={application.importModalController} />
<KeyboardShortcutsModal keyboardService={application.keyboardService} />
</>
{application.routeService.isDotOrg && <DotOrgNotice />}
{isIOS() && <IosKeyboardClose />}

View File

@@ -57,6 +57,8 @@ const ChangeEditorButton: FunctionComponent<Props> = ({ noteViewController, onCl
useEffect(() => {
return application.keyboardService.addCommandHandler({
command: CHANGE_EDITOR_COMMAND,
category: 'Current note',
description: 'Change note type',
onKeyDown: () => {
void toggleMenu()
},

View File

@@ -178,6 +178,8 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
return application.keyboardService.addCommandHandlers([
{
command: CREATE_NEW_NOTE_KEYBOARD_COMMAND,
category: 'General',
description: 'Create new note',
onKeyDown: (event) => {
event.preventDefault()
void addNewItem()
@@ -185,6 +187,8 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
},
{
command: NEXT_LIST_ITEM_KEYBOARD_COMMAND,
category: 'Notes list',
description: 'Go to next item',
elements: [document.body, ...(searchBarElement ? [searchBarElement] : [])],
onKeyDown: () => {
if (searchBarElement === document.activeElement) {
@@ -198,6 +202,8 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
},
{
command: PREVIOUS_LIST_ITEM_KEYBOARD_COMMAND,
category: 'Notes list',
description: 'Go to previous item',
element: document.body,
onKeyDown: () => {
if (shouldUseTableView) {
@@ -208,6 +214,8 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
},
{
command: SEARCH_KEYBOARD_COMMAND,
category: 'General',
description: 'Toggle global search',
onKeyDown: (event) => {
if (searchBarElement) {
event.preventDefault()
@@ -225,6 +233,8 @@ const ContentListView = forwardRef<HTMLDivElement, Props>(
},
{
command: SELECT_ALL_ITEMS_KEYBOARD_COMMAND,
category: 'General',
description: 'Select all items',
onKeyDown: (event) => {
const isTargetInsideContentList = (event.target as HTMLElement).closest(`#${ElementIds.ContentList}`)

View File

@@ -180,6 +180,8 @@ const EditorWidthSelectionModalWrapper = () => {
useEffect(() => {
return application.keyboardService.addCommandHandler({
command: CHANGE_EDITOR_WIDTH_COMMAND,
category: 'Current note',
description: 'Change editor width',
onKeyDown: (_, data) => {
if (typeof data === 'boolean' && data) {
setIsGlobal(data)

View File

@@ -25,6 +25,8 @@ const QuickSettingsButton = ({ application, isMobileNavigation = false }: Props)
useEffect(() => {
return commandService.addCommandHandler({
command: TOGGLE_DARK_MODE_COMMAND,
category: 'General',
description: 'Toggle dark mode',
onKeyDown: () => {
void application.componentManager.toggleTheme(new UIFeature(GetDarkThemeFeature()))
},

View File

@@ -1,31 +1,31 @@
import { classNames } from '@standardnotes/snjs'
import {
PlatformedKeyboardShortcut,
keyboardCharacterForModifier,
isMacPlatform,
isMobilePlatform,
keyboardCharacterForKeyOrCode,
} from '@standardnotes/ui-services'
import { useMemo } from 'react'
type Props = {
shortcut: PlatformedKeyboardShortcut
shortcut: Omit<PlatformedKeyboardShortcut, 'command'>
small?: boolean
dimmed?: boolean
className?: string
}
export const KeyboardShortcutIndicator = ({ shortcut, className }: Props) => {
const addPluses = !isMacPlatform(shortcut.platform)
const spacingClass = addPluses ? '' : 'ml-0.5'
export const KeyboardShortcutIndicator = ({ shortcut, small = true, dimmed = true, className }: Props) => {
const keys = useMemo(() => {
const modifiers = shortcut.modifiers || []
const primaryKey = (shortcut.key || '').toUpperCase()
const primaryKey = shortcut.key
? keyboardCharacterForKeyOrCode(shortcut.key)
: shortcut.code
? keyboardCharacterForKeyOrCode(shortcut.code)
: undefined
const results: string[] = []
modifiers.forEach((modifier, index) => {
modifiers.forEach((modifier) => {
results.push(keyboardCharacterForModifier(modifier, shortcut.platform))
if (addPluses && (primaryKey || index !== modifiers.length - 1)) {
results.push('+')
}
})
if (primaryKey) {
@@ -33,19 +33,25 @@ export const KeyboardShortcutIndicator = ({ shortcut, className }: Props) => {
}
return results
}, [shortcut, addPluses])
}, [shortcut])
if (isMobilePlatform(shortcut.platform)) {
return null
}
return (
<div className={`keyboard-shortcut-indicator flex opacity-[0.35] ${className}`}>
<div className={classNames('flex items-center gap-1', dimmed && 'opacity-70', className)}>
{keys.map((key, index) => {
return (
<div className={index !== 0 ? spacingClass : ''} key={index}>
<kbd
className={classNames(
'rounded border-[0.5px] border-passive-3 bg-default p-1 text-center font-sans capitalize leading-none text-text shadow-[var(--tw-shadow-color)_0px_2px_0px_0px] shadow-passive-3',
small ? 'text-[length:0.65rem]' : 'text-xs',
)}
key={index}
>
{key}
</div>
</kbd>
)
})}
</div>

View File

@@ -0,0 +1,78 @@
import { useCallback, useEffect, useState } from 'react'
import Modal from '../Modal/Modal'
import ModalOverlay from '../Modal/ModalOverlay'
import {
KeyboardService,
KeyboardShortcutCategory,
KeyboardShortcutHelpItem,
TOGGLE_KEYBOARD_SHORTCUTS_MODAL,
} from '@standardnotes/ui-services'
import { observer } from 'mobx-react-lite'
import { KeyboardShortcutIndicator } from '../KeyboardShortcutIndicator/KeyboardShortcutIndicator'
type GroupedItems = {
[category in KeyboardShortcutCategory]: KeyboardShortcutHelpItem[]
}
const createGroupedItems = (items: KeyboardShortcutHelpItem[]): GroupedItems => {
const groupedItems: GroupedItems = {
'Current note': [],
Formatting: [],
'Super notes': [],
'Notes list': [],
General: [],
}
return items.reduce((acc, item) => {
acc[item.category].push(item)
return acc
}, groupedItems)
}
const Item = ({ item }: { item: KeyboardShortcutHelpItem }) => {
return (
<div className="flex items-center gap-2 px-4 py-2.5">
<div>{item.description}</div>
<KeyboardShortcutIndicator className="ml-auto" shortcut={item} small={false} dimmed={false} />
</div>
)
}
const KeyboardShortcutsModal = ({ keyboardService }: { keyboardService: KeyboardService }) => {
const [isOpen, setIsOpen] = useState(false)
const [items, setItems] = useState(() => createGroupedItems(keyboardService.getRegisteredKeyboardShorcutHelpItems()))
const close = useCallback(() => {
setIsOpen(false)
}, [])
useEffect(() => {
return keyboardService.addCommandHandler({
command: TOGGLE_KEYBOARD_SHORTCUTS_MODAL,
description: 'Toggle keyboard shortcuts help',
onKeyDown: () => {
setItems(createGroupedItems(keyboardService.getRegisteredKeyboardShorcutHelpItems()))
setIsOpen((open) => !open)
},
})
}, [keyboardService])
return (
<ModalOverlay isOpen={isOpen} close={close}>
<Modal title="Keyboard shortcuts" close={close}>
{Object.entries(items).map(
([category, items]) =>
items.length > 0 && (
<div key={category}>
<div className="p-4 pb-0.5 pt-4 text-base font-semibold capitalize">{category}</div>
{items.map((item, index) => (
<Item item={item} key={index} />
))}
</div>
),
)}
</Modal>
</ModalOverlay>
)
}
export default observer(KeyboardShortcutsModal)

View File

@@ -60,6 +60,8 @@ const LinkedItemBubblesContainer = ({
useEffect(() => {
return commandService.addCommandHandler({
command: FOCUS_TAGS_INPUT_COMMAND,
category: 'Current note',
description: 'Link tags, notes, files',
onKeyDown: () => {
const input = document.getElementById(ElementIds.ItemLinkAutocompleteInput)
if (input) {

View File

@@ -15,6 +15,8 @@ const PreferencesViewWrapper: FunctionComponent<PreferencesViewWrapperProps> = (
useEffect(() => {
return commandService.addCommandHandler({
command: OPEN_PREFERENCES_COMMAND,
category: 'General',
description: 'Open preferences',
onKeyDown: () => application.preferencesController.openPreferences(),
})
}, [commandService, application])

View File

@@ -38,6 +38,8 @@ export const SearchPlugin = () => {
return application.keyboardService.addCommandHandlers([
{
command: SUPER_TOGGLE_SEARCH,
category: 'Super notes',
description: 'Search in current note',
onKeyDown: (event) => {
if (!isFocusInEditor()) {
return
@@ -50,6 +52,8 @@ export const SearchPlugin = () => {
},
{
command: SUPER_SEARCH_TOGGLE_REPLACE_MODE,
category: 'Super notes',
description: 'Search and replace in current note',
onKeyDown: (event) => {
if (!isFocusInEditor()) {
return
@@ -72,6 +76,8 @@ export const SearchPlugin = () => {
},
{
command: SUPER_SEARCH_NEXT_RESULT,
category: 'Super notes',
description: 'Go to next search result',
onKeyDown(event) {
if (!isFocusInEditor()) {
return
@@ -85,6 +91,8 @@ export const SearchPlugin = () => {
},
{
command: SUPER_SEARCH_PREVIOUS_RESULT,
category: 'Super notes',
description: 'Go to previous search result',
onKeyDown(event) {
if (!isFocusInEditor()) {
return

View File

@@ -553,6 +553,8 @@ const ToolbarPlugin = () => {
useEffect(() => {
return application.keyboardService.addCommandHandler({
command: SUPER_TOGGLE_TOOLBAR,
category: 'Super notes',
description: 'Toggle Super note toolbar',
onKeyDown(event) {
if (isMobile) {
return

View File

@@ -28,7 +28,7 @@ import {
ChangeEditorFunction,
} from './Plugins/ChangeContentCallback/ChangeContentCallback'
import { useCommandService } from '@/Components/CommandProvider'
import { SUPER_SHOW_MARKDOWN_PREVIEW } from '@standardnotes/ui-services'
import { SUPER_SHOW_MARKDOWN_PREVIEW, getPrimaryModifier } from '@standardnotes/ui-services'
import { SuperNoteMarkdownPreview } from './SuperNoteMarkdownPreview'
import GetMarkdownPlugin, { GetMarkdownPluginInterface } from './Plugins/GetMarkdownPlugin/GetMarkdownPlugin'
import { useResponsiveEditorFontSize } from '@/Utils/getPlaintextFontSize'
@@ -83,10 +83,48 @@ export const SuperEditor: FunctionComponent<Props> = ({
useEffect(() => {
return commandService.addCommandHandler({
command: SUPER_SHOW_MARKDOWN_PREVIEW,
category: 'Super notes',
description: 'Show markdown preview for current note',
onKeyDown: () => setShowMarkdownPreview(true),
})
}, [commandService])
useEffect(() => {
const platform = application.platform
const primaryModifier = getPrimaryModifier(application.platform)
return commandService.registerExternalKeyboardShortcutHelpItems([
{
key: 'b',
modifiers: [primaryModifier],
description: 'Bold',
category: 'Formatting',
platform: platform,
},
{
key: 'i',
modifiers: [primaryModifier],
description: 'Italic',
category: 'Formatting',
platform: platform,
},
{
key: 'u',
modifiers: [primaryModifier],
description: 'Underline',
category: 'Formatting',
platform: platform,
},
{
key: 'k',
modifiers: [primaryModifier],
description: 'Link',
category: 'Formatting',
platform: platform,
},
])
}, [application.platform, commandService])
const closeMarkdownPreview = useCallback(() => {
setShowMarkdownPreview(false)
}, [])

View File

@@ -189,6 +189,8 @@ export class NavigationController
this.disposers.push(
this.keyboardService.addCommandHandler({
command: CREATE_NEW_TAG_COMMAND,
category: 'General',
description: 'Create new tag',
onKeyDown: () => {
this.createNewTemplate()
},

View File

@@ -27,6 +27,8 @@ export class HistoryModalController extends AbstractViewController {
this.disposers.push(
keyboardService.addCommandHandler({
command: OPEN_NOTE_HISTORY_COMMAND,
category: 'Current note',
description: 'Open note history',
onKeyDown: () => {
this.openModal(notesController.firstSelectedNote)
return true

View File

@@ -90,12 +90,16 @@ export class NotesController
this.disposers.push(
this.keyboardService.addCommandHandler({
command: PIN_NOTE_COMMAND,
category: 'Current note',
description: 'Pin current note',
onKeyDown: () => {
this.togglePinSelectedNotes()
},
}),
this.keyboardService.addCommandHandler({
command: STAR_NOTE_COMMAND,
category: 'Current note',
description: 'Star current note',
onKeyDown: () => {
this.toggleStarSelectedNotes()
},

View File

@@ -102,6 +102,8 @@ export class PaneController extends AbstractViewController implements InternalEv
this.disposers.push(
keyboardService.addCommandHandler({
command: TOGGLE_FOCUS_MODE_COMMAND,
category: 'General',
description: 'Toggle focus mode',
onKeyDown: (event) => {
event.preventDefault()
this.setFocusModeEnabled(!this.focusModeEnabled)
@@ -110,6 +112,8 @@ export class PaneController extends AbstractViewController implements InternalEv
}),
keyboardService.addCommandHandler({
command: TOGGLE_LIST_PANE_KEYBOARD_COMMAND,
category: 'General',
description: 'Toggle notes panel',
onKeyDown: (event) => {
event.preventDefault()
this.toggleListPane()
@@ -117,6 +121,8 @@ export class PaneController extends AbstractViewController implements InternalEv
}),
keyboardService.addCommandHandler({
command: TOGGLE_NAVIGATION_PANE_KEYBOARD_COMMAND,
category: 'General',
description: 'Toggle tags panel',
onKeyDown: (event) => {
event.preventDefault()
this.toggleNavigationPane()

View File

@@ -2,7 +2,7 @@ import { DeviceInterface, MobileDeviceInterface, Platform, platformFromString }
import { IsDesktopPlatform, IsWebPlatform } from '@/Constants/Version'
import { EMAIL_REGEX } from '../Constants/Constants'
import { MutuallyExclusiveMediaQueryBreakpoints } from '@/Hooks/useMediaQuery'
import { isIOS } from '@standardnotes/ui-services'
import { isAndroid, isIOS } from '@standardnotes/ui-services'
declare const process: {
env: {
@@ -16,9 +16,9 @@ export function getPlatformString() {
try {
const platform = navigator.platform.toLowerCase()
let trimmed = ''
if (platform.includes('iphone')) {
if (platform.includes('iphone') || isIOS()) {
trimmed = 'ios'
} else if (platform.includes('android')) {
} else if (platform.includes('android') || isAndroid()) {
trimmed = 'android'
} else if (platform.includes('mac')) {
trimmed = 'mac'