stimeo-ui 0.16.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 +31 -0
- data/README.md +8 -1
- data/dist/cable/index.js +363 -436
- data/dist/controllers/accordion_controller.js +166 -55
- data/dist/controllers/alert_dialog_controller.js +862 -221
- data/dist/controllers/announcer_controller.js +135 -198
- data/dist/controllers/aspect_ratio_controller.js +0 -10
- data/dist/controllers/auto_submit_controller.js +271 -127
- data/dist/controllers/avatar_controller.js +171 -105
- data/dist/controllers/breadcrumb_controller.js +0 -130
- data/dist/controllers/bulk_select_controller.js +262 -52
- data/dist/controllers/calendar_controller.js +175 -209
- data/dist/controllers/carousel_controller.js +461 -399
- data/dist/controllers/character_counter_controller.js +246 -156
- data/dist/controllers/checkbox_controller.js +32 -74
- data/dist/controllers/clipboard_controller.js +168 -147
- data/dist/controllers/collapsible_controller.js +301 -120
- data/dist/controllers/color_picker_controller.js +149 -139
- data/dist/controllers/combobox_controller.js +360 -133
- data/dist/controllers/command_palette_controller.js +970 -358
- data/dist/controllers/conditional_fields_controller.js +166 -124
- data/dist/controllers/confirm_controller.js +939 -248
- data/dist/controllers/context_menu_controller.js +256 -128
- data/dist/controllers/count_up_controller.js +73 -21
- data/dist/controllers/countdown_controller.js +183 -148
- data/dist/controllers/currency_input_controller.js +134 -158
- data/dist/controllers/data_grid_controller.js +114 -186
- data/dist/controllers/date_range_picker_controller.js +288 -206
- data/dist/controllers/dialog_controller.js +858 -223
- data/dist/controllers/direct_upload_controller.js +73 -153
- data/dist/controllers/dirty_form_controller.js +14 -48
- data/dist/controllers/dismissible_controller.js +175 -21
- data/dist/controllers/drawer_controller.js +933 -341
- data/dist/controllers/dropdown_controller.js +312 -82
- data/dist/controllers/editable_controller.js +158 -68
- data/dist/controllers/empty_state_controller.js +67 -53
- data/dist/controllers/file_dropzone_controller.js +311 -207
- data/dist/controllers/filter_controller.js +67 -39
- data/dist/controllers/flash_controller.js +172 -314
- data/dist/controllers/focus_controller.js +654 -240
- data/dist/controllers/form_field_controller.js +147 -132
- data/dist/controllers/form_validation_controller.js +13 -86
- data/dist/controllers/frame_loading_controller.js +264 -213
- data/dist/controllers/highlight_controller.js +107 -75
- data/dist/controllers/hover_card_controller.js +309 -118
- data/dist/controllers/idle_controller.js +267 -101
- data/dist/controllers/input_mask_controller.js +71 -72
- data/dist/controllers/intersection_controller.js +147 -106
- data/dist/controllers/lazy_frame_controller.js +9 -65
- data/dist/controllers/listbox_controller.js +306 -204
- data/dist/controllers/local_time_controller.js +54 -64
- data/dist/controllers/masonry_controller.js +70 -93
- data/dist/controllers/menu_controller.js +214 -156
- data/dist/controllers/menubar_controller.js +170 -329
- data/dist/controllers/meter_controller.js +151 -67
- data/dist/controllers/multi_select_controller.js +593 -358
- data/dist/controllers/navigation_menu_controller.js +124 -232
- data/dist/controllers/nested_form_controller.js +352 -182
- data/dist/controllers/network_status_controller.js +242 -54
- data/dist/controllers/number_input_controller.js +320 -263
- data/dist/controllers/optimistic_controller.js +16 -76
- data/dist/controllers/otp_controller.js +198 -193
- data/dist/controllers/overflow_indicator_controller.js +116 -113
- data/dist/controllers/overflow_menu_controller.js +343 -283
- data/dist/controllers/pagination_controller.js +295 -119
- data/dist/controllers/password_reveal_controller.js +389 -121
- data/dist/controllers/password_strength_controller.js +99 -153
- data/dist/controllers/persist_controller.js +237 -103
- data/dist/controllers/pointer_drag_controller.js +185 -165
- data/dist/controllers/popover_controller.js +236 -97
- data/dist/controllers/portal_controller.js +209 -76
- data/dist/controllers/preview_guard_controller.js +232 -116
- data/dist/controllers/progress_controller.js +125 -56
- data/dist/controllers/radio_group_controller.js +117 -137
- data/dist/controllers/range_slider_controller.js +192 -105
- data/dist/controllers/rating_controller.js +418 -207
- data/dist/controllers/read_more_controller.js +134 -63
- 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 +255 -139
- data/dist/controllers/roving_controller.js +39 -84
- data/dist/controllers/scroll_area_controller.js +428 -225
- data/dist/controllers/scroll_restore_controller.js +0 -53
- data/dist/controllers/scroll_visibility_controller.js +395 -145
- data/dist/controllers/scrollspy_controller.js +181 -242
- data/dist/controllers/separator_controller.js +267 -140
- data/dist/controllers/sidebar_controller.js +1023 -386
- data/dist/controllers/skeleton_controller.js +248 -97
- data/dist/controllers/slider_controller.js +159 -79
- data/dist/controllers/smart_sticky_header_controller.js +162 -63
- data/dist/controllers/sortable_controller.js +0 -112
- data/dist/controllers/spinner_controller.js +277 -169
- data/dist/controllers/step_indicator_controller.js +99 -46
- data/dist/controllers/stepper_controller.js +114 -73
- data/dist/controllers/stick_to_bottom_controller.js +262 -138
- data/dist/controllers/sticky_observer_controller.js +214 -49
- data/dist/controllers/submit_once_controller.js +294 -205
- data/dist/controllers/switch_controller.js +33 -60
- data/dist/controllers/tabs_controller.js +35 -29
- data/dist/controllers/tags_input_controller.js +136 -145
- data/dist/controllers/textarea_autosize_controller.js +71 -58
- data/dist/controllers/theme_controller.js +130 -112
- data/dist/controllers/time_picker_controller.js +146 -80
- data/dist/controllers/toast_controller.js +186 -297
- data/dist/controllers/toggle_group_controller.js +95 -136
- data/dist/controllers/toolbar_controller.js +68 -98
- data/dist/controllers/tooltip_controller.js +158 -124
- data/dist/controllers/transition_controller.js +123 -119
- data/dist/controllers/tree_view_controller.js +150 -345
- data/dist/index.js +6370 -9346
- data/dist/positioning/index.js +148 -47
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +3 -3
|
@@ -1,8 +1,152 @@
|
|
|
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
|
+
}
|
|
12
|
+
|
|
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
|
+
};
|
|
4
149
|
|
|
5
|
-
// src/utils/composition_tracker.ts
|
|
6
150
|
var COMPOSITION_INPUT_TYPES = /* @__PURE__ */ new Set([
|
|
7
151
|
"insertCompositionText",
|
|
8
152
|
"insertFromComposition",
|
|
@@ -14,13 +158,11 @@ var CompositionTracker = class {
|
|
|
14
158
|
#activeTargets = /* @__PURE__ */ new Set();
|
|
15
159
|
#onStart;
|
|
16
160
|
#onEnd;
|
|
17
|
-
/** The field whose confirming `input` is still owed, while the window is open. */
|
|
18
161
|
#confirmedTarget = null;
|
|
19
162
|
constructor(options = {}) {
|
|
20
163
|
this.#onStart = options.onStart;
|
|
21
164
|
this.#onEnd = options.onEnd;
|
|
22
165
|
}
|
|
23
|
-
/** Starts lifecycle tracking for `target`; repeated calls are idempotent. */
|
|
24
166
|
observe(target) {
|
|
25
167
|
if (this.#observedTargets.has(target)) return;
|
|
26
168
|
target.addEventListener("compositionstart", this.#handleStart);
|
|
@@ -28,7 +170,6 @@ var CompositionTracker = class {
|
|
|
28
170
|
target.addEventListener("keydown", this.#handleKeydown);
|
|
29
171
|
this.#observedTargets.add(target);
|
|
30
172
|
}
|
|
31
|
-
/** Stops tracking one target and clears any active composition it owned. */
|
|
32
173
|
unobserve(target) {
|
|
33
174
|
if (!this.#observedTargets.delete(target)) return;
|
|
34
175
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -37,7 +178,6 @@ var CompositionTracker = class {
|
|
|
37
178
|
this.#activeTargets.delete(target);
|
|
38
179
|
if (this.#confirmedTarget === target) this.#confirmedTarget = null;
|
|
39
180
|
}
|
|
40
|
-
/** Releases every listener and clears state so reconnect starts cleanly. */
|
|
41
181
|
disconnect() {
|
|
42
182
|
for (const target of this.#observedTargets) {
|
|
43
183
|
target.removeEventListener("compositionstart", this.#handleStart);
|
|
@@ -48,16 +188,9 @@ var CompositionTracker = class {
|
|
|
48
188
|
this.#activeTargets.clear();
|
|
49
189
|
this.#confirmedTarget = null;
|
|
50
190
|
}
|
|
51
|
-
/** True when lifecycle tracking or the current event reports composition. */
|
|
52
191
|
isComposing(event) {
|
|
53
192
|
return this.#activeTargets.size > 0 || event?.isComposing === true;
|
|
54
193
|
}
|
|
55
|
-
/**
|
|
56
|
-
* Whether `event` is the `input` echoing the composition just confirmed.
|
|
57
|
-
*
|
|
58
|
-
* Asking closes the window either way, so one confirmation is folded at most
|
|
59
|
-
* once and a consumer asks once per `input`.
|
|
60
|
-
*/
|
|
61
194
|
consumesConfirmedInput(event) {
|
|
62
195
|
const confirmed = this.#confirmedTarget;
|
|
63
196
|
this.#confirmedTarget = null;
|
|
@@ -75,13 +208,11 @@ var CompositionTracker = class {
|
|
|
75
208
|
this.#confirmedTarget = event.target;
|
|
76
209
|
this.#onEnd?.(event);
|
|
77
210
|
};
|
|
78
|
-
/** A key on an observed field opens an edit of its own, so no echo is owed. */
|
|
79
211
|
#handleKeydown = () => {
|
|
80
212
|
this.#confirmedTarget = null;
|
|
81
213
|
};
|
|
82
214
|
};
|
|
83
215
|
|
|
84
|
-
// src/controllers/editable_controller.ts
|
|
85
216
|
var EditableController = class extends Controller {
|
|
86
217
|
static targets = ["display", "input"];
|
|
87
218
|
static values = {
|
|
@@ -89,31 +220,11 @@ var EditableController = class extends Controller {
|
|
|
89
220
|
};
|
|
90
221
|
static actions = ["cancel", "edit", "onDisplayKeydown", "onKeydown", "revert", "save"];
|
|
91
222
|
static events = ["cancel", "change"];
|
|
92
|
-
/** The value captured when edit mode began, used to detect real changes. */
|
|
93
223
|
#previousValue = "";
|
|
94
|
-
/**
|
|
95
|
-
* The value the last save replaced, or `null` when there is nothing to undo.
|
|
96
|
-
* Cleared on connect: a value from before a page restore is unrecoverable, so
|
|
97
|
-
* `revert()` must not resurrect one.
|
|
98
|
-
*/
|
|
99
224
|
#revertValue = null;
|
|
100
|
-
/**
|
|
101
|
-
* Owns IME lifecycle state for the edit surface, so a keydown that belongs to
|
|
102
|
-
* a composition (cancel or confirm) is never treated as an edit command.
|
|
103
|
-
*/
|
|
104
225
|
#composition = new CompositionTracker();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
*
|
|
108
|
-
* `focusout` bubbles where `blur` does not, so one listener on the root sees
|
|
109
|
-
* every departure — including one from a Save or Cancel button the consumer
|
|
110
|
-
* placed beside the input. Binding the input alone would make the promise
|
|
111
|
-
* "saves wherever focus moved" true only for focus that leaves the input
|
|
112
|
-
* itself, and tabbing straight past an inner button would strand the editor
|
|
113
|
-
* open.
|
|
114
|
-
*
|
|
115
|
-
* @stimeoRuntimeOnly `submitOnBlur` decides whether this one blur commits the edit.
|
|
116
|
-
*/
|
|
226
|
+
#hidden = new AttributeLease("hidden", this.identifier);
|
|
227
|
+
#connected = false;
|
|
117
228
|
#onFocusOut = (event) => {
|
|
118
229
|
const from = event.target;
|
|
119
230
|
if (this.hasDisplayTarget && from instanceof Node && this.displayTarget.contains(from)) return;
|
|
@@ -121,32 +232,34 @@ var EditableController = class extends Controller {
|
|
|
121
232
|
if (next instanceof Node && this.element.contains(next)) return;
|
|
122
233
|
if (this.submitOnBlurValue) this.#commit(false);
|
|
123
234
|
};
|
|
124
|
-
/** Establishes the initial display mode (display shown, input hidden). */
|
|
125
235
|
connect() {
|
|
236
|
+
this.#connected = true;
|
|
126
237
|
if (this.hasInputTarget) this.#composition.observe(this.inputTarget);
|
|
127
238
|
this.element.addEventListener("focusout", this.#onFocusOut);
|
|
128
239
|
this.#revertValue = null;
|
|
129
240
|
this.#setMode("display");
|
|
130
241
|
}
|
|
131
|
-
/** Releases the composition and focus listeners so nothing outlives the element. */
|
|
132
242
|
disconnect() {
|
|
243
|
+
this.#connected = false;
|
|
133
244
|
this.#composition.disconnect();
|
|
134
245
|
this.element.removeEventListener("focusout", this.#onFocusOut);
|
|
135
246
|
}
|
|
136
|
-
/** Tracks an input added initially or after connect (e.g. a Turbo swap). */
|
|
137
247
|
inputTargetConnected(input) {
|
|
138
248
|
this.#composition.observe(input);
|
|
139
249
|
this.#applyMode();
|
|
140
250
|
}
|
|
141
|
-
/** Removes composition listeners when the active input is replaced or removed. */
|
|
142
251
|
inputTargetDisconnected(input) {
|
|
143
252
|
this.#composition.unobserve(input);
|
|
253
|
+
if (!this.inputTargets.includes(input)) this.#hidden.return(input);
|
|
254
|
+
if (this.#connected) this.#applyMode();
|
|
144
255
|
}
|
|
145
|
-
/** Re-hides or re-shows a display element that arrived after the mode was set. */
|
|
146
256
|
displayTargetConnected() {
|
|
147
257
|
this.#applyMode();
|
|
148
258
|
}
|
|
149
|
-
|
|
259
|
+
displayTargetDisconnected(display) {
|
|
260
|
+
if (!this.displayTargets.includes(display)) this.#hidden.return(display);
|
|
261
|
+
if (this.#connected) this.#applyMode();
|
|
262
|
+
}
|
|
150
263
|
edit() {
|
|
151
264
|
if (this.#isEditing || !this.hasInputTarget || !this.hasDisplayTarget) return;
|
|
152
265
|
this.#previousValue = this.#currentValue;
|
|
@@ -155,35 +268,26 @@ var EditableController = class extends Controller {
|
|
|
155
268
|
this.inputTarget.focus();
|
|
156
269
|
this.inputTarget.select();
|
|
157
270
|
}
|
|
158
|
-
/** Commits the edit and returns focus to the display element. */
|
|
159
271
|
save() {
|
|
160
272
|
this.#commit(true);
|
|
161
273
|
}
|
|
162
|
-
/** Discards edits, returns to display mode, and dispatches `cancel`. */
|
|
163
274
|
cancel() {
|
|
164
275
|
if (!this.#isEditing) return;
|
|
165
276
|
this.#setMode("display");
|
|
166
277
|
if (this.hasDisplayTarget) this.displayTarget.focus();
|
|
167
278
|
this.dispatch("cancel", { detail: {} });
|
|
168
279
|
}
|
|
169
|
-
/**
|
|
170
|
-
* Puts back the value the last save replaced — for a consumer whose server
|
|
171
|
-
* rejected it. Silent by design: a `change` here would re-enter the same
|
|
172
|
-
* handler that asked for the undo. One save, one undo.
|
|
173
|
-
*/
|
|
174
280
|
revert() {
|
|
175
281
|
if (this.#revertValue === null || this.#isEditing) return;
|
|
176
282
|
this.#writeValue(this.#revertValue);
|
|
177
283
|
this.#revertValue = null;
|
|
178
284
|
}
|
|
179
|
-
/** Adds `F2` as an editing entry point alongside the button's native activation. */
|
|
180
285
|
onDisplayKeydown(event) {
|
|
181
286
|
if (event.key === "F2") {
|
|
182
287
|
event.preventDefault();
|
|
183
288
|
this.edit();
|
|
184
289
|
}
|
|
185
290
|
}
|
|
186
|
-
/** Commits on `Enter` (or `Ctrl+Enter` when multiline) and cancels on `Escape`. */
|
|
187
291
|
onKeydown(event) {
|
|
188
292
|
if (this.#composition.isComposing(event)) return;
|
|
189
293
|
if (event.key === "Escape") {
|
|
@@ -199,13 +303,6 @@ var EditableController = class extends Controller {
|
|
|
199
303
|
this.#commit(true);
|
|
200
304
|
}
|
|
201
305
|
}
|
|
202
|
-
/**
|
|
203
|
-
* Returns to display mode, storing the input's value and dispatching `change`
|
|
204
|
-
* when it differs from where editing began.
|
|
205
|
-
*
|
|
206
|
-
* @param restoreFocus - Move focus back to the display element (explicit
|
|
207
|
-
* commit) rather than honoring the user's new focus target (blur).
|
|
208
|
-
*/
|
|
209
306
|
#commit(restoreFocus) {
|
|
210
307
|
if (!this.#isEditing || !this.hasInputTarget) return;
|
|
211
308
|
const value = this.inputTarget.value.trim();
|
|
@@ -218,39 +315,32 @@ var EditableController = class extends Controller {
|
|
|
218
315
|
this.dispatch("change", { detail: { value, previous } });
|
|
219
316
|
}
|
|
220
317
|
}
|
|
221
|
-
/** Stores `value` wherever the display element declares it. */
|
|
222
318
|
#writeValue(value) {
|
|
223
319
|
if (!this.hasDisplayTarget) return;
|
|
224
320
|
const display = this.displayTarget;
|
|
225
321
|
if (display.hasAttribute("data-value")) display.dataset.value = value;
|
|
226
322
|
else display.textContent = value;
|
|
227
323
|
}
|
|
228
|
-
/** Derives both elements' visibility from the mode currently in the DOM. */
|
|
229
324
|
#applyMode() {
|
|
230
325
|
const editing = this.#isEditing;
|
|
231
|
-
if (this.hasDisplayTarget) this.displayTarget
|
|
232
|
-
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 : "");
|
|
233
328
|
}
|
|
234
|
-
/** Records the mode, then brings both elements in line with it. */
|
|
235
329
|
#setMode(mode) {
|
|
236
330
|
this.element.dataset.mode = mode;
|
|
237
331
|
this.#applyMode();
|
|
238
332
|
}
|
|
239
|
-
/** The value the display element declares, trimmed. */
|
|
240
333
|
get #currentValue() {
|
|
241
334
|
const display = this.displayTarget;
|
|
242
335
|
return (display.dataset.value ?? display.textContent ?? "").trim();
|
|
243
336
|
}
|
|
244
|
-
/** Whether the editing control is a multi-line `<textarea>`. */
|
|
245
337
|
get #isMultiline() {
|
|
246
338
|
return this.inputTarget.tagName === "TEXTAREA";
|
|
247
339
|
}
|
|
248
|
-
/** Whether the controller is currently in edit mode. */
|
|
249
340
|
get #isEditing() {
|
|
250
341
|
return this.element.dataset.mode === "editing";
|
|
251
342
|
}
|
|
252
343
|
};
|
|
253
344
|
|
|
254
345
|
export { EditableController };
|
|
255
|
-
//# sourceMappingURL=editable_controller.js.map
|
|
256
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
|