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.
Files changed (31) hide show
  1. cjm_fasthtml_token_selector/__init__.py +1 -0
  2. cjm_fasthtml_token_selector/_modidx.py +104 -0
  3. cjm_fasthtml_token_selector/components/__init__.py +0 -0
  4. cjm_fasthtml_token_selector/components/inputs.py +46 -0
  5. cjm_fasthtml_token_selector/components/tokens.py +217 -0
  6. cjm_fasthtml_token_selector/core/__init__.py +0 -0
  7. cjm_fasthtml_token_selector/core/config.py +57 -0
  8. cjm_fasthtml_token_selector/core/constants.py +50 -0
  9. cjm_fasthtml_token_selector/core/html_ids.py +51 -0
  10. cjm_fasthtml_token_selector/core/models.py +37 -0
  11. cjm_fasthtml_token_selector/helpers/__init__.py +0 -0
  12. cjm_fasthtml_token_selector/helpers/tokenizer.py +76 -0
  13. cjm_fasthtml_token_selector/js/__init__.py +0 -0
  14. cjm_fasthtml_token_selector/js/core.py +181 -0
  15. cjm_fasthtml_token_selector/js/display.py +146 -0
  16. cjm_fasthtml_token_selector/js/navigation.py +228 -0
  17. cjm_fasthtml_token_selector/js/repeat.py +119 -0
  18. cjm_fasthtml_token_selector/keyboard/__init__.py +0 -0
  19. cjm_fasthtml_token_selector/keyboard/actions.py +157 -0
  20. cjm_fasthtml_token_selector-0.0.1.dist-info/METADATA +760 -0
  21. cjm_fasthtml_token_selector-0.0.1.dist-info/RECORD +31 -0
  22. cjm_fasthtml_token_selector-0.0.1.dist-info/WHEEL +5 -0
  23. cjm_fasthtml_token_selector-0.0.1.dist-info/entry_points.txt +2 -0
  24. cjm_fasthtml_token_selector-0.0.1.dist-info/licenses/LICENSE +201 -0
  25. cjm_fasthtml_token_selector-0.0.1.dist-info/top_level.txt +2 -0
  26. demos/__init__.py +0 -0
  27. demos/data.py +17 -0
  28. demos/gap.py +93 -0
  29. demos/shared.py +148 -0
  30. demos/span.py +92 -0
  31. demos/word.py +88 -0
@@ -0,0 +1,76 @@
1
+ """Tokenization utilities for splitting text into tokens and converting between token indices and character positions."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/helpers/tokenizer.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['count_tokens', 'get_token_list', 'token_index_to_char_position', 'tokenize']
7
+
8
+ # %% ../../nbs/helpers/tokenizer.ipynb #c161bb62
9
+ from typing import Any, List, Optional, Union
10
+
11
+ from ..core.models import Token
12
+
13
+ # %% ../../nbs/helpers/tokenizer.ipynb #1cded653
14
+ def count_tokens(
15
+ text:str, # text to count tokens in
16
+ ) -> int: # token count
17
+ """Count the number of whitespace-delimited tokens in text."""
18
+ if not text:
19
+ return 0
20
+ return len(text.split())
21
+
22
+ # %% ../../nbs/helpers/tokenizer.ipynb #71f880db
23
+ def get_token_list(
24
+ text:str, # text to split into tokens
25
+ ) -> List[str]: # list of token strings
26
+ """Split text into a list of whitespace-delimited tokens."""
27
+ if not text:
28
+ return []
29
+ return text.split()
30
+
31
+ # %% ../../nbs/helpers/tokenizer.ipynb #b0f6d4c0
32
+ def token_index_to_char_position(
33
+ text:str, # full text string
34
+ token_index:int, # 0-based token index
35
+ ) -> int: # character position for split
36
+ """Convert a token index to the character position where a split should occur."""
37
+ if token_index <= 0:
38
+ return 0
39
+
40
+ words = text.split()
41
+ if token_index >= len(words):
42
+ return len(text)
43
+
44
+ # Walk through the original text to find the character position
45
+ # before the word at token_index
46
+ pos = 0
47
+ for i, word in enumerate(words):
48
+ if i == token_index:
49
+ break
50
+ pos += len(word)
51
+ # Advance past whitespace between words
52
+ while pos < len(text) and text[pos] == ' ':
53
+ pos += 1
54
+
55
+ return pos
56
+
57
+ # %% ../../nbs/helpers/tokenizer.ipynb #c4c9d469
58
+ def tokenize(
59
+ text_or_tokens:Union[str, List[str]], # raw text string or pre-tokenized list
60
+ metadata:Optional[List[Any]] = None, # per-token metadata (must match token count)
61
+ ) -> List[Token]: # list of Token objects
62
+ """Convert text or a pre-tokenized list into Token objects."""
63
+ if isinstance(text_or_tokens, str):
64
+ words = get_token_list(text_or_tokens)
65
+ else:
66
+ words = list(text_or_tokens)
67
+
68
+ if metadata is not None and len(metadata) != len(words):
69
+ raise ValueError(
70
+ f"metadata length ({len(metadata)}) must match token count ({len(words)})"
71
+ )
72
+
73
+ return [
74
+ Token(text=w, index=i, metadata=metadata[i] if metadata else None)
75
+ for i, w in enumerate(words)
76
+ ]
File without changes
@@ -0,0 +1,181 @@
1
+ """Master IIFE composer for the token selector JS runtime."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/core.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['global_callback_name', 'generate_token_selector_js']
7
+
8
+ # %% ../../nbs/js/core.ipynb #3dd234c7
9
+ from typing import Any, Tuple
10
+
11
+ from fasthtml.common import Script
12
+
13
+ from ..core.config import TokenSelectorConfig
14
+ from ..core.models import TokenSelectorState
15
+ from ..core.html_ids import TokenSelectorHtmlIds
16
+ from .navigation import generate_navigation_js
17
+ from .display import generate_display_js
18
+ from .repeat import generate_key_repeat_js
19
+
20
+ # %% ../../nbs/js/core.ipynb #bee1f126
21
+ _GLOBAL_CALLBACKS = (
22
+ "activate",
23
+ "deactivate",
24
+ "selectGap",
25
+ "selectWord",
26
+ "selectToken",
27
+ "moveToFirst",
28
+ "moveToLast",
29
+ )
30
+
31
+ def global_callback_name(
32
+ prefix:str, # token selector instance prefix
33
+ callback:str, # base callback name
34
+ ) -> str: # global function name
35
+ """Generate a prefix-unique global callback name."""
36
+ return f"{prefix}_{callback}"
37
+
38
+ # %% ../../nbs/js/core.ipynb #173e8f33
39
+ def _generate_state_init_js(
40
+ config:TokenSelectorConfig, # config for this instance
41
+ state:TokenSelectorState, # initial state
42
+ ) -> str: # JS code fragment
43
+ """Generate state initialization code."""
44
+ return f"""
45
+ // State
46
+ ns.anchor = {state.anchor};
47
+ ns.focus = {state.focus};
48
+ ns.wordCount = {state.word_count};
49
+ ns.active = false;
50
+ ns.mode = '{config.selection_mode}';
51
+ ns._prevAnchor = -1;
52
+ """
53
+
54
+ # %% ../../nbs/js/core.ipynb #94cc4f28
55
+ def _generate_on_change_js(
56
+ config:TokenSelectorConfig, # config for this instance
57
+ ) -> str: # JS code fragment
58
+ """Generate the on-change callback dispatcher."""
59
+ if config.on_change_callback:
60
+ return f"""
61
+ ns._fireOnChange = function() {{
62
+ if (typeof window['{config.on_change_callback}'] === 'function') {{
63
+ window['{config.on_change_callback}'](ns.anchor, ns.focus, ns.mode);
64
+ }}
65
+ }};
66
+ """
67
+ return """
68
+ ns._fireOnChange = function() {};
69
+ """
70
+
71
+ # %% ../../nbs/js/core.ipynb #0089ec56
72
+ def _generate_activation_js(
73
+ config:TokenSelectorConfig, # config for this instance
74
+ ids:TokenSelectorHtmlIds, # HTML IDs
75
+ ) -> str: # JS code fragment
76
+ """Generate activate/deactivate functions."""
77
+ return f"""
78
+ ns.activate = function() {{
79
+ ns.active = true;
80
+ // Re-read word count from DOM
81
+ var grid = document.getElementById('{ids.token_grid}');
82
+ if (grid) ns.wordCount = parseInt(grid.dataset.wordCount) || 0;
83
+ // Reset position
84
+ ns.anchor = 0;
85
+ ns.focus = 0;
86
+ // Add key listeners
87
+ document.addEventListener('keydown', ns._handleKeyDown, true);
88
+ document.addEventListener('keyup', ns._handleKeyUp, true);
89
+ ns.updateDisplay();
90
+ ns.updateInputs();
91
+ }};
92
+
93
+ ns.deactivate = function() {{
94
+ ns.active = false;
95
+ // Cancel repeat
96
+ if (ns._repeatTimer) {{
97
+ clearTimeout(ns._repeatTimer);
98
+ ns._repeatTimer = null;
99
+ }}
100
+ ns._repeatKey = null;
101
+ ns._repeatShift = false;
102
+ // Remove key listeners
103
+ document.removeEventListener('keydown', ns._handleKeyDown, true);
104
+ document.removeEventListener('keyup', ns._handleKeyUp, true);
105
+ }};
106
+ """
107
+
108
+ # %% ../../nbs/js/core.ipynb #d62a060d
109
+ def _generate_global_callbacks_js(
110
+ config:TokenSelectorConfig, # config for this instance
111
+ ) -> str: # JS code fragment
112
+ """Generate global callback wrappers."""
113
+ lines = []
114
+ for cb_name in _GLOBAL_CALLBACKS:
115
+ global_name = global_callback_name(config.prefix, cb_name)
116
+ lines.append(
117
+ f" window['{global_name}'] = function() {{ "
118
+ f"if (ns.{cb_name}) ns.{cb_name}.apply(ns, arguments); }};"
119
+ )
120
+ return "\n".join(lines)
121
+
122
+ # %% ../../nbs/js/core.ipynb #111900aa
123
+ def _generate_settle_handler_js(
124
+ config:TokenSelectorConfig, # config for this instance
125
+ ids:TokenSelectorHtmlIds, # HTML IDs
126
+ ) -> str: # JS code fragment
127
+ """Generate htmx:afterSettle handler for swap resilience."""
128
+ guard = f"_tsMasterListener_{config.prefix}"
129
+ return f"""
130
+ // HTMX afterSettle: re-read word count and update display
131
+ if (!window['{guard}']) {{
132
+ window['{guard}'] = true;
133
+ document.body.addEventListener('htmx:afterSettle', function() {{
134
+ var grid = document.getElementById('{ids.token_grid}');
135
+ if (grid && ns.active) {{
136
+ ns.wordCount = parseInt(grid.dataset.wordCount) || 0;
137
+ ns.updateDisplay();
138
+ ns.updateInputs();
139
+ }}
140
+ }});
141
+ }}
142
+ """
143
+
144
+ # %% ../../nbs/js/core.ipynb #48c0e44b
145
+ def generate_token_selector_js(
146
+ config:TokenSelectorConfig, # config for this instance
147
+ ids:TokenSelectorHtmlIds, # HTML IDs
148
+ state:TokenSelectorState = None, # initial state
149
+ extra_scripts:Tuple[str, ...] = (), # additional JS to include in the IIFE
150
+ ) -> Any: # Script element with the complete IIFE
151
+ """Compose all token selector JS into a single namespaced IIFE."""
152
+ if state is None:
153
+ state = TokenSelectorState()
154
+
155
+ # Collect all JS fragments
156
+ fragments = [
157
+ _generate_state_init_js(config, state),
158
+ _generate_on_change_js(config),
159
+ generate_display_js(config, ids),
160
+ generate_navigation_js(config, ids),
161
+ generate_key_repeat_js(config),
162
+ _generate_activation_js(config, ids),
163
+ _generate_global_callbacks_js(config),
164
+ _generate_settle_handler_js(config, ids),
165
+ ]
166
+
167
+ # Consumer extra scripts
168
+ if extra_scripts:
169
+ fragments.append("\n".join(extra_scripts))
170
+
171
+ body = "\n".join(fragments)
172
+ prefix = config.prefix
173
+
174
+ iife = f"""(function() {{
175
+ 'use strict';
176
+ window.tokenSelectors = window.tokenSelectors || {{}};
177
+ var ns = window.tokenSelectors['{prefix}'] = {{}};
178
+ {body}
179
+ }})();"""
180
+
181
+ return Script(iife)
@@ -0,0 +1,146 @@
1
+ """Generates JS functions for updating token display state (caret indicators, highlights, dimming, hidden inputs)."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/display.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['generate_display_js']
7
+
8
+ # %% ../../nbs/js/display.ipynb #4614e222
9
+ from cjm_fasthtml_token_selector.core.constants import (
10
+ CARET_INDICATOR_CLS, OPACITY_50_CLS, HIGHLIGHT_CLS,
11
+ )
12
+ from ..core.config import TokenSelectorConfig
13
+ from ..core.html_ids import TokenSelectorHtmlIds
14
+
15
+ # %% ../../nbs/js/display.ipynb #85e3702e
16
+ def _generate_update_inputs_js(
17
+ ids:TokenSelectorHtmlIds, # HTML IDs
18
+ ) -> str: # JS code fragment
19
+ """Generate the hidden input sync function."""
20
+ return f"""
21
+ ns.updateInputs = function() {{
22
+ var anchorEl = document.getElementById('{ids.anchor_input}');
23
+ var focusEl = document.getElementById('{ids.focus_input}');
24
+ if (anchorEl) anchorEl.value = ns.anchor;
25
+ if (focusEl) focusEl.value = ns.focus;
26
+ }};
27
+ """
28
+
29
+ # %% ../../nbs/js/display.ipynb #9c85ab30
30
+ def _generate_gap_display_js(
31
+ ids:TokenSelectorHtmlIds, # HTML IDs
32
+ ) -> str: # JS code fragment
33
+ """Generate gap mode display update function."""
34
+ return f"""
35
+ ns.updateDisplay = function() {{
36
+ var grid = document.getElementById('{ids.token_grid}');
37
+ if (!grid) return;
38
+
39
+ // Remove existing caret indicators
40
+ grid.querySelectorAll('.caret-indicator').forEach(function(el) {{ el.remove(); }});
41
+
42
+ var tokens = grid.querySelectorAll('.token');
43
+ var prevAnchor = ns._prevAnchor !== undefined ? ns._prevAnchor : -1;
44
+
45
+ tokens.forEach(function(token) {{
46
+ var idx = parseInt(token.dataset.tokenIndex);
47
+
48
+ // Dimming: tokens before caret
49
+ if (idx < ns.anchor) {{
50
+ if (!token.classList.contains('{OPACITY_50_CLS}')) {{
51
+ token.classList.add('{OPACITY_50_CLS}');
52
+ }}
53
+ }} else {{
54
+ token.classList.remove('{OPACITY_50_CLS}');
55
+ }}
56
+
57
+ // Caret indicator at anchor position
58
+ if (idx === ns.anchor) {{
59
+ var caret = document.createElement('div');
60
+ caret.className = '{CARET_INDICATOR_CLS}';
61
+ token.insertBefore(caret, token.firstChild);
62
+ }}
63
+ }});
64
+
65
+ ns._prevAnchor = ns.anchor;
66
+ }};
67
+ """
68
+
69
+ # %% ../../nbs/js/display.ipynb #90b62704
70
+ def _generate_word_display_js(
71
+ ids:TokenSelectorHtmlIds, # HTML IDs
72
+ ) -> str: # JS code fragment
73
+ """Generate word mode display update function."""
74
+ return f"""
75
+ ns.updateDisplay = function() {{
76
+ var grid = document.getElementById('{ids.token_grid}');
77
+ if (!grid) return;
78
+
79
+ var tokens = grid.querySelectorAll('.token');
80
+
81
+ tokens.forEach(function(token) {{
82
+ var idx = parseInt(token.dataset.tokenIndex);
83
+
84
+ if (idx === ns.anchor) {{
85
+ if (!token.classList.contains('{HIGHLIGHT_CLS}')) {{
86
+ token.classList.add('{HIGHLIGHT_CLS}');
87
+ }}
88
+ }} else {{
89
+ token.classList.remove('{HIGHLIGHT_CLS}');
90
+ }}
91
+ }});
92
+ }};
93
+ """
94
+
95
+ # %% ../../nbs/js/display.ipynb #f3f54861
96
+ def _generate_span_display_js(
97
+ ids:TokenSelectorHtmlIds, # HTML IDs
98
+ ) -> str: # JS code fragment
99
+ """Generate span mode display update function."""
100
+ return f"""
101
+ ns.updateDisplay = function() {{
102
+ var grid = document.getElementById('{ids.token_grid}');
103
+ if (!grid) return;
104
+
105
+ // Remove existing caret indicators
106
+ grid.querySelectorAll('.caret-indicator').forEach(function(el) {{ el.remove(); }});
107
+
108
+ var tokens = grid.querySelectorAll('.token');
109
+ var lo = Math.min(ns.anchor, ns.focus);
110
+ var hi = Math.max(ns.anchor, ns.focus);
111
+
112
+ tokens.forEach(function(token) {{
113
+ var idx = parseInt(token.dataset.tokenIndex);
114
+
115
+ // Highlight tokens in range
116
+ if (idx >= lo && idx <= hi) {{
117
+ if (!token.classList.contains('{HIGHLIGHT_CLS}')) {{
118
+ token.classList.add('{HIGHLIGHT_CLS}');
119
+ }}
120
+ }} else {{
121
+ token.classList.remove('{HIGHLIGHT_CLS}');
122
+ }}
123
+
124
+ // Caret at focus position
125
+ if (idx === ns.focus) {{
126
+ var caret = document.createElement('div');
127
+ caret.className = '{CARET_INDICATOR_CLS}';
128
+ token.insertBefore(caret, token.firstChild);
129
+ }}
130
+ }});
131
+ }};
132
+ """
133
+
134
+ # %% ../../nbs/js/display.ipynb #c782dde5
135
+ def generate_display_js(
136
+ config:TokenSelectorConfig, # config for this instance
137
+ ids:TokenSelectorHtmlIds, # HTML IDs
138
+ ) -> str: # JS code fragment for the IIFE
139
+ """Generate display update and hidden input sync JS functions."""
140
+ display_generators = {
141
+ "gap": _generate_gap_display_js,
142
+ "word": _generate_word_display_js,
143
+ "span": _generate_span_display_js,
144
+ }
145
+ gen = display_generators.get(config.selection_mode, _generate_gap_display_js)
146
+ return _generate_update_inputs_js(ids) + gen(ids)
@@ -0,0 +1,228 @@
1
+ """Generates mode-specific navigation and selection JS functions."""
2
+
3
+ # AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/js/navigation.ipynb.
4
+
5
+ # %% auto #0
6
+ __all__ = ['generate_navigation_js']
7
+
8
+ # %% ../../nbs/js/navigation.ipynb #386f20ca
9
+ from ..core.config import TokenSelectorConfig
10
+ from ..core.html_ids import TokenSelectorHtmlIds
11
+
12
+ # %% ../../nbs/js/navigation.ipynb #7146756c
13
+ def _generate_gap_nav_js(
14
+ config:TokenSelectorConfig, # config for this instance
15
+ ids:TokenSelectorHtmlIds, # HTML IDs
16
+ ) -> str: # JS code fragment
17
+ """Generate gap mode navigation functions."""
18
+ wrap = "true" if config.wrap_navigation else "false"
19
+ return f"""
20
+ ns.moveLeft = function() {{
21
+ if (ns.anchor > 0) {{
22
+ ns.anchor--;
23
+ ns.focus = ns.anchor;
24
+ }} else if ({wrap}) {{
25
+ ns.anchor = ns.wordCount;
26
+ ns.focus = ns.anchor;
27
+ }}
28
+ ns.updateDisplay();
29
+ ns.updateInputs();
30
+ ns._fireOnChange();
31
+ }};
32
+
33
+ ns.moveRight = function() {{
34
+ if (ns.anchor < ns.wordCount) {{
35
+ ns.anchor++;
36
+ ns.focus = ns.anchor;
37
+ }} else if ({wrap}) {{
38
+ ns.anchor = 0;
39
+ ns.focus = ns.anchor;
40
+ }}
41
+ ns.updateDisplay();
42
+ ns.updateInputs();
43
+ ns._fireOnChange();
44
+ }};
45
+
46
+ ns.selectGap = function(position) {{
47
+ ns.anchor = Math.max(0, Math.min(position, ns.wordCount));
48
+ ns.focus = ns.anchor;
49
+ ns.updateDisplay();
50
+ ns.updateInputs();
51
+ ns._fireOnChange();
52
+ }};
53
+
54
+ ns.moveToFirst = function() {{
55
+ ns.anchor = 0;
56
+ ns.focus = ns.anchor;
57
+ ns.updateDisplay();
58
+ ns.updateInputs();
59
+ ns._fireOnChange();
60
+ }};
61
+
62
+ ns.moveToLast = function() {{
63
+ ns.anchor = ns.wordCount;
64
+ ns.focus = ns.anchor;
65
+ ns.updateDisplay();
66
+ ns.updateInputs();
67
+ ns._fireOnChange();
68
+ }};
69
+ """
70
+
71
+ # %% ../../nbs/js/navigation.ipynb #4ef3a24d
72
+ def _generate_word_nav_js(
73
+ config:TokenSelectorConfig, # config for this instance
74
+ ids:TokenSelectorHtmlIds, # HTML IDs
75
+ ) -> str: # JS code fragment
76
+ """Generate word mode navigation functions."""
77
+ wrap = "true" if config.wrap_navigation else "false"
78
+ return f"""
79
+ ns.moveLeft = function() {{
80
+ if (ns.anchor > 0) {{
81
+ ns.anchor--;
82
+ ns.focus = ns.anchor;
83
+ }} else if ({wrap} && ns.wordCount > 0) {{
84
+ ns.anchor = ns.wordCount - 1;
85
+ ns.focus = ns.anchor;
86
+ }}
87
+ ns.updateDisplay();
88
+ ns.updateInputs();
89
+ ns._fireOnChange();
90
+ }};
91
+
92
+ ns.moveRight = function() {{
93
+ if (ns.anchor < ns.wordCount - 1) {{
94
+ ns.anchor++;
95
+ ns.focus = ns.anchor;
96
+ }} else if ({wrap}) {{
97
+ ns.anchor = 0;
98
+ ns.focus = ns.anchor;
99
+ }}
100
+ ns.updateDisplay();
101
+ ns.updateInputs();
102
+ ns._fireOnChange();
103
+ }};
104
+
105
+ ns.selectWord = function(index) {{
106
+ ns.anchor = Math.max(0, Math.min(index, ns.wordCount - 1));
107
+ ns.focus = ns.anchor;
108
+ ns.updateDisplay();
109
+ ns.updateInputs();
110
+ ns._fireOnChange();
111
+ }};
112
+
113
+ ns.moveToFirst = function() {{
114
+ ns.anchor = 0;
115
+ ns.focus = ns.anchor;
116
+ ns.updateDisplay();
117
+ ns.updateInputs();
118
+ ns._fireOnChange();
119
+ }};
120
+
121
+ ns.moveToLast = function() {{
122
+ ns.anchor = Math.max(0, ns.wordCount - 1);
123
+ ns.focus = ns.anchor;
124
+ ns.updateDisplay();
125
+ ns.updateInputs();
126
+ ns._fireOnChange();
127
+ }};
128
+ """
129
+
130
+ # %% ../../nbs/js/navigation.ipynb #7a3c234e
131
+ def _generate_span_nav_js(
132
+ config:TokenSelectorConfig, # config for this instance
133
+ ids:TokenSelectorHtmlIds, # HTML IDs
134
+ ) -> str: # JS code fragment
135
+ """Generate span mode navigation functions."""
136
+ wrap = "true" if config.wrap_navigation else "false"
137
+ return f"""
138
+ ns.moveLeft = function() {{
139
+ if (ns.focus > 0) {{
140
+ ns.focus--;
141
+ ns.anchor = ns.focus;
142
+ }} else if ({wrap} && ns.wordCount > 0) {{
143
+ ns.focus = ns.wordCount - 1;
144
+ ns.anchor = ns.focus;
145
+ }}
146
+ ns.updateDisplay();
147
+ ns.updateInputs();
148
+ ns._fireOnChange();
149
+ }};
150
+
151
+ ns.moveRight = function() {{
152
+ if (ns.focus < ns.wordCount - 1) {{
153
+ ns.focus++;
154
+ ns.anchor = ns.focus;
155
+ }} else if ({wrap}) {{
156
+ ns.focus = 0;
157
+ ns.anchor = ns.focus;
158
+ }}
159
+ ns.updateDisplay();
160
+ ns.updateInputs();
161
+ ns._fireOnChange();
162
+ }};
163
+
164
+ ns.extendLeft = function() {{
165
+ if (ns.focus > 0) {{
166
+ ns.focus--;
167
+ }}
168
+ ns.updateDisplay();
169
+ ns.updateInputs();
170
+ ns._fireOnChange();
171
+ }};
172
+
173
+ ns.extendRight = function() {{
174
+ if (ns.focus < ns.wordCount - 1) {{
175
+ ns.focus++;
176
+ }}
177
+ ns.updateDisplay();
178
+ ns.updateInputs();
179
+ ns._fireOnChange();
180
+ }};
181
+
182
+ ns.selectToken = function(index) {{
183
+ ns.focus = Math.max(0, Math.min(index, ns.wordCount - 1));
184
+ ns.updateDisplay();
185
+ ns.updateInputs();
186
+ ns._fireOnChange();
187
+ }};
188
+
189
+ ns.selectGap = function(position) {{
190
+ var idx = Math.max(0, Math.min(position, ns.wordCount - 1));
191
+ ns.anchor = idx;
192
+ ns.focus = idx;
193
+ ns.updateDisplay();
194
+ ns.updateInputs();
195
+ ns._fireOnChange();
196
+ }};
197
+
198
+ ns.moveToFirst = function() {{
199
+ ns.anchor = 0;
200
+ ns.focus = 0;
201
+ ns.updateDisplay();
202
+ ns.updateInputs();
203
+ ns._fireOnChange();
204
+ }};
205
+
206
+ ns.moveToLast = function() {{
207
+ var last = Math.max(0, ns.wordCount - 1);
208
+ ns.anchor = last;
209
+ ns.focus = last;
210
+ ns.updateDisplay();
211
+ ns.updateInputs();
212
+ ns._fireOnChange();
213
+ }};
214
+ """
215
+
216
+ # %% ../../nbs/js/navigation.ipynb #eb0cbbbb
217
+ def generate_navigation_js(
218
+ config:TokenSelectorConfig, # config for this instance
219
+ ids:TokenSelectorHtmlIds, # HTML IDs
220
+ ) -> str: # JS code fragment for the IIFE
221
+ """Generate mode-specific navigation and selection JS functions."""
222
+ generators = {
223
+ "gap": _generate_gap_nav_js,
224
+ "word": _generate_word_nav_js,
225
+ "span": _generate_span_nav_js,
226
+ }
227
+ gen = generators.get(config.selection_mode, _generate_gap_nav_js)
228
+ return gen(config, ids)