Files
MyFastHtml/docs/Keyboard Support.md
2025-11-26 20:53:12 +01:00

9.5 KiB

Keyboard Support - Test Instructions

⚠️ Breaking Change

Version 2.0 uses HTMX configuration objects instead of simple URL strings. The old format is not supported.

Old format (no longer supported):

{"A": "/url"}

New format (required):

{"A": {"hx-post": "/url"}}

Files

  • keyboard_support.js - Main keyboard support library with smart timeout logic
  • test_keyboard_support.html - Test page to verify functionality

Key Features

Multiple Simultaneous Triggers

IMPORTANT: If multiple elements listen to the same combination, ALL of them will be triggered:

add_keyboard_support('modal', '{"esc": "/close-modal"}');
add_keyboard_support('editor', '{"esc": "/cancel-edit"}');
add_keyboard_support('sidebar', '{"esc": "/hide-sidebar"}');

// Pressing ESC will trigger all 3 URLs simultaneously

This is crucial for use cases like the ESC key, which often needs to cancel multiple actions at once (close modal, cancel edit, hide panels, etc.).

Smart Timeout Logic (Longest Match)

The library uses a single global timeout based on the sequence state, not on individual elements:

Key principle: If any element has a longer sequence possible, all matching elements wait.

Examples:

Example 1: Three elements, same combination

add_keyboard_support('elem1', '{"esc": "/url1"}');
add_keyboard_support('elem2', '{"esc": "/url2"}');
add_keyboard_support('elem3', '{"esc": "/url3"}');
// Press ESC → ALL 3 trigger immediately (no longer sequences exist)

Example 2: Mixed - one has longer sequence

add_keyboard_support('elem1', '{"A": "/url1"}');
add_keyboard_support('elem2', '{"A": "/url2"}');
add_keyboard_support('elem3', '{"A": "/url3", "A B": "/url3b"}');
// Press A:
// - elem3 has "A B" possible → EVERYONE WAITS 500ms
// - If B arrives: only elem3 triggers with "A B"
// - If timeout expires: elem1, elem2, elem3 ALL trigger with "A"

Example 3: Different combinations

add_keyboard_support('elem1', '{"A B": "/url1"}');
add_keyboard_support('elem2', '{"C D": "/url2"}');
// Press A: elem1 waits for B, elem2 not affected
// Press C: elem2 waits for D, elem1 not affected

The timeout is tied to the sequence being typed, not to individual elements.

Smart Timeout Logic (Longest Match)

Keyboard shortcuts are disabled when typing in input fields:

  • <input> elements
  • <textarea> elements
  • Any contenteditable element

This ensures normal typing (Ctrl+C, Ctrl+A, etc.) works as expected in forms.

How to Test

  1. Download both files to the same directory

  2. Open test_keyboard_support.html in a web browser

  3. Try the configured shortcuts:

    • a - Simple key (waits if "A B" might follow)
    • Ctrl+S - Save combination (immediate)
    • Ctrl+C - Copy combination (waits because "Ctrl+C C" exists)
    • A B - Sequence (waits because "A B C" exists)
    • A B C - Triple sequence (triggers immediately)
    • Ctrl+C C - Press Ctrl+C together, release, then press C alone
    • Ctrl+C Ctrl+C - Press Ctrl+C, keep Ctrl, release C, press C again
    • shift shift - Press Shift twice in sequence
    • esc - Escape key (immediate)
  4. Test focus behavior:

    • Click on the test element to focus it (turns blue)
    • Try shortcuts with focus
    • Click outside to remove focus
    • Try shortcuts without focus
    • The log shows whether the element had focus when triggered
  5. Test input protection:

    • Try typing in the input field
    • Use Ctrl+C, Ctrl+A, etc. - should work normally
    • Shortcuts should NOT trigger while typing in input

Expected Behavior

Smart Timeout Examples

Scenario 1: Only "A" is configured (no element has "A B")

  • Press A → Triggers immediately

Scenario 2: At least one element has "A B"

  • Press A → ALL elements with "A" wait 500ms
  • If B pressed within 500ms → Only elements with "A B" trigger
  • If timeout expires → ALL elements with "A" trigger

Scenario 3: "A", "A B", and "A B C" all configured (same or different elements)

  • Press A → Waits (because "A B" exists)
  • Press B → Waits (because "A B C" exists)
  • Press C → Triggers "A B C" immediately

Scenario 4: Multiple elements, ESC on all

add_keyboard_support('modal', '{"esc": "/close"}');
add_keyboard_support('editor', '{"esc": "/cancel"}');
  • Press ESC → Both trigger simultaneously (no longer sequences)

Integration in Your Project

Integration in Your Project

Configuration Format

The library now uses HTMX configuration objects instead of simple URL strings:

# New format with HTMX configuration
combinations = {
    "Ctrl+S": {
        "hx-post": "/save-url",
        "hx-target": "#result",
        "hx-swap": "innerHTML"
    },
    "A B": {
        "hx-post": "/sequence-url",
        "hx-vals": {"extra": "data"}
    },
    "esc": {
        "hx-get": "/cancel-url"
    }
}

# This will generate the JavaScript call
f"add_keyboard_support('{element_id}', '{json.dumps(combinations)}')"

Supported HTMX Attributes

You can use any HTMX attribute in the configuration object:

HTTP Methods (one required):

  • hx-post - POST request
  • hx-get - GET request
  • hx-put - PUT request
  • hx-delete - DELETE request
  • hx-patch - PATCH request

Common Options:

  • hx-target - Target element selector
  • hx-swap - Swap strategy (innerHTML, outerHTML, etc.)
  • hx-vals - Additional values to send (object)
  • hx-headers - Custom headers (object)
  • hx-select - Select specific content from response
  • hx-confirm - Confirmation message

All other hx-* attributes are supported and will be converted to the appropriate htmx.ajax() parameters.

Automatic Parameters

The library automatically adds these parameters to every request:

  • combination - The combination that triggered the action (e.g., "Ctrl+S")
  • has_focus - Boolean indicating if the element had focus
  • is_inside - Boolean indicating if the focus is inside the element (element itself or any child)

Example final request:

htmx.ajax('POST', '/save-url', {
  target: '#result',
  swap: 'innerHTML',
  values: {
    extra: "data",           // from hx-vals
    combination: "Ctrl+S",   // automatic
    has_focus: true,         // automatic
    is_inside: true          // automatic
  }
})

Integration Examples

Basic Usage

combinations = {
    "Ctrl+S": {
        "hx-post": "/save"
    }
}

With Target and Swap

combinations = {
    "Ctrl+D": {
        "hx-delete": "/item",
        "hx-target": "#item-list",
        "hx-swap": "outerHTML"
    }
}

With Extra Values

combinations = {
    "Ctrl+N": {
        "hx-post": "/create",
        "hx-vals": json.dumps({"type": "quick", "mode": "keyboard"})
    }
}

Multiple Elements Example

# Modal close
modal_combinations = {
    "esc": {
        "hx-post": "/modal/close",
        "hx-target": "#modal",
        "hx-swap": "outerHTML"
    }
}

# Editor cancel
editor_combinations = {
    "esc": {
        "hx-post": "/editor/cancel",
        "hx-target": "#editor",
        "hx-swap": "innerHTML"
    }
}

# Both will trigger when ESC is pressed
f"add_keyboard_support('modal', '{json.dumps(modal_combinations)}')"
f"add_keyboard_support('editor', '{json.dumps(editor_combinations)}')"

Removing Keyboard Support

When you no longer need keyboard support for an element:

# Remove keyboard support
f"remove_keyboard_support('{element_id}')"

Behavior:

  • Removes the element from the keyboard registry
  • If this was the last element, automatically detaches global event listeners
  • Cleans up all associated state (timeouts, snapshots, etc.)
  • Other elements continue to work normally

Example:

// Add support
add_keyboard_support('modal', '{"esc": {"hx-post": "/close"}}');

// Later, remove support
remove_keyboard_support('modal');
// If no other elements remain, keyboard listeners are completely removed

API Reference

add_keyboard_support(elementId, combinationsJson)

Adds keyboard support to an element.

Parameters:

  • elementId (string): ID of the HTML element
  • combinationsJson (string): JSON string of combinations with HTMX configs

Returns: void

remove_keyboard_support(elementId)

Removes keyboard support from an element.

Parameters:

  • elementId (string): ID of the HTML element

Returns: void

Side effects:

  • If last element: detaches global event listeners and cleans up all state

Technical Details

Trie-based Matching

The library uses a prefix tree (trie) data structure:

  • Each node represents a keyboard snapshot (set of pressed keys)
  • Leaf nodes contain the HTMX configuration object
  • Intermediate nodes indicate longer sequences exist
  • Enables efficient O(n) matching where n is sequence length

HTMX Integration

Configuration objects are mapped to htmx.ajax() calls:

  • hx-* attributes are converted to camelCase parameters
  • HTTP method is extracted from hx-post, hx-get, etc.
  • combination, has_focus, and is_inside are automatically added to values
  • All standard HTMX options are supported

Key Normalization

  • Case-insensitive: "ctrl" = "Ctrl" = "CTRL"
  • Mapped keys: "Control" → "ctrl", "Escape" → "esc", "Delete" → "del"
  • Simultaneous keys represented as sorted sets

Notes

  • The test page mocks htmx.ajax to display results in the console
  • In production, real AJAX calls will be made to your backend
  • Sequence timeout is 500ms between keys
  • Maximum 10 snapshots kept in history to prevent memory issues