poetry-core 0.0.2
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +3 -0
- data/DESIGN.md +145 -0
- data/LICENSE.txt +21 -0
- data/README.md +30 -0
- data/THIRD_PARTY_NOTICES.md +507 -0
- data/app/components/poetry/core/box/component.rb +82 -0
- data/app/components/poetry/core/component.rb +390 -0
- data/app/components/poetry/core/concerns/agent_tools.rb +418 -0
- data/app/components/poetry/core/concerns/declared_attributes.rb +267 -0
- data/app/components/poetry/core/concerns/introspection.rb +533 -0
- data/app/components/poetry/core/concerns/options.rb +262 -0
- data/app/components/poetry/core/concerns/parts.rb +148 -0
- data/app/components/poetry/core/concerns/stimulus.rb +363 -0
- data/app/components/poetry/core/concerns/styles.rb +323 -0
- data/app/components/poetry/core/style.rb +150 -0
- data/app/components/poetry/core/wrapper/component.rb +90 -0
- data/app/javascript/poetry/core/accordion_controller.js +165 -0
- data/app/javascript/poetry/core/action_bar_controller.js +109 -0
- data/app/javascript/poetry/core/autocomplete_controller.js +220 -0
- data/app/javascript/poetry/core/calendar_controller.js +406 -0
- data/app/javascript/poetry/core/carousel_controller.js +167 -0
- data/app/javascript/poetry/core/checkbox_group_controller.js +97 -0
- data/app/javascript/poetry/core/checked_controller.js +208 -0
- data/app/javascript/poetry/core/clipboard_text_controller.js +111 -0
- data/app/javascript/poetry/core/combobox_controller.js +1095 -0
- data/app/javascript/poetry/core/command_controller.js +510 -0
- data/app/javascript/poetry/core/context_menu_controller.js +204 -0
- data/app/javascript/poetry/core/date_field_controller.js +584 -0
- data/app/javascript/poetry/core/date_picker_controller.js +111 -0
- data/app/javascript/poetry/core/deferred_controller.js +111 -0
- data/app/javascript/poetry/core/dialog_controller.js +206 -0
- data/app/javascript/poetry/core/dismissable_controller.js +182 -0
- data/app/javascript/poetry/core/drawer_controller.js +365 -0
- data/app/javascript/poetry/core/file_input_controller.js +191 -0
- data/app/javascript/poetry/core/focus_scope_controller.js +162 -0
- data/app/javascript/poetry/core/helpers/announce.js +228 -0
- data/app/javascript/poetry/core/helpers/breakpoint.js +28 -0
- data/app/javascript/poetry/core/helpers/collection.js +18 -0
- data/app/javascript/poetry/core/helpers/direction.js +15 -0
- data/app/javascript/poetry/core/helpers/escape.js +38 -0
- data/app/javascript/poetry/core/helpers/filter_rank.js +104 -0
- data/app/javascript/poetry/core/helpers/focus_guards.js +46 -0
- data/app/javascript/poetry/core/helpers/hotkey.js +43 -0
- data/app/javascript/poetry/core/helpers/id_integrity.js +65 -0
- data/app/javascript/poetry/core/helpers/incomplete_date.js +328 -0
- data/app/javascript/poetry/core/helpers/mask.js +283 -0
- data/app/javascript/poetry/core/helpers/portal.js +217 -0
- data/app/javascript/poetry/core/helpers/presence.js +210 -0
- data/app/javascript/poetry/core/helpers/registration_guard.js +76 -0
- data/app/javascript/poetry/core/helpers/scroll_lock.js +67 -0
- data/app/javascript/poetry/core/helpers/scroller_geometry.js +422 -0
- data/app/javascript/poetry/core/helpers/state.js +80 -0
- data/app/javascript/poetry/core/helpers/tabbable.js +56 -0
- data/app/javascript/poetry/core/helpers/turbo_cache.js +21 -0
- data/app/javascript/poetry/core/helpers/typeahead.js +85 -0
- data/app/javascript/poetry/core/hotkey_controller.js +55 -0
- data/app/javascript/poetry/core/hover_card_controller.js +431 -0
- data/app/javascript/poetry/core/index.js +218 -0
- data/app/javascript/poetry/core/mask_controller.js +500 -0
- data/app/javascript/poetry/core/menu_controller.js +1050 -0
- data/app/javascript/poetry/core/menubar_controller.js +329 -0
- data/app/javascript/poetry/core/message_scroller_controller.js +930 -0
- data/app/javascript/poetry/core/navigation_menu_controller.js +458 -0
- data/app/javascript/poetry/core/number_field_controller.js +404 -0
- data/app/javascript/poetry/core/optimistic_form_controller.js +56 -0
- data/app/javascript/poetry/core/otp_controller.js +219 -0
- data/app/javascript/poetry/core/popover_controller.js +300 -0
- data/app/javascript/poetry/core/popper_controller.js +439 -0
- data/app/javascript/poetry/core/pressed_controller.js +74 -0
- data/app/javascript/poetry/core/questionnaire_controller.js +468 -0
- data/app/javascript/poetry/core/radio_group_controller.js +197 -0
- data/app/javascript/poetry/core/resizable_controller.js +202 -0
- data/app/javascript/poetry/core/roving_focus_controller.js +215 -0
- data/app/javascript/poetry/core/scroll_spy_controller.js +84 -0
- data/app/javascript/poetry/core/search_field_controller.js +95 -0
- data/app/javascript/poetry/core/select_controller.js +924 -0
- data/app/javascript/poetry/core/sensitive_input_controller.js +195 -0
- data/app/javascript/poetry/core/sheet_controller.js +24 -0
- data/app/javascript/poetry/core/sidebar_controller.js +272 -0
- data/app/javascript/poetry/core/slider_controller.js +419 -0
- data/app/javascript/poetry/core/state_controller.js +82 -0
- data/app/javascript/poetry/core/table_selection_controller.js +153 -0
- data/app/javascript/poetry/core/tabs_controller.js +151 -0
- data/app/javascript/poetry/core/tag_group_controller.js +134 -0
- data/app/javascript/poetry/core/toast_controller.js +228 -0
- data/app/javascript/poetry/core/toast_trigger_controller.js +26 -0
- data/app/javascript/poetry/core/toaster_controller.js +197 -0
- data/app/javascript/poetry/core/toggle_group_controller.js +206 -0
- data/app/javascript/poetry/core/tooltip_controller.js +541 -0
- data/app/javascript/poetry/core/tree_controller.js +265 -0
- data/app/javascript/poetry/core/vendor/floating_ui_core.js +1043 -0
- data/app/javascript/poetry/core/vendor/floating_ui_dom.js +794 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils.js +141 -0
- data/app/javascript/poetry/core/vendor/floating_ui_utils_dom.js +170 -0
- data/app/views/poetry/core/preview.html.erb +13 -0
- data/config/component_registry.yml +20 -0
- data/config/controllers_manifest.json +1528 -0
- data/config/importmap.rb +11 -0
- data/config/state_vocabulary.json +31 -0
- data/lib/active_model/type/list.rb +33 -0
- data/lib/active_model/type/symbol.rb +36 -0
- data/lib/poetry/core/check/stable_identity.rb +104 -0
- data/lib/poetry/core/check.rb +1445 -0
- data/lib/poetry/core/config.rb +286 -0
- data/lib/poetry/core/contrib/wrapped_helper.rb +68 -0
- data/lib/poetry/core/css/bem_merger.rb +45 -0
- data/lib/poetry/core/css/bem_reference.rb +87 -0
- data/lib/poetry/core/css/override_scan.rb +110 -0
- data/lib/poetry/core/css/resolver.rb +161 -0
- data/lib/poetry/core/css/safelist.rb +41 -0
- data/lib/poetry/core/css/tailwind_merger.rb +102 -0
- data/lib/poetry/core/css/template_classes.rb +102 -0
- data/lib/poetry/core/css/theme_coverage.rb +72 -0
- data/lib/poetry/core/css/var_coverage.rb +75 -0
- data/lib/poetry/core/css/verifier.rb +106 -0
- data/lib/poetry/core/design_lint.rb +720 -0
- data/lib/poetry/core/design_md/import.rb +250 -0
- data/lib/poetry/core/design_md.rb +461 -0
- data/lib/poetry/core/engine.rb +71 -0
- data/lib/poetry/core/errors.rb +53 -0
- data/lib/poetry/core/html/attributes.rb +667 -0
- data/lib/poetry/core/icons.rb +157 -0
- data/lib/poetry/core/llms_text.rb +354 -0
- data/lib/poetry/core/page_architectures.rb +304 -0
- data/lib/poetry/core/part_contract.rb +285 -0
- data/lib/poetry/core/preview/abstract.rb +121 -0
- data/lib/poetry/core/preview/base.rb +227 -0
- data/lib/poetry/core/preview/sidecarable.rb +136 -0
- data/lib/poetry/core/preview/template.rb +186 -0
- data/lib/poetry/core/recipe_items.rb +82 -0
- data/lib/poetry/core/registry.rb +363 -0
- data/lib/poetry/core/registry_address.rb +115 -0
- data/lib/poetry/core/registry_client.rb +217 -0
- data/lib/poetry/core/registry_installer.rb +234 -0
- data/lib/poetry/core/registry_items.rb +184 -0
- data/lib/poetry/core/skill_text.rb +288 -0
- data/lib/poetry/core/stable_id.rb +110 -0
- data/lib/poetry/core/stimulus/builder.rb +395 -0
- data/lib/poetry/core/stimulus/declarations.rb +444 -0
- data/lib/poetry/core/stimulus/manifest.rb +68 -0
- data/lib/poetry/core/stimulus/merger.rb +131 -0
- data/lib/poetry/core/stimulus_contract.rb +263 -0
- data/lib/poetry/core/tag_helper.rb +10 -0
- data/lib/poetry/core/template_compile.rb +74 -0
- data/lib/poetry/core/token_import.rb +317 -0
- data/lib/poetry/core/tokens/color.rb +215 -0
- data/lib/poetry/core/tokens/contrast_gate.rb +120 -0
- data/lib/poetry/core/tokens/generator.rb +211 -0
- data/lib/poetry/core/tokens.rb +117 -0
- data/lib/poetry/core/version.rb +9 -0
- data/lib/poetry/core.rb +52 -0
- data/lib/poetry-core.rb +5 -0
- data/tokens/aliases.css +145 -0
- data/tokens/tailwind-theme.css +47 -0
- data/tokens/tokens.css +78 -0
- data/tokens/tokens.dtcg.json +779 -0
- data/vendor/floating-ui/LICENSE +20 -0
- data/vendor/floating-ui/VENDORED_VERSIONS +15 -0
- data/vendor/shadcn-tailwind/LICENSE +21 -0
- data/vendor/shadcn-tailwind/VENDORED_COMMIT +1 -0
- data/vendor/shadcn-tailwind/tailwind.css +629 -0
- data/vendor/tw-animate-css/LICENSE +21 -0
- data/vendor/tw-animate-css/VENDORED_VERSION +1 -0
- data/vendor/tw-animate-css/tw-animate.css +1 -0
- metadata +267 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { Controller } from "@hotwired/stimulus"
|
|
2
|
+
import {
|
|
3
|
+
applyMaskToRaw,
|
|
4
|
+
buildDisplayValue,
|
|
5
|
+
checkComplete,
|
|
6
|
+
extractRaw,
|
|
7
|
+
findNextEditablePosition,
|
|
8
|
+
generatePattern,
|
|
9
|
+
nextTokenPosition,
|
|
10
|
+
parseMask,
|
|
11
|
+
prevTokenPosition,
|
|
12
|
+
processInput
|
|
13
|
+
} from "@poetry/controllers/helpers/mask"
|
|
14
|
+
|
|
15
|
+
// The input-mask machine (the engine is adapted from an MIT-licensed
|
|
16
|
+
// source - source and license in THIRD_PARTY_NOTICES.md) on a bare native
|
|
17
|
+
// <input> - this.element IS the input. Three layers:
|
|
18
|
+
//
|
|
19
|
+
// 1. THE PRIMARY PATH is keydown with preventDefault: a printable char
|
|
20
|
+
// skips forward over literals from the caret and pattern-tests (a
|
|
21
|
+
// failing char is a NO-OP, it never reaches the field), a selection is
|
|
22
|
+
// replaced via a raw rebuild, and the caret lands past the next literal
|
|
23
|
+
// run. Backspace scans BACKWARD over literals (the caret lands at the
|
|
24
|
+
// deleted token), Delete scans FORWARD (the caret stays),
|
|
25
|
+
// Cmd/Ctrl+Backspace kills to start, arrows hop literal runs. Because
|
|
26
|
+
// preventDefault kills the native undo stack, a custom
|
|
27
|
+
// {raw, selectionStart} history (max 100, deduped) backs
|
|
28
|
+
// Cmd/Ctrl+Z / Shift+Cmd/Ctrl+Z / Ctrl+Y.
|
|
29
|
+
//
|
|
30
|
+
// 2. THE FALLBACK is the input-event diff (IME, mobile keyboards,
|
|
31
|
+
// autofill, password managers - paths that never emit clean keydowns):
|
|
32
|
+
// the longest common prefix+suffix against the previous display
|
|
33
|
+
// isolates the inserted text and the removed span, the value is rebuilt
|
|
34
|
+
// from raw pieces, re-masked, and the caret lands after the insertion.
|
|
35
|
+
//
|
|
36
|
+
// 3. THE DISPLAY: skeleton padding ("__/__") shows on focus
|
|
37
|
+
// (showMaskOnFocus, default) or permanently (alwaysShowMask); blur
|
|
38
|
+
// strips it back to the filled region - or clears the field entirely
|
|
39
|
+
// when autoClear is set and the mask is incomplete. A COLLAPSED caret
|
|
40
|
+
// is clamped into [first token, end of filled region] on focus (rAF),
|
|
41
|
+
// mousedown (rAF) and mouseup; selections are never touched.
|
|
42
|
+
//
|
|
43
|
+
// A deliberate divergence from the adapted source: every programmatic value write dispatches a
|
|
44
|
+
// native bubbling `input` event (#painting guards the controller against
|
|
45
|
+
// its own echo) so Rails/Turbo listeners stay live. The raw value mirrors
|
|
46
|
+
// to data-raw after every change; `pattern` is set on connect from
|
|
47
|
+
// generatePattern("full-inexact") unless the input already carries one.
|
|
48
|
+
const MAX_UNDO_HISTORY = 100
|
|
49
|
+
|
|
50
|
+
export default class MaskController extends Controller {
|
|
51
|
+
// The events this controller dispatches (manifest surface;
|
|
52
|
+
// events_declaration.test.js enforces the list stays honest).
|
|
53
|
+
static events = ["poetry:mask:change", "poetry:mask:complete"]
|
|
54
|
+
|
|
55
|
+
static values = {
|
|
56
|
+
mask: String,
|
|
57
|
+
slotChar: { type: String, default: "_" },
|
|
58
|
+
alwaysShowMask: Boolean,
|
|
59
|
+
showMaskOnFocus: { type: Boolean, default: true },
|
|
60
|
+
autoClear: Boolean,
|
|
61
|
+
upcase: Boolean
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
#slots = []
|
|
65
|
+
#processed = "" // the masked value, no skeleton padding
|
|
66
|
+
#display = "" // what the input currently shows (may include skeleton)
|
|
67
|
+
#raw = ""
|
|
68
|
+
#wasComplete = false
|
|
69
|
+
#focused = false
|
|
70
|
+
#painting = false // set while dispatching our own native input echo
|
|
71
|
+
#undoStack = []
|
|
72
|
+
#redoStack = []
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Parses the mask, stamps the pattern attribute (unless the input
|
|
76
|
+
* carries one), wires the listener set, and processes a server-rendered
|
|
77
|
+
* value silently.
|
|
78
|
+
*/
|
|
79
|
+
connect() {
|
|
80
|
+
if (this.element.tagName !== "INPUT") {
|
|
81
|
+
console.warn("poetry--core--mask: must be attached to an <input>")
|
|
82
|
+
return
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.#slots = parseMask(this.maskValue)
|
|
86
|
+
|
|
87
|
+
if (!this.element.hasAttribute("pattern")) {
|
|
88
|
+
this.element.setAttribute("pattern", generatePattern(this.#slots, "full-inexact"))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.#listen("addEventListener")
|
|
92
|
+
this.#initialize()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Unwires the listener set. */
|
|
96
|
+
disconnect() {
|
|
97
|
+
this.#listen("removeEventListener")
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
#listen(method) {
|
|
101
|
+
for (const [type, handler] of [
|
|
102
|
+
["input", this.#onInput],
|
|
103
|
+
["focus", this.#onFocus],
|
|
104
|
+
["blur", this.#onBlur],
|
|
105
|
+
["mousedown", this.#onMousedown],
|
|
106
|
+
["mouseup", this.#onMouseup],
|
|
107
|
+
["keydown", this.#onKeydown],
|
|
108
|
+
["paste", this.#onPaste]
|
|
109
|
+
]) this.element[method](type, handler)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Mount: a server-rendered value is processed through the mask SILENTLY -
|
|
113
|
+
// no poetry events, no native input echo (nothing changed semantically).
|
|
114
|
+
#initialize() {
|
|
115
|
+
const value = this.element.value
|
|
116
|
+
|
|
117
|
+
if (value) {
|
|
118
|
+
const processed = processInput(value, this.#slots)
|
|
119
|
+
|
|
120
|
+
this.#processed = processed
|
|
121
|
+
this.#raw = extractRaw(processed, this.#slots)
|
|
122
|
+
this.#display = buildDisplayValue(processed, this.#slots, this.slotCharValue, this.#showSlots(processed))
|
|
123
|
+
this.#wasComplete = checkComplete(processed, this.#slots)
|
|
124
|
+
this.element.value = this.#display
|
|
125
|
+
} else if (this.alwaysShowMaskValue) {
|
|
126
|
+
this.#display = buildDisplayValue("", this.#slots, this.slotCharValue, true)
|
|
127
|
+
this.element.value = this.#display
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
this.element.setAttribute("data-raw", this.#raw)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// --- the keydown path (primary: full editing control) ---
|
|
134
|
+
|
|
135
|
+
#onKeydown = (event) => {
|
|
136
|
+
const input = this.element
|
|
137
|
+
const start = input.selectionStart ?? 0
|
|
138
|
+
const end = input.selectionEnd ?? 0
|
|
139
|
+
const processed = this.#processed
|
|
140
|
+
const slots = this.#slots
|
|
141
|
+
const modifier = event.metaKey || (event.ctrlKey && !event.altKey)
|
|
142
|
+
const key = (event.key ?? "").toLowerCase()
|
|
143
|
+
|
|
144
|
+
if (modifier && key === "z" && !event.shiftKey) {
|
|
145
|
+
event.preventDefault()
|
|
146
|
+
this.#restore(this.#undoStack, this.#redoStack)
|
|
147
|
+
return
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (modifier && ((key === "z" && event.shiftKey) || (key === "y" && !event.shiftKey))) {
|
|
151
|
+
event.preventDefault()
|
|
152
|
+
this.#restore(this.#redoStack, this.#undoStack)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (event.key === "Backspace") {
|
|
157
|
+
event.preventDefault()
|
|
158
|
+
|
|
159
|
+
if (modifier) {
|
|
160
|
+
// Cmd/Ctrl+Backspace: kill to start, keep the tail.
|
|
161
|
+
const from = Math.min(start, processed.length)
|
|
162
|
+
const afterRaw = extractRaw(processed.slice(from), slots.slice(from))
|
|
163
|
+
|
|
164
|
+
this.#pushUndo()
|
|
165
|
+
this.#updateValue(this.#mask(afterRaw), 0)
|
|
166
|
+
return
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (start !== end) {
|
|
170
|
+
this.#deleteSelection(start, end)
|
|
171
|
+
return
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (start === 0) return
|
|
175
|
+
|
|
176
|
+
// Scan BACKWARD over literals; the caret lands at the deleted token.
|
|
177
|
+
let pos = start - 1
|
|
178
|
+
|
|
179
|
+
while (pos >= 0 && slots[pos]?.type === "literal") pos--
|
|
180
|
+
|
|
181
|
+
if (pos < 0) return
|
|
182
|
+
|
|
183
|
+
const beforeRaw = extractRaw(processed.slice(0, pos), slots.slice(0, pos))
|
|
184
|
+
const afterRaw = extractRaw(processed.slice(pos + 1), slots.slice(pos + 1))
|
|
185
|
+
|
|
186
|
+
this.#pushUndo()
|
|
187
|
+
this.#updateValue(this.#mask(beforeRaw + afterRaw), pos)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (event.key === "Delete") {
|
|
192
|
+
event.preventDefault()
|
|
193
|
+
|
|
194
|
+
if (start !== end) {
|
|
195
|
+
this.#deleteSelection(start, end)
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Scan FORWARD over literals; the caret stays put.
|
|
200
|
+
let pos = start
|
|
201
|
+
|
|
202
|
+
while (pos < slots.length && slots[pos]?.type === "literal") pos++
|
|
203
|
+
|
|
204
|
+
if (pos >= processed.length) return
|
|
205
|
+
|
|
206
|
+
const beforeRaw = extractRaw(processed.slice(0, start), slots.slice(0, start))
|
|
207
|
+
const afterRaw = extractRaw(processed.slice(pos + 1), slots.slice(pos + 1))
|
|
208
|
+
|
|
209
|
+
this.#pushUndo()
|
|
210
|
+
this.#updateValue(this.#mask(beforeRaw + afterRaw), start)
|
|
211
|
+
return
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (event.key === "ArrowRight" && !event.shiftKey) {
|
|
215
|
+
const next = findNextEditablePosition(start + 1, slots, input.value.length)
|
|
216
|
+
|
|
217
|
+
if (next !== start + 1) {
|
|
218
|
+
event.preventDefault()
|
|
219
|
+
input.setSelectionRange(next, next)
|
|
220
|
+
}
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (event.key === "ArrowLeft" && !event.shiftKey) {
|
|
225
|
+
if (start === 0) return
|
|
226
|
+
|
|
227
|
+
const previous = prevTokenPosition(slots, start - 1)
|
|
228
|
+
|
|
229
|
+
if (previous >= 0 && previous !== start - 1) {
|
|
230
|
+
event.preventDefault()
|
|
231
|
+
input.setSelectionRange(previous + 1, previous + 1)
|
|
232
|
+
}
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if ((event.key ?? "").length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
237
|
+
event.preventDefault() // a failing char is a no-op, never the field's problem
|
|
238
|
+
|
|
239
|
+
let pos = Math.min(start, processed.length)
|
|
240
|
+
|
|
241
|
+
while (pos < slots.length && slots[pos]?.type === "literal") pos++
|
|
242
|
+
|
|
243
|
+
if (pos >= slots.length) return
|
|
244
|
+
|
|
245
|
+
const transform = this.#transform()
|
|
246
|
+
const char = transform ? transform(event.key) : event.key
|
|
247
|
+
|
|
248
|
+
if (!slots[pos].pattern.test(char)) return
|
|
249
|
+
|
|
250
|
+
const beforeRaw = extractRaw(processed.slice(0, pos), slots.slice(0, pos))
|
|
251
|
+
const afterFrom = start < end ? Math.min(end, processed.length) : pos
|
|
252
|
+
const afterRaw = extractRaw(processed.slice(afterFrom), slots.slice(afterFrom))
|
|
253
|
+
const masked = this.#mask(beforeRaw + char + afterRaw)
|
|
254
|
+
|
|
255
|
+
this.#pushUndo()
|
|
256
|
+
this.#updateValue(masked, findNextEditablePosition(pos + 1, slots, masked.length))
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Selection deletion (Backspace and Delete share it): keep the raw
|
|
261
|
+
// before the selection, keep the raw after it, re-mask, caret at start.
|
|
262
|
+
#deleteSelection(start, end) {
|
|
263
|
+
const processed = this.#processed
|
|
264
|
+
const to = Math.min(end, processed.length)
|
|
265
|
+
const beforeRaw = extractRaw(processed.slice(0, start), this.#slots.slice(0, start))
|
|
266
|
+
const afterRaw = extractRaw(processed.slice(to), this.#slots.slice(to))
|
|
267
|
+
|
|
268
|
+
this.#pushUndo()
|
|
269
|
+
this.#updateValue(this.#mask(beforeRaw + afterRaw), start)
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// --- the input-event diff fallback (IME / mobile / autofill) ---
|
|
273
|
+
|
|
274
|
+
#onInput = () => {
|
|
275
|
+
if (this.#painting) return // our own native echo
|
|
276
|
+
|
|
277
|
+
const prev = this.#display
|
|
278
|
+
const curr = this.element.value
|
|
279
|
+
|
|
280
|
+
let prefix = 0
|
|
281
|
+
const maxPrefix = Math.min(prev.length, curr.length)
|
|
282
|
+
|
|
283
|
+
while (prefix < maxPrefix && prev[prefix] === curr[prefix]) prefix++
|
|
284
|
+
|
|
285
|
+
let suffix = 0
|
|
286
|
+
const maxSuffix = Math.min(prev.length - prefix, curr.length - prefix)
|
|
287
|
+
|
|
288
|
+
while (suffix < maxSuffix && prev[prev.length - 1 - suffix] === curr[curr.length - 1 - suffix]) suffix++
|
|
289
|
+
|
|
290
|
+
const inserted = curr.slice(prefix, curr.length - suffix)
|
|
291
|
+
const removedEnd = prev.length - suffix
|
|
292
|
+
const beforeRaw = extractRaw(prev.slice(0, prefix), this.#slots.slice(0, prefix))
|
|
293
|
+
const afterRaw = extractRaw(prev.slice(removedEnd), this.#slots.slice(removedEnd))
|
|
294
|
+
const masked = this.#mask(beforeRaw + inserted + afterRaw)
|
|
295
|
+
|
|
296
|
+
if (masked !== prev) this.#pushUndo()
|
|
297
|
+
|
|
298
|
+
this.#updateValue(masked, this.#mask(beforeRaw + inserted).length)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// --- paste: raw insertion at the clamped selection ---
|
|
302
|
+
|
|
303
|
+
#onPaste = (event) => {
|
|
304
|
+
event.preventDefault()
|
|
305
|
+
|
|
306
|
+
const text = event.clipboardData?.getData("text") ?? ""
|
|
307
|
+
const input = this.element
|
|
308
|
+
const processed = this.#processed
|
|
309
|
+
const start = Math.min(input.selectionStart ?? 0, processed.length)
|
|
310
|
+
const end = Math.min(input.selectionEnd ?? 0, processed.length)
|
|
311
|
+
const beforeRaw = extractRaw(processed.slice(0, start), this.#slots.slice(0, start))
|
|
312
|
+
const afterRaw = extractRaw(processed.slice(end), this.#slots.slice(end))
|
|
313
|
+
|
|
314
|
+
this.#pushUndo()
|
|
315
|
+
this.#updateValue(this.#mask(beforeRaw + text + afterRaw))
|
|
316
|
+
|
|
317
|
+
// The caret lands after the MASKED insertion (invalid chars dropped).
|
|
318
|
+
const caret = Math.min(this.#mask(beforeRaw + text).length, this.#slots.length)
|
|
319
|
+
|
|
320
|
+
if (document.activeElement === input) input.setSelectionRange(caret, caret)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// --- focus / blur: skeleton lifecycle ---
|
|
324
|
+
|
|
325
|
+
#onFocus = () => {
|
|
326
|
+
this.#focused = true
|
|
327
|
+
|
|
328
|
+
if (this.showMaskOnFocusValue || this.alwaysShowMaskValue) {
|
|
329
|
+
this.#display = buildDisplayValue(this.#processed, this.#slots, this.slotCharValue, true)
|
|
330
|
+
this.#paint(this.#display)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// rAF: the browser places the caret AFTER focus fires.
|
|
334
|
+
requestAnimationFrame(() => {
|
|
335
|
+
if (document.activeElement === this.element) this.#clampCaret({ floor: true })
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
#onBlur = () => {
|
|
340
|
+
this.#focused = false
|
|
341
|
+
|
|
342
|
+
const slots = this.#slots
|
|
343
|
+
const skeleton = buildDisplayValue(this.#processed, slots, this.slotCharValue, true)
|
|
344
|
+
// An externally mutated value (no input event fired) still re-parses.
|
|
345
|
+
const processed = this.element.value === skeleton
|
|
346
|
+
? this.#processed
|
|
347
|
+
: processInput(this.element.value, slots)
|
|
348
|
+
const complete = checkComplete(processed, slots)
|
|
349
|
+
|
|
350
|
+
if (this.autoClearValue && !complete && processed.length > 0) {
|
|
351
|
+
this.#clear()
|
|
352
|
+
|
|
353
|
+
if (this.alwaysShowMaskValue) {
|
|
354
|
+
this.#display = buildDisplayValue("", slots, this.slotCharValue, true)
|
|
355
|
+
this.#paint(this.#display)
|
|
356
|
+
}
|
|
357
|
+
return
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (!this.alwaysShowMaskValue && !complete) {
|
|
361
|
+
if (extractRaw(processed, slots).length === 0) {
|
|
362
|
+
this.#clear()
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Strip the skeleton padding; the filled region stays.
|
|
367
|
+
this.#processed = processed
|
|
368
|
+
this.#raw = extractRaw(processed, slots)
|
|
369
|
+
this.#display = processed
|
|
370
|
+
this.element.setAttribute("data-raw", this.#raw)
|
|
371
|
+
this.#paint(processed)
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// --- caret clamping (focus rAF / mousedown rAF / mouseup) ---
|
|
376
|
+
|
|
377
|
+
#onMouseup = () => {
|
|
378
|
+
if (document.activeElement === this.element) this.#clampCaret({ floor: true })
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
#onMousedown = () => {
|
|
382
|
+
// rAF: the click's caret placement hasn't happened yet at mousedown.
|
|
383
|
+
requestAnimationFrame(() => {
|
|
384
|
+
if (document.activeElement === this.element) this.#clampCaret({ floor: false })
|
|
385
|
+
})
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Clamp a COLLAPSED caret into [first token, end of the filled region];
|
|
389
|
+
// selections are never touched (floor: the mousedown variant only caps).
|
|
390
|
+
#clampCaret({ floor }) {
|
|
391
|
+
const input = this.element
|
|
392
|
+
const start = input.selectionStart ?? 0
|
|
393
|
+
|
|
394
|
+
if (start !== (input.selectionEnd ?? 0)) return
|
|
395
|
+
|
|
396
|
+
const first = nextTokenPosition(this.#slots)
|
|
397
|
+
const limit = this.#processed.length > 0
|
|
398
|
+
? findNextEditablePosition(this.#processed.length, this.#slots, this.#processed.length)
|
|
399
|
+
: first
|
|
400
|
+
|
|
401
|
+
if (start > limit || (floor && start < first)) input.setSelectionRange(limit, limit)
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// --- the undo/redo history (preventDefault kills the native stack) ---
|
|
405
|
+
|
|
406
|
+
#pushUndo() {
|
|
407
|
+
const state = { raw: this.#raw, selectionStart: this.element.selectionStart ?? this.#raw.length }
|
|
408
|
+
const top = this.#undoStack[this.#undoStack.length - 1]
|
|
409
|
+
|
|
410
|
+
if (top && top.raw === state.raw && top.selectionStart === state.selectionStart) return
|
|
411
|
+
|
|
412
|
+
this.#undoStack.push(state)
|
|
413
|
+
|
|
414
|
+
if (this.#undoStack.length > MAX_UNDO_HISTORY) this.#undoStack.shift()
|
|
415
|
+
|
|
416
|
+
this.#redoStack = []
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
#restore(from, to) {
|
|
420
|
+
const state = from.pop()
|
|
421
|
+
|
|
422
|
+
if (!state) return
|
|
423
|
+
|
|
424
|
+
to.push({ raw: this.#raw, selectionStart: this.element.selectionStart ?? 0 })
|
|
425
|
+
this.#updateValue(this.#mask(state.raw), state.selectionStart)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// --- the single value sink ---
|
|
429
|
+
|
|
430
|
+
// Canonicalize -> re-derive raw + display -> write the DOM -> fire the
|
|
431
|
+
// events. change fires per user-driven mutation; complete fires ONCE per
|
|
432
|
+
// rise into complete (editing below re-arms it).
|
|
433
|
+
#updateValue(masked, caret) {
|
|
434
|
+
const processed = processInput(masked, this.#slots)
|
|
435
|
+
const raw = extractRaw(processed, this.#slots)
|
|
436
|
+
const display = buildDisplayValue(processed, this.#slots, this.slotCharValue, this.#showSlots(processed))
|
|
437
|
+
|
|
438
|
+
this.#processed = processed
|
|
439
|
+
this.#raw = raw
|
|
440
|
+
this.#display = display
|
|
441
|
+
this.element.setAttribute("data-raw", raw)
|
|
442
|
+
this.#paint(display, caret === undefined ? undefined : Math.min(caret, processed.length))
|
|
443
|
+
|
|
444
|
+
const complete = checkComplete(processed, this.#slots)
|
|
445
|
+
const detail = { raw, masked: display, complete }
|
|
446
|
+
|
|
447
|
+
this.dispatch("change", { prefix: "poetry:mask", detail })
|
|
448
|
+
|
|
449
|
+
if (complete && !this.#wasComplete) {
|
|
450
|
+
this.dispatch("complete", { prefix: "poetry:mask", detail })
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
this.#wasComplete = complete
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// autoClear / empty blur: everything resets and listeners hear about it.
|
|
457
|
+
#clear() {
|
|
458
|
+
this.#processed = ""
|
|
459
|
+
this.#display = ""
|
|
460
|
+
this.#raw = ""
|
|
461
|
+
this.#wasComplete = false
|
|
462
|
+
this.element.setAttribute("data-raw", "")
|
|
463
|
+
this.#paint("")
|
|
464
|
+
this.dispatch("change", { prefix: "poetry:mask", detail: { raw: "", masked: "", complete: false } })
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Every programmatic value write echoes a native bubbling input event
|
|
468
|
+
// (the recorded port caveat - Rails/Turbo listeners must stay
|
|
469
|
+
// live); #painting keeps #onInput from re-processing the echo.
|
|
470
|
+
#paint(display, caret) {
|
|
471
|
+
const changed = this.element.value !== display
|
|
472
|
+
|
|
473
|
+
if (changed) this.element.value = display
|
|
474
|
+
|
|
475
|
+
if (caret !== undefined && document.activeElement === this.element) {
|
|
476
|
+
this.element.setSelectionRange(caret, caret)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (!changed) return
|
|
480
|
+
|
|
481
|
+
this.#painting = true
|
|
482
|
+
this.element.dispatchEvent(new Event("input", { bubbles: true }))
|
|
483
|
+
this.#painting = false
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
#showSlots(processed) {
|
|
487
|
+
return (this.alwaysShowMaskValue || this.#focused) &&
|
|
488
|
+
(this.showMaskOnFocusValue || processed.length > 0)
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
#mask(raw) {
|
|
492
|
+
return applyMaskToRaw(raw, this.#slots, this.#transform())
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// A transform could generalize to any fn; poetry keeps ONE
|
|
496
|
+
// declarative knob - uppercase before validation.
|
|
497
|
+
#transform() {
|
|
498
|
+
return this.upcaseValue ? (char) => char.toUpperCase() : undefined
|
|
499
|
+
}
|
|
500
|
+
}
|