from cjm_fasthtml_card_stack.core.config import _reset_prefix_counterJS: Core
Master composer for card stack JavaScript. Combines viewport height,
Master Coordinator
Applies all viewport settings (width, height, scroll, touch) then reveals the viewport using a double-RAF pattern. Also handles HTMX afterSettle events.
Global Callback Registration
The cjm-fasthtml-keyboard-navigation library looks up JS callbacks via window[action.jsCallback](). Since our functions live on the namespaced ns object, we register global wrappers that delegate to the namespace.
global_callback_name
def global_callback_name(
prefix:str, # Card stack instance prefix
callback:str, # Base callback name (e.g., "jumpPageUp")
)->str: # Global function name (e.g., "cs0_jumpPageUp")
Generate a prefix-unique global callback name for keyboard navigation.
generate_card_stack_js
The main entry point that composes all JS fragments into a single namespaced IIFE. The consumer calls this once in their step renderer.
generate_card_stack_js
def generate_card_stack_js(
ids:CardStackHtmlIds, # HTML IDs for this instance
button_ids:CardStackButtonIds, # Button IDs for keyboard triggers
config:CardStackConfig, # Card stack configuration
urls:CardStackUrls, # URL bundle for routing
container_id:str='', # Consumer's parent container ID (for height calc)
extra_scripts:Tuple=(), # Additional JS to include in the IIFE
focus_position:Optional=None, # Focus slot offset (None=center, -1=bottom, 0=top)
)->Any: # Script element with all card stack JavaScript
Compose all card stack JS into a single namespaced IIFE.
# Test setup: shared fixtures for composition tests
_reset_prefix_counter()
config = CardStackConfig()
ids = CardStackHtmlIds(prefix=config.prefix)
btn = CardStackButtonIds(prefix=config.prefix)
urls = CardStackUrls(
nav_up="/cs/nav_up", nav_down="/cs/nav_down",
nav_first="/cs/nav_first", nav_last="/cs/nav_last",
nav_page_up="/cs/nav_page_up", nav_page_down="/cs/nav_page_down",
nav_to_index="/cs/nav_to_index",
update_viewport="/cs/update_viewport",
save_width="/cs/save_width", save_scale="/cs/save_scale",
)
script = generate_card_stack_js(ids, btn, config, urls, container_id="my-app")
js_text = script.children[0] if script.children else ""
# Namespace setup
assert "window.cardStacks" in js_text
assert f"'{config.prefix}'" in js_text
# All sections present in composed output
for section in [
"Viewport Height", "Scroll Navigation", "Touch Navigation",
"Page Navigation", "Width Management", "Scale Management",
"Card Count Management", "Auto Visible Count Adjustment",
"Grid Template Management", "Global Keyboard Callbacks",
"Master Coordinator", "HTMX Event Listeners",
]:
assert section in js_text, f"Missing section: {section}"
# Scroll-to-top before height calculation (fixes HTMX navigation scroll position issue)
assert "window.scrollTo(0, 0)" in js_text, "Missing scroll-to-top in initialization"
# Zone activation in scroll and touch handlers
assert f"setActiveZone('{ids.card_stack}')" in js_text
# Scrollbar JS IIFE included (show_scrollbar=True by default)
assert "Virtual Scrollbar" in js_text
assert f"{config.prefix}-scrollbar-track" in js_text
assert f"{config.prefix}-scrollbar-thumb" in js_text
assert "dataset.position" in js_text # Self-contained position sync from track
assert "/cs/nav_to_index" in js_text # Posts to nav_to_index URL
# Scrollbar zone activation callback
assert "scrollbarActivate" in js_text
print("Composition: namespace, section presence, zone activation, and scrollbar tests passed!")# Test key namespace functions are exposed
for fn in [
"ns.updateWidth", "ns.updateScale", "ns.updateCardCount",
"ns.handleCountChange", "ns._autoUpdateCount", "ns._runAutoAdjust",
"ns.triggerAutoAdjust", "ns.applyGridTemplate", "ns.jumpToFirstItem",
"ns.jumpToLastItem", "ns.applyAllViewportSettings", "ns.recalculateHeight",
"ns.decreaseWidth", "ns.increaseWidth", "ns.decreaseScale", "ns.increaseScale",
"ns._setupScrollNav", "ns._setupTouchNav", "ns.syncCountDropdown",
"ns._setupSiblingObserver",
]:
assert fn in js_text, f"Missing function: {fn}"
# Touch navigation (Pointer Events API)
assert "pointerdown" in js_text
assert "pointermove" in js_text
assert "setPointerCapture" in js_text
assert "momentumTick" in js_text
print("Composition: namespace function exposure tests passed!")Composition: namespace function exposure tests passed!
# Test auto-adjust and growth validation in composed output
assert "_autoAdjusting" in js_text
assert "_autoGrowing" in js_text
assert "_preGrowthItemIds" in js_text
assert "_snapshotItemIds" in js_text
assert "_hideNewItems" in js_text
assert "_revealNewItems" in js_text
assert "_validateGrowth" in js_text
assert "ns._cancelAutoGrowth" in js_text
assert "htmx:afterSwap" in js_text
# Card count uses swap:'none' for OOB updates
assert "swap: 'none',\n values: { visible_count:" in js_text
print("Composition: auto-adjust and growth validation tests passed!")Composition: auto-adjust and growth validation tests passed!
# Test global callback wrappers and HTMX listener cleanup
prefix = config.prefix
for cb in ["jumpPageUp", "jumpToFirstItem", "decreaseWidth", "increaseWidth",
"decreaseScale", "increaseScale"]:
assert f"window['{prefix}_{cb}']" in js_text, f"Missing global callback: {cb}"
# HTMX listeners use remove-and-replace pattern
handler_key = f"_csHandlers_{config.prefix.replace('-', '_')}"
assert f"window.{handler_key}" in js_text
assert "removeEventListener" in js_text
assert "_afterSwapHandler" in js_text
assert "_afterSettleHandler" in js_text
print("Composition: global callbacks and HTMX listener tests passed!")Composition: global callbacks and HTMX listener tests passed!
# Test focus_position parameter produces correct JS literals
_reset_prefix_counter()
_fp_config = CardStackConfig()
_fp_ids = CardStackHtmlIds(prefix=_fp_config.prefix)
_fp_btn = CardStackButtonIds(prefix=_fp_config.prefix)
# Default (None) → JS null (center focus)
_fp_script = generate_card_stack_js(_fp_ids, _fp_btn, _fp_config, urls)
_fp_js = _fp_script.children[0] if _fp_script.children else ""
assert "const focusPosRaw = null;" in _fp_js
# Bottom focus (-1)
_fp_script_bottom = generate_card_stack_js(_fp_ids, _fp_btn, _fp_config, urls, focus_position=-1)
_fp_js_bottom = _fp_script_bottom.children[0] if _fp_script_bottom.children else ""
assert "const focusPosRaw = -1;" in _fp_js_bottom
# Top focus (0)
_fp_script_top = generate_card_stack_js(_fp_ids, _fp_btn, _fp_config, urls, focus_position=0)
_fp_js_top = _fp_script_top.children[0] if _fp_script_top.children else ""
assert "const focusPosRaw = 0;" in _fp_js_top
print("Focus position parameter tests passed!")Focus position parameter tests passed!
# Test extra_scripts injection
_reset_prefix_counter()
config2 = CardStackConfig()
ids2 = CardStackHtmlIds(prefix=config2.prefix)
btn2 = CardStackButtonIds(prefix=config2.prefix)
script2 = generate_card_stack_js(
ids2, btn2, config2, urls,
extra_scripts=("ns.customFunction = function() { console.log('custom'); };",)
)
js2 = script2.children[0] if script2.children else ""
assert "ns.customFunction" in js2
print("Extra scripts injection test passed!")Extra scripts injection test passed!
# Test with disable_scroll_in_modes
config3 = CardStackConfig(prefix="split-test", disable_scroll_in_modes=("split",))
ids3 = CardStackHtmlIds(prefix=config3.prefix)
btn3 = CardStackButtonIds(prefix=config3.prefix)
script3 = generate_card_stack_js(ids3, btn3, config3, urls)
js3 = script3.children[0] if script3.children else ""
assert "isScrollDisabled" in js3
assert "isTouchDisabled" in js3
assert "'split'" in js3
print("Scroll/touch mode disabling in composed JS test passed!")
# Auto-adjust is always included (no longer optional)
assert "Auto Visible Count Adjustment" in js3
assert "_snapshotItemIds" in js3
assert "_revealNewItems" in js3
assert "ns.handleCountChange" in js3
print("Auto-adjust always included test passed!")