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,13 +1,31 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
var FOCUS_EVENTS = /* @__PURE__ */ new Set(["blur", "focus", "focusin", "focusout"]);
|
|
4
|
+
var POINTER_EVENTS = /* @__PURE__ */ new Set(["mouseenter", "mouseleave", "pointerenter", "pointerleave"]);
|
|
5
|
+
function stateReasonFor(event) {
|
|
6
|
+
if (!event) return "api";
|
|
7
|
+
if (FOCUS_EVENTS.has(event.type)) return "focus";
|
|
8
|
+
if (POINTER_EVENTS.has(event.type)) return "pointer";
|
|
9
|
+
return "user";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function actionSource(source) {
|
|
13
|
+
if (source instanceof HTMLElement) {
|
|
14
|
+
return { event: null, host: source, origin: source, reason: "api" };
|
|
15
|
+
}
|
|
16
|
+
const event = source ?? null;
|
|
17
|
+
return {
|
|
18
|
+
event,
|
|
19
|
+
host: event?.currentTarget instanceof Element ? event.currentTarget : null,
|
|
20
|
+
origin: event?.target instanceof Element ? event.target : null,
|
|
21
|
+
reason: stateReasonFor(event)
|
|
22
|
+
};
|
|
23
|
+
}
|
|
4
24
|
|
|
5
|
-
// src/utils/logical_scroll.ts
|
|
6
25
|
function isRtl(element) {
|
|
7
26
|
return window.getComputedStyle(element).direction === "rtl";
|
|
8
27
|
}
|
|
9
28
|
|
|
10
|
-
// src/utils/arrow_step.ts
|
|
11
29
|
function logicalArrowKey(key, element) {
|
|
12
30
|
if (key !== "ArrowRight" && key !== "ArrowLeft") return key;
|
|
13
31
|
if (!isRtl(element)) return key;
|
|
@@ -18,7 +36,6 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
18
36
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
19
37
|
}
|
|
20
38
|
|
|
21
|
-
// src/utils/field_mirror.ts
|
|
22
39
|
function writeFields(container, values, { name, form = "" }) {
|
|
23
40
|
const current = [...container.children];
|
|
24
41
|
const submits = current.length === values.length && current.every(
|
|
@@ -41,7 +58,6 @@ function commitField(target) {
|
|
|
41
58
|
target.dispatchEvent(new Event("change", { bubbles: true }));
|
|
42
59
|
}
|
|
43
60
|
|
|
44
|
-
// src/utils/interactive_host.ts
|
|
45
61
|
var INTERACTIVE_HOST_SELECTOR = "button, input, select, textarea, label, a[href], area[href], summary, details, audio[controls], video[controls], iframe, object, embed";
|
|
46
62
|
function isInteractiveHost(element) {
|
|
47
63
|
if (element.matches(INTERACTIVE_HOST_SELECTOR)) return true;
|
|
@@ -58,27 +74,33 @@ function isInteractiveHost(element) {
|
|
|
58
74
|
return false;
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
|
|
77
|
+
var ListenerSet = class {
|
|
78
|
+
#abort = new AbortController();
|
|
79
|
+
add(target, type, handler, options) {
|
|
80
|
+
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
81
|
+
}
|
|
82
|
+
dispose() {
|
|
83
|
+
this.#abort.abort();
|
|
84
|
+
this.#abort = new AbortController();
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
|
|
62
88
|
var MicrotaskCoalescer = class {
|
|
63
89
|
#run;
|
|
64
90
|
#queued = false;
|
|
65
91
|
#active = false;
|
|
66
92
|
#generation = 0;
|
|
67
|
-
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
68
93
|
constructor(run) {
|
|
69
94
|
this.#run = run;
|
|
70
95
|
}
|
|
71
|
-
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
72
96
|
activate() {
|
|
73
97
|
this.#active = true;
|
|
74
98
|
}
|
|
75
|
-
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
76
99
|
cancel() {
|
|
77
100
|
this.#active = false;
|
|
78
101
|
this.#queued = false;
|
|
79
102
|
this.#generation += 1;
|
|
80
103
|
}
|
|
81
|
-
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
82
104
|
schedule() {
|
|
83
105
|
if (!this.#active || this.#queued) return;
|
|
84
106
|
this.#queued = true;
|
|
@@ -91,7 +113,46 @@ var MicrotaskCoalescer = class {
|
|
|
91
113
|
}
|
|
92
114
|
};
|
|
93
115
|
|
|
94
|
-
|
|
116
|
+
var MorphRenderWatcher = class {
|
|
117
|
+
#listeners = new ListenerSet();
|
|
118
|
+
#pass;
|
|
119
|
+
#element = null;
|
|
120
|
+
#onMorph = () => this.#pass.schedule();
|
|
121
|
+
constructor(run) {
|
|
122
|
+
this.#pass = new MicrotaskCoalescer(run);
|
|
123
|
+
}
|
|
124
|
+
observe(element) {
|
|
125
|
+
if (this.#element === element) return;
|
|
126
|
+
this.disconnect();
|
|
127
|
+
this.#element = element;
|
|
128
|
+
this.#pass.activate();
|
|
129
|
+
this.#listeners.add(element, "turbo:morph-element", this.#onMorph);
|
|
130
|
+
}
|
|
131
|
+
schedule() {
|
|
132
|
+
this.#pass.schedule();
|
|
133
|
+
}
|
|
134
|
+
disconnect() {
|
|
135
|
+
this.#listeners.dispose();
|
|
136
|
+
this.#pass.cancel();
|
|
137
|
+
this.#element = null;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
var MoveCounter = class {
|
|
142
|
+
#sequence = 0;
|
|
143
|
+
record() {
|
|
144
|
+
this.#sequence += 1;
|
|
145
|
+
return this.#sequence;
|
|
146
|
+
}
|
|
147
|
+
isLatest(token) {
|
|
148
|
+
return token === this.#sequence;
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
function targetSelector(identifier, name) {
|
|
153
|
+
return `[data-${identifier}-target~="${name}"]`;
|
|
154
|
+
}
|
|
155
|
+
|
|
95
156
|
var SORT_CYCLE = ["none", "ascending", "descending"];
|
|
96
157
|
var OBSERVED_ATTRIBUTES = ["aria-selected", "data-value"];
|
|
97
158
|
function setAttributeIfChanged(element, name, value) {
|
|
@@ -110,6 +171,8 @@ function nextSortDirection(current) {
|
|
|
110
171
|
return SORT_CYCLE[(from + 1) % SORT_CYCLE.length] ?? "ascending";
|
|
111
172
|
}
|
|
112
173
|
var DataGridController = class extends Controller {
|
|
174
|
+
#moves = new MoveCounter();
|
|
175
|
+
#stopHolder = null;
|
|
113
176
|
static targets = ["columnHeader", "row", "cell", "fields"];
|
|
114
177
|
static values = {
|
|
115
178
|
selection: { type: String, default: "none" },
|
|
@@ -118,57 +181,25 @@ var DataGridController = class extends Controller {
|
|
|
118
181
|
};
|
|
119
182
|
static actions = ["onKeydown", "sort", "toggleSelect"];
|
|
120
183
|
static events = ["selectionchange", "sort", "reconcile"];
|
|
121
|
-
|
|
122
|
-
* Collapses the target callbacks, Value changes and observed row writes of one
|
|
123
|
-
* DOM mutation into a single pass, and refuses to run before `connect()` or
|
|
124
|
-
* after `disconnect()`.
|
|
125
|
-
*
|
|
126
|
-
* Stimulus reports every target one at a time, so an ungated pass would re-walk
|
|
127
|
-
* the whole grid once per authored cell on mount and once per streamed cell
|
|
128
|
-
* afterwards — quadratic in the cell count both times.
|
|
129
|
-
*/
|
|
130
|
-
#reconcile = new MicrotaskCoalescer(() => this.#reconcileRows());
|
|
131
|
-
/** Watches the row attributes a page can rewrite in place; set while connected. */
|
|
184
|
+
#reconcile = new MorphRenderWatcher(() => this.#reconcileRows());
|
|
132
185
|
#observer = null;
|
|
133
|
-
/** The selection last settled: on connect, by the user, or by a reported pass. */
|
|
134
186
|
#settled = /* @__PURE__ */ new Map();
|
|
135
|
-
/**
|
|
136
|
-
* Establishes a single tab stop across all navigable cells/headers, brings the
|
|
137
|
-
* rows to their baseline, and settles the selection without reporting it.
|
|
138
|
-
*
|
|
139
|
-
* The whole baseline runs here rather than in the Value callbacks: Stimulus
|
|
140
|
-
* delivers those before `connect()`, where the pass is refused, and a
|
|
141
|
-
* re-attached element reuses its cached context, whose value observer does not
|
|
142
|
-
* fire again.
|
|
143
|
-
*/
|
|
144
187
|
connect() {
|
|
145
188
|
this.#restoreBaseline();
|
|
146
189
|
this.#settled = this.#selection();
|
|
147
|
-
this.#reconcile.
|
|
190
|
+
this.#reconcile.observe(this.element);
|
|
148
191
|
this.#observeRows();
|
|
149
192
|
}
|
|
150
|
-
/** Releases the row observer and drops a queued pass, so neither outlives the element. */
|
|
151
193
|
disconnect() {
|
|
152
|
-
this.#reconcile.
|
|
194
|
+
this.#reconcile.disconnect();
|
|
153
195
|
this.#observer?.disconnect();
|
|
154
196
|
this.#observer = null;
|
|
155
197
|
}
|
|
156
|
-
/**
|
|
157
|
-
* Rebuilds the DOM-owned baselines from the live grid: exactly one navigable
|
|
158
|
-
* cell is in the Tab sequence, `aria-multiselectable` follows `selection`, every
|
|
159
|
-
* selectable row carries an explicit `aria-selected`, and the fields mirror the
|
|
160
|
-
* selected rows.
|
|
161
|
-
*
|
|
162
|
-
* The tab stop keeps whichever cell already holds it, so a rebuild triggered by
|
|
163
|
-
* an unrelated row arriving does not throw the user's position away; only when
|
|
164
|
-
* no cell holds it — the grid is fresh, or the holder was removed — does the
|
|
165
|
-
* first navigable cell take over. Without that fallback a grid whose active row
|
|
166
|
-
* is removed keeps every cell at `-1` and drops out of the Tab sequence
|
|
167
|
-
* entirely.
|
|
168
|
-
*/
|
|
169
198
|
#restoreBaseline() {
|
|
170
199
|
const cells = this.#navigableCells();
|
|
171
|
-
const
|
|
200
|
+
const holder = this.#stopHolder;
|
|
201
|
+
const kept = holder !== null && holder.tabIndex === 0 && cells.includes(holder);
|
|
202
|
+
const active = (kept ? holder : cells.find((cell) => cell.tabIndex === 0)) ?? cells[0];
|
|
172
203
|
if (active) this.#setActiveCell(active, { focus: false }, cells);
|
|
173
204
|
this.#syncSelectable();
|
|
174
205
|
const pageWrote = this.#takePageRecords();
|
|
@@ -176,83 +207,49 @@ var DataGridController = class extends Controller {
|
|
|
176
207
|
this.#dropOwnRecords(pageWrote);
|
|
177
208
|
this.#mirrorFields(false);
|
|
178
209
|
}
|
|
179
|
-
/**
|
|
180
|
-
* The pass the page's changes run: the baselines are rebuilt, then a selection
|
|
181
|
-
* that moved is reported. The report comes last, so rows a subscriber rewrites
|
|
182
|
-
* are the next pass's to settle.
|
|
183
|
-
*/
|
|
184
210
|
#reconcileRows() {
|
|
185
211
|
this.#restoreBaseline();
|
|
186
212
|
this.#reportMove();
|
|
187
213
|
}
|
|
188
|
-
/**
|
|
189
|
-
* Reports the selection as `reconcile` when it differs from the one last
|
|
190
|
-
* settled. The settled selection is replaced before dispatching, so a move a
|
|
191
|
-
* subscriber makes is measured against what it was told.
|
|
192
|
-
*/
|
|
193
214
|
#reportMove() {
|
|
194
215
|
const selection = this.#selection();
|
|
195
216
|
if (sameSelection(selection, this.#settled)) return;
|
|
196
217
|
this.#settled = selection;
|
|
197
218
|
this.dispatch("reconcile", { detail: { rows: [...selection.keys()] } });
|
|
198
219
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Re-renders `aria-multiselectable`, the rows and the fields when application
|
|
201
|
-
* code (or a Turbo morph) changes `selection` at runtime; a selection the new
|
|
202
|
-
* mode collapses is reported as `reconcile`.
|
|
203
|
-
*/
|
|
204
220
|
selectionValueChanged() {
|
|
205
221
|
this.#reconcile.schedule();
|
|
206
222
|
}
|
|
207
|
-
/** Rebuilds the submitted fields when the public name changes at runtime. */
|
|
208
223
|
nameValueChanged() {
|
|
209
224
|
this.#reconcile.schedule();
|
|
210
225
|
}
|
|
211
|
-
/** Repoints the submitted fields when the owning form changes at runtime. */
|
|
212
226
|
formValueChanged() {
|
|
213
227
|
this.#reconcile.schedule();
|
|
214
228
|
}
|
|
215
|
-
/** Seeds a fields container inserted after connect from the current selection. */
|
|
216
229
|
fieldsTargetConnected() {
|
|
217
230
|
this.#reconcile.schedule();
|
|
218
231
|
}
|
|
219
|
-
|
|
232
|
+
fieldsTargetDisconnected() {
|
|
233
|
+
this.#reconcile.schedule();
|
|
234
|
+
}
|
|
220
235
|
rowTargetConnected() {
|
|
221
236
|
this.#reconcile.schedule();
|
|
222
237
|
}
|
|
223
|
-
/** Settles the selection again when a row leaves the grid or drops its target token. */
|
|
224
238
|
rowTargetDisconnected() {
|
|
225
239
|
this.#reconcile.schedule();
|
|
226
240
|
}
|
|
227
|
-
/** Re-establishes the tab stop when a cell joins the grid after connect. */
|
|
228
241
|
cellTargetConnected() {
|
|
229
242
|
this.#reconcile.schedule();
|
|
230
243
|
}
|
|
231
|
-
/** Re-establishes the tab stop when a cell leaves the grid. */
|
|
232
244
|
cellTargetDisconnected() {
|
|
233
245
|
this.#reconcile.schedule();
|
|
234
246
|
}
|
|
235
|
-
/** Re-establishes the tab stop when a header joins the grid after connect. */
|
|
236
247
|
columnHeaderTargetConnected() {
|
|
237
248
|
this.#reconcile.schedule();
|
|
238
249
|
}
|
|
239
|
-
/** Re-establishes the tab stop when a header leaves the grid. */
|
|
240
250
|
columnHeaderTargetDisconnected() {
|
|
241
251
|
this.#reconcile.schedule();
|
|
242
252
|
}
|
|
243
|
-
/**
|
|
244
|
-
* Brings the authored rows to the shape the APG requires, without changing
|
|
245
|
-
* which rows the author chose.
|
|
246
|
-
*
|
|
247
|
-
* Every selectable row gets an explicit value — an absent `aria-selected` means
|
|
248
|
-
* "not selectable" in ARIA, so a forgotten attribute hides a selectable row —
|
|
249
|
-
* and a single-select grid keeps at most one `true`, first in DOM order.
|
|
250
|
-
* A grid that declares `selection="none"` has no selectable rows, so the
|
|
251
|
-
* attribute is removed rather than written: in ARIA its absence is what "not
|
|
252
|
-
* selectable" looks like.
|
|
253
|
-
*
|
|
254
|
-
* @stimeoRenderRoot
|
|
255
|
-
*/
|
|
256
253
|
#normalizeSelection() {
|
|
257
254
|
const rows = this.rowTargets;
|
|
258
255
|
if (this.selectionValue === "none") {
|
|
@@ -269,13 +266,6 @@ var DataGridController = class extends Controller {
|
|
|
269
266
|
}
|
|
270
267
|
}
|
|
271
268
|
}
|
|
272
|
-
/**
|
|
273
|
-
* Mirrors `selection="multiple"` onto `aria-multiselectable` (APG Grid) so SRs
|
|
274
|
-
* announce that more than one row can be selected; cleared for single/none so a
|
|
275
|
-
* grid never carries a misleading attribute.
|
|
276
|
-
*
|
|
277
|
-
* @stimeoRenderRoot
|
|
278
|
-
*/
|
|
279
269
|
#syncSelectable() {
|
|
280
270
|
if (this.selectionValue === "multiple") {
|
|
281
271
|
setAttributeIfChanged(this.element, "aria-multiselectable", "true");
|
|
@@ -283,30 +273,32 @@ var DataGridController = class extends Controller {
|
|
|
283
273
|
this.element.removeAttribute("aria-multiselectable");
|
|
284
274
|
}
|
|
285
275
|
}
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
276
|
+
sort(source) {
|
|
277
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
278
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
279
|
+
return;
|
|
280
|
+
const header = host?.closest(targetSelector(this.identifier, "columnHeader"));
|
|
281
|
+
if (!header || !this.columnHeaderTargets.includes(header)) return;
|
|
282
|
+
if (event?.defaultPrevented) return;
|
|
283
|
+
const control = event ? this.#claimingControl(event, header) : null;
|
|
292
284
|
if (control && !(control instanceof HTMLButtonElement)) return;
|
|
293
285
|
const direction = nextSortDirection(header.getAttribute("aria-sort") ?? "none");
|
|
294
286
|
for (const other of this.columnHeaderTargets) {
|
|
295
287
|
other.setAttribute("aria-sort", other === header ? direction : "none");
|
|
296
288
|
}
|
|
297
289
|
this.#setActiveCell(header, { focus: false });
|
|
298
|
-
this.dispatch("sort", { detail: { column: header, direction } });
|
|
290
|
+
this.dispatch("sort", { detail: { column: header, direction, reason } });
|
|
299
291
|
}
|
|
300
|
-
|
|
301
|
-
|
|
292
|
+
toggleSelect(source) {
|
|
293
|
+
const { event, host, origin, reason } = actionSource(source);
|
|
294
|
+
if (origin && origin.closest(`[data-controller~="${this.identifier}"]`) !== this.element)
|
|
295
|
+
return;
|
|
302
296
|
if (this.selectionValue === "none") return;
|
|
303
|
-
if (event
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (row && this.rowTargets.includes(row)) this.#toggleRow(row);
|
|
297
|
+
if (event?.defaultPrevented) return;
|
|
298
|
+
if (event && host instanceof HTMLElement && this.#claimedByDescendant(event, host)) return;
|
|
299
|
+
const row = host?.closest(targetSelector(this.identifier, "row"));
|
|
300
|
+
if (row && this.rowTargets.includes(row)) this.#toggleRow(row, reason);
|
|
308
301
|
}
|
|
309
|
-
/** Grid navigation + sort/select activation. Bound to cells and headers. */
|
|
310
302
|
onKeydown(event) {
|
|
311
303
|
if (event.defaultPrevented) return;
|
|
312
304
|
if (isReservedArrowChord(event)) return;
|
|
@@ -351,38 +343,15 @@ var DataGridController = class extends Controller {
|
|
|
351
343
|
this.#setActiveCell(target, { focus: true }, matrix.flat());
|
|
352
344
|
}
|
|
353
345
|
}
|
|
354
|
-
/**
|
|
355
|
-
* Whether the event was addressed to a control inside `host` rather than to the
|
|
356
|
-
* grid.
|
|
357
|
-
*
|
|
358
|
-
* Cells and headers hold consumer markup, and APG's grid pattern expects that
|
|
359
|
-
* markup to include working controls — a row action button, an inline editor.
|
|
360
|
-
* Those own their own keystrokes and clicks, so the grid stands down entirely
|
|
361
|
-
* rather than acting in parallel. An editable host (its `contenteditable` state
|
|
362
|
-
* is inherited, so the walk is explicit) counts the same way.
|
|
363
|
-
*/
|
|
364
346
|
#claimedByDescendant(event, host) {
|
|
365
347
|
return this.#claimingControl(event, host) !== null;
|
|
366
348
|
}
|
|
367
|
-
/**
|
|
368
|
-
* The nested control this event belongs to, or `null` when the host owns it.
|
|
369
|
-
*
|
|
370
|
-
* Naming the control, rather than answering yes or no, is what lets the click
|
|
371
|
-
* path treat a sortable header's `<button>` as the activation it is while every
|
|
372
|
-
* other control still takes the event away.
|
|
373
|
-
*/
|
|
374
349
|
#claimingControl(event, host) {
|
|
375
350
|
const source = event.target;
|
|
376
351
|
const control = source.closest(INTERACTIVE_HOST_SELECTOR);
|
|
377
352
|
if (control && host.contains(control)) return control;
|
|
378
353
|
return isInteractiveHost(source) ? source : null;
|
|
379
354
|
}
|
|
380
|
-
/**
|
|
381
|
-
* Performs a header's sort or a cell row's selection toggle on activation.
|
|
382
|
-
*
|
|
383
|
-
* @stimeoRuntimeOnly `selection` decides whether this one activation toggles the row; the sort it
|
|
384
|
-
* cycles does not depend on it.
|
|
385
|
-
*/
|
|
386
355
|
#activate(cell) {
|
|
387
356
|
if (this.columnHeaderTargets.includes(cell)) {
|
|
388
357
|
this.#cycleSort(cell);
|
|
@@ -392,20 +361,14 @@ var DataGridController = class extends Controller {
|
|
|
392
361
|
const row = cell.closest("[role='row']");
|
|
393
362
|
if (row && this.rowTargets.includes(row)) this.#toggleRow(row);
|
|
394
363
|
}
|
|
395
|
-
/** Cycles a header's sort on keyboard activation and emits `sort`. */
|
|
396
364
|
#cycleSort(header) {
|
|
397
365
|
const direction = nextSortDirection(header.getAttribute("aria-sort") ?? "none");
|
|
398
366
|
for (const other of this.columnHeaderTargets) {
|
|
399
367
|
other.setAttribute("aria-sort", other === header ? direction : "none");
|
|
400
368
|
}
|
|
401
|
-
this.dispatch("sort", { detail: { column: header, direction } });
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
* Toggles a row's `aria-selected`, honoring single vs. multiple selection.
|
|
405
|
-
*
|
|
406
|
-
* @stimeoRuntimeOnly `selection` decides how this one toggle treats the other rows.
|
|
407
|
-
*/
|
|
408
|
-
#toggleRow(row) {
|
|
369
|
+
this.dispatch("sort", { detail: { column: header, direction, reason: "user" } });
|
|
370
|
+
}
|
|
371
|
+
#toggleRow(row, reason = "user") {
|
|
409
372
|
const selected = row.getAttribute("aria-selected") === "true";
|
|
410
373
|
const pageWrote = this.#takePageRecords();
|
|
411
374
|
if (this.selectionValue === "single") {
|
|
@@ -415,22 +378,23 @@ var DataGridController = class extends Controller {
|
|
|
415
378
|
}
|
|
416
379
|
row.setAttribute("aria-selected", selected ? "false" : "true");
|
|
417
380
|
this.#dropOwnRecords(pageWrote);
|
|
418
|
-
|
|
381
|
+
const selection = this.#selection();
|
|
382
|
+
const changed = !sameSelection(selection, this.#settled);
|
|
383
|
+
this.#settled = selection;
|
|
384
|
+
if (!changed) {
|
|
385
|
+
this.#mirrorFields(false);
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const token = this.#moves.record();
|
|
419
389
|
this.#mirrorFields(true);
|
|
420
|
-
|
|
390
|
+
if (!this.#moves.isLatest(token)) return;
|
|
391
|
+
this.dispatch("selectionchange", { detail: { rows: [...selection.keys()], reason } });
|
|
421
392
|
}
|
|
422
|
-
/** The selected rows in DOM order, each with the `data-value` it submits, if any. */
|
|
423
393
|
#selection() {
|
|
424
394
|
return new Map(
|
|
425
395
|
this.rowTargets.filter((row) => row.getAttribute("aria-selected") === "true").map((row) => [row, row.dataset.value])
|
|
426
396
|
);
|
|
427
397
|
}
|
|
428
|
-
/**
|
|
429
|
-
* Watches the rows' `aria-selected` and `data-value`. Every write the grid makes
|
|
430
|
-
* to them sits between a take and a drop of the observer's queue, so each record
|
|
431
|
-
* the callback receives is the page's, and only a record on one of the grid's own
|
|
432
|
-
* rows schedules a pass.
|
|
433
|
-
*/
|
|
434
398
|
#observeRows() {
|
|
435
399
|
const observer = new MutationObserver((records) => {
|
|
436
400
|
if (this.#concernsRows(records)) this.#reconcile.schedule();
|
|
@@ -442,40 +406,18 @@ var DataGridController = class extends Controller {
|
|
|
442
406
|
});
|
|
443
407
|
this.#observer = observer;
|
|
444
408
|
}
|
|
445
|
-
/**
|
|
446
|
-
* Empties the observer's queue before the grid writes to the rows, and says
|
|
447
|
-
* whether the page had queued anything about them. Those records still owe a
|
|
448
|
-
* pass: a toggle in `multiple` mode rewrites only its own row, so it does not fold
|
|
449
|
-
* in what the page wrote to the others.
|
|
450
|
-
*/
|
|
451
409
|
#takePageRecords() {
|
|
452
410
|
const records = this.#observer?.takeRecords() ?? [];
|
|
453
411
|
return records.length > 0 && this.#concernsRows(records);
|
|
454
412
|
}
|
|
455
|
-
/**
|
|
456
|
-
* Whether any record is about one of this grid's own rows. A widget nested in a
|
|
457
|
-
* cell writes the same attributes on its own elements, and those are no reason
|
|
458
|
-
* to reconcile the grid.
|
|
459
|
-
*/
|
|
460
413
|
#concernsRows(records) {
|
|
461
414
|
const rows = new Set(this.rowTargets);
|
|
462
415
|
return records.some((record) => rows.has(record.target));
|
|
463
416
|
}
|
|
464
|
-
/**
|
|
465
|
-
* Drops the records the grid's own writes just queued, so the observer never
|
|
466
|
-
* takes them for the page's, then schedules the pass the page's records owe.
|
|
467
|
-
*/
|
|
468
417
|
#dropOwnRecords(pageWrote) {
|
|
469
418
|
this.#observer?.takeRecords();
|
|
470
419
|
if (pageWrote) this.#reconcile.schedule();
|
|
471
420
|
}
|
|
472
|
-
/**
|
|
473
|
-
* Mirrors the selected rows' `data-value` into the optional fields container.
|
|
474
|
-
* A selected row without one submits nothing, so a grid whose rows carry no
|
|
475
|
-
* value submits an empty set rather than a row of blanks.
|
|
476
|
-
*
|
|
477
|
-
* @stimeoRenderRoot
|
|
478
|
-
*/
|
|
479
421
|
#mirrorFields(notify) {
|
|
480
422
|
if (!this.hasFieldsTarget) return;
|
|
481
423
|
const values = this.rowTargets.filter((row) => row.getAttribute("aria-selected") === "true").map((row) => row.dataset.value).filter((value) => value !== void 0);
|
|
@@ -484,17 +426,8 @@ var DataGridController = class extends Controller {
|
|
|
484
426
|
commitField(this.fieldsTarget);
|
|
485
427
|
}
|
|
486
428
|
}
|
|
487
|
-
/**
|
|
488
|
-
* Makes `cell` the single tabbable cell (roving) and optionally focuses it.
|
|
489
|
-
*
|
|
490
|
-
* `cells` lets a caller that already walked the grid hand its collection over,
|
|
491
|
-
* so one keystroke rebuilds the matrix once instead of twice. The write is
|
|
492
|
-
* skipped where the attribute already holds the wanted value — comparing the
|
|
493
|
-
* attribute rather than the IDL property, because a cell with no `tabindex` at
|
|
494
|
-
* all reports `-1` and would then never receive the attribute it needs to be
|
|
495
|
-
* focusable.
|
|
496
|
-
*/
|
|
497
429
|
#setActiveCell(cell, { focus }, cells) {
|
|
430
|
+
this.#stopHolder = cell;
|
|
498
431
|
for (const candidate of cells ?? this.#navigableCells()) {
|
|
499
432
|
const wanted = candidate === cell ? "0" : "-1";
|
|
500
433
|
if (candidate.getAttribute("tabindex") !== wanted) {
|
|
@@ -503,11 +436,9 @@ var DataGridController = class extends Controller {
|
|
|
503
436
|
}
|
|
504
437
|
if (focus) cell.focus();
|
|
505
438
|
}
|
|
506
|
-
/** All navigable elements (headers + cells) in DOM order. */
|
|
507
439
|
#navigableCells() {
|
|
508
440
|
return this.#matrix().flat();
|
|
509
441
|
}
|
|
510
|
-
/** The grid as rows of navigable cells, derived from each `role="row"`. */
|
|
511
442
|
#matrix() {
|
|
512
443
|
const navigable = /* @__PURE__ */ new Set([...this.columnHeaderTargets, ...this.cellTargets]);
|
|
513
444
|
const rows = Array.from(this.element.querySelectorAll("[role='row']"));
|
|
@@ -517,7 +448,6 @@ var DataGridController = class extends Controller {
|
|
|
517
448
|
)
|
|
518
449
|
).filter((cells) => cells.length > 0);
|
|
519
450
|
}
|
|
520
|
-
/** Finds `[rowIndex, colIndex]` of `cell` within `matrix`, or null. */
|
|
521
451
|
#locate(matrix, cell) {
|
|
522
452
|
for (let row = 0; row < matrix.length; row++) {
|
|
523
453
|
const col = (matrix[row] ?? []).indexOf(cell);
|
|
@@ -525,7 +455,6 @@ var DataGridController = class extends Controller {
|
|
|
525
455
|
}
|
|
526
456
|
return null;
|
|
527
457
|
}
|
|
528
|
-
/** The cell at `[row, col]`, clamping `col` to that row's last cell. */
|
|
529
458
|
#cellInRow(matrix, row, col) {
|
|
530
459
|
const cells = matrix[row];
|
|
531
460
|
if (!cells || cells.length === 0) return void 0;
|
|
@@ -534,5 +463,4 @@ var DataGridController = class extends Controller {
|
|
|
534
463
|
};
|
|
535
464
|
|
|
536
465
|
export { DataGridController };
|
|
537
|
-
//# sourceMappingURL=data_grid_controller.js.map
|
|
538
466
|
//# sourceMappingURL=data_grid_controller.js.map
|