stimeo-ui 0.15.0 → 0.17.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +209 -0
- data/README.md +128 -1
- data/dist/cable/index.js +392 -417
- data/dist/controllers/accordion_controller.js +228 -28
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -191
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +295 -110
- data/dist/controllers/avatar_controller.js +172 -106
- data/dist/controllers/breadcrumb_controller.js +0 -129
- data/dist/controllers/bulk_select_controller.js +264 -53
- data/dist/controllers/calendar_controller.js +346 -180
- data/dist/controllers/carousel_controller.js +587 -300
- data/dist/controllers/character_counter_controller.js +271 -143
- data/dist/controllers/checkbox_controller.js +77 -75
- data/dist/controllers/clipboard_controller.js +187 -108
- data/dist/controllers/collapsible_controller.js +374 -108
- data/dist/controllers/color_picker_controller.js +183 -127
- data/dist/controllers/combobox_controller.js +426 -109
- data/dist/controllers/command_palette_controller.js +994 -350
- data/dist/controllers/conditional_fields_controller.js +212 -125
- data/dist/controllers/confirm_controller.js +939 -245
- data/dist/controllers/context_menu_controller.js +275 -127
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +235 -97
- data/dist/controllers/currency_input_controller.js +241 -118
- data/dist/controllers/data_grid_controller.js +229 -135
- data/dist/controllers/date_range_picker_controller.js +359 -165
- data/dist/controllers/dialog_controller.js +887 -225
- data/dist/controllers/direct_upload_controller.js +80 -142
- data/dist/controllers/dirty_form_controller.js +26 -47
- data/dist/controllers/dismissible_controller.js +175 -20
- data/dist/controllers/drawer_controller.js +963 -336
- data/dist/controllers/dropdown_controller.js +342 -85
- data/dist/controllers/editable_controller.js +182 -58
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +426 -229
- data/dist/controllers/filter_controller.js +78 -36
- data/dist/controllers/flash_controller.js +328 -211
- data/dist/controllers/focus_controller.js +654 -239
- data/dist/controllers/form_field_controller.js +153 -136
- data/dist/controllers/form_validation_controller.js +29 -96
- data/dist/controllers/frame_loading_controller.js +263 -198
- data/dist/controllers/highlight_controller.js +119 -70
- data/dist/controllers/hover_card_controller.js +335 -118
- data/dist/controllers/idle_controller.js +316 -65
- data/dist/controllers/input_mask_controller.js +116 -61
- data/dist/controllers/intersection_controller.js +147 -103
- data/dist/controllers/lazy_frame_controller.js +9 -56
- data/dist/controllers/listbox_controller.js +413 -153
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -95
- data/dist/controllers/menu_controller.js +254 -167
- data/dist/controllers/menubar_controller.js +206 -331
- data/dist/controllers/meter_controller.js +159 -71
- data/dist/controllers/multi_select_controller.js +797 -388
- data/dist/controllers/navigation_menu_controller.js +156 -231
- data/dist/controllers/nested_form_controller.js +373 -174
- data/dist/controllers/network_status_controller.js +241 -45
- data/dist/controllers/number_input_controller.js +387 -227
- data/dist/controllers/optimistic_controller.js +28 -75
- data/dist/controllers/otp_controller.js +344 -196
- data/dist/controllers/overflow_indicator_controller.js +167 -93
- data/dist/controllers/overflow_menu_controller.js +408 -277
- data/dist/controllers/pagination_controller.js +342 -120
- data/dist/controllers/password_reveal_controller.js +424 -99
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +252 -96
- data/dist/controllers/pointer_drag_controller.js +263 -164
- data/dist/controllers/popover_controller.js +267 -94
- data/dist/controllers/portal_controller.js +209 -72
- data/dist/controllers/preview_guard_controller.js +243 -112
- data/dist/controllers/progress_controller.js +132 -59
- data/dist/controllers/radio_group_controller.js +138 -133
- data/dist/controllers/range_slider_controller.js +245 -112
- data/dist/controllers/rating_controller.js +429 -194
- data/dist/controllers/read_more_controller.js +185 -47
- data/dist/controllers/reading_progress_controller.js +147 -135
- data/dist/controllers/relative_time_controller.js +145 -110
- data/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- data/dist/controllers/resizable_controller.js +294 -118
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -221
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -146
- data/dist/controllers/scrollspy_controller.js +181 -236
- data/dist/controllers/separator_controller.js +288 -132
- data/dist/controllers/sidebar_controller.js +1071 -375
- data/dist/controllers/skeleton_controller.js +248 -92
- data/dist/controllers/slider_controller.js +211 -96
- data/dist/controllers/smart_sticky_header_controller.js +163 -54
- data/dist/controllers/sortable_controller.js +13 -110
- data/dist/controllers/spinner_controller.js +277 -161
- data/dist/controllers/step_indicator_controller.js +107 -53
- data/dist/controllers/stepper_controller.js +171 -48
- data/dist/controllers/stick_to_bottom_controller.js +310 -116
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -192
- data/dist/controllers/switch_controller.js +107 -43
- data/dist/controllers/tabs_controller.js +50 -25
- data/dist/controllers/tags_input_controller.js +290 -149
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +148 -80
- data/dist/controllers/time_picker_controller.js +162 -81
- data/dist/controllers/toast_controller.js +365 -193
- data/dist/controllers/toggle_group_controller.js +194 -99
- data/dist/controllers/toolbar_controller.js +92 -90
- data/dist/controllers/tooltip_controller.js +185 -125
- data/dist/controllers/transition_controller.js +123 -115
- data/dist/controllers/tree_view_controller.js +248 -290
- data/dist/index.js +8649 -8572
- data/dist/positioning/index.js +148 -45
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,55 +1,218 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
function sharedRegistry(key, create) {
|
|
4
|
+
const symbol = Symbol.for(key);
|
|
5
|
+
const scope = globalThis;
|
|
6
|
+
const existing = scope[symbol];
|
|
7
|
+
if (existing !== void 0) return existing;
|
|
8
|
+
const registry = create();
|
|
9
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
10
|
+
return registry;
|
|
11
|
+
}
|
|
4
12
|
|
|
5
|
-
|
|
13
|
+
function isAttributeValue(value) {
|
|
14
|
+
return value === null || typeof value === "string";
|
|
15
|
+
}
|
|
16
|
+
function readLeaseRecord(element, record) {
|
|
17
|
+
try {
|
|
18
|
+
const parsed = JSON.parse(element.getAttribute(record) ?? "null");
|
|
19
|
+
if (!Array.isArray(parsed)) return void 0;
|
|
20
|
+
const [original, written, ...rest] = parsed;
|
|
21
|
+
const linked = rest[0] !== false;
|
|
22
|
+
const beneath = linked ? rest : rest.slice(1);
|
|
23
|
+
if (!beneath.every((under) => typeof under === "string")) return void 0;
|
|
24
|
+
return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
|
|
25
|
+
} catch {
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
var AttributeLease = class _AttributeLease {
|
|
30
|
+
static #registry = sharedRegistry(
|
|
31
|
+
"stimeo-ui.attribute-lease.registry.v1",
|
|
32
|
+
() => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
|
|
33
|
+
);
|
|
34
|
+
#attribute;
|
|
35
|
+
#record;
|
|
36
|
+
#base;
|
|
37
|
+
#held = /* @__PURE__ */ new Set();
|
|
38
|
+
constructor(attribute, owner, options = {}) {
|
|
39
|
+
this.#attribute = attribute;
|
|
40
|
+
this.#record = `data-${owner}-${attribute}-lease`;
|
|
41
|
+
this.#base = options.base === true;
|
|
42
|
+
const names = _AttributeLease.#registry.names;
|
|
43
|
+
names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
|
|
44
|
+
}
|
|
45
|
+
write(element, value) {
|
|
46
|
+
const stack = this.#stack(element);
|
|
47
|
+
const hold = this.#take(stack) ?? this.#acquire(element, stack);
|
|
48
|
+
this.#held.add(element);
|
|
49
|
+
const shows = !this.#covered(element, stack, hold);
|
|
50
|
+
hold.written = value;
|
|
51
|
+
this.#mark(element, stack);
|
|
52
|
+
if (shows) this.#reflect(element, value);
|
|
53
|
+
}
|
|
54
|
+
return(element) {
|
|
55
|
+
const held = this.#held.delete(element);
|
|
56
|
+
const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
|
|
57
|
+
const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
|
|
58
|
+
const hold = this.#take(stack);
|
|
59
|
+
if (!hold) return false;
|
|
60
|
+
const carries = element.getAttribute(this.#attribute) === hold.written;
|
|
61
|
+
const owns = carries && !this.#covered(element, stack, hold);
|
|
62
|
+
const index = stack.indexOf(hold);
|
|
63
|
+
stack.splice(index, 1);
|
|
64
|
+
const above = stack[index];
|
|
65
|
+
if (above) above.linked &&= hold.linked;
|
|
66
|
+
this.#mark(element, stack);
|
|
67
|
+
if (!owns) return true;
|
|
68
|
+
const below = hold.linked ? stack[index - 1] : void 0;
|
|
69
|
+
this.#reflect(element, below ? below.written : hold.original);
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
returnAll() {
|
|
73
|
+
for (const element of Array.from(this.#held)) this.return(element);
|
|
74
|
+
}
|
|
75
|
+
#stack(element) {
|
|
76
|
+
const stacks = _AttributeLease.#registry.stacks;
|
|
77
|
+
const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
|
|
78
|
+
stacks.set(element, byAttribute);
|
|
79
|
+
const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
|
|
80
|
+
byAttribute.set(this.#attribute, stack);
|
|
81
|
+
return stack;
|
|
82
|
+
}
|
|
83
|
+
#seed(element) {
|
|
84
|
+
const found = [];
|
|
85
|
+
for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
|
|
86
|
+
const read = readLeaseRecord(element, record);
|
|
87
|
+
if (read) found.push({ ...read, record });
|
|
88
|
+
}
|
|
89
|
+
const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
|
|
90
|
+
return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
|
|
91
|
+
lease: null,
|
|
92
|
+
record,
|
|
93
|
+
written,
|
|
94
|
+
original,
|
|
95
|
+
linked: linked && index > 0
|
|
96
|
+
}));
|
|
97
|
+
}
|
|
98
|
+
#take(stack) {
|
|
99
|
+
const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
|
|
100
|
+
if (hold) hold.lease = this;
|
|
101
|
+
return hold;
|
|
102
|
+
}
|
|
103
|
+
#acquire(element, stack) {
|
|
104
|
+
const current = element.getAttribute(this.#attribute);
|
|
105
|
+
const top = stack.at(-1);
|
|
106
|
+
const bottom = stack[0];
|
|
107
|
+
const over = top?.written === current ? top : void 0;
|
|
108
|
+
const from = this.#base ? bottom : over;
|
|
109
|
+
const hold = {
|
|
110
|
+
lease: this,
|
|
111
|
+
record: this.#record,
|
|
112
|
+
written: current,
|
|
113
|
+
original: from ? from.original : current,
|
|
114
|
+
linked: !this.#base && over !== void 0
|
|
115
|
+
};
|
|
116
|
+
if (this.#base && bottom) {
|
|
117
|
+
bottom.linked = true;
|
|
118
|
+
stack.unshift(hold);
|
|
119
|
+
} else stack.push(hold);
|
|
120
|
+
return hold;
|
|
121
|
+
}
|
|
122
|
+
#covered(element, stack, hold) {
|
|
123
|
+
const current = element.getAttribute(this.#attribute);
|
|
124
|
+
return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
|
|
125
|
+
}
|
|
126
|
+
#mark(element, stack) {
|
|
127
|
+
const records = /* @__PURE__ */ new Map([[this.#record, null]]);
|
|
128
|
+
stack.forEach((hold, index) => {
|
|
129
|
+
const names = stack.slice(0, index).map((under) => under.record);
|
|
130
|
+
const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
|
|
131
|
+
const marker = hold.linked || beneath.length === 0 ? [] : [false];
|
|
132
|
+
records.set(
|
|
133
|
+
hold.record,
|
|
134
|
+
hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
|
|
135
|
+
);
|
|
136
|
+
});
|
|
137
|
+
for (const [name, recorded] of records) {
|
|
138
|
+
if (element.getAttribute(name) === recorded) continue;
|
|
139
|
+
if (recorded === null) element.removeAttribute(name);
|
|
140
|
+
else element.setAttribute(name, recorded);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
#reflect(element, value) {
|
|
144
|
+
if (element.getAttribute(this.#attribute) === value) return;
|
|
145
|
+
if (value === null) element.removeAttribute(this.#attribute);
|
|
146
|
+
else element.setAttribute(this.#attribute, value);
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
151
|
+
"insertCompositionText",
|
|
152
|
+
"insertFromComposition",
|
|
153
|
+
"deleteCompositionText",
|
|
154
|
+
"deleteByComposition"
|
|
155
|
+
]);
|
|
6
156
|
var CompositionTracker = class {
|
|
7
157
|
#observedTargets = /* @__PURE__ */ new Set();
|
|
8
158
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
9
159
|
#onStart;
|
|
10
160
|
#onEnd;
|
|
161
|
+
#confirmedTarget = null;
|
|
11
162
|
constructor(options = {}) {
|
|
12
163
|
this.#onStart = options.onStart;
|
|
13
164
|
this.#onEnd = options.onEnd;
|
|
14
165
|
}
|
|
15
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
16
166
|
observe(target) {
|
|
17
167
|
if (this.#observedTargets.has(target)) return;
|
|
18
168
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
19
169
|
target.addEventListener("compositionend", this.#handleEnd);
|
|
170
|
+
target.addEventListener("keydown", this.#handleKeydown);
|
|
20
171
|
this.#observedTargets.add(target);
|
|
21
172
|
}
|
|
22
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
23
173
|
unobserve(target) {
|
|
24
174
|
if (!this.#observedTargets.delete(target)) return;
|
|
25
175
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
26
176
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
177
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
27
178
|
this.#activeTargets.delete(target);
|
|
179
|
+
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
28
180
|
}
|
|
29
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
30
181
|
disconnect() {
|
|
31
182
|
for (const target of this.#observedTargets) {
|
|
32
183
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
33
184
|
target.removeEventListener("compositionend", this.#handleEnd);
|
|
185
|
+
target.removeEventListener("keydown", this.#handleKeydown);
|
|
34
186
|
}
|
|
35
187
|
this.#observedTargets.clear();
|
|
36
188
|
this.#activeTargets.clear();
|
|
189
|
+
this.#confirmedTarget = null;
|
|
37
190
|
}
|
|
38
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
39
191
|
isComposing(event) {
|
|
40
192
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
41
193
|
}
|
|
194
|
+
consumesConfirmedInput(event) {
|
|
195
|
+
const confirmed = this.#confirmedTarget;
|
|
196
|
+
this.#confirmedTarget = null;
|
|
197
|
+
if (confirmed === null || confirmed !== event.target) return false;
|
|
198
|
+
const inputType = event.inputType;
|
|
199
|
+
return !inputType || COMPOSITION_INPUT_TYPES.has(inputType);
|
|
200
|
+
}
|
|
42
201
|
#handleStart = (event) => {
|
|
202
|
+
this.#confirmedTarget = null;
|
|
43
203
|
if (event.currentTarget) this.#activeTargets.add(event.currentTarget);
|
|
44
204
|
this.#onStart?.(event);
|
|
45
205
|
};
|
|
46
206
|
#handleEnd = (event) => {
|
|
47
207
|
if (event.currentTarget) this.#activeTargets.delete(event.currentTarget);
|
|
208
|
+
this.#confirmedTarget = event.target;
|
|
48
209
|
this.#onEnd?.(event);
|
|
49
210
|
};
|
|
211
|
+
#handleKeydown = () => {
|
|
212
|
+
this.#confirmedTarget = null;
|
|
213
|
+
};
|
|
50
214
|
};
|
|
51
215
|
|
|
52
|
-
// src/controllers/editable_controller.ts
|
|
53
216
|
var EditableController = class extends Controller {
|
|
54
217
|
static targets = ["display", "input"];
|
|
55
218
|
static values = {
|
|
@@ -57,29 +220,11 @@ var EditableController = class extends Controller {
|
|
|
57
220
|
};
|
|
58
221
|
static actions = ["cancel", "edit", "onDisplayKeydown", "onKeydown", "revert", "save"];
|
|
59
222
|
static events = ["cancel", "change"];
|
|
60
|
-
/** The value captured when edit mode began, used to detect real changes. */
|
|
61
223
|
#previousValue = "";
|
|
62
|
-
/**
|
|
63
|
-
* The value the last save replaced, or `null` when there is nothing to undo.
|
|
64
|
-
* Cleared on connect: a value from before a page restore is unrecoverable, so
|
|
65
|
-
* `revert()` must not resurrect one.
|
|
66
|
-
*/
|
|
67
224
|
#revertValue = null;
|
|
68
|
-
/**
|
|
69
|
-
* Owns IME lifecycle state for the edit surface, so a keydown that belongs to
|
|
70
|
-
* a composition (cancel or confirm) is never treated as an edit command.
|
|
71
|
-
*/
|
|
72
225
|
#composition = new CompositionTracker();
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
* `focusout` bubbles where `blur` does not, so one listener on the root sees
|
|
77
|
-
* every departure — including one from a Save or Cancel button the consumer
|
|
78
|
-
* placed beside the input. Binding the input alone would make the promise
|
|
79
|
-
* "saves wherever focus moved" true only for focus that leaves the input
|
|
80
|
-
* itself, and tabbing straight past an inner button would strand the editor
|
|
81
|
-
* open.
|
|
82
|
-
*/
|
|
226
|
+
#hidden = new AttributeLease("hidden", this.identifier);
|
|
227
|
+
#connected = false;
|
|
83
228
|
#onFocusOut = (event) => {
|
|
84
229
|
const from = event.target;
|
|
85
230
|
if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;
|
|
@@ -87,32 +232,34 @@ var EditableController = class extends Controller {
|
|
|
87
232
|
if (next instanceof Node && this.element.contains(next)) return;
|
|
88
233
|
if (this.submitOnBlurValue) this.#commit(false);
|
|
89
234
|
};
|
|
90
|
-
/** Establishes the initial display mode (display shown, input hidden). */
|
|
91
235
|
connect() {
|
|
236
|
+
this.#connected = true;
|
|
92
237
|
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
93
238
|
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
94
239
|
this.#revertValue = null;
|
|
95
240
|
this.#setMode("display");
|
|
96
241
|
}
|
|
97
|
-
/** Releases the composition and focus listeners so nothing outlives the element. */
|
|
98
242
|
disconnect() {
|
|
243
|
+
this.#connected = false;
|
|
99
244
|
this.#composition.disconnect();
|
|
100
245
|
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
101
246
|
}
|
|
102
|
-
/** Tracks an input added initially or after connect (e.g. a Turbo swap). */
|
|
103
247
|
inputTargetConnected(input) {
|
|
104
248
|
this.#composition.observe(input);
|
|
105
249
|
this.#applyMode();
|
|
106
250
|
}
|
|
107
|
-
/** Removes composition listeners when the active input is replaced or removed. */
|
|
108
251
|
inputTargetDisconnected(input) {
|
|
109
252
|
this.#composition.unobserve(input);
|
|
253
|
+
if (!this.inputTargets.includes(input)) this.#hidden.return(input);
|
|
254
|
+
if (this.#connected) this.#applyMode();
|
|
110
255
|
}
|
|
111
|
-
/** Re-hides or re-shows a display element that arrived after the mode was set. */
|
|
112
256
|
displayTargetConnected() {
|
|
113
257
|
this.#applyMode();
|
|
114
258
|
}
|
|
115
|
-
|
|
259
|
+
displayTargetDisconnected(display) {
|
|
260
|
+
if (!this.displayTargets.includes(display)) this.#hidden.return(display);
|
|
261
|
+
if (this.#connected) this.#applyMode();
|
|
262
|
+
}
|
|
116
263
|
edit() {
|
|
117
264
|
if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;
|
|
118
265
|
this.#previousValue = this.#currentValue;
|
|
@@ -121,35 +268,26 @@ var EditableController = class extends Controller {
|
|
|
121
268
|
this.inputTarget.focus();
|
|
122
269
|
this.inputTarget.select();
|
|
123
270
|
}
|
|
124
|
-
/** Commits the edit and returns focus to the display element. */
|
|
125
271
|
save() {
|
|
126
272
|
this.#commit(true);
|
|
127
273
|
}
|
|
128
|
-
/** Discards edits, returns to display mode, and dispatches `cancel`. */
|
|
129
274
|
cancel() {
|
|
130
275
|
if (!this.#isEditing) return;
|
|
131
276
|
this.#setMode("display");
|
|
132
277
|
if (this.hasDisplayTarget) this.displayTarget.focus();
|
|
133
278
|
this.dispatch("cancel", { detail: {} });
|
|
134
279
|
}
|
|
135
|
-
/**
|
|
136
|
-
* Puts back the value the last save replaced — for a consumer whose server
|
|
137
|
-
* rejected it. Silent by design: a `change` here would re-enter the same
|
|
138
|
-
* handler that asked for the undo. One save, one undo.
|
|
139
|
-
*/
|
|
140
280
|
revert() {
|
|
141
281
|
if (this.#revertValue === null || this.#isEditing) return;
|
|
142
282
|
this.#writeValue(this.#revertValue);
|
|
143
283
|
this.#revertValue = null;
|
|
144
284
|
}
|
|
145
|
-
/** Adds `F2` as an editing entry point alongside the button's native activation. */
|
|
146
285
|
onDisplayKeydown(event) {
|
|
147
286
|
if (event.key === "F2") {
|
|
148
287
|
event.preventDefault();
|
|
149
288
|
this.edit();
|
|
150
289
|
}
|
|
151
290
|
}
|
|
152
|
-
/** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */
|
|
153
291
|
onKeydown(event) {
|
|
154
292
|
if (this.#composition.isComposing(event)) return;
|
|
155
293
|
if (event.key === "Escape") {
|
|
@@ -165,13 +303,6 @@ var EditableController = class extends Controller {
|
|
|
165
303
|
this.#commit(true);
|
|
166
304
|
}
|
|
167
305
|
}
|
|
168
|
-
/**
|
|
169
|
-
* Returns to display mode, storing the input's value and dispatching `change`
|
|
170
|
-
* when it differs from where editing began.
|
|
171
|
-
*
|
|
172
|
-
* @param restoreFocus - Move focus back to the display element (explicit
|
|
173
|
-
* commit) rather than honoring the user's new focus target (blur).
|
|
174
|
-
*/
|
|
175
306
|
#commit(restoreFocus) {
|
|
176
307
|
if (!this.#isEditing || !this.hasInputTarget) return;
|
|
177
308
|
const value = this.inputTarget.value.trim();
|
|
@@ -184,39 +315,32 @@ var EditableController = class extends Controller {
|
|
|
184
315
|
this.dispatch("change", { detail: { value, previous } });
|
|
185
316
|
}
|
|
186
317
|
}
|
|
187
|
-
/** Stores `value` wherever the display element declares it. */
|
|
188
318
|
#writeValue(value) {
|
|
189
319
|
if (!this.hasDisplayTarget) return;
|
|
190
320
|
const display = this.displayTarget;
|
|
191
321
|
if (display.hasAttribute("data-value")) display.dataset.value = value;
|
|
192
322
|
else display.textContent = value;
|
|
193
323
|
}
|
|
194
|
-
/** Derives both elements' visibility from the mode currently in the DOM. */
|
|
195
324
|
#applyMode() {
|
|
196
325
|
const editing = this.#isEditing;
|
|
197
|
-
if (this.hasDisplayTarget) this.displayTarget
|
|
198
|
-
if (this.hasInputTarget) this.inputTarget
|
|
326
|
+
if (this.hasDisplayTarget) this.#hidden.write(this.displayTarget, editing ? "" : null);
|
|
327
|
+
if (this.hasInputTarget) this.#hidden.write(this.inputTarget, editing ? null : "");
|
|
199
328
|
}
|
|
200
|
-
/** Records the mode, then brings both elements in line with it. */
|
|
201
329
|
#setMode(mode) {
|
|
202
330
|
this.element.dataset.mode = mode;
|
|
203
331
|
this.#applyMode();
|
|
204
332
|
}
|
|
205
|
-
/** The value the display element declares, trimmed. */
|
|
206
333
|
get #currentValue() {
|
|
207
334
|
const display = this.displayTarget;
|
|
208
335
|
return (display.dataset.value ?? display.textContent ?? "").trim();
|
|
209
336
|
}
|
|
210
|
-
/** Whether the editing control is a multi-line `<textarea>`. */
|
|
211
337
|
get #isMultiline() {
|
|
212
338
|
return this.inputTarget.tagName === "TEXTAREA";
|
|
213
339
|
}
|
|
214
|
-
/** Whether the controller is currently in edit mode. */
|
|
215
340
|
get #isEditing() {
|
|
216
341
|
return this.element.dataset.mode === "editing";
|
|
217
342
|
}
|
|
218
343
|
};
|
|
219
344
|
|
|
220
345
|
export { EditableController };
|
|
221
|
-
//# sourceMappingURL=editable_controller.js.map
|
|
222
346
|
//# sourceMappingURL=editable_controller.js.map
|
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/empty_state_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/announce.ts
|
|
6
3
|
function announce(message, options = {}) {
|
|
7
4
|
const text = message.trim();
|
|
8
5
|
if (text.length === 0) return;
|
|
@@ -19,7 +16,70 @@ function fillTemplate(template, values) {
|
|
|
19
16
|
});
|
|
20
17
|
}
|
|
21
18
|
|
|
22
|
-
|
|
19
|
+
var ListenerSet = class {
|
|
20
|
+
#abort = new AbortController();
|
|
21
|
+
add(target, type, handler, options) {
|
|
22
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
23
|
+
}
|
|
24
|
+
dispose() {
|
|
25
|
+
this.#abort.abort();
|
|
26
|
+
this.#abort = new AbortController();
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
var MicrotaskCoalescer = class {
|
|
31
|
+
#run;
|
|
32
|
+
#queued = false;
|
|
33
|
+
#active = false;
|
|
34
|
+
#generation = 0;
|
|
35
|
+
constructor(run) {
|
|
36
|
+
this.#run = run;
|
|
37
|
+
}
|
|
38
|
+
activate() {
|
|
39
|
+
this.#active = true;
|
|
40
|
+
}
|
|
41
|
+
cancel() {
|
|
42
|
+
this.#active = false;
|
|
43
|
+
this.#queued = false;
|
|
44
|
+
this.#generation += 1;
|
|
45
|
+
}
|
|
46
|
+
schedule() {
|
|
47
|
+
if (!this.#active || this.#queued) return;
|
|
48
|
+
this.#queued = true;
|
|
49
|
+
const generation = this.#generation;
|
|
50
|
+
queueMicrotask(() => {
|
|
51
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
52
|
+
this.#queued = false;
|
|
53
|
+
this.#run();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
var MorphRenderWatcher = class {
|
|
59
|
+
#listeners = new ListenerSet();
|
|
60
|
+
#pass;
|
|
61
|
+
#element = null;
|
|
62
|
+
#onMorph = () => this.#pass.schedule();
|
|
63
|
+
constructor(run) {
|
|
64
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
65
|
+
}
|
|
66
|
+
observe(element) {
|
|
67
|
+
if (this.#element === element) return;
|
|
68
|
+
this.disconnect();
|
|
69
|
+
this.#element = element;
|
|
70
|
+
this.#pass.activate();
|
|
71
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
72
|
+
}
|
|
73
|
+
schedule() {
|
|
74
|
+
this.#pass.schedule();
|
|
75
|
+
}
|
|
76
|
+
disconnect() {
|
|
77
|
+
this.#listeners.dispose();
|
|
78
|
+
this.#pass.cancel();
|
|
79
|
+
this.#element = null;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
23
83
|
var EmptyStateController = class extends Controller {
|
|
24
84
|
static targets = ["list", "empty"];
|
|
25
85
|
static values = {
|
|
@@ -29,68 +89,40 @@ var EmptyStateController = class extends Controller {
|
|
|
29
89
|
};
|
|
30
90
|
static events = ["change"];
|
|
31
91
|
#observer = null;
|
|
32
|
-
/** Whether the controller is between `connect()` and `disconnect()`. */
|
|
33
92
|
#connected = false;
|
|
34
|
-
/** Last applied empty state; `null` until the first sync so connect emits nothing. */
|
|
35
93
|
#empty = null;
|
|
94
|
+
#morphRender = new MorphRenderWatcher(() => this.#resync());
|
|
36
95
|
connect() {
|
|
96
|
+
this.#morphRender.observe(this.element);
|
|
37
97
|
this.#connected = true;
|
|
38
98
|
this.#syncObservation();
|
|
39
99
|
this.#update();
|
|
40
100
|
}
|
|
41
101
|
disconnect() {
|
|
102
|
+
this.#morphRender.disconnect();
|
|
42
103
|
this.#connected = false;
|
|
43
104
|
this.#stopObserving();
|
|
44
105
|
}
|
|
45
|
-
/** Follows a `list` element swapped in at runtime (Turbo Stream `replace` / morph). */
|
|
46
106
|
listTargetConnected() {
|
|
47
107
|
this.#resync();
|
|
48
108
|
}
|
|
49
|
-
/** Releases the observation when the `list` element leaves the target set. */
|
|
50
109
|
listTargetDisconnected() {
|
|
51
110
|
this.#resync();
|
|
52
111
|
}
|
|
53
|
-
/** Syncs an `empty` element that arrives — or is replaced — at runtime. */
|
|
54
112
|
emptyTargetConnected() {
|
|
55
113
|
this.#resync();
|
|
56
114
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Syncs the `empty` element that remains when one leaves the target set. A
|
|
59
|
-
* single-target getter resolves to the first `empty` element in document order,
|
|
60
|
-
* so a swap that inserts the replacement *before* removing the original (Turbo
|
|
61
|
-
* Stream `after` / `before` / `append` followed by `remove`) leaves the
|
|
62
|
-
* replacement untouched until the original goes — this callback is that moment.
|
|
63
|
-
*/
|
|
64
115
|
emptyTargetDisconnected() {
|
|
65
116
|
this.#resync();
|
|
66
117
|
}
|
|
67
|
-
/** Re-renders when application code (or a Turbo morph) changes `itemSelector` at runtime. */
|
|
68
118
|
itemSelectorValueChanged() {
|
|
69
119
|
this.#resync();
|
|
70
120
|
}
|
|
71
|
-
/**
|
|
72
|
-
* Re-points the observation and re-renders after a target or selector change.
|
|
73
|
-
* The `#connected` guard is load-bearing: Stimulus runs value and target
|
|
74
|
-
* callbacks for the initial markup *before* `connect()` and runs target
|
|
75
|
-
* callbacks during teardown *after* `disconnect()`, and re-observing there
|
|
76
|
-
* would outlive the controller.
|
|
77
|
-
*/
|
|
78
121
|
#resync() {
|
|
79
122
|
if (!this.#connected) return;
|
|
80
123
|
this.#syncObservation();
|
|
81
124
|
this.#update();
|
|
82
125
|
}
|
|
83
|
-
/**
|
|
84
|
-
* Points the mutation observation at the current `list` target — re-resolved on
|
|
85
|
-
* every sync rather than captured at connect, so an element swapped in at
|
|
86
|
-
* runtime is observed instead of the detached original.
|
|
87
|
-
*
|
|
88
|
-
* The observation covers exactly what the count predicate reads. With no
|
|
89
|
-
* `itemSelector` the count is the child element count, which only `childList`
|
|
90
|
-
* can change. With one, the predicate reads the children themselves, so
|
|
91
|
-
* attribute and descendant mutations are watched too — and the controller's own
|
|
92
|
-
* writes are filtered back out, or toggling `hidden` would re-enter the render.
|
|
93
|
-
*/
|
|
94
126
|
#syncObservation() {
|
|
95
127
|
this.#stopObserving();
|
|
96
128
|
if (!this.hasListTarget || typeof MutationObserver === "undefined") return;
|
|
@@ -108,21 +140,11 @@ var EmptyStateController = class extends Controller {
|
|
|
108
140
|
this.#observer?.disconnect();
|
|
109
141
|
this.#observer = null;
|
|
110
142
|
}
|
|
111
|
-
/**
|
|
112
|
-
* Whether a mutation can change the item count. An attribute written on a
|
|
113
|
-
* target this controller owns is its own echo — `hidden` on `list` / `empty`,
|
|
114
|
-
* and the hooks on the controller element when the list *is* that element.
|
|
115
|
-
*/
|
|
116
143
|
#affectsCount(record) {
|
|
117
144
|
if (record.type !== "attributes") return true;
|
|
118
145
|
const own = record.target === this.listTarget || this.hasEmptyTarget && record.target === this.emptyTarget;
|
|
119
146
|
return !own;
|
|
120
147
|
}
|
|
121
|
-
/**
|
|
122
|
-
* Renders the count, then reports a crossed boundary. The announcement copy is
|
|
123
|
-
* read here rather than while rendering: it is the wording of the report, not
|
|
124
|
-
* an input to what is displayed.
|
|
125
|
-
*/
|
|
126
148
|
#update() {
|
|
127
149
|
const count = this.#render();
|
|
128
150
|
if (count === null) return;
|
|
@@ -135,12 +157,6 @@ var EmptyStateController = class extends Controller {
|
|
|
135
157
|
fillTemplate(empty ? this.announceTextValue : this.announceFilledTextValue, { count })
|
|
136
158
|
);
|
|
137
159
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Syncs visibility and the state hooks to the current item count, and returns
|
|
140
|
-
* it. `null` when there is no `list` target to count.
|
|
141
|
-
*
|
|
142
|
-
* @stimeoRenderRoot
|
|
143
|
-
*/
|
|
144
160
|
#render() {
|
|
145
161
|
if (!this.hasListTarget) return null;
|
|
146
162
|
const count = this.#count();
|
|
@@ -155,7 +171,6 @@ var EmptyStateController = class extends Controller {
|
|
|
155
171
|
if (this.hasEmptyTarget) this.emptyTarget.hidden = !empty;
|
|
156
172
|
return count;
|
|
157
173
|
}
|
|
158
|
-
/** Item count: element children matching `itemSelector`, or all element children. */
|
|
159
174
|
#count() {
|
|
160
175
|
const selector = this.itemSelectorValue;
|
|
161
176
|
if (selector.length === 0) return this.listTarget.childElementCount;
|
|
@@ -168,5 +183,4 @@ var EmptyStateController = class extends Controller {
|
|
|
168
183
|
};
|
|
169
184
|
|
|
170
185
|
export { EmptyStateController };
|
|
171
|
-
//# sourceMappingURL=empty_state_controller.js.map
|
|
172
186
|
//# sourceMappingURL=empty_state_controller.js.map
|