stimeo-ui 0.8.0 → 0.9.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 +89 -0
- data/dist/controllers/bulk_select_controller.js +139 -28
- data/dist/controllers/clipboard_controller.js +102 -20
- data/dist/controllers/color_picker_controller.js +180 -43
- data/dist/controllers/data_grid_controller.js +150 -24
- data/dist/controllers/editable_controller.js +83 -30
- data/dist/controllers/filter_controller.js +32 -1
- data/dist/controllers/masonry_controller.js +129 -17
- data/dist/controllers/otp_controller.js +29 -16
- data/dist/controllers/reset_before_cache_controller.js +51 -5
- data/dist/controllers/resizable_controller.js +128 -55
- data/dist/index.js +860 -339
- data/lib/stimeo/ui/version.rb +1 -1
- metadata +2 -2
|
@@ -8,6 +8,39 @@ function isReservedArrowChord(event, allow = []) {
|
|
|
8
8
|
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
// src/utils/microtask_coalescer.ts
|
|
12
|
+
var MicrotaskCoalescer = class {
|
|
13
|
+
#run;
|
|
14
|
+
#queued = false;
|
|
15
|
+
#active = false;
|
|
16
|
+
#generation = 0;
|
|
17
|
+
/** @param run - the single reconciliation pass, invoked at most once per batch. */
|
|
18
|
+
constructor(run) {
|
|
19
|
+
this.#run = run;
|
|
20
|
+
}
|
|
21
|
+
/** Opens the window in which {@link schedule} is honoured; call from `connect()`. */
|
|
22
|
+
activate() {
|
|
23
|
+
this.#active = true;
|
|
24
|
+
}
|
|
25
|
+
/** Closes the window and drops any pending pass; call from `disconnect()`. */
|
|
26
|
+
cancel() {
|
|
27
|
+
this.#active = false;
|
|
28
|
+
this.#queued = false;
|
|
29
|
+
this.#generation += 1;
|
|
30
|
+
}
|
|
31
|
+
/** Requests one pass after the batch settles. Idempotent; inert outside the window. */
|
|
32
|
+
schedule() {
|
|
33
|
+
if (!this.#active || this.#queued) return;
|
|
34
|
+
this.#queued = true;
|
|
35
|
+
const generation = this.#generation;
|
|
36
|
+
queueMicrotask(() => {
|
|
37
|
+
if (generation !== this.#generation || !this.#queued || !this.#active) return;
|
|
38
|
+
this.#queued = false;
|
|
39
|
+
this.#run();
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
|
|
11
44
|
// src/utils/before_cache_reset.ts
|
|
12
45
|
var BeforeCacheReset = class _BeforeCacheReset {
|
|
13
46
|
/** Every subscribed instance, iterated by the one shared document listener. */
|
|
@@ -69,26 +102,34 @@ var TabindexLoan = class {
|
|
|
69
102
|
};
|
|
70
103
|
|
|
71
104
|
// src/controllers/resizable_controller.ts
|
|
105
|
+
var DEFAULT_MIN = 0;
|
|
106
|
+
var DEFAULT_MAX = 100;
|
|
107
|
+
var DEFAULT_VALUE = 50;
|
|
108
|
+
var DEFAULT_STEP = 1;
|
|
72
109
|
var ResizableController = class extends Controller {
|
|
73
110
|
static targets = ["primary", "secondary", "separator"];
|
|
74
111
|
static values = {
|
|
75
|
-
min: { type: Number, default:
|
|
76
|
-
max: { type: Number, default:
|
|
77
|
-
step: { type: Number, default:
|
|
78
|
-
value: { type: Number, default:
|
|
112
|
+
min: { type: Number, default: DEFAULT_MIN },
|
|
113
|
+
max: { type: Number, default: DEFAULT_MAX },
|
|
114
|
+
step: { type: Number, default: DEFAULT_STEP },
|
|
115
|
+
value: { type: Number, default: DEFAULT_VALUE }
|
|
79
116
|
};
|
|
80
117
|
static actions = ["onKeydown", "onPointerDown", "toggle"];
|
|
81
118
|
static events = ["change"];
|
|
82
|
-
/**
|
|
83
|
-
#valueBeforeCollapse =
|
|
119
|
+
/** Where the divider sat before the current collapse; `null` when unknown. */
|
|
120
|
+
#valueBeforeCollapse = null;
|
|
84
121
|
/** Aborts in-progress pointer-drag listeners when the drag ends or on teardown. */
|
|
85
122
|
#dragAbort = null;
|
|
86
123
|
/** `tabindex` lent to a pane so `F6` can put focus on it; panes carry none. */
|
|
87
124
|
#paneTabindex = new TabindexLoan();
|
|
88
125
|
/** Releases the pane cycle listener bound in {@link connect}. */
|
|
89
126
|
#cycleAbort = null;
|
|
127
|
+
/** Folds the range and position inputs of one morph batch into a single paint. */
|
|
128
|
+
#repaint = new MicrotaskCoalescer(() => this.#render());
|
|
90
129
|
connect() {
|
|
91
|
-
this
|
|
130
|
+
this.element.removeAttribute("data-dragging");
|
|
131
|
+
this.#repaint.activate();
|
|
132
|
+
this.#render();
|
|
92
133
|
this.#cycleAbort = new AbortController();
|
|
93
134
|
this.element.addEventListener("keydown", this.#onCycleKeydown, {
|
|
94
135
|
signal: this.#cycleAbort.signal
|
|
@@ -100,17 +141,19 @@ var ResizableController = class extends Controller {
|
|
|
100
141
|
this.#dragAbort = null;
|
|
101
142
|
this.#cycleAbort?.abort();
|
|
102
143
|
this.#cycleAbort = null;
|
|
144
|
+
this.#repaint.cancel();
|
|
103
145
|
this.#paneTabindex.returnAll();
|
|
146
|
+
this.element.removeAttribute("data-dragging");
|
|
104
147
|
}
|
|
105
148
|
/** Moves focus to the next pane on `F6`, wrapping; entering at the first one. */
|
|
106
149
|
#onCycleKeydown = (event) => {
|
|
107
150
|
if (event.key !== "F6") return;
|
|
151
|
+
if (event.isComposing) return;
|
|
108
152
|
if (event.defaultPrevented) return;
|
|
109
153
|
if (event.altKey || event.ctrlKey || event.metaKey || event.shiftKey) return;
|
|
110
154
|
const panes = [];
|
|
111
155
|
if (this.hasPrimaryTarget) panes.push(this.primaryTarget);
|
|
112
156
|
if (this.hasSecondaryTarget) panes.push(this.secondaryTarget);
|
|
113
|
-
if (panes.length === 0) return;
|
|
114
157
|
const target = event.target;
|
|
115
158
|
const current = panes.findIndex((pane) => target instanceof Node && pane.contains(target));
|
|
116
159
|
const next = panes[(current + 1) % panes.length];
|
|
@@ -119,12 +162,21 @@ var ResizableController = class extends Controller {
|
|
|
119
162
|
this.#paneTabindex.lend(next);
|
|
120
163
|
next.focus();
|
|
121
164
|
};
|
|
122
|
-
/**
|
|
123
|
-
* Stimulus lifecycle callback when the valueValue changes.
|
|
124
|
-
* Keeps CSS fractions and ARIA status completely aligned.
|
|
125
|
-
*/
|
|
165
|
+
/** Re-renders when application code (or a Turbo morph) changes `value` at runtime. */
|
|
126
166
|
valueValueChanged() {
|
|
127
|
-
this.#
|
|
167
|
+
this.#repaint.schedule();
|
|
168
|
+
}
|
|
169
|
+
/** Re-renders when application code (or a Turbo morph) changes `min` at runtime. */
|
|
170
|
+
minValueChanged() {
|
|
171
|
+
this.#repaint.schedule();
|
|
172
|
+
}
|
|
173
|
+
/** Re-renders when application code (or a Turbo morph) changes `max` at runtime. */
|
|
174
|
+
maxValueChanged() {
|
|
175
|
+
this.#repaint.schedule();
|
|
176
|
+
}
|
|
177
|
+
/** Re-publishes range and position onto a separator swapped in after connect. */
|
|
178
|
+
separatorTargetConnected() {
|
|
179
|
+
this.#repaint.schedule();
|
|
128
180
|
}
|
|
129
181
|
/** Starts active pointer drag tracking and locks capture. */
|
|
130
182
|
onPointerDown(event) {
|
|
@@ -145,44 +197,44 @@ var ResizableController = class extends Controller {
|
|
|
145
197
|
onKeydown(event) {
|
|
146
198
|
if (isReservedArrowChord(event)) return;
|
|
147
199
|
if (!this.hasSeparatorTarget) return;
|
|
148
|
-
const
|
|
149
|
-
const isVertical =
|
|
200
|
+
const { min, max } = this.#range;
|
|
201
|
+
const isVertical = this.#isVertical;
|
|
150
202
|
let handled = true;
|
|
151
|
-
let nextValue = this
|
|
203
|
+
let nextValue = this.#position;
|
|
152
204
|
switch (event.key) {
|
|
153
205
|
case "ArrowLeft":
|
|
154
206
|
if (isVertical) {
|
|
155
|
-
nextValue -= this
|
|
207
|
+
nextValue -= this.#step;
|
|
156
208
|
} else {
|
|
157
209
|
handled = false;
|
|
158
210
|
}
|
|
159
211
|
break;
|
|
160
212
|
case "ArrowRight":
|
|
161
213
|
if (isVertical) {
|
|
162
|
-
nextValue += this
|
|
214
|
+
nextValue += this.#step;
|
|
163
215
|
} else {
|
|
164
216
|
handled = false;
|
|
165
217
|
}
|
|
166
218
|
break;
|
|
167
219
|
case "ArrowUp":
|
|
168
220
|
if (!isVertical) {
|
|
169
|
-
nextValue -= this
|
|
221
|
+
nextValue -= this.#step;
|
|
170
222
|
} else {
|
|
171
223
|
handled = false;
|
|
172
224
|
}
|
|
173
225
|
break;
|
|
174
226
|
case "ArrowDown":
|
|
175
227
|
if (!isVertical) {
|
|
176
|
-
nextValue += this
|
|
228
|
+
nextValue += this.#step;
|
|
177
229
|
} else {
|
|
178
230
|
handled = false;
|
|
179
231
|
}
|
|
180
232
|
break;
|
|
181
233
|
case "Home":
|
|
182
|
-
nextValue =
|
|
234
|
+
nextValue = min;
|
|
183
235
|
break;
|
|
184
236
|
case "End":
|
|
185
|
-
nextValue =
|
|
237
|
+
nextValue = max;
|
|
186
238
|
break;
|
|
187
239
|
case "Enter":
|
|
188
240
|
event.preventDefault();
|
|
@@ -194,65 +246,86 @@ var ResizableController = class extends Controller {
|
|
|
194
246
|
}
|
|
195
247
|
if (handled) {
|
|
196
248
|
event.preventDefault();
|
|
197
|
-
this.valueValue = Math.max(
|
|
198
|
-
this.#
|
|
249
|
+
this.valueValue = Math.max(min, Math.min(nextValue, max));
|
|
250
|
+
this.#render();
|
|
199
251
|
this.#dispatchChange();
|
|
200
252
|
}
|
|
201
253
|
}
|
|
202
|
-
/** Double-click or Enter to collapse
|
|
254
|
+
/** Double-click or Enter to collapse the primary pane, or put it back. */
|
|
203
255
|
toggle() {
|
|
204
|
-
const
|
|
205
|
-
if (this
|
|
206
|
-
this.#valueBeforeCollapse = this
|
|
207
|
-
this.valueValue =
|
|
256
|
+
const { min, max } = this.#range;
|
|
257
|
+
if (this.#position > min) {
|
|
258
|
+
this.#valueBeforeCollapse = this.#position;
|
|
259
|
+
this.valueValue = min;
|
|
208
260
|
} else {
|
|
209
|
-
this.valueValue = this.#valueBeforeCollapse
|
|
261
|
+
this.valueValue = this.#valueBeforeCollapse ?? max;
|
|
262
|
+
this.#valueBeforeCollapse = null;
|
|
210
263
|
}
|
|
211
|
-
this.#
|
|
264
|
+
this.#render();
|
|
212
265
|
this.#dispatchChange();
|
|
213
266
|
}
|
|
214
267
|
#onPointerMove = (event) => {
|
|
215
268
|
if (!this.hasSeparatorTarget) return;
|
|
216
269
|
const rect = this.element.getBoundingClientRect();
|
|
217
|
-
const
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
} else {
|
|
223
|
-
fraction = (event.clientY - rect.top) / rect.height;
|
|
224
|
-
}
|
|
225
|
-
fraction = Math.max(0, Math.min(fraction, 1));
|
|
226
|
-
const percent = Math.round(fraction * 100);
|
|
227
|
-
this.valueValue = Math.max(this.minValue, Math.min(percent, this.maxValue));
|
|
228
|
-
this.#clampAndSync();
|
|
270
|
+
const raw = this.#isVertical ? (event.clientX - rect.left) / rect.width : (event.clientY - rect.top) / rect.height;
|
|
271
|
+
const fraction = Number.isFinite(raw) ? Math.max(0, Math.min(raw, 1)) : 0;
|
|
272
|
+
const { min, max } = this.#range;
|
|
273
|
+
this.valueValue = Math.max(min, Math.min(Math.round(fraction * 100), max));
|
|
274
|
+
this.#render();
|
|
229
275
|
};
|
|
230
276
|
#onPointerUp = (event) => {
|
|
231
|
-
if (!this.hasSeparatorTarget) return;
|
|
232
|
-
const separator = this.separatorTarget;
|
|
233
|
-
separator.releasePointerCapture(event.pointerId);
|
|
234
277
|
this.element.removeAttribute("data-dragging");
|
|
235
278
|
this.#dragAbort?.abort();
|
|
236
279
|
this.#dragAbort = null;
|
|
280
|
+
if (this.hasSeparatorTarget) {
|
|
281
|
+
this.separatorTarget.releasePointerCapture(event.pointerId);
|
|
282
|
+
}
|
|
237
283
|
this.#dispatchChange();
|
|
238
284
|
};
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
285
|
+
/**
|
|
286
|
+
* Publishes the position: the fraction consumer CSS multiplies a pane by, and
|
|
287
|
+
* the range assistive tech reads off the separator. The clamped position is
|
|
288
|
+
* written back to the Value so it, ARIA, CSS, and dispatched events agree.
|
|
289
|
+
*
|
|
290
|
+
* @stimeoRenderRoot
|
|
291
|
+
*/
|
|
292
|
+
#render() {
|
|
293
|
+
const { min, max } = this.#range;
|
|
294
|
+
const position = this.#position;
|
|
295
|
+
if (this.valueValue !== position) {
|
|
296
|
+
this.valueValue = position;
|
|
243
297
|
}
|
|
244
|
-
|
|
245
|
-
this.element.style.setProperty("--stimeo--resizable-fraction", String(fraction));
|
|
298
|
+
this.element.style.setProperty("--stimeo--resizable-fraction", String(position / 100));
|
|
246
299
|
if (this.hasSeparatorTarget) {
|
|
247
|
-
this.separatorTarget.setAttribute("aria-valuenow", String(
|
|
248
|
-
this.separatorTarget.setAttribute("aria-valuemin", String(
|
|
249
|
-
this.separatorTarget.setAttribute("aria-valuemax", String(
|
|
300
|
+
this.separatorTarget.setAttribute("aria-valuenow", String(position));
|
|
301
|
+
this.separatorTarget.setAttribute("aria-valuemin", String(min));
|
|
302
|
+
this.separatorTarget.setAttribute("aria-valuemax", String(max));
|
|
250
303
|
}
|
|
251
304
|
}
|
|
252
305
|
#dispatchChange() {
|
|
253
306
|
const fraction = this.valueValue / 100;
|
|
254
307
|
this.dispatch("change", { detail: { value: this.valueValue, fraction } });
|
|
255
308
|
}
|
|
309
|
+
/** The declared range after validation; an unreadable bound uses its default. */
|
|
310
|
+
get #range() {
|
|
311
|
+
const min = Number.isFinite(this.minValue) ? this.minValue : DEFAULT_MIN;
|
|
312
|
+
const max = Number.isFinite(this.maxValue) ? this.maxValue : DEFAULT_MAX;
|
|
313
|
+
return { min, max: Math.max(min, max) };
|
|
314
|
+
}
|
|
315
|
+
/** The declared position, clamped into the validated range. */
|
|
316
|
+
get #position() {
|
|
317
|
+
const { min, max } = this.#range;
|
|
318
|
+
const value = Number.isFinite(this.valueValue) ? this.valueValue : DEFAULT_VALUE;
|
|
319
|
+
return Math.max(min, Math.min(value, max));
|
|
320
|
+
}
|
|
321
|
+
/** The keyboard increment; a non-positive or unreadable one uses the default. */
|
|
322
|
+
get #step() {
|
|
323
|
+
return Number.isFinite(this.stepValue) && this.stepValue > 0 ? this.stepValue : DEFAULT_STEP;
|
|
324
|
+
}
|
|
325
|
+
/** Whether the divider runs vertically; a separator that does not say is horizontal. */
|
|
326
|
+
get #isVertical() {
|
|
327
|
+
return this.hasSeparatorTarget && this.separatorTarget.getAttribute("aria-orientation") === "vertical";
|
|
328
|
+
}
|
|
256
329
|
};
|
|
257
330
|
|
|
258
331
|
export { ResizableController };
|