stimeo-ui 0.9.0 → 0.11.0
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.
- package/CHANGELOG.md +149 -0
- package/dist/cable/index.d.ts +79 -14
- package/dist/cable/index.js +226 -43
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.js +17 -7
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.js +1 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.js +2 -2
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +1 -1
- package/dist/controllers/collapsible_controller.js +1 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +1 -1
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.js +15 -5
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.js +15 -5
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +5 -4
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.js +1 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.js +1 -1
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +15 -5
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +2 -2
- package/dist/controllers/direct_upload_controller.js +1 -5
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +2 -2
- package/dist/controllers/drawer_controller.js +17 -7
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +5 -0
- package/dist/controllers/focus_controller.js +45 -7
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +1 -1
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +4 -3
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -2
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +3 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +1 -1
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +2 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +1 -1
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.js +1 -1
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js +2 -2
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +1 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +27 -2
- package/dist/controllers/password_reveal_controller.js +102 -3
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +71 -17
- package/dist/controllers/password_strength_controller.js +287 -42
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +23 -5
- package/dist/controllers/pointer_drag_controller.js +64 -18
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +17 -4
- package/dist/controllers/portal_controller.js +32 -11
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +22 -14
- package/dist/controllers/preview_guard_controller.js +180 -20
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_before_cache_controller.d.ts +1 -2
- package/dist/controllers/reset_before_cache_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +1 -1
- package/dist/controllers/resizable_controller.js +1 -1
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +17 -4
- package/dist/controllers/roving_controller.js +127 -14
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.d.ts +33 -11
- package/dist/controllers/scroll_restore_controller.js +127 -27
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +42 -10
- package/dist/controllers/scroll_visibility_controller.js +167 -14
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +15 -5
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.js +0 -5
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +26 -9
- package/dist/controllers/stick_to_bottom_controller.js +149 -28
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +32 -6
- package/dist/controllers/theme_controller.js +151 -40
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +2 -2
- package/dist/controllers/toast_controller.js +2 -2
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +11 -7
- package/dist/controllers/transition_controller.js +79 -15
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +1188 -318
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +3 -4
- package/dist/inspector/cli.js +1 -0
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +6 -5
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +6 -6
- package/dist/inspector/manifest.json +15 -32
- package/dist/positioning/index.d.ts +18 -4
- package/dist/positioning/index.js +84 -25
- package/dist/positioning/index.js.map +1 -1
- package/package.json +6 -6
|
@@ -2,6 +2,35 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/password_reveal_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/before_cache_reset.ts
|
|
6
|
+
var BeforeCacheReset = class _BeforeCacheReset {
|
|
7
|
+
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
8
|
+
static #subscribers = /* @__PURE__ */ new Set();
|
|
9
|
+
/** The shared listener; installed while at least one instance is subscribed. */
|
|
10
|
+
static #onBeforeCache = () => {
|
|
11
|
+
for (const subscriber of _BeforeCacheReset.#subscribers) subscriber.#rewind();
|
|
12
|
+
};
|
|
13
|
+
#rewind;
|
|
14
|
+
/** @param rewind - the pass that returns this controller's state to its initial form. */
|
|
15
|
+
constructor(rewind) {
|
|
16
|
+
this.#rewind = rewind;
|
|
17
|
+
}
|
|
18
|
+
/** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */
|
|
19
|
+
activate() {
|
|
20
|
+
const first = _BeforeCacheReset.#subscribers.size === 0;
|
|
21
|
+
_BeforeCacheReset.#subscribers.add(this);
|
|
22
|
+
if (first) {
|
|
23
|
+
document.addEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */
|
|
27
|
+
deactivate() {
|
|
28
|
+
_BeforeCacheReset.#subscribers.delete(this);
|
|
29
|
+
if (_BeforeCacheReset.#subscribers.size > 0) return;
|
|
30
|
+
document.removeEventListener("turbo:before-cache", _BeforeCacheReset.#onBeforeCache);
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
5
34
|
// src/utils/safe_timeout.ts
|
|
6
35
|
var TimerRegistry = class {
|
|
7
36
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -56,6 +85,7 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
56
85
|
};
|
|
57
86
|
|
|
58
87
|
// src/controllers/password_reveal_controller.ts
|
|
88
|
+
var MAX_DELAY = 2 ** 31 - 1;
|
|
59
89
|
var PasswordRevealController = class extends Controller {
|
|
60
90
|
static targets = ["input", "toggle"];
|
|
61
91
|
static values = {
|
|
@@ -65,11 +95,54 @@ var PasswordRevealController = class extends Controller {
|
|
|
65
95
|
static events = ["toggle"];
|
|
66
96
|
/** Auto re-mask timer; torn down on disconnect. */
|
|
67
97
|
#timers = new SafeTimeout();
|
|
98
|
+
/**
|
|
99
|
+
* Whether this controller is between `connect()` and `disconnect()`.
|
|
100
|
+
*
|
|
101
|
+
* Target callbacks outlive the controller: Stimulus stops the target observer
|
|
102
|
+
* after `disconnect()`, so a field leaving after teardown still reaches
|
|
103
|
+
* {@link PasswordRevealController.inputTargetDisconnected}. Arming from there
|
|
104
|
+
* would put a timer back that nothing will clear. The element staying in the
|
|
105
|
+
* document does not answer this — unloading the controller leaves it there.
|
|
106
|
+
*/
|
|
107
|
+
#connected = false;
|
|
108
|
+
/** Masks the field before Turbo copies the page into its snapshot. */
|
|
109
|
+
#beforeCache = new BeforeCacheReset(() => this.#rewindToMasked());
|
|
68
110
|
connect() {
|
|
69
|
-
this.#
|
|
111
|
+
this.#connected = true;
|
|
112
|
+
const visible = this.#isVisible;
|
|
113
|
+
this.#reflect(visible);
|
|
114
|
+
this.#beforeCache.activate();
|
|
115
|
+
this.#arm(visible);
|
|
70
116
|
}
|
|
71
117
|
disconnect() {
|
|
118
|
+
this.#connected = false;
|
|
72
119
|
this.#timers.clearAll();
|
|
120
|
+
this.#beforeCache.deactivate();
|
|
121
|
+
}
|
|
122
|
+
/** Re-derives the hooks and the re-mask for a field swapped in after connect. */
|
|
123
|
+
inputTargetConnected() {
|
|
124
|
+
const visible = this.#connected && this.#isVisible;
|
|
125
|
+
this.#reflect(visible);
|
|
126
|
+
this.#arm(visible);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Re-derives from whatever field is left rather than assuming none is. A swap
|
|
130
|
+
* delivers this callback next to the arrival in either order, so a revealed
|
|
131
|
+
* replacement that answered "masked" here would be described as hidden while
|
|
132
|
+
* showing the password, and would carry no re-mask.
|
|
133
|
+
*
|
|
134
|
+
* Teardown is the one case with nothing to derive: target callbacks run after
|
|
135
|
+
* `disconnect()`, so the field is still revealed and re-arming from it would
|
|
136
|
+
* outlive the controller. A detached root is the signal to stand down.
|
|
137
|
+
*/
|
|
138
|
+
inputTargetDisconnected() {
|
|
139
|
+
const visible = this.#connected && this.element.isConnected && this.#isVisible;
|
|
140
|
+
this.#reflect(visible);
|
|
141
|
+
this.#arm(visible);
|
|
142
|
+
}
|
|
143
|
+
/** Re-derives the pressed state for a button swapped in after connect. */
|
|
144
|
+
toggleTargetConnected() {
|
|
145
|
+
this.#reflect(this.#isVisible);
|
|
73
146
|
}
|
|
74
147
|
/** Toggles the input between masked and revealed. Bound via `data-action`. */
|
|
75
148
|
toggle() {
|
|
@@ -98,11 +171,37 @@ var PasswordRevealController = class extends Controller {
|
|
|
98
171
|
}
|
|
99
172
|
this.#reflect(visible);
|
|
100
173
|
this.dispatch("toggle", { detail: { visible } });
|
|
174
|
+
this.#arm(visible);
|
|
175
|
+
}
|
|
176
|
+
/** Schedules the auto re-mask for a revealed field, replacing any pending one. */
|
|
177
|
+
#arm(visible) {
|
|
101
178
|
this.#timers.clearAll();
|
|
102
|
-
|
|
103
|
-
|
|
179
|
+
const delay = this.#autoHideDelay;
|
|
180
|
+
if (visible && delay > 0) {
|
|
181
|
+
this.#timers.set(() => this.#setVisible(false), delay);
|
|
104
182
|
}
|
|
105
183
|
}
|
|
184
|
+
/**
|
|
185
|
+
* The auto re-mask delay, held to what `setTimeout` can carry. Past that limit
|
|
186
|
+
* a delay folds to zero, turning "keep it showing" into "hide it at once" —
|
|
187
|
+
* the opposite of what the declaration asked for. A value that is no delay at
|
|
188
|
+
* all stays out of the positive range {@link PasswordRevealController.#arm}
|
|
189
|
+
* requires, so it schedules nothing.
|
|
190
|
+
*/
|
|
191
|
+
get #autoHideDelay() {
|
|
192
|
+
return Math.min(this.autoHideValue, MAX_DELAY);
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Returns the field to masked before Turbo copies the page. Silent and
|
|
196
|
+
* focus-free: the page is about to be frozen, so there is no one to tell and
|
|
197
|
+
* nowhere for focus to go.
|
|
198
|
+
*/
|
|
199
|
+
#rewindToMasked() {
|
|
200
|
+
this.#timers.clearAll();
|
|
201
|
+
if (!this.hasInputTarget) return;
|
|
202
|
+
this.inputTarget.type = "password";
|
|
203
|
+
this.#reflect(false);
|
|
204
|
+
}
|
|
106
205
|
/** Reflects the visible state onto `aria-pressed` and `data-state`. */
|
|
107
206
|
#reflect(visible) {
|
|
108
207
|
if (this.hasToggleTarget) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/password_reveal_controller.ts"],"names":[],"mappings":"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","file":"password_reveal_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/**\n * Headless password show/hide (unmask) toggle behavior.\n *\n * Markup contract (identifier: `stimeo--password-reveal`):\n * <div data-controller=\"stimeo--password-reveal\">\n * <input type=\"password\" aria-label=\"Password\"\n * data-stimeo--password-reveal-target=\"input\">\n * <button type=\"button\" aria-pressed=\"false\" aria-label=\"Show password\"\n * data-stimeo--password-reveal-target=\"toggle\"\n * data-action=\"stimeo--password-reveal#toggle\"></button>\n * </div>\n *\n * No dedicated APG pattern; this follows the toggle **Button** practice. The\n * accessible name stays state-independent (\"Show password\") while the pressed\n * state is conveyed by `aria-pressed`.\n *\n * `toggle` dispatches `{ visible: boolean }`.\n *\n * @remarks\n * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /\n * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and\n * the caret, so when (and only when) the input was the focused element its focus\n * and selection are restored afterward; when the toggle button holds focus\n * (keyboard use) focus is left on the button. An optional `autoHide` re-masks\n * after a delay, and that timer is torn down on disconnect (Turbo).\n */\nexport class PasswordRevealController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"toggle\"];\n static override values = {\n autoHide: { type: Number, default: 0 },\n };\n static actions = [\"toggle\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly toggleTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasToggleTarget: boolean;\n\n declare autoHideValue: number;\n\n /** Auto re-mask timer; torn down on disconnect. */\n #timers = new SafeTimeout();\n\n override connect(): void {\n this.#reflect(this.#isVisible);\n }\n\n override disconnect(): void {\n this.#timers.clearAll();\n }\n\n /** Toggles the input between masked and revealed. Bound via `data-action`. */\n toggle(): void {\n this.#setVisible(!this.#isVisible);\n }\n\n /** Whether the input is currently revealed (`type=\"text\"`). */\n get #isVisible(): boolean {\n return this.hasInputTarget && this.inputTarget.type === \"text\";\n }\n\n /** Switches the masked/revealed state, preserving focus and caret. */\n #setVisible(visible: boolean): void {\n if (!this.hasInputTarget) return;\n const input = this.inputTarget;\n\n // Only the input's *own* focus is restored across the type change; if the\n // toggle button (keyboard) holds focus, it is left untouched.\n const restoreInputFocus = document.activeElement === input;\n const selectionStart = input.selectionStart;\n const selectionEnd = input.selectionEnd;\n\n input.type = visible ? \"text\" : \"password\";\n\n if (restoreInputFocus) {\n input.focus();\n // `selectionStart` / `selectionEnd` are `number | null` (null for input\n // types that don't expose a selection); only restore when both are present.\n if (selectionStart !== null && selectionEnd !== null) {\n try {\n input.setSelectionRange(selectionStart, selectionEnd);\n } catch {\n // Some input types reject selection access; focus alone is enough.\n }\n }\n }\n\n this.#reflect(visible);\n this.dispatch(\"toggle\", { detail: { visible } });\n\n this.#timers.clearAll();\n if (visible && this.autoHideValue > 0) {\n this.#timers.set(() => this.#setVisible(false), this.autoHideValue);\n }\n }\n\n /** Reflects the visible state onto `aria-pressed` and `data-state`. */\n #reflect(visible: boolean): void {\n if (this.hasToggleTarget) {\n this.toggleTarget.setAttribute(\"aria-pressed\", visible ? \"true\" : \"false\");\n }\n this.element.setAttribute(\"data-state\", visible ? \"visible\" : \"hidden\");\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/controllers/password_reveal_controller.ts"],"names":[],"mappings":"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","file":"password_reveal_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** The longest delay `setTimeout` can hold; past it a delay folds to zero. */\nconst MAX_DELAY = 2 ** 31 - 1;\n\n/**\n * Headless password show/hide (unmask) toggle behavior.\n *\n * Markup contract (identifier: `stimeo--password-reveal`):\n * <div data-controller=\"stimeo--password-reveal\">\n * <input type=\"password\" aria-label=\"Password\"\n * data-stimeo--password-reveal-target=\"input\">\n * <button type=\"button\" aria-pressed=\"false\" aria-label=\"Show password\"\n * data-stimeo--password-reveal-target=\"toggle\"\n * data-action=\"stimeo--password-reveal#toggle\"></button>\n * </div>\n *\n * No dedicated APG pattern; this follows the toggle **Button** practice. The\n * accessible name stays state-independent (\"Show password\") while the pressed\n * state is conveyed by `aria-pressed`.\n *\n * `toggle` dispatches `{ visible: boolean }`.\n *\n * @remarks\n * Behavior only — icon rendering is the consumer's, keyed off `aria-pressed` /\n * `data-state` (`hidden` / `visible`). Flipping `input.type` can drop focus and\n * the caret, so when (and only when) the input was the focused element its focus\n * and selection are restored afterward; when the toggle button holds focus\n * (keyboard use) focus is left on the button.\n *\n * The hooks are derived from the input's `type`, so they are re-derived whenever\n * a target enters or leaves — a field or button swapped in by a Turbo Stream\n * describes the state it actually has rather than the one the server rendered.\n *\n * An optional `autoHide` re-masks after a delay. The delay is armed on connect as\n * well, so a controller that reconnects onto an already revealed field still owes\n * the re-mask it promised; a value that is not a delay `setTimeout` can hold falls\n * back rather than inverting into an immediate one. Revealing is never carried\n * into a Turbo snapshot: the field is masked again before the page is cached, so\n * returning to it does not put a credential back on screen.\n */\nexport class PasswordRevealController extends Controller<HTMLElement> {\n static override targets = [\"input\", \"toggle\"];\n static override values = {\n autoHide: { type: Number, default: 0 },\n };\n static actions = [\"toggle\"] as const;\n static events = [\"toggle\"] as const;\n\n declare readonly inputTarget: HTMLInputElement;\n declare readonly toggleTarget: HTMLElement;\n declare readonly hasInputTarget: boolean;\n declare readonly hasToggleTarget: boolean;\n\n declare autoHideValue: number;\n\n /** Auto re-mask timer; torn down on disconnect. */\n #timers = new SafeTimeout();\n\n /**\n * Whether this controller is between `connect()` and `disconnect()`.\n *\n * Target callbacks outlive the controller: Stimulus stops the target observer\n * after `disconnect()`, so a field leaving after teardown still reaches\n * {@link PasswordRevealController.inputTargetDisconnected}. Arming from there\n * would put a timer back that nothing will clear. The element staying in the\n * document does not answer this — unloading the controller leaves it there.\n */\n #connected = false;\n\n /** Masks the field before Turbo copies the page into its snapshot. */\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindToMasked());\n\n override connect(): void {\n this.#connected = true;\n const visible = this.#isVisible;\n this.#reflect(visible);\n this.#beforeCache.activate();\n // Reconnecting onto an already revealed field inherits the promise the\n // declaration made: without re-arming, the re-mask would never arrive.\n this.#arm(visible);\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#timers.clearAll();\n this.#beforeCache.deactivate();\n }\n\n /** Re-derives the hooks and the re-mask for a field swapped in after connect. */\n inputTargetConnected(): void {\n const visible = this.#connected && this.#isVisible;\n this.#reflect(visible);\n this.#arm(visible);\n }\n\n /**\n * Re-derives from whatever field is left rather than assuming none is. A swap\n * delivers this callback next to the arrival in either order, so a revealed\n * replacement that answered \"masked\" here would be described as hidden while\n * showing the password, and would carry no re-mask.\n *\n * Teardown is the one case with nothing to derive: target callbacks run after\n * `disconnect()`, so the field is still revealed and re-arming from it would\n * outlive the controller. A detached root is the signal to stand down.\n */\n inputTargetDisconnected(): void {\n const visible = this.#connected && this.element.isConnected && this.#isVisible;\n this.#reflect(visible);\n this.#arm(visible);\n }\n\n /** Re-derives the pressed state for a button swapped in after connect. */\n toggleTargetConnected(): void {\n this.#reflect(this.#isVisible);\n }\n\n /** Toggles the input between masked and revealed. Bound via `data-action`. */\n toggle(): void {\n this.#setVisible(!this.#isVisible);\n }\n\n /** Whether the input is currently revealed (`type=\"text\"`). */\n get #isVisible(): boolean {\n return this.hasInputTarget && this.inputTarget.type === \"text\";\n }\n\n /** Switches the masked/revealed state, preserving focus and caret. */\n #setVisible(visible: boolean): void {\n if (!this.hasInputTarget) return;\n const input = this.inputTarget;\n\n // Only the input's *own* focus is restored across the type change; if the\n // toggle button (keyboard) holds focus, it is left untouched.\n const restoreInputFocus = document.activeElement === input;\n const selectionStart = input.selectionStart;\n const selectionEnd = input.selectionEnd;\n\n input.type = visible ? \"text\" : \"password\";\n\n if (restoreInputFocus) {\n input.focus();\n // `selectionStart` / `selectionEnd` are `number | null` (null for input\n // types that don't expose a selection); only restore when both are present.\n if (selectionStart !== null && selectionEnd !== null) {\n try {\n input.setSelectionRange(selectionStart, selectionEnd);\n } catch {\n // Some input types reject selection access; focus alone is enough.\n }\n }\n }\n\n this.#reflect(visible);\n this.dispatch(\"toggle\", { detail: { visible } });\n this.#arm(visible);\n }\n\n /** Schedules the auto re-mask for a revealed field, replacing any pending one. */\n #arm(visible: boolean): void {\n this.#timers.clearAll();\n const delay = this.#autoHideDelay;\n if (visible && delay > 0) {\n this.#timers.set(() => this.#setVisible(false), delay);\n }\n }\n\n /**\n * The auto re-mask delay, held to what `setTimeout` can carry. Past that limit\n * a delay folds to zero, turning \"keep it showing\" into \"hide it at once\" —\n * the opposite of what the declaration asked for. A value that is no delay at\n * all stays out of the positive range {@link PasswordRevealController.#arm}\n * requires, so it schedules nothing.\n */\n get #autoHideDelay(): number {\n return Math.min(this.autoHideValue, MAX_DELAY);\n }\n\n /**\n * Returns the field to masked before Turbo copies the page. Silent and\n * focus-free: the page is about to be frozen, so there is no one to tell and\n * nowhere for focus to go.\n */\n #rewindToMasked(): void {\n this.#timers.clearAll();\n if (!this.hasInputTarget) return;\n this.inputTarget.type = \"password\";\n this.#reflect(false);\n }\n\n /** Reflects the visible state onto `aria-pressed` and `data-state`. */\n #reflect(visible: boolean): void {\n if (this.hasToggleTarget) {\n this.toggleTarget.setAttribute(\"aria-pressed\", visible ? \"true\" : \"false\");\n }\n this.element.setAttribute(\"data-state\", visible ? \"visible\" : \"hidden\");\n }\n}\n"]}
|
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Event shape {@link PasswordStrengthController.setScore} accepts: an action param
|
|
5
|
+
* `score` or a `detail.score`. Both are typed `number | string` because, while
|
|
6
|
+
* Stimulus coerces numeric action params to numbers, a `CustomEvent` (or a
|
|
7
|
+
* non-numeric-looking param) may carry a string.
|
|
8
|
+
*/
|
|
9
|
+
type SetScoreEvent = Event & {
|
|
10
|
+
params?: {
|
|
11
|
+
score?: number | string;
|
|
12
|
+
};
|
|
13
|
+
detail?: {
|
|
14
|
+
score?: number | string;
|
|
15
|
+
};
|
|
16
|
+
};
|
|
3
17
|
/**
|
|
4
18
|
* Headless password-strength behavior: scores the field with a lightweight
|
|
5
|
-
* zero-dependency heuristic and drives a meter plus
|
|
19
|
+
* zero-dependency heuristic and drives a meter plus a visible level readout. No
|
|
6
20
|
* dedicated APG pattern; the meter display follows {@link MeterController}.
|
|
7
21
|
*
|
|
8
22
|
* Markup contract (identifier: `stimeo--password-strength`):
|
|
9
|
-
* <div data-controller="stimeo--password-strength"
|
|
23
|
+
* <div data-controller="stimeo--password-strength"
|
|
24
|
+
* data-stimeo--password-strength-announce-text-value="Password strength: {level}">
|
|
10
25
|
* <input type="password" data-stimeo--password-strength-target="input"
|
|
11
26
|
* data-action="input->stimeo--password-strength#evaluate" aria-describedby="pw">
|
|
12
27
|
* <div data-stimeo--password-strength-target="meter" role="meter"
|
|
13
|
-
* aria-valuemin="0" aria-valuemax="4"></div>
|
|
14
|
-
* <span id="pw" data-stimeo--password-strength-target="label"
|
|
28
|
+
* aria-label="Password strength" aria-valuemin="0" aria-valuemax="4"></div>
|
|
29
|
+
* <span id="pw" data-stimeo--password-strength-target="label"></span>
|
|
15
30
|
* </div>
|
|
16
31
|
*
|
|
17
32
|
* On each input the controller scores the password (length milestones + character
|
|
@@ -20,22 +35,30 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
20
35
|
* `--stimeo--password-strength`, and (when `minScore` is set) `data-below-min`,
|
|
21
36
|
* and writes the level label into the label target.
|
|
22
37
|
*
|
|
23
|
-
* `change`
|
|
38
|
+
* `change` and `reconcile` dispatch `{ score, level, max, meetsMin }`.
|
|
24
39
|
*
|
|
25
40
|
* @remarks
|
|
26
41
|
* Behavior only — the meter/bar look is the consumer's, keyed off the data hooks.
|
|
27
42
|
* `data-strength` is one of the fixed {@link STRENGTH_BANDS} (not the localizable
|
|
28
43
|
* `levels` text), so consumers can style by it regardless of locale; the visible
|
|
29
|
-
* label receives the matching `levels` entry.
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
44
|
+
* label receives the matching `levels` entry. Every visible and non-text output
|
|
45
|
+
* updates on the keystroke that caused it, so the readout never trails the meter.
|
|
46
|
+
*
|
|
47
|
+
* Assistive notification is opt-in and i18n-neutral: `announceText` accepts
|
|
48
|
+
* `{level}`, `{score}`, `{max}` and `{band}`, and one settled message is handed to
|
|
49
|
+
* the page's shared `stimeo--announcer` after a short debounce. Only a level the
|
|
50
|
+
* reader has not already heard is sent, so typing inside one level stays quiet.
|
|
51
|
+
* The label target is plain visible output and is not itself a live region.
|
|
52
|
+
*
|
|
53
|
+
* The score is a pure function of the field value (no module-scope state), so
|
|
54
|
+
* `connect()` re-evaluates idempotently; the initial reflection never dispatches
|
|
55
|
+
* or announces. Runtime Value and target changes repaint on a microtask and
|
|
56
|
+
* dispatch `reconcile` only when the public derived state actually moves. Because
|
|
57
|
+
* the field value is not part of a Turbo snapshot, everything derived from it is
|
|
58
|
+
* rewound on `turbo:before-cache` — silently, since `connect()` derives it again
|
|
59
|
+
* after a restore. The estimator is intentionally not a dictionary/zxcvbn-grade
|
|
60
|
+
* one (kept zero-dep); a consumer that needs a stronger one computes the score
|
|
61
|
+
* itself and hands it over through {@link setScore}.
|
|
39
62
|
*/
|
|
40
63
|
declare class PasswordStrengthController extends Controller<HTMLElement> {
|
|
41
64
|
#private;
|
|
@@ -49,9 +72,13 @@ declare class PasswordStrengthController extends Controller<HTMLElement> {
|
|
|
49
72
|
type: StringConstructor;
|
|
50
73
|
default: string;
|
|
51
74
|
};
|
|
75
|
+
announceText: {
|
|
76
|
+
type: StringConstructor;
|
|
77
|
+
default: string;
|
|
78
|
+
};
|
|
52
79
|
};
|
|
53
|
-
static actions: readonly ["evaluate"];
|
|
54
|
-
static events: readonly ["change"];
|
|
80
|
+
static actions: readonly ["evaluate", "setScore"];
|
|
81
|
+
static events: readonly ["change", "reconcile"];
|
|
55
82
|
readonly inputTarget: HTMLInputElement;
|
|
56
83
|
readonly meterTarget: HTMLElement;
|
|
57
84
|
readonly labelTarget: HTMLElement;
|
|
@@ -60,10 +87,37 @@ declare class PasswordStrengthController extends Controller<HTMLElement> {
|
|
|
60
87
|
readonly hasLabelTarget: boolean;
|
|
61
88
|
minScoreValue: number;
|
|
62
89
|
levelsValue: string;
|
|
90
|
+
announceTextValue: string;
|
|
91
|
+
/** Reflects the current DOM state and opens the reconciliation window. */
|
|
63
92
|
connect(): void;
|
|
93
|
+
/** Releases the reconciliation window, the cache subscription and the debounce. */
|
|
64
94
|
disconnect(): void;
|
|
65
95
|
/** Re-evaluates strength from the input. Bound via `data-action` (`input`). */
|
|
66
96
|
evaluate(): void;
|
|
97
|
+
/**
|
|
98
|
+
* Adopts a score computed outside the built-in heuristic, clamped into the
|
|
99
|
+
* declared scale. An unreadable value leaves the current score standing, and
|
|
100
|
+
* the next `evaluate` hands scoring back to the heuristic.
|
|
101
|
+
*/
|
|
102
|
+
setScore(event: SetScoreEvent): void;
|
|
103
|
+
/** Re-reads the scale when application code or a Turbo morph changes `levels`. */
|
|
104
|
+
levelsValueChanged(): void;
|
|
105
|
+
/** Repaints when application code or a Turbo morph changes `minScore`. */
|
|
106
|
+
minScoreValueChanged(): void;
|
|
107
|
+
/** Repaints when application code or a Turbo morph changes `announceText`. */
|
|
108
|
+
announceTextValueChanged(): void;
|
|
109
|
+
/** Scores the field added or replaced at runtime. */
|
|
110
|
+
inputTargetConnected(): void;
|
|
111
|
+
/** Falls back to the pristine state after the scored field is removed. */
|
|
112
|
+
inputTargetDisconnected(): void;
|
|
113
|
+
/** Syncs a meter added or replaced at runtime, which carries no value yet. */
|
|
114
|
+
meterTargetConnected(): void;
|
|
115
|
+
/** Repaints the remaining output after a meter is removed. */
|
|
116
|
+
meterTargetDisconnected(): void;
|
|
117
|
+
/** Fills a readout added or replaced at runtime. */
|
|
118
|
+
labelTargetConnected(): void;
|
|
119
|
+
/** Repaints the remaining output after a readout is removed. */
|
|
120
|
+
labelTargetDisconnected(): void;
|
|
67
121
|
}
|
|
68
122
|
|
|
69
123
|
export { PasswordStrengthController };
|