Skip to content

Commit

Permalink
Add initial keymap configuration
Browse files Browse the repository at this point in the history
Add shortcuts for desktop and web app
- Sidenav toggle
- Global Search toggle
- Preferences toggle
Comment out keymaps not implemented
  • Loading branch information
Komediruzecki committed Nov 30, 2021
1 parent fc801b6 commit 86f47df
Show file tree
Hide file tree
Showing 18 changed files with 1,145 additions and 56 deletions.
73 changes: 58 additions & 15 deletions src/cloud/components/Application.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import {
import { isActiveElementAnInput, InputableDomElement } from '../lib/dom'
import { useEffectOnce } from 'react-use'
import { useSettings } from '../lib/stores/settings'
import { isPageSearchShortcut, isSidebarToggleShortcut } from '../lib/shortcuts'
import { useSearch } from '../lib/stores/search'
import AnnouncementAlert from './AnnouncementAlert'
import {
Expand All @@ -20,6 +19,8 @@ import {
newDocEventEmitter,
switchSpaceEventEmitter,
SwitchSpaceEventDetails,
togglePreviewModeEventEmitter,
toggleSplitEditModeEventEmitter,
} from '../lib/utils/events'
import { usePathnameChangeEffect, useRouter } from '../lib/router'
import { useNav } from '../lib/stores/nav'
Expand Down Expand Up @@ -76,6 +77,7 @@ import {
} from './molecules/PageSearch/InPageSearchPortal'
import SidebarToggleButton from './SidebarToggleButton'
import { getTeamURL } from '../lib/utils/patterns'
import { compareEventKeyWithKeymap } from '../../lib/keymap'

interface ApplicationProps {
className?: string
Expand Down Expand Up @@ -140,7 +142,9 @@ const Application = ({

useEffect(() => {
const handler = () => {
setShowFuzzyNavigation((prev) => !prev)
if (usingElectron) {
setShowFuzzyNavigation((prev) => !prev)
}
}
searchEventEmitter.listen(handler)
return () => {
Expand Down Expand Up @@ -229,11 +233,47 @@ const Application = ({
return
}

if (isSidebarToggleShortcut(event)) {
preventKeyboardEventPropagation(event)
setPreferences((prev) => {
return { sidebarIsHidden: !prev.sidebarIsHidden }
})
const keymap = preferences['keymap']
if (keymap != null) {
const sidenavToggleShortcut = keymap.get('toggleSideNav')
if (compareEventKeyWithKeymap(sidenavToggleShortcut, event)) {
preventKeyboardEventPropagation(event)
setPreferences((prev) => {
return { sidebarIsHidden: !prev.sidebarIsHidden }
})
}
}

if (!usingElectron && keymap != null) {
const toggleGlobalSearchShortcut = keymap.get('toggleGlobalSearch')
if (compareEventKeyWithKeymap(toggleGlobalSearchShortcut, event)) {
preventKeyboardEventPropagation(event)
searchEventEmitter.dispatch()
}
}

if (!usingElectron && keymap != null) {
const openPreferencesShortcut = keymap.get('openPreferences')
if (compareEventKeyWithKeymap(openPreferencesShortcut, event)) {
preventKeyboardEventPropagation(event)
openSettingsTab('preferences')
}
}

if (!usingElectron && keymap != null) {
const togglePreviewModeShortcut = keymap.get('togglePreviewMode')
if (compareEventKeyWithKeymap(togglePreviewModeShortcut, event)) {
preventKeyboardEventPropagation(event)
togglePreviewModeEventEmitter.dispatch()
}
}

if (!usingElectron && keymap != null) {
const toggleSplitEditModeShortcut = keymap.get('toggleSplitEditMode')
if (compareEventKeyWithKeymap(toggleSplitEditModeShortcut, event)) {
preventKeyboardEventPropagation(event)
toggleSplitEditModeEventEmitter.dispatch()
}
}

if (isSingleKeyEvent(event, 'escape') && isActiveElementAnInput()) {
Expand All @@ -244,17 +284,20 @@ const Application = ({
;(document.activeElement as InputableDomElement).blur()
}

if (usingElectron && isPageSearchShortcut(event)) {
preventKeyboardEventPropagation(event)
if (showInPageSearch) {
setShowInPageSearch(false)
setShowInPageSearch(true)
} else {
setShowInPageSearch(true)
if (usingElectron && keymap != null) {
const inPageSearchShortcut = keymap.get('toggleInPageSearch')
if (compareEventKeyWithKeymap(inPageSearchShortcut, event)) {
preventKeyboardEventPropagation(event)
if (showInPageSearch) {
setShowInPageSearch(false)
setShowInPageSearch(true)
} else {
setShowInPageSearch(true)
}
}
}
},
[team, setPreferences, showInPageSearch]
[team, preferences, setPreferences, openSettingsTab, showInPageSearch]
)
useGlobalKeyDownHandler(overrideBrowserCtrlsHandler)

Expand Down
222 changes: 222 additions & 0 deletions src/cloud/components/molecules/KeymapItemSection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import React, {
KeyboardEventHandler,
useCallback,
useMemo,
useRef,
useState,
} from 'react'
import {
getGenericShortcutString,
KeymapItemEditableProps,
} from '../../../lib/keymap'
import Button from '../../../design/components/atoms/Button'
import styled from '../../../design/lib/styled'
import { inputStyle } from '../../../design/lib/styled/styleFunctions'
import cc from 'classcat'
import { useToast } from '../../../design/lib/stores/toast'

const invalidShortcutInputs = [' ']
const rejectedShortcutInputs = [' ', 'control', 'alt', 'shift', 'meta']

interface KeymapItemSectionProps {
keymapKey: string
currentKeymapItem?: KeymapItemEditableProps
updateKeymap: (
key: string,
shortcutFirst: KeymapItemEditableProps,
shortcutSecond?: KeymapItemEditableProps
) => Promise<void>
removeKeymap: (key: string) => void
description: string
}

const KeymapItemSection = ({
keymapKey,
currentKeymapItem,
updateKeymap,
removeKeymap,
description,
}: KeymapItemSectionProps) => {
const [inputError, setInputError] = useState<boolean>(false)
const [shortcutInputValue, setShortcutInputValue] = useState<string>('')
const [changingShortcut, setChangingShortcut] = useState<boolean>(false)
const [
currentShortcut,
setCurrentShortcut,
] = useState<KeymapItemEditableProps | null>(
currentKeymapItem != null ? currentKeymapItem : null
)
const [
previousShortcut,
setPreviousShortcut,
] = useState<KeymapItemEditableProps | null>(null)
const shortcutInputRef = useRef<HTMLInputElement>(null)

const { pushMessage } = useToast()

const fetchInputShortcuts: KeyboardEventHandler<HTMLInputElement> = (
event
) => {
event.stopPropagation()
event.preventDefault()
if (invalidShortcutInputs.includes(event.key.toLowerCase())) {
setInputError(true)
return
}

setInputError(false)

const shortcut: KeymapItemEditableProps = {
key: event.key.toUpperCase(),
keycode: event.keyCode,
modifiers: {
ctrl: event.ctrlKey,
alt: event.altKey,
shift: event.shiftKey,
meta: event.metaKey,
},
}
setCurrentShortcut(shortcut)
setShortcutInputValue(getGenericShortcutString(shortcut))
}

const applyKeymap = useCallback(() => {
if (currentShortcut == null) {
return
}
if (rejectedShortcutInputs.includes(currentShortcut.key.toLowerCase())) {
setInputError(true)
if (shortcutInputRef.current != null) {
shortcutInputRef.current.focus()
}
return
}

updateKeymap(keymapKey, currentShortcut, undefined)
.then(() => {
setChangingShortcut(false)
setInputError(false)
})
.catch(() => {
pushMessage({
title: 'Keymap assignment failed',
description: 'Cannot assign to already assigned shortcut',
})
setInputError(true)
})
}, [currentShortcut, keymapKey, updateKeymap, pushMessage])

const toggleChangingShortcut = useCallback(() => {
if (changingShortcut) {
applyKeymap()
} else {
setChangingShortcut(true)
setPreviousShortcut(currentShortcut)
if (currentShortcut != null) {
setShortcutInputValue(getGenericShortcutString(currentShortcut))
}
}
}, [applyKeymap, currentShortcut, changingShortcut])

const handleCancelKeymapChange = useCallback(() => {
setCurrentShortcut(previousShortcut)
setChangingShortcut(false)
setShortcutInputValue('')
setInputError(false)
}, [previousShortcut])

const handleRemoveKeymap = useCallback(() => {
setCurrentShortcut(null)
setPreviousShortcut(null)
setShortcutInputValue('')
removeKeymap(keymapKey)
}, [keymapKey, removeKeymap])

const shortcutString = useMemo(() => {
return currentShortcut != null && currentKeymapItem != null
? getGenericShortcutString(currentKeymapItem)
: ''
}, [currentKeymapItem, currentShortcut])
return (
<KeymapItemSectionContainer>
<div>{description}</div>
<KeymapItemInputSection>
{currentShortcut != null && currentKeymapItem != null && (
<ShortcutItemStyle>{shortcutString}</ShortcutItemStyle>
)}
{changingShortcut && (
<StyledInput
className={cc([inputError && 'error'])}
placeholder={'Press key'}
autoFocus={true}
ref={shortcutInputRef}
value={shortcutInputValue}
onChange={() => undefined}
onKeyDown={fetchInputShortcuts}
/>
)}
<Button variant={'primary'} onClick={toggleChangingShortcut}>
{currentShortcut == null
? 'Assign'
: changingShortcut
? 'Apply'
: 'Change'}
</Button>
{changingShortcut && (
<Button onClick={handleCancelKeymapChange}>Cancel</Button>
)}

{currentShortcut != null && !changingShortcut && (
<Button onClick={handleRemoveKeymap}>Un-assign</Button>
)}
</KeymapItemInputSection>
</KeymapItemSectionContainer>
)
}

const ShortcutItemStyle = styled.div`
min-width: 88px;
max-width: 120px;
height: 32px;
font-size: 15px;
display: flex;
align-items: center;
justify-content: center;
background-color: ${({ theme }) => theme.colors.background.tertiary};
color: ${({ theme }) => theme.colors.text.primary};
border: 1px solid ${({ theme }) => theme.colors.border.main};
border-radius: 4px;
`

const StyledInput = styled.input`
${inputStyle};
max-width: 120px;
min-width: 110px;
height: 1.3em;
&.error {
border: 1px solid red;
}
`

const KeymapItemSectionContainer = styled.div`
display: grid;
grid-template-columns: 45% minmax(55%, 400px);
`

const KeymapItemInputSection = styled.div`
display: grid;
grid-auto-flow: column;
align-items: center;
max-width: 380px;
justify-items: left;
margin-right: auto;
column-gap: 1em;
`

export default KeymapItemSection

0 comments on commit 86f47df

Please sign in to comment.