Script Generators

Generate complete keyboard navigation JavaScript from configuration.

Zone State Management


js_zone_state


def js_zone_state(
    
)->str: # JavaScript state and getter/setter code

Generate JavaScript code for zone state management.

Focus Management


js_focus_management


def js_focus_management(
    
)->str: # JavaScript focus management code

Generate JavaScript code for focus management.

Zone Switching


js_zone_switching


def js_zone_switching(
    
)->str: # JavaScript zone switching code

Generate JavaScript code for zone switching.

Mode Management


js_mode_management


def js_mode_management(
    
)->str: # JavaScript mode management code

Generate JavaScript code for mode management.

Action Dispatch


js_action_dispatch


def js_action_dispatch(
    
)->str: # JavaScript action dispatch code

Generate JavaScript code for action dispatch.

Keyboard Handler


js_keyboard_handler


def js_keyboard_handler(
    
)->str: # JavaScript keyboard handler code

Generate JavaScript code for keyboard event handling.

State Notification


js_state_notification


def js_state_notification(
    
)->str: # JavaScript state notification code

Generate JavaScript code for state change notification.

Initialization

After HTMX swaps, the DOM changes but our state (focus indices) may be stale. The initialization function: 1. Reads the hidden input values (set before the swap) 2. Finds items with matching data attributes in the new DOM 3. Updates focus indices to follow moved/reordered items 4. Falls back to clamping if item not found (deleted)


js_initialization


def js_initialization(
    
)->str: # JavaScript initialization code

Generate JavaScript code for initialization with focus recovery.

Global API

Expose mode control functions globally for programmatic access from external code (e.g., HTMX responses that need to sync mode state).


js_global_api


def js_global_api(
    
)->str: # JavaScript global API exposure code

Generate JavaScript code to expose control functions globally.


generate_keyboard_script


def generate_keyboard_script(
    manager:ZoneManager, # the zone manager configuration
)->str: # complete JavaScript code wrapped in IIFE

Generate complete keyboard navigation JavaScript from ZoneManager.

# Test complete script generation
from cjm_fasthtml_keyboard_navigation.core.focus_zone import FocusZone
from cjm_fasthtml_keyboard_navigation.core.actions import KeyAction

browser = FocusZone(
    id="browser",
    item_selector="tr.item",
    data_attributes=("job-id",)
)
queue = FocusZone(
    id="queue",
    item_selector="li.item"
)

manager = ZoneManager(
    zones=(browser, queue),
    actions=(
        KeyAction(key=" ", htmx_trigger="toggle-btn"),
        KeyAction(key="Delete", htmx_trigger="delete-btn"),
        # Documentation-only action — verifies the JS dispatcher short-circuit gets emitted
        KeyAction.documentation_only(
            key="ArrowLeft",
            description="Move caret left",
            zone_ids=("browser",),
            hint_group="Selection",
        ),
    )
)

script = generate_keyboard_script(manager)

# Verify coordinator is included
assert "window.kbCoordinator" in script
assert "register" in script

# Verify key parts are present
assert "(function()" in script
assert "const cfg" in script
assert '"systemId"' in script
assert "isInputFocused" in script
assert "handleKeydown" in script
assert "initialize" in script
assert "window.kbNav" in script
assert "})();" in script

# Verify coordinator registration replaces direct listener
assert "kbCoordinator.register(cfg.systemId" in script
# Verify direct listener is NOT used
assert "document.addEventListener('keydown', handleKeydown)" not in script

# Verify handleKeydown returns result objects
assert "NOT_HANDLED" in script
assert "handled: true" in script
assert "handled: false" in script

# Verify Escape-to-deactivate
assert "deactivateChild" in script
assert "hasParent" in script

# Verify per-system API
assert "window.kbNav[cfg.systemId]" in script

# Verify documentation-only short-circuit predicate is emitted in the dispatcher.
# This is the load-bearing assertion: without this predicate, a documentation-only
# KeyAction would still call e.preventDefault() / e.stopPropagation() and break
# client-side event listeners that handle the same key (e.g., caret movement).
assert "!action.htmxTrigger && !action.jsCallback && !action.modeEnter && !action.modeExit" in script, \
    "JS dispatcher must short-circuit documentation-only KeyActions so client-side handlers receive the event unaltered"

# Verify activate-child branch is emitted in the dispatcher.
# Defensive: the manager above has no `activate_child_id`/`activate_child_callback`
# set on its zones, but the JS branch should still be emitted (gates at runtime
# based on the zone's data). This catches accidental removal of the dispatch step.
assert "cfg.activateKeys" in script, \
    "JS dispatcher must consult cfg.activateKeys for library-baked child activation"
assert "zone.activateChildCallback" in script, \
    "JS dispatcher must dispatch via zone.activateChildCallback (dynamic path)"
assert "zone.activateChildId" in script, \
    "JS dispatcher must dispatch via zone.activateChildId (declarative path)"
assert "setActiveChild(cfg.systemId, zone.activateChildId)" in script, \
    "Declarative path must call coord.setActiveChild with parent's systemId"

# Verify activate-child branch fires AFTER findMatchingAction so consumer
# KeyActions can override the library default. We assert the substring order
# rather than parse the JS — sufficient for catching reorderings.
fma_pos = script.find("findMatchingAction(key, mods)")
activate_pos = script.find("cfg.activateKeys && cfg.activateKeys.includes(key)")
assert fma_pos >= 0 and activate_pos >= 0
assert fma_pos < activate_pos, \
    "Activate-child branch must run AFTER findMatchingAction so consumer KeyActions win"

# Verify activate_keys is plumbed through to_js_config → JS-side cfg
assert '"activateKeys"' in script, \
    "activate_keys must be serialized in the JS config"