cjm-fasthtml-token-selector 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cjm_fasthtml_token_selector/__init__.py +1 -0
- cjm_fasthtml_token_selector/_modidx.py +104 -0
- cjm_fasthtml_token_selector/components/__init__.py +0 -0
- cjm_fasthtml_token_selector/components/inputs.py +46 -0
- cjm_fasthtml_token_selector/components/tokens.py +217 -0
- cjm_fasthtml_token_selector/core/__init__.py +0 -0
- cjm_fasthtml_token_selector/core/config.py +57 -0
- cjm_fasthtml_token_selector/core/constants.py +50 -0
- cjm_fasthtml_token_selector/core/html_ids.py +51 -0
- cjm_fasthtml_token_selector/core/models.py +37 -0
- cjm_fasthtml_token_selector/helpers/__init__.py +0 -0
- cjm_fasthtml_token_selector/helpers/tokenizer.py +76 -0
- cjm_fasthtml_token_selector/js/__init__.py +0 -0
- cjm_fasthtml_token_selector/js/core.py +181 -0
- cjm_fasthtml_token_selector/js/display.py +146 -0
- cjm_fasthtml_token_selector/js/navigation.py +228 -0
- cjm_fasthtml_token_selector/js/repeat.py +119 -0
- cjm_fasthtml_token_selector/keyboard/__init__.py +0 -0
- cjm_fasthtml_token_selector/keyboard/actions.py +157 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/METADATA +760 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/RECORD +31 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/WHEEL +5 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/entry_points.txt +2 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/licenses/LICENSE +201 -0
- cjm_fasthtml_token_selector-0.0.1.dist-info/top_level.txt +2 -0
- demos/__init__.py +0 -0
- demos/data.py +17 -0
- demos/gap.py +93 -0
- demos/shared.py +148 -0
- demos/span.py +92 -0
- demos/word.py +88 -0
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Custom key repeat engine with configurable initial delay, repeat interval, and throttle floor."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/repeat.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['generate_key_repeat_js']
|
|
7
|
+
|
|
8
|
+
# %% ../../nbs/js/repeat.ipynb #684c3b7a
|
|
9
|
+
from ..core.config import TokenSelectorConfig
|
|
10
|
+
|
|
11
|
+
# %% ../../nbs/js/repeat.ipynb #b02029c9
|
|
12
|
+
def _generate_movement_dispatch_js(
|
|
13
|
+
config:TokenSelectorConfig, # config for this instance
|
|
14
|
+
) -> str: # JS code fragment for the movement dispatcher
|
|
15
|
+
"""Generate the key-to-movement dispatch logic."""
|
|
16
|
+
left_key = config.left_key
|
|
17
|
+
right_key = config.right_key
|
|
18
|
+
mode = config.selection_mode
|
|
19
|
+
|
|
20
|
+
# Base movement for unmodified keys
|
|
21
|
+
base_dispatch = f"""
|
|
22
|
+
if (key === '{left_key}') return ns.moveLeft;
|
|
23
|
+
if (key === '{right_key}') return ns.moveRight;
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
# Shift-modified keys for span extend
|
|
27
|
+
if mode == "span":
|
|
28
|
+
shift_dispatch = f"""
|
|
29
|
+
if (key === '{left_key}') return ns.extendLeft;
|
|
30
|
+
if (key === '{right_key}') return ns.extendRight;
|
|
31
|
+
"""
|
|
32
|
+
else:
|
|
33
|
+
shift_dispatch = """
|
|
34
|
+
return null;
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
return f"""
|
|
38
|
+
ns._getMovementFn = function(key, shiftKey) {{
|
|
39
|
+
if (shiftKey) {{{shift_dispatch}
|
|
40
|
+
}}
|
|
41
|
+
{base_dispatch}
|
|
42
|
+
return null;
|
|
43
|
+
}};
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
# %% ../../nbs/js/repeat.ipynb #0122283a
|
|
47
|
+
def generate_key_repeat_js(
|
|
48
|
+
config:TokenSelectorConfig, # config with timing settings
|
|
49
|
+
) -> str: # JS code fragment for the IIFE
|
|
50
|
+
"""Generate the custom key repeat engine JS."""
|
|
51
|
+
initial_delay = config.initial_delay
|
|
52
|
+
repeat_interval = config.repeat_interval
|
|
53
|
+
throttle_floor = config.throttle_floor
|
|
54
|
+
|
|
55
|
+
dispatch = _generate_movement_dispatch_js(config)
|
|
56
|
+
|
|
57
|
+
return dispatch + f"""
|
|
58
|
+
// Key repeat engine state
|
|
59
|
+
ns._repeatTimer = null;
|
|
60
|
+
ns._repeatKey = null;
|
|
61
|
+
ns._repeatShift = false;
|
|
62
|
+
ns._lastMoveTime = 0;
|
|
63
|
+
|
|
64
|
+
ns._handleKeyDown = function(e) {{
|
|
65
|
+
if (!ns.active) return;
|
|
66
|
+
|
|
67
|
+
var moveFn = ns._getMovementFn(e.key, e.shiftKey);
|
|
68
|
+
if (!moveFn) return;
|
|
69
|
+
|
|
70
|
+
e.preventDefault();
|
|
71
|
+
e.stopPropagation();
|
|
72
|
+
|
|
73
|
+
// If same key already held, let the timer run
|
|
74
|
+
if (ns._repeatKey === e.key && ns._repeatShift === e.shiftKey) return;
|
|
75
|
+
|
|
76
|
+
// Cancel any existing repeat
|
|
77
|
+
if (ns._repeatTimer) {{
|
|
78
|
+
clearTimeout(ns._repeatTimer);
|
|
79
|
+
ns._repeatTimer = null;
|
|
80
|
+
}}
|
|
81
|
+
|
|
82
|
+
ns._repeatKey = e.key;
|
|
83
|
+
ns._repeatShift = e.shiftKey;
|
|
84
|
+
|
|
85
|
+
// Immediate first movement
|
|
86
|
+
var now = Date.now();
|
|
87
|
+
if (now - ns._lastMoveTime >= {throttle_floor}) {{
|
|
88
|
+
moveFn();
|
|
89
|
+
ns._lastMoveTime = now;
|
|
90
|
+
}}
|
|
91
|
+
|
|
92
|
+
// Start repeat after initial delay, then switch to interval
|
|
93
|
+
ns._repeatTimer = setTimeout(function repeatTick() {{
|
|
94
|
+
var fn = ns._getMovementFn(ns._repeatKey, ns._repeatShift);
|
|
95
|
+
if (!fn || !ns.active) {{
|
|
96
|
+
ns._repeatKey = null;
|
|
97
|
+
ns._repeatShift = false;
|
|
98
|
+
return;
|
|
99
|
+
}}
|
|
100
|
+
var tickNow = Date.now();
|
|
101
|
+
if (tickNow - ns._lastMoveTime >= {throttle_floor}) {{
|
|
102
|
+
fn();
|
|
103
|
+
ns._lastMoveTime = tickNow;
|
|
104
|
+
}}
|
|
105
|
+
ns._repeatTimer = setTimeout(repeatTick, {repeat_interval});
|
|
106
|
+
}}, {initial_delay});
|
|
107
|
+
}};
|
|
108
|
+
|
|
109
|
+
ns._handleKeyUp = function(e) {{
|
|
110
|
+
if (e.key === ns._repeatKey) {{
|
|
111
|
+
if (ns._repeatTimer) {{
|
|
112
|
+
clearTimeout(ns._repeatTimer);
|
|
113
|
+
ns._repeatTimer = null;
|
|
114
|
+
}}
|
|
115
|
+
ns._repeatKey = null;
|
|
116
|
+
ns._repeatShift = false;
|
|
117
|
+
}}
|
|
118
|
+
}};
|
|
119
|
+
"""
|
|
File without changes
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"""Keyboard navigation library integration factories for token selector mode, actions, URL maps, and hidden action buttons."""
|
|
2
|
+
|
|
3
|
+
# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/keyboard/actions.ipynb.
|
|
4
|
+
|
|
5
|
+
# %% auto #0
|
|
6
|
+
__all__ = ['create_token_selector_mode', 'create_token_nav_actions', 'build_token_selector_url_map',
|
|
7
|
+
'render_token_action_buttons']
|
|
8
|
+
|
|
9
|
+
# %% ../../nbs/keyboard/actions.ipynb #9cae2c4c
|
|
10
|
+
from typing import Any, Dict, Tuple
|
|
11
|
+
|
|
12
|
+
from fasthtml.common import Div, Button, Hidden
|
|
13
|
+
|
|
14
|
+
from cjm_fasthtml_tailwind.utilities.layout import display_tw
|
|
15
|
+
|
|
16
|
+
from cjm_fasthtml_keyboard_navigation.core.actions import KeyAction
|
|
17
|
+
from cjm_fasthtml_keyboard_navigation.core.modes import KeyboardMode
|
|
18
|
+
|
|
19
|
+
from ..core.config import TokenSelectorConfig
|
|
20
|
+
from ..core.html_ids import TokenSelectorHtmlIds
|
|
21
|
+
from ..js.core import global_callback_name
|
|
22
|
+
|
|
23
|
+
# %% ../../nbs/keyboard/actions.ipynb #a1223d9d
|
|
24
|
+
def create_token_selector_mode(
|
|
25
|
+
config:TokenSelectorConfig, # config for this instance
|
|
26
|
+
mode_name:str = "token-select", # mode name for the keyboard nav system
|
|
27
|
+
indicator_text:str = "Token Select", # mode indicator text
|
|
28
|
+
exit_key:str = "", # exit key (empty = programmatic only via Escape KeyAction)
|
|
29
|
+
exit_on_zone_change:bool = False, # whether to exit on zone change
|
|
30
|
+
) -> KeyboardMode: # configured keyboard mode
|
|
31
|
+
"""Create a keyboard mode that activates/deactivates the token selector."""
|
|
32
|
+
return KeyboardMode(
|
|
33
|
+
name=mode_name,
|
|
34
|
+
on_enter=global_callback_name(config.prefix, "activate"),
|
|
35
|
+
on_exit=global_callback_name(config.prefix, "deactivate"),
|
|
36
|
+
indicator_text=indicator_text,
|
|
37
|
+
exit_key=exit_key,
|
|
38
|
+
exit_on_zone_change=exit_on_zone_change,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
# %% ../../nbs/keyboard/actions.ipynb #464fca0c
|
|
42
|
+
def create_token_nav_actions(
|
|
43
|
+
config:TokenSelectorConfig, # config for this instance
|
|
44
|
+
zone_id:str, # focus zone ID
|
|
45
|
+
mode_name:str = "token-select", # mode name (must match the mode)
|
|
46
|
+
confirm_button_id:str = "", # HTMX button ID for confirm action
|
|
47
|
+
cancel_button_id:str = "", # HTMX button ID for cancel action
|
|
48
|
+
) -> Tuple[KeyAction, ...]: # non-movement keyboard actions
|
|
49
|
+
"""Create keyboard actions for the token selector."""
|
|
50
|
+
actions = []
|
|
51
|
+
mode_scope = (mode_name,)
|
|
52
|
+
|
|
53
|
+
# Confirm: Enter
|
|
54
|
+
if confirm_button_id:
|
|
55
|
+
actions.append(KeyAction(
|
|
56
|
+
key="Enter",
|
|
57
|
+
htmx_trigger=confirm_button_id,
|
|
58
|
+
mode_names=mode_scope,
|
|
59
|
+
mode_exit=True,
|
|
60
|
+
description="Confirm selection",
|
|
61
|
+
hint_group="Token Select",
|
|
62
|
+
zone_ids=(zone_id,),
|
|
63
|
+
))
|
|
64
|
+
# Space as alias (hidden from hints)
|
|
65
|
+
actions.append(KeyAction(
|
|
66
|
+
key=" ",
|
|
67
|
+
htmx_trigger=confirm_button_id,
|
|
68
|
+
mode_names=mode_scope,
|
|
69
|
+
mode_exit=True,
|
|
70
|
+
description="Confirm selection",
|
|
71
|
+
hint_group="Token Select",
|
|
72
|
+
show_in_hints=False,
|
|
73
|
+
zone_ids=(zone_id,),
|
|
74
|
+
))
|
|
75
|
+
|
|
76
|
+
# Cancel: Escape
|
|
77
|
+
if cancel_button_id:
|
|
78
|
+
actions.append(KeyAction(
|
|
79
|
+
key="Escape",
|
|
80
|
+
htmx_trigger=cancel_button_id,
|
|
81
|
+
mode_names=mode_scope,
|
|
82
|
+
mode_exit=True,
|
|
83
|
+
description="Cancel",
|
|
84
|
+
hint_group="Token Select",
|
|
85
|
+
zone_ids=(zone_id,),
|
|
86
|
+
))
|
|
87
|
+
|
|
88
|
+
# Home: move to first position
|
|
89
|
+
actions.append(KeyAction(
|
|
90
|
+
key="Home",
|
|
91
|
+
js_callback=global_callback_name(config.prefix, "moveToFirst"),
|
|
92
|
+
mode_names=mode_scope,
|
|
93
|
+
description="Go to start",
|
|
94
|
+
hint_group="Token Select",
|
|
95
|
+
zone_ids=(zone_id,),
|
|
96
|
+
))
|
|
97
|
+
|
|
98
|
+
# End: move to last position
|
|
99
|
+
actions.append(KeyAction(
|
|
100
|
+
key="End",
|
|
101
|
+
js_callback=global_callback_name(config.prefix, "moveToLast"),
|
|
102
|
+
mode_names=mode_scope,
|
|
103
|
+
description="Go to end",
|
|
104
|
+
hint_group="Token Select",
|
|
105
|
+
zone_ids=(zone_id,),
|
|
106
|
+
))
|
|
107
|
+
|
|
108
|
+
return tuple(actions)
|
|
109
|
+
|
|
110
|
+
# %% ../../nbs/keyboard/actions.ipynb #8b6a950a
|
|
111
|
+
def build_token_selector_url_map(
|
|
112
|
+
confirm_button_id:str, # button ID for confirm action
|
|
113
|
+
cancel_button_id:str, # button ID for cancel action
|
|
114
|
+
confirm_url:str, # URL for confirm action
|
|
115
|
+
cancel_url:str, # URL for cancel action
|
|
116
|
+
) -> Dict[str, str]: # button ID -> URL mapping
|
|
117
|
+
"""Build URL map for keyboard system with token selector action buttons."""
|
|
118
|
+
url_map = {}
|
|
119
|
+
if confirm_button_id and confirm_url:
|
|
120
|
+
url_map[confirm_button_id] = confirm_url
|
|
121
|
+
if cancel_button_id and cancel_url:
|
|
122
|
+
url_map[cancel_button_id] = cancel_url
|
|
123
|
+
return url_map
|
|
124
|
+
|
|
125
|
+
# %% ../../nbs/keyboard/actions.ipynb #039e1b6e
|
|
126
|
+
def render_token_action_buttons(
|
|
127
|
+
confirm_button_id:str, # button ID for confirm
|
|
128
|
+
cancel_button_id:str, # button ID for cancel
|
|
129
|
+
confirm_url:str, # URL for confirm action
|
|
130
|
+
cancel_url:str, # URL for cancel action
|
|
131
|
+
ids:TokenSelectorHtmlIds, # HTML IDs (for hx_include)
|
|
132
|
+
extra_include:str = "", # additional hx_include selectors
|
|
133
|
+
) -> Any: # Div containing hidden action buttons
|
|
134
|
+
"""Render hidden HTMX buttons for confirm/cancel actions."""
|
|
135
|
+
include = f"#{ids.anchor_input}, #{ids.focus_input}"
|
|
136
|
+
if extra_include:
|
|
137
|
+
include = f"{include}, {extra_include}"
|
|
138
|
+
|
|
139
|
+
buttons = []
|
|
140
|
+
if confirm_button_id and confirm_url:
|
|
141
|
+
buttons.append(Button(
|
|
142
|
+
id=confirm_button_id,
|
|
143
|
+
hx_post=confirm_url,
|
|
144
|
+
hx_swap="none",
|
|
145
|
+
hx_include=include,
|
|
146
|
+
cls=str(display_tw.hidden),
|
|
147
|
+
))
|
|
148
|
+
if cancel_button_id and cancel_url:
|
|
149
|
+
buttons.append(Button(
|
|
150
|
+
id=cancel_button_id,
|
|
151
|
+
hx_post=cancel_url,
|
|
152
|
+
hx_swap="none",
|
|
153
|
+
hx_include=include,
|
|
154
|
+
cls=str(display_tw.hidden),
|
|
155
|
+
))
|
|
156
|
+
|
|
157
|
+
return Div(*buttons, cls=str(display_tw.hidden))
|