@witchcraft/spellcraft
    Preparing search index...

    Type Alias Manager<THooks, TKeys, TShortcuts, TCommands, TContext, TListener>

    type Manager<
        THooks extends Partial<Hooks> = Partial<Hooks>,
        TKeys extends Keys = Keys,
        TShortcuts extends Shortcuts = Shortcuts,
        TCommands extends Commands = Commands,
        TContext extends Context = Context,
        TListener extends ManagerListener = ManagerListener,
    > = {
        commands: TCommands;
        context: TContext;
        hooks: THooks;
        keys: TKeys;
        listener?: TListener;
        name: string;
        options: {
            cb: (
                manager: Manager,
                error: MultipleErrors<
                    | typeof MULTIPLE_MATCHING_SHORTCUTS
                    | typeof NO_MATCHING_SHORTCUT
                    | typeof UNKNOWN_KEY_EVENT
                    | typeof UNKNOWN_KEY_ID
                    | typeof UNKNOWN_KEY,
                >,
                e?: AnyInputEvent,
            ) => void;
            conditionEquals: ConditionComparer;
            enableListeners: boolean;
            enableShortcuts: boolean;
            evaluateCondition: ConditionEvaluator<TContext>;
            sorter: IKeysSorter;
            stringifier: IStringifier;
            updateStateOnAllEvents: boolean;
        };
        shortcuts: TShortcuts;
        state: {
            chain: string[][];
            isAwaitingKeyup: boolean;
            isRecording: boolean;
            nextIsChord: boolean;
            untrigger: false
            | TriggerableShortcut;
        };
        type: "manager";
    }

    Type Parameters

    Index

    Properties

    commands: TCommands
    context: TContext
    hooks: THooks

    Hooks provide a way to set further limits on what can be changed (can hooks), and provide listeners for when something is about to change (on* hooks).

    on* hooks allow hooking into changes for certain properties (such as key size/pos changed) to trigger recalculations (e.g. of layouts).

    They could also technically be used as an escape hatch for frameworks that don't support working mutable data, but I would advice against it. Instead I would recommend using a proxy based state management solution like valtio that allows for mutations and optimized rendering.

    keys: TKeys
    listener?: TListener

    An event listener for all events the manager handles including virtualPress/virtualRelease.

    The listener is called right after keys are found so you can have access to the keys as the manager understood them, but before everything else (setting the state, labeling, adding/removing from the chain). Note that isKeydown is always true for wheel events since their keyup is emulated within the same event handler.

    This exists because not all events trigger shortcuts (which can access only the event that triggered them) but there are many times when you might still want access to the event to do things like e.preventdefault():

    • When recording, we usually always need to do e.preventDefault() except in some rare cases:
      • For example say you have a div which records while focused, if the manager is attached to a parent element, you will have to allow clicks to the parent so losing focus stops the recording. You do not generally need to allow keys through as you can set all shortcut conditions to only trigger when not recording, temporily swap out the shortcuts while recording, or use the onSetKeyProp hook (note this would mean hardcoding keys, but might be acceptable for a case such as this).
      • We also need to e.preventDefault() for any chords that are also browser shortcuts (though note not all browser shortcuts can be overriden). If you really need to there is the Keyboard API which allows requesting some keyboard shortcuts be locked (see Keyboard Locking).

    The following should give you a good starting point:

    manager.listener = (({event, isKeydown, keys}) => {
    if (
    (manager.isRecording && !(event instanceof MouseEvent))
    // || TODO browser shortcuts filter
    ) {
    event.preventDefault()
    }
    })
    name: string

    A name for the manager.

    options: {
        cb: (
            manager: Manager,
            error: MultipleErrors<
                | typeof MULTIPLE_MATCHING_SHORTCUTS
                | typeof NO_MATCHING_SHORTCUT
                | typeof UNKNOWN_KEY_EVENT
                | typeof UNKNOWN_KEY_ID
                | typeof UNKNOWN_KEY,
            >,
            e?: AnyInputEvent,
        ) => void;
        conditionEquals: ConditionComparer;
        enableListeners: boolean;
        enableShortcuts: boolean;
        evaluateCondition: ConditionEvaluator<TContext>;
        sorter: IKeysSorter;
        stringifier: IStringifier;
        updateStateOnAllEvents: boolean;
    }

    Most options can be set directly, except any readonly options which must go through setManagerProperties to ensure state remains consistent.

    Type Declaration

    • cb: (
          manager: Manager,
          error: MultipleErrors<
              | typeof MULTIPLE_MATCHING_SHORTCUTS
              | typeof NO_MATCHING_SHORTCUT
              | typeof UNKNOWN_KEY_EVENT
              | typeof UNKNOWN_KEY_ID
              | typeof UNKNOWN_KEY,
          >,
          e?: AnyInputEvent,
      ) => void

      The error callback for recoverable errors such as multiple shortcuts matching, no shortcut matching once a chord chain has been "started", or an unknown key event because no matching key was found (only for keyboard events).

      Usually you will want to clear the manager's chain when this happens and display the error to the user. The default callback logs the error and clears the chain.

      In the case of multiple valid shortcuts, if you trigger any of the shortcuts "manually" note that you will need to simulate both the keydown/keyup calls if you differentiate between them.

      Also the input event can be undefined if you set the manager chain directly since it will check if it should trigger shortcuts.

    • conditionEquals: ConditionComparer

      Determines if two conditions are equal.

      This is actually not a good idea to implement if you use boolean conditions. See ConditionComparer for why.

    • enableListeners: boolean

      Enable/disable listeners. Listeners will remain attached but do nothing.

    • enableShortcuts: boolean

      Enable/disable triggering of shortcuts. The manager otherwise works as normal.

    • evaluateCondition: ConditionEvaluator<TContext>

      Determines how conditions are evaluated.

    • sorter: IKeysSorter
    • stringifier: IStringifier
    • updateStateOnAllEvents: boolean

      Whether to check the state of modifier or toggle keys using event.getModifierState.

      This is set to true by default when using createManager because it is usually what you want. It tracks the state with the most accuracy when, for example, the user focuses out, toggles a key, then focuses back.

      But, if you are allowing the user to change key states in some way (e.g. clicking on keys in the settings to visualize shortcuts), you will want to disable this temporarily so that they can click modifier keys. Otherwise they'd be immediately toggled off again (by the state check during the click) and nothing would happen.

      Keys also have their own individual Key.updateStateOnAllEvents in case you need more fine grained control.

      When this is set to false, the default mouseenter handler will ignore the event.

      If you will always have this set to false, you can forego the mouseenter event listener.

    shortcuts: TShortcuts
    state: {
        chain: string[][];
        isAwaitingKeyup: boolean;
        isRecording: boolean;
        nextIsChord: boolean;
        untrigger: false | TriggerableShortcut;
    }

    The manager requires some state to function and be a little bit more efficient. All properties are readonly because they should not be modified unless they are allowed to be by setManagerProp.

    Type Declaration

    • Readonlychain: string[][]

      The current chain of chords.

      Note that the manager's chain is not neccesarily valid and should be checked before assigning to a shortcut.

    • ReadonlyisAwaitingKeyup: boolean

      Whether the manager is waiting for non-modifier keys to be release.

    • ReadonlyisRecording: boolean

      Whether the manager is in recording mode.

      When enabling/disabling this property, you should clear the chain first with safeSetSetManagerChain.

      safeSetManagerChain(manager, [])
      setManagerProp(manager, "state.isRecording", true)
      //...
      safeSetManagerChain(manager, [])
      setManagerProp(manager, "state.isRecording", false)

      To allow users to record shortcuts, you can do something like have an input element that on focus starts recording and stops when focused is blurred (this has the advantage of working with keyboard navigation). In such a case, you can use the Manager.listener to e.preventDefault() all events except clicks outside the input. See it for more details.

    • ReadonlynextIsChord: boolean

      The manager keeps track of whether the next key press should start a new chord or not.

    • Readonlyuntrigger: false | TriggerableShortcut

      There are times, such as after a keydown event, that a shortcut command will trigger, but we also need to "untrigger" it later on key up. If there is a triggerable shortcut it is saved here. See Manager.state.chain.

    type: "manager"