Skip to content

Commit

Permalink
Merge pull request #13 from RobertWHurst/feature/keycombo-seq-and-bin…
Browse files Browse the repository at this point in the history
…d-env

Introduce `checkKeyComboSequenceIndex` and improve `bindEnvironment`
  • Loading branch information
RobertWHurst authored Jul 23, 2023
2 parents 17cac18 + 0e66b5c commit 32bae21
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 20 deletions.
9 changes: 9 additions & 0 deletions packages/keystrokes/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,15 @@ using Keystrokes in. They are always case insensitive. The default behavior,
intended for browser environments, is to use the value of the key property from
keyboard events. You get get a list of valid [key names here][key-names].

Key combos are made up of key names and operators. The operators separate the
key combo into it's parts.

| Operator | Name | Description
|----------|--------------------|-----------------------------------------------------
| + | Key Unit | A group of key names. Can be pressed in any order.
| > | Group Separator | Separates key units. Each key unit must be pressed and held in order.
| , | Sequence Separator | Separates groups. Each group must be pressed and released in order. This will be familiar to vim users.

```js
import { bindKey, bindKeyCombo } from '@rwh/keystrokes'

Expand Down
15 changes: 15 additions & 0 deletions packages/keystrokes/src/key-combo-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,21 @@ export class KeyComboState<OriginalEvent, KeyEventProps, KeyComboEventProps> {

const s = keyComboStr.toLowerCase()

// operator
let o = ''

// unit
let k: string[] = []

// group
let x: string[][] = [k]

// sequence
let y: string[][][] = [x]

// combo
const z: string[][][][] = [y]

let isEscaped = false

for (let i = 0; i < keyComboStr.length; i += 1) {
Expand Down Expand Up @@ -100,6 +110,11 @@ export class KeyComboState<OriginalEvent, KeyEventProps, KeyComboEventProps> {
return !!this._isPressedWithFinalKey
}

get sequenceIndex() {
if (this.isPressed) return this._parsedKeyCombo.length
return this._sequenceIndex
}

private _normalizedKeyCombo: string
private _parsedKeyCombo: string[][][]
private _handlerState: HandlerState<
Expand Down
77 changes: 57 additions & 20 deletions packages/keystrokes/src/keystrokes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,12 @@ export type KeystrokesOptions<
OriginalEvent = KeyboardEvent,
KeyEventProps = MaybeBrowserKeyEventProps<OriginalEvent>,
KeyComboEventProps = MaybeBrowserKeyComboEventProps<OriginalEvent>,
> = BindEnvironmentOptions<OriginalEvent, KeyEventProps, KeyComboEventProps>

export type BindEnvironmentOptions<
OriginalEvent = KeyboardEvent,
KeyEventProps = MaybeBrowserKeyEventProps<OriginalEvent>,
KeyComboEventProps = MaybeBrowserKeyComboEventProps<OriginalEvent>,
> = {
onActive?: OnActiveEventBinder
onInactive?: OnActiveEventBinder
Expand Down Expand Up @@ -123,15 +129,13 @@ export class Keystrokes<
this._isActive = true
this._isUpdatingKeyComboState = false

this._onActiveBinder = options.onActive ?? browserOnActiveBinder
this._onInactiveBinder = options.onInactive ?? browserOnInactiveBinder
this._onKeyPressedBinder =
options.onKeyPressed ?? (browserOnKeyPressedBinder as any)
this._onKeyReleasedBinder =
options.onKeyReleased ?? (browserOnKeyReleasedBinder as any)
this._keyComboEventMapper = options.mapKeyComboEvent ?? (() => ({}) as any)
this._selfReleasingKeys = options.selfReleasingKeys ?? []
this._keyRemap = options.keyRemap ?? {}
this._onActiveBinder = () => {}
this._onInactiveBinder = () => {}
this._onKeyPressedBinder = () => {}
this._onKeyReleasedBinder = () => {}
this._keyComboEventMapper = () => ({}) as any
this._selfReleasingKeys = []
this._keyRemap = {}

this._handlerStates = {}
this._keyComboStates = {}
Expand All @@ -141,7 +145,7 @@ export class Keystrokes<

this._watchedKeyComboStates = {}

this.bindEnvironment()
this.bindEnvironment(options)
}

get pressedKeys() {
Expand Down Expand Up @@ -237,21 +241,41 @@ export class Keystrokes<
}

checkKeyCombo(keyCombo: string) {
keyCombo = KeyComboState.normalizeKeyCombo(keyCombo)
if (!this._watchedKeyComboStates[keyCombo]) {
this._watchedKeyComboStates[keyCombo] = new KeyComboState(
keyCombo,
this._keyComboEventMapper,
)
}
const keyComboState = this._watchedKeyComboStates[keyCombo]
keyComboState.updateState(this._activeKeyPresses)
const keyComboState = this._ensureCachedKeyComboState(keyCombo)
return keyComboState.isPressed
}

bindEnvironment() {
checkKeyComboSequenceIndex(keyCombo: string) {
const keyComboState = this._ensureCachedKeyComboState(keyCombo)
return keyComboState.sequenceIndex
}

bindEnvironment(
options: BindEnvironmentOptions<
OriginalEvent,
KeyEventProps,
KeyComboEventProps
> = {},
) {
this.unbindEnvironment()

this._onActiveBinder = options.onActive ?? browserOnActiveBinder
this._onInactiveBinder = options.onInactive ?? browserOnInactiveBinder
this._onKeyPressedBinder =
options.onKeyPressed ??
(browserOnKeyPressedBinder as OnKeyEventBinder<
OriginalEvent,
KeyEventProps
>)
this._onKeyReleasedBinder =
options.onKeyReleased ??
(browserOnKeyReleasedBinder as OnKeyEventBinder<
OriginalEvent,
KeyEventProps
>)
this._keyComboEventMapper = options.mapKeyComboEvent ?? (() => ({}) as any)
this._selfReleasingKeys = options.selfReleasingKeys ?? []

const unbindActive = this._onActiveBinder(() => {
this._isActive = true
})
Expand All @@ -277,6 +301,19 @@ export class Keystrokes<
this._unbinder?.()
}

private _ensureCachedKeyComboState(keyCombo: string) {
keyCombo = KeyComboState.normalizeKeyCombo(keyCombo)
if (!this._watchedKeyComboStates[keyCombo]) {
this._watchedKeyComboStates[keyCombo] = new KeyComboState(
keyCombo,
this._keyComboEventMapper,
)
}
const keyComboState = this._watchedKeyComboStates[keyCombo]
keyComboState.updateState(this._activeKeyPresses)
return keyComboState
}

private _handleKeyPress(event: KeyEvent<OriginalEvent, KeyEventProps>) {
;(async () => {
if (!this._isActive) {
Expand Down
105 changes: 105 additions & 0 deletions packages/keystrokes/src/tests/keystrokes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,63 @@ describe('new Keystrokes(options)', () => {
expect(keystrokes.checkKeyCombo('meta > z')).toBe(false)
})

describe('#bindEnvironment(options)', () => {
it('accepts a custom focus, blur, key pressed and key released binder', () => {
const keystrokes = new Keystrokes()

type EmptyObject = Record<never, never>

let press: ((event: KeyEvent<EmptyObject, EmptyObject>) => void) | null =
null
let release:
| ((event: KeyEvent<EmptyObject, EmptyObject>) => void)
| null = null

const onActive = vi.fn()
const onInactive = vi.fn()
const onKeyPressed = vi.fn((p) => (press = p))
const onKeyReleased = vi.fn((r) => (release = r))

const aPressed = vi.fn()
const aReleased = vi.fn()

// Replaces the default browser binders
keystrokes.bindEnvironment({
onActive,
onInactive,
onKeyPressed,
onKeyReleased,
})

expect(onActive).toBeCalledTimes(1)
expect(onActive).toBeCalledWith(expect.any(Function))

expect(onInactive).toBeCalledTimes(1)
expect(onInactive).toBeCalledWith(expect.any(Function))

expect(onKeyPressed).toBeCalledTimes(1)
expect(onKeyPressed).toBeCalledWith(expect.any(Function))

expect(onKeyReleased).toBeCalledTimes(1)
expect(onKeyReleased).toBeCalledWith(expect.any(Function))

keystrokes.bindKey('a', { onPressed: aPressed, onReleased: aReleased })

expect(aPressed).toBeCalledTimes(0)
expect(aReleased).toBeCalledTimes(0)

press!({ key: 'a' })

expect(aPressed).toBeCalledTimes(1)
expect(aReleased).toBeCalledTimes(0)

release!({ key: 'a' })

expect(aPressed).toBeCalledTimes(1)
expect(aReleased).toBeCalledTimes(1)
})
})

describe('#bindKey(keyCombo, handler)', () => {
it('accepts a key and handler which is executed repeatedly while the key is pressed', () => {
const keystrokes = createTestKeystrokes()
Expand Down Expand Up @@ -443,4 +500,52 @@ describe('new Keystrokes(options)', () => {
expect(keystrokes.checkKeyCombo('a>b')).toBe(false)
})
})

describe('#checkKeyComboSequenceIndex(keyCombo)', () => {
it('will return the index of the last active key combo sequence', () => {
const keystrokes = createTestKeystrokes()

const keyCombo = 'a>b,c+d,e,f>g'

expect(keystrokes.checkKeyComboSequenceIndex(keyCombo)).toBe(0)

keystrokes.press({ key: 'a' })
keystrokes.press({ key: 'b' })

expect(keystrokes.checkKeyComboSequenceIndex(keyCombo)).toBe(1)

keystrokes.release({ key: 'a' })
keystrokes.release({ key: 'b' })

expect(keystrokes.checkKeyComboSequenceIndex(keyCombo)).toBe(1)

keystrokes.press({ key: 'd' })
keystrokes.press({ key: 'c' })

expect(keystrokes.checkKeyComboSequenceIndex(keyCombo)).toBe(2)

keystrokes.release({ key: 'c' })
keystrokes.release({ key: 'd' })

expect(keystrokes.checkKeyComboSequenceIndex(keyCombo)).toBe(2)

keystrokes.press({ key: 'e' })

expect(keystrokes.checkKeyComboSequenceIndex(keyCombo)).toBe(3)

keystrokes.release({ key: 'e' })

expect(keystrokes.checkKeyComboSequenceIndex(keyCombo)).toBe(3)

keystrokes.press({ key: 'f' })
keystrokes.press({ key: 'g' })

expect(keystrokes.checkKeyComboSequenceIndex(keyCombo)).toBe(4)

keystrokes.release({ key: 'f' })
keystrokes.release({ key: 'g' })

expect(keystrokes.checkKeyComboSequenceIndex(keyCombo)).toBe(0)
})
})
})

0 comments on commit 32bae21

Please sign in to comment.