stimeo-ui 0.5.0 → 0.7.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 +178 -0
- package/dist/controllers/alert_dialog_controller.d.ts +2 -0
- package/dist/controllers/alert_dialog_controller.js +75 -4
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.d.ts +1 -3
- package/dist/controllers/aspect_ratio_controller.js +19 -11
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +2 -0
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.d.ts +36 -15
- package/dist/controllers/avatar_controller.js +237 -40
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +2 -0
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +2 -0
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +2 -0
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +12 -3
- package/dist/controllers/carousel_controller.js +85 -9
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +52 -20
- package/dist/controllers/character_counter_controller.js +338 -63
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +34 -5
- package/dist/controllers/checkbox_controller.js +136 -25
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +10 -1
- package/dist/controllers/color_picker_controller.js +35 -9
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +2 -0
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +2 -0
- package/dist/controllers/command_palette_controller.js +75 -4
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +42 -14
- package/dist/controllers/conditional_fields_controller.js +345 -51
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +2 -0
- package/dist/controllers/confirm_controller.js +75 -4
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +2 -0
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +4 -0
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +2 -0
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +3 -0
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +23 -4
- package/dist/controllers/date_range_picker_controller.js +157 -30
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.js +75 -4
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +56 -24
- package/dist/controllers/direct_upload_controller.js +201 -45
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.d.ts +14 -6
- package/dist/controllers/dirty_form_controller.js +192 -29
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +2 -0
- package/dist/controllers/dismissible_controller.js +83 -18
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.js +75 -4
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.d.ts +2 -0
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +9 -1
- package/dist/controllers/file_dropzone_controller.js +26 -3
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.d.ts +2 -0
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +4 -0
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +4 -3
- package/dist/controllers/focus_controller.js +75 -4
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.d.ts +50 -10
- package/dist/controllers/form_field_controller.js +280 -62
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.d.ts +10 -8
- package/dist/controllers/form_validation_controller.js +208 -83
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +2 -0
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +2 -0
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +2 -2
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +5 -3
- package/dist/controllers/idle_controller.js +27 -5
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +2 -0
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +2 -0
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +2 -0
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +2 -0
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +2 -0
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.js +5 -3
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +2 -0
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +48 -12
- package/dist/controllers/multi_select_controller.js +460 -151
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +2 -0
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +2 -0
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -6
- package/dist/controllers/number_input_controller.js +317 -51
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +3 -0
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +2 -0
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +2 -0
- package/dist/controllers/overflow_menu_controller.js +6 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +2 -0
- package/dist/controllers/pagination_controller.js +35 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +2 -0
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +5 -3
- package/dist/controllers/password_strength_controller.js +20 -2
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +33 -19
- package/dist/controllers/persist_controller.js +449 -120
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +3 -0
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +2 -2
- package/dist/controllers/popover_controller.js +77 -4
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +3 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +2 -0
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +3 -0
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +44 -15
- package/dist/controllers/radio_group_controller.js +540 -56
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +38 -31
- package/dist/controllers/rating_controller.js +276 -89
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +2 -0
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/resizable_controller.d.ts +2 -0
- package/dist/controllers/resizable_controller.js +33 -0
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +6 -0
- package/dist/controllers/roving_controller.js +60 -5
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +25 -11
- package/dist/controllers/scroll_area_controller.js +557 -125
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +2 -0
- package/dist/controllers/scroll_visibility_controller.js +33 -0
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +2 -0
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +54 -11
- package/dist/controllers/separator_controller.js +354 -38
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.js +83 -10
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -0
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +2 -0
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +2 -0
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +2 -0
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +2 -0
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +2 -0
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +2 -0
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +2 -0
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +94 -38
- package/dist/controllers/submit_once_controller.js +399 -121
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +2 -0
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +43 -11
- package/dist/controllers/tags_input_controller.js +356 -120
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +2 -0
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +2 -0
- package/dist/controllers/theme_controller.js +8 -6
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +44 -13
- package/dist/controllers/time_picker_controller.js +296 -107
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +4 -0
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +41 -13
- package/dist/controllers/toggle_group_controller.js +378 -55
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +5 -3
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +2 -2
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +4 -3
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +2 -0
- package/dist/controllers/tree_view_controller.js +7 -4
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +4963 -1581
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +123 -6
- package/dist/inspector/cli.js +212 -18
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +274 -50
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +15 -15
- package/dist/inspector/manifest.json +602 -80
- package/dist/positioning/index.d.ts +4 -2
- package/dist/positioning/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -2,6 +2,64 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
2
2
|
|
|
3
3
|
// src/controllers/persist_controller.ts
|
|
4
4
|
|
|
5
|
+
// src/utils/microtask_coalescer.ts
|
|
6
|
+
var MicrotaskCoalescer = class {
|
|
7
|
+
#run;
|
|
8
|
+
#queued = false;
|
|
9
|
+
#active = false;
|
|
10
|
+
#generation = 0;
|
|
11
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
12
|
+
constructor(run) {
|
|
13
|
+
this.#run = run;
|
|
14
|
+
}
|
|
15
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
16
|
+
activate() {
|
|
17
|
+
this.#active = true;
|
|
18
|
+
}
|
|
19
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
20
|
+
cancel() {
|
|
21
|
+
this.#active = false;
|
|
22
|
+
this.#queued = false;
|
|
23
|
+
this.#generation += 1;
|
|
24
|
+
}
|
|
25
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
26
|
+
schedule() {
|
|
27
|
+
if (!this.#active || this.#queued) return;
|
|
28
|
+
this.#queued = true;
|
|
29
|
+
const generation = this.#generation;
|
|
30
|
+
queueMicrotask(() => {
|
|
31
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
32
|
+
this.#queued = false;
|
|
33
|
+
this.#run();
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// src/utils/safe_storage.ts
|
|
39
|
+
function readLocalStorage(key) {
|
|
40
|
+
try {
|
|
41
|
+
return { ok: true, value: window.localStorage.getItem(key) };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
return { ok: false, error };
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function writeLocalStorage(key, value) {
|
|
47
|
+
try {
|
|
48
|
+
window.localStorage.setItem(key, value);
|
|
49
|
+
return { ok: true, value: void 0 };
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return { ok: false, error };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function removeLocalStorage(key) {
|
|
55
|
+
try {
|
|
56
|
+
window.localStorage.removeItem(key);
|
|
57
|
+
return { ok: true, value: void 0 };
|
|
58
|
+
} catch (error) {
|
|
59
|
+
return { ok: false, error };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
5
63
|
// src/utils/safe_timeout.ts
|
|
6
64
|
var TimerRegistry = class {
|
|
7
65
|
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
@@ -55,203 +113,474 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
55
113
|
}
|
|
56
114
|
};
|
|
57
115
|
|
|
116
|
+
// src/utils/string_list.ts
|
|
117
|
+
function parseStringList(raw, fallback = []) {
|
|
118
|
+
const text = raw.trim();
|
|
119
|
+
if (text.length === 0) return [...fallback];
|
|
120
|
+
let parsed;
|
|
121
|
+
try {
|
|
122
|
+
parsed = JSON.parse(text);
|
|
123
|
+
} catch {
|
|
124
|
+
return [...fallback];
|
|
125
|
+
}
|
|
126
|
+
if (!Array.isArray(parsed)) return [...fallback];
|
|
127
|
+
return parsed.filter((entry) => typeof entry === "string");
|
|
128
|
+
}
|
|
129
|
+
|
|
58
130
|
// src/controllers/persist_controller.ts
|
|
59
131
|
var NON_VALUE_TYPES = /* @__PURE__ */ new Set(["file", "submit", "reset", "button", "image"]);
|
|
132
|
+
var SENSITIVE_TYPES = /* @__PURE__ */ new Set(["password"]);
|
|
133
|
+
var DEFAULT_EXCLUDE = ["authenticity_token", "_method", "utf8"];
|
|
60
134
|
var STORAGE_PREFIX = "stimeo--persist:";
|
|
61
135
|
var OCCURRENCE_SEP = "\0";
|
|
136
|
+
var OWNERSHIP_ATTRIBUTE = "data-controller";
|
|
137
|
+
var DEFAULT_DEBOUNCE = 400;
|
|
138
|
+
var isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
139
|
+
var isPersistValue = (value) => typeof value === "string" || typeof value === "boolean" || Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
140
|
+
var parsePayload = (raw) => {
|
|
141
|
+
let parsed;
|
|
142
|
+
try {
|
|
143
|
+
parsed = JSON.parse(raw);
|
|
144
|
+
} catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
if (!isRecord(parsed) || parsed.version !== 1 || !Array.isArray(parsed.fields)) return null;
|
|
148
|
+
const fields = [];
|
|
149
|
+
const keys = /* @__PURE__ */ new Set();
|
|
150
|
+
for (const candidate of parsed.fields) {
|
|
151
|
+
if (!isRecord(candidate)) return null;
|
|
152
|
+
const { key, value } = candidate;
|
|
153
|
+
if (typeof key !== "string" || key.length === 0 || keys.has(key)) return null;
|
|
154
|
+
if (!isPersistValue(value)) return null;
|
|
155
|
+
keys.add(key);
|
|
156
|
+
fields.push({ key, value });
|
|
157
|
+
}
|
|
158
|
+
return { version: 1, fields };
|
|
159
|
+
};
|
|
160
|
+
var isPersistField = (node) => node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement || node instanceof HTMLSelectElement;
|
|
62
161
|
var PersistController = class extends Controller {
|
|
63
162
|
static targets = ["field"];
|
|
64
163
|
static values = {
|
|
65
164
|
key: { type: String, default: "" },
|
|
66
|
-
debounce: { type: Number, default:
|
|
67
|
-
|
|
165
|
+
debounce: { type: Number, default: DEFAULT_DEBOUNCE },
|
|
166
|
+
// A malformed JSON list must fall back without aborting Stimulus connection.
|
|
167
|
+
exclude: { type: String, default: "" },
|
|
68
168
|
clearOn: { type: String, default: "" }
|
|
69
169
|
};
|
|
70
170
|
static actions = ["clear"];
|
|
71
|
-
static events = ["restore", "save", "clear"];
|
|
171
|
+
static events = ["restore", "save", "clear", "error"];
|
|
72
172
|
#timeouts = new SafeTimeout();
|
|
73
|
-
#
|
|
74
|
-
#
|
|
173
|
+
#restoreDynamic = new MicrotaskCoalescer(() => this.#restoreDynamicFields());
|
|
174
|
+
#observer = new MutationObserver((records) => this.#onMutations(records));
|
|
175
|
+
#pendingSave = null;
|
|
176
|
+
#connected = false;
|
|
177
|
+
#logicalKey = null;
|
|
178
|
+
#payload = null;
|
|
179
|
+
#registeredClearOn = null;
|
|
180
|
+
#excluded = DEFAULT_EXCLUDE;
|
|
181
|
+
#knownFields = /* @__PURE__ */ new WeakSet();
|
|
182
|
+
#forcedRestore = /* @__PURE__ */ new Set();
|
|
183
|
+
#onInput = (event) => {
|
|
184
|
+
const field = event.target;
|
|
185
|
+
if (!isPersistField(field) || !this.#ownsField(field)) return;
|
|
186
|
+
if (this.hasFieldTarget && !this.fieldTargets.includes(field)) return;
|
|
187
|
+
if (this.#keyOf(field) === null || !this.#persistable(field, this.#excluded)) return;
|
|
75
188
|
this.#scheduleSave();
|
|
76
189
|
};
|
|
77
190
|
#onClearEvent = () => {
|
|
78
191
|
this.clear();
|
|
79
192
|
};
|
|
80
193
|
connect() {
|
|
81
|
-
if (this.#
|
|
82
|
-
this.#
|
|
194
|
+
if (this.#connected) return;
|
|
195
|
+
this.#connected = true;
|
|
196
|
+
this.#logicalKey = this.#resolveLogicalKey();
|
|
197
|
+
this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
|
|
198
|
+
this.#knownFields = /* @__PURE__ */ new WeakSet();
|
|
199
|
+
this.#forcedRestore.clear();
|
|
200
|
+
this.element.removeAttribute("data-persist-restored");
|
|
83
201
|
this.element.addEventListener("input", this.#onInput);
|
|
84
202
|
this.element.addEventListener("change", this.#onInput);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
203
|
+
this.#syncClearOn();
|
|
204
|
+
this.#restoreDynamic.activate();
|
|
205
|
+
this.#observer.observe(this.element, {
|
|
206
|
+
attributes: true,
|
|
207
|
+
attributeFilter: [
|
|
208
|
+
"checked",
|
|
209
|
+
"data-controller",
|
|
210
|
+
"data-stimeo--persist-target",
|
|
211
|
+
"id",
|
|
212
|
+
"multiple",
|
|
213
|
+
"name",
|
|
214
|
+
"selected",
|
|
215
|
+
"type",
|
|
216
|
+
"value"
|
|
217
|
+
],
|
|
218
|
+
childList: true,
|
|
219
|
+
subtree: true
|
|
220
|
+
});
|
|
221
|
+
this.#loadActiveDraft();
|
|
88
222
|
}
|
|
89
223
|
disconnect() {
|
|
224
|
+
this.#flushPendingSave();
|
|
225
|
+
this.#connected = false;
|
|
90
226
|
this.element.removeEventListener("input", this.#onInput);
|
|
91
227
|
this.element.removeEventListener("change", this.#onInput);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
this.#timeouts.clear(this.#saveId);
|
|
97
|
-
this.#saveId = null;
|
|
98
|
-
this.#write();
|
|
99
|
-
}
|
|
228
|
+
this.#unbindClearOn();
|
|
229
|
+
this.#observer.disconnect();
|
|
230
|
+
this.#restoreDynamic.cancel();
|
|
231
|
+
this.#forcedRestore.clear();
|
|
100
232
|
this.#timeouts.clearAll();
|
|
233
|
+
this.element.removeAttribute("data-persist-restored");
|
|
234
|
+
this.#payload = null;
|
|
101
235
|
}
|
|
102
|
-
/** Drops the
|
|
236
|
+
/** Drops the active draft after storage confirms the removal. */
|
|
103
237
|
clear() {
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
238
|
+
const logicalKey = this.#logicalKey ?? this.#resolveLogicalKey();
|
|
239
|
+
if (logicalKey === null) return;
|
|
240
|
+
this.#cancelPendingSave();
|
|
241
|
+
const result = removeLocalStorage(this.#storageKey(logicalKey));
|
|
242
|
+
if (!result.ok) {
|
|
243
|
+
this.#dispatchError(logicalKey, "remove", "unavailable");
|
|
244
|
+
return;
|
|
109
245
|
}
|
|
110
|
-
this.#
|
|
246
|
+
this.#payload = null;
|
|
111
247
|
this.element.removeAttribute("data-persist-restored");
|
|
112
|
-
this.dispatch("clear", { detail: { key:
|
|
248
|
+
this.dispatch("clear", { detail: { key: logicalKey } });
|
|
249
|
+
}
|
|
250
|
+
/** Rebinds the external clear event when its Value changes. */
|
|
251
|
+
clearOnValueChanged() {
|
|
252
|
+
if (this.#connected) this.#syncClearOn();
|
|
253
|
+
}
|
|
254
|
+
/** Switches storage namespaces without writing old edits under the new key. */
|
|
255
|
+
keyValueChanged() {
|
|
256
|
+
if (this.#connected) this.#switchLogicalKey();
|
|
257
|
+
}
|
|
258
|
+
/** Reschedules a pending write against the current debounce delay. */
|
|
259
|
+
debounceValueChanged() {
|
|
260
|
+
if (this.#connected && this.#pendingSave !== null) this.#scheduleSave();
|
|
261
|
+
}
|
|
262
|
+
/** Restores every field the new exclusion list makes eligible. */
|
|
263
|
+
excludeValueChanged() {
|
|
264
|
+
const previous = this.#excluded;
|
|
265
|
+
this.#excluded = parseStringList(this.excludeValue, DEFAULT_EXCLUDE);
|
|
266
|
+
if (!this.#connected) return;
|
|
267
|
+
for (const field of this.#candidateFields()) {
|
|
268
|
+
if (this.#persistable(field, this.#excluded) && !this.#persistable(field, previous)) {
|
|
269
|
+
this.#forcedRestore.add(field);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
this.#restoreDynamic.schedule();
|
|
113
273
|
}
|
|
114
274
|
/** Schedules a debounced save. */
|
|
115
275
|
#scheduleSave() {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
276
|
+
const logicalKey = this.#logicalKey;
|
|
277
|
+
if (logicalKey === null) return;
|
|
278
|
+
this.#cancelPendingSave();
|
|
279
|
+
const id = this.#timeouts.set(() => {
|
|
280
|
+
this.#pendingSave = null;
|
|
281
|
+
this.#save(logicalKey);
|
|
282
|
+
}, this.#debounceDelay);
|
|
283
|
+
this.#pendingSave = { id, logicalKey };
|
|
284
|
+
}
|
|
285
|
+
/** Cancels the currently pending save, if any. */
|
|
286
|
+
#cancelPendingSave() {
|
|
287
|
+
const pending = this.#pendingSave;
|
|
288
|
+
if (pending === null) return;
|
|
289
|
+
this.#timeouts.clear(pending.id);
|
|
290
|
+
this.#pendingSave = null;
|
|
291
|
+
}
|
|
292
|
+
/** Flushes a pending edit through the same success/error event path as a timer. */
|
|
293
|
+
#flushPendingSave() {
|
|
294
|
+
const pending = this.#pendingSave;
|
|
295
|
+
if (pending === null) return;
|
|
296
|
+
this.#cancelPendingSave();
|
|
297
|
+
this.#save(pending.logicalKey);
|
|
298
|
+
}
|
|
299
|
+
/** Writes the active payload and emits `save` only after storage succeeds. */
|
|
300
|
+
#save(logicalKey) {
|
|
301
|
+
const payload = this.#serialize();
|
|
302
|
+
const result = writeLocalStorage(this.#storageKey(logicalKey), JSON.stringify(payload));
|
|
303
|
+
if (!result.ok) {
|
|
304
|
+
this.#dispatchError(logicalKey, "write", "unavailable");
|
|
305
|
+
return false;
|
|
306
|
+
}
|
|
307
|
+
this.#payload = payload;
|
|
308
|
+
this.dispatch("save", { detail: { key: logicalKey } });
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
/** Serializes every persistable field into a versioned, typed payload. */
|
|
312
|
+
#serialize() {
|
|
313
|
+
const fields = [];
|
|
314
|
+
for (const { field, key } of this.#fieldEntries()) {
|
|
133
315
|
if (field instanceof HTMLInputElement && field.type === "checkbox") {
|
|
134
|
-
|
|
316
|
+
fields.push({ key, value: field.checked });
|
|
135
317
|
} else if (field instanceof HTMLInputElement && field.type === "radio") {
|
|
136
|
-
if (field.checked)
|
|
318
|
+
if (field.checked) fields.push({ key, value: field.value });
|
|
137
319
|
} else if (field instanceof HTMLSelectElement && field.multiple) {
|
|
138
|
-
|
|
320
|
+
fields.push({
|
|
321
|
+
key,
|
|
322
|
+
value: Array.from(field.selectedOptions).map((option) => option.value)
|
|
323
|
+
});
|
|
139
324
|
} else {
|
|
140
|
-
|
|
325
|
+
fields.push({ key, value: field.value });
|
|
141
326
|
}
|
|
142
327
|
}
|
|
143
|
-
|
|
144
|
-
}
|
|
145
|
-
/**
|
|
146
|
-
#
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
} catch {
|
|
328
|
+
return { version: 1, fields };
|
|
329
|
+
}
|
|
330
|
+
/** Reads, validates, and restores the active namespace. */
|
|
331
|
+
#loadActiveDraft() {
|
|
332
|
+
this.element.removeAttribute("data-persist-restored");
|
|
333
|
+
this.#payload = null;
|
|
334
|
+
this.#knownFields = /* @__PURE__ */ new WeakSet();
|
|
335
|
+
this.#forcedRestore.clear();
|
|
336
|
+
const logicalKey = this.#logicalKey;
|
|
337
|
+
if (logicalKey === null) {
|
|
338
|
+
this.#markCurrentFieldsKnown();
|
|
155
339
|
return;
|
|
156
340
|
}
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
this.#
|
|
161
|
-
|
|
341
|
+
const result = readLocalStorage(this.#storageKey(logicalKey));
|
|
342
|
+
if (!result.ok) {
|
|
343
|
+
this.#dispatchError(logicalKey, "read", "unavailable");
|
|
344
|
+
this.#markCurrentFieldsKnown();
|
|
345
|
+
return;
|
|
162
346
|
}
|
|
163
|
-
if (
|
|
347
|
+
if (result.value === null) {
|
|
348
|
+
this.#markCurrentFieldsKnown();
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const payload = parsePayload(result.value);
|
|
352
|
+
if (payload === null) {
|
|
353
|
+
const removal = removeLocalStorage(this.#storageKey(logicalKey));
|
|
354
|
+
this.#dispatchError(logicalKey, "read", "invalid-payload");
|
|
355
|
+
if (!removal.ok) this.#dispatchError(logicalKey, "remove", "unavailable");
|
|
356
|
+
this.#markCurrentFieldsKnown();
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
this.#payload = payload;
|
|
360
|
+
this.#restoreEntries(null);
|
|
361
|
+
}
|
|
362
|
+
/** Restores fields inserted or made eligible after the initial connection. */
|
|
363
|
+
#restoreDynamicFields() {
|
|
364
|
+
const selected = new Set(this.#forcedRestore);
|
|
365
|
+
this.#forcedRestore.clear();
|
|
366
|
+
for (const { field } of this.#fieldEntries()) {
|
|
367
|
+
if (!this.#knownFields.has(field)) selected.add(field);
|
|
368
|
+
}
|
|
369
|
+
this.#restoreEntries(selected);
|
|
370
|
+
}
|
|
371
|
+
/** Applies the active payload to all fields, or only the supplied candidates. */
|
|
372
|
+
#restoreEntries(selected) {
|
|
373
|
+
const entries = this.#fieldEntries();
|
|
374
|
+
for (const { field } of entries) this.#knownFields.add(field);
|
|
375
|
+
const payload = this.#payload;
|
|
376
|
+
if (payload === null) return;
|
|
377
|
+
const savedFields = new Map(payload.fields.map((field) => [field.key, field]));
|
|
378
|
+
const restoredKeys = /* @__PURE__ */ new Set();
|
|
379
|
+
const processedRadios = /* @__PURE__ */ new Set();
|
|
380
|
+
for (const entry of entries) {
|
|
381
|
+
if (selected !== null && !selected.has(entry.field)) continue;
|
|
382
|
+
const saved = savedFields.get(entry.key);
|
|
383
|
+
if (saved === void 0) continue;
|
|
384
|
+
const { value } = saved;
|
|
385
|
+
if (entry.field instanceof HTMLInputElement && entry.field.type === "radio") {
|
|
386
|
+
if (processedRadios.has(entry.key)) continue;
|
|
387
|
+
processedRadios.add(entry.key);
|
|
388
|
+
const radios = entries.filter(
|
|
389
|
+
({ key, field }) => key === entry.key && field instanceof HTMLInputElement && field.type === "radio"
|
|
390
|
+
).map(({ field }) => field);
|
|
391
|
+
if (typeof value !== "string" || !radios.some((radio) => radio.value === value)) continue;
|
|
392
|
+
for (const radio of radios) radio.checked = radio.value === value;
|
|
393
|
+
restoredKeys.add(entry.key);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (this.#applyValue(entry.field, value)) restoredKeys.add(entry.key);
|
|
397
|
+
}
|
|
398
|
+
if (restoredKeys.size > 0) {
|
|
399
|
+
this.#observer.takeRecords();
|
|
164
400
|
this.element.setAttribute("data-persist-restored", "true");
|
|
165
401
|
this.dispatch("restore", { detail: { key: this.#logicalKey } });
|
|
166
402
|
}
|
|
167
403
|
}
|
|
168
|
-
/**
|
|
404
|
+
/** Applies a type-compatible stored value without coercion. */
|
|
169
405
|
#applyValue(field, value) {
|
|
170
406
|
if (field instanceof HTMLInputElement && field.type === "checkbox") {
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
}
|
|
175
|
-
|
|
407
|
+
if (typeof value !== "boolean") return false;
|
|
408
|
+
field.checked = value;
|
|
409
|
+
return true;
|
|
410
|
+
}
|
|
411
|
+
if (field instanceof HTMLSelectElement && field.multiple) {
|
|
412
|
+
if (!Array.isArray(value)) return false;
|
|
413
|
+
const available = new Set(Array.from(field.options).map((option) => option.value));
|
|
414
|
+
const selected = value.filter((candidate) => available.has(candidate));
|
|
415
|
+
if (value.length > 0 && selected.length === 0) return false;
|
|
416
|
+
const selectedValues = new Set(selected);
|
|
176
417
|
for (const option of Array.from(field.options)) {
|
|
177
|
-
option.selected =
|
|
418
|
+
option.selected = selectedValues.has(option.value);
|
|
178
419
|
}
|
|
179
|
-
|
|
180
|
-
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
if (typeof value !== "string") return false;
|
|
423
|
+
if (field instanceof HTMLSelectElement) {
|
|
424
|
+
const exists = Array.from(field.options).some((option) => option.value === value);
|
|
425
|
+
if (!exists) return false;
|
|
181
426
|
}
|
|
427
|
+
field.value = value;
|
|
428
|
+
return true;
|
|
429
|
+
}
|
|
430
|
+
/** Every control this instance owns, before the exclusion list narrows them. */
|
|
431
|
+
#candidateFields() {
|
|
432
|
+
return this.hasFieldTarget ? this.fieldTargets.filter(isPersistField) : Array.from(this.element.querySelectorAll("input, textarea, select")).filter(
|
|
433
|
+
(field) => this.#ownsField(field)
|
|
434
|
+
);
|
|
182
435
|
}
|
|
183
436
|
/**
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* array text inputs) are disambiguated by DOM-order occurrence — the first keeps
|
|
187
|
-
* its plain `name`, later ones get a NUL-separated index suffix — so each is
|
|
188
|
-
* stored and restored individually instead of the last one clobbering the rest.
|
|
189
|
-
* Radios are the exception: a group intentionally shares one key (one value per
|
|
190
|
-
* name).
|
|
437
|
+
* Pairs persistable fields with DOM-order occurrence keys. Every HTML radio
|
|
438
|
+
* group (same name and form owner) occupies one shared occurrence slot.
|
|
191
439
|
*/
|
|
192
440
|
#fieldEntries() {
|
|
193
441
|
const entries = [];
|
|
194
|
-
const
|
|
195
|
-
for (const field of this.#
|
|
442
|
+
const eligible = [];
|
|
443
|
+
for (const field of this.#candidateFields()) {
|
|
196
444
|
const name = this.#keyOf(field);
|
|
197
|
-
if (name === null) continue;
|
|
445
|
+
if (name === null || !this.#persistable(field, this.#excluded)) continue;
|
|
446
|
+
eligible.push({ field, name });
|
|
447
|
+
}
|
|
448
|
+
const nextOccurrence = /* @__PURE__ */ new Map();
|
|
449
|
+
const radioSlots = /* @__PURE__ */ new Map();
|
|
450
|
+
const allocate = (name) => {
|
|
451
|
+
const occurrence = nextOccurrence.get(name) ?? 0;
|
|
452
|
+
nextOccurrence.set(name, occurrence + 1);
|
|
453
|
+
return occurrence === 0 ? name : `${name}${OCCURRENCE_SEP}${occurrence}`;
|
|
454
|
+
};
|
|
455
|
+
for (const { field, name } of eligible) {
|
|
198
456
|
if (field instanceof HTMLInputElement && field.type === "radio") {
|
|
199
|
-
|
|
457
|
+
const groups = radioSlots.get(name) ?? /* @__PURE__ */ new Map();
|
|
458
|
+
radioSlots.set(name, groups);
|
|
459
|
+
let key = groups.get(field.form);
|
|
460
|
+
if (key === void 0) {
|
|
461
|
+
key = allocate(name);
|
|
462
|
+
groups.set(field.form, key);
|
|
463
|
+
}
|
|
464
|
+
entries.push({ field, key });
|
|
200
465
|
continue;
|
|
201
466
|
}
|
|
202
|
-
|
|
203
|
-
occurrence.set(name, seen + 1);
|
|
204
|
-
entries.push({ field, key: seen === 0 ? name : `${name}${OCCURRENCE_SEP}${seen}` });
|
|
467
|
+
entries.push({ field, key: allocate(name) });
|
|
205
468
|
}
|
|
206
469
|
return entries;
|
|
207
470
|
}
|
|
208
|
-
/** The fields to persist: `field` targets, or the element's named controls. */
|
|
209
|
-
#fields() {
|
|
210
|
-
const candidates = this.hasFieldTarget ? this.fieldTargets : Array.from(this.element.querySelectorAll("input, textarea, select"));
|
|
211
|
-
return candidates.filter((field) => this.#persistable(field));
|
|
212
|
-
}
|
|
213
471
|
/** Whether a field carries a restorable, non-excluded value. */
|
|
214
|
-
#persistable(field) {
|
|
215
|
-
if (
|
|
216
|
-
|
|
217
|
-
if (
|
|
218
|
-
if (this.excludeValue.includes(type)) return false;
|
|
219
|
-
if (field.name.length > 0 && this.excludeValue.includes(field.name)) return false;
|
|
472
|
+
#persistable(field, excluded) {
|
|
473
|
+
if (NON_VALUE_TYPES.has(field.type) || SENSITIVE_TYPES.has(field.type)) return false;
|
|
474
|
+
if (excluded.includes(field.type)) return false;
|
|
475
|
+
if (field.name.length > 0 && excluded.includes(field.name)) return false;
|
|
220
476
|
return true;
|
|
221
477
|
}
|
|
222
478
|
/** A stable storage sub-key for a field (its name, else id). */
|
|
223
479
|
#keyOf(field) {
|
|
224
480
|
return field.name || field.id || null;
|
|
225
481
|
}
|
|
226
|
-
/**
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
482
|
+
/** Whether this instance, rather than a nested Persist host, owns a field. */
|
|
483
|
+
#ownsField(field) {
|
|
484
|
+
return field.closest('[data-controller~="stimeo--persist"]') === this.element;
|
|
485
|
+
}
|
|
486
|
+
/** Marks all currently eligible fields without applying a payload. */
|
|
487
|
+
#markCurrentFieldsKnown() {
|
|
488
|
+
for (const { field } of this.#fieldEntries()) this.#knownFields.add(field);
|
|
230
489
|
}
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
-
const
|
|
234
|
-
|
|
490
|
+
/** Switches namespace after flushing the old namespace's pending edit. */
|
|
491
|
+
#switchLogicalKey() {
|
|
492
|
+
const next = this.#resolveLogicalKey();
|
|
493
|
+
if (next === this.#logicalKey) return;
|
|
494
|
+
this.#flushPendingSave();
|
|
495
|
+
this.#logicalKey = next;
|
|
496
|
+
this.#loadActiveDraft();
|
|
235
497
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
498
|
+
/** Synchronizes the exact event name owned by the `clearOn` Value. */
|
|
499
|
+
#syncClearOn() {
|
|
500
|
+
const next = this.#validEventName(this.clearOnValue);
|
|
501
|
+
if (next === this.#registeredClearOn) return;
|
|
502
|
+
this.#unbindClearOn();
|
|
503
|
+
if (next === null) return;
|
|
504
|
+
this.element.addEventListener(next, this.#onClearEvent);
|
|
505
|
+
this.#registeredClearOn = next;
|
|
506
|
+
}
|
|
507
|
+
/** Removes the event listener using the name that was actually registered. */
|
|
508
|
+
#unbindClearOn() {
|
|
509
|
+
if (this.#registeredClearOn === null) return;
|
|
510
|
+
this.element.removeEventListener(this.#registeredClearOn, this.#onClearEvent);
|
|
511
|
+
this.#registeredClearOn = null;
|
|
512
|
+
}
|
|
513
|
+
/** Returns a non-whitespace event type, or null when the hook is disabled. */
|
|
514
|
+
#validEventName(value) {
|
|
515
|
+
const name = value.trim();
|
|
516
|
+
return name.length > 0 && !/\s/.test(name) ? name : null;
|
|
517
|
+
}
|
|
518
|
+
/** Collects dynamic controls and select-option changes into one restore pass. */
|
|
519
|
+
#onMutations(records) {
|
|
520
|
+
let rootIdChanged = false;
|
|
521
|
+
for (const record of records) {
|
|
522
|
+
if (record.type === "attributes") {
|
|
523
|
+
if (record.target === this.element && record.attributeName === "id") {
|
|
524
|
+
rootIdChanged = true;
|
|
525
|
+
}
|
|
526
|
+
this.#collectAttributeCandidate(record.target, record.attributeName);
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
for (const node of record.addedNodes) this.#collectRestoreCandidate(node);
|
|
530
|
+
}
|
|
531
|
+
if (rootIdChanged && this.keyValue.length === 0) this.#switchLogicalKey();
|
|
532
|
+
this.#restoreDynamic.schedule();
|
|
533
|
+
}
|
|
534
|
+
/**
|
|
535
|
+
* Adds the controls one mutated attribute can affect. Only an ownership change on a
|
|
536
|
+
* descendant moves fields between Persist hosts, so that is the single case worth a
|
|
537
|
+
* subtree sweep; every other observed attribute describes one control, and sweeping
|
|
538
|
+
* from its container would re-apply the stored draft over edits still being debounced.
|
|
539
|
+
*/
|
|
540
|
+
#collectAttributeCandidate(target, attributeName) {
|
|
541
|
+
if (attributeName === OWNERSHIP_ATTRIBUTE && target !== this.element) {
|
|
542
|
+
this.#collectRestoreCandidate(target);
|
|
543
|
+
return;
|
|
241
544
|
}
|
|
545
|
+
this.#collectControlCandidate(target);
|
|
242
546
|
}
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
547
|
+
/** Adds a native control, or the select that owns a mutated option. */
|
|
548
|
+
#collectControlCandidate(node) {
|
|
549
|
+
if (isPersistField(node)) {
|
|
550
|
+
this.#forcedRestore.add(node);
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (node instanceof HTMLOptionElement) {
|
|
554
|
+
const select = node.closest("select");
|
|
555
|
+
if (select) this.#forcedRestore.add(select);
|
|
247
556
|
}
|
|
248
557
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
558
|
+
/** Adds every control inside an inserted or newly owned subtree. */
|
|
559
|
+
#collectRestoreCandidate(node) {
|
|
560
|
+
this.#collectControlCandidate(node);
|
|
561
|
+
if (!(node instanceof Element)) return;
|
|
562
|
+
for (const field of node.querySelectorAll("input, textarea, select")) {
|
|
563
|
+
this.#forcedRestore.add(field);
|
|
253
564
|
}
|
|
254
565
|
}
|
|
566
|
+
/** Resolves the logical key from the Value, falling back to the host id. */
|
|
567
|
+
#resolveLogicalKey() {
|
|
568
|
+
const key = this.keyValue || this.element.id;
|
|
569
|
+
return key.length > 0 ? key : null;
|
|
570
|
+
}
|
|
571
|
+
/** Namespaces one logical key within localStorage. */
|
|
572
|
+
#storageKey(logicalKey) {
|
|
573
|
+
return `${STORAGE_PREFIX}${logicalKey}`;
|
|
574
|
+
}
|
|
575
|
+
/** Normalizes invalid debounce Values to the documented default. */
|
|
576
|
+
get #debounceDelay() {
|
|
577
|
+
const value = this.debounceValue;
|
|
578
|
+
return Number.isFinite(value) && value >= 0 ? value : DEFAULT_DEBOUNCE;
|
|
579
|
+
}
|
|
580
|
+
/** Dispatches one observable storage failure without exposing browser errors. */
|
|
581
|
+
#dispatchError(key, operation, reason) {
|
|
582
|
+
this.dispatch("error", { detail: { key, operation, reason } });
|
|
583
|
+
}
|
|
255
584
|
};
|
|
256
585
|
|
|
257
586
|
export { PersistController };
|