stimeo-ui 0.2.0 → 0.3.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 +163 -0
- data/dist/controllers/accordion_controller.js +10 -0
- data/dist/controllers/alert_dialog_controller.js +318 -0
- data/dist/controllers/breadcrumb_controller.js +225 -13
- data/dist/controllers/calendar_controller.js +89 -22
- data/dist/controllers/carousel_controller.js +313 -0
- data/dist/controllers/clipboard_controller.js +144 -0
- data/dist/controllers/collapsible_controller.js +327 -0
- data/dist/controllers/color_picker_controller.js +252 -0
- data/dist/controllers/combobox_controller.js +162 -23
- data/dist/controllers/command_palette_controller.js +194 -17
- data/dist/controllers/context_menu_controller.js +32 -10
- data/dist/controllers/count_up_controller.js +8 -1
- data/dist/controllers/currency_input_controller.js +147 -0
- data/dist/controllers/data_grid_controller.js +246 -0
- data/dist/controllers/date_range_picker_controller.js +441 -0
- data/dist/controllers/dismissible_controller.js +117 -0
- data/dist/controllers/drawer_controller.js +630 -0
- data/dist/controllers/editable_controller.js +169 -0
- data/dist/controllers/file_dropzone_controller.js +165 -0
- data/dist/controllers/filter_controller.js +86 -0
- data/dist/controllers/flash_controller.js +36 -5
- data/dist/controllers/form_validation_controller.js +1 -1
- data/dist/controllers/highlight_controller.js +6 -4
- data/dist/controllers/intersection_controller.js +67 -19
- data/dist/controllers/lazy_frame_controller.js +54 -11
- data/dist/controllers/listbox_controller.js +257 -53
- data/dist/controllers/local_time_controller.js +2 -2
- data/dist/controllers/masonry_controller.js +142 -0
- data/dist/controllers/menu_controller.js +104 -17
- data/dist/controllers/menubar_controller.js +785 -0
- data/dist/controllers/multi_select_controller.js +755 -0
- data/dist/controllers/navigation_menu_controller.js +511 -0
- data/dist/controllers/number_input_controller.js +7 -0
- data/dist/controllers/otp_controller.js +18 -1
- data/dist/controllers/overflow_indicator_controller.js +246 -27
- data/dist/controllers/overflow_menu_controller.js +381 -57
- data/dist/controllers/pagination_controller.js +163 -32
- data/dist/controllers/password_reveal_controller.js +117 -0
- data/dist/controllers/persist_controller.js +6 -6
- data/dist/controllers/pointer_drag_controller.js +9 -1
- data/dist/controllers/popover_controller.js +2 -2
- data/dist/controllers/radio_group_controller.js +22 -3
- data/dist/controllers/range_slider_controller.js +192 -0
- data/dist/controllers/rating_controller.js +16 -2
- data/dist/controllers/read_more_controller.js +238 -0
- data/dist/controllers/resizable_controller.js +65 -1
- data/dist/controllers/roving_controller.js +17 -2
- data/dist/controllers/scroll_area_controller.js +101 -14
- data/dist/controllers/scroll_restore_controller.js +93 -0
- data/dist/controllers/scroll_visibility_controller.js +40 -6
- data/dist/controllers/scrollspy_controller.js +369 -74
- data/dist/controllers/separator_controller.js +96 -0
- data/dist/controllers/sidebar_controller.js +761 -0
- data/dist/controllers/skeleton_controller.js +1 -1
- data/dist/controllers/slider_controller.js +32 -6
- data/dist/controllers/sortable_controller.js +34 -3
- data/dist/controllers/spinner_controller.js +1 -1
- data/dist/controllers/stepper_controller.js +28 -12
- data/dist/controllers/stick_to_bottom_controller.js +9 -4
- data/dist/controllers/sticky_observer_controller.js +109 -20
- data/dist/controllers/switch_controller.js +1 -0
- data/dist/controllers/tabs_controller.js +26 -3
- data/dist/controllers/tags_input_controller.js +295 -0
- data/dist/controllers/theme_controller.js +42 -13
- data/dist/controllers/time_picker_controller.js +231 -0
- data/dist/controllers/toast_controller.js +40 -14
- data/dist/controllers/toggle_group_controller.js +23 -2
- data/dist/controllers/toolbar_controller.js +230 -31
- data/dist/controllers/transition_controller.js +153 -38
- data/dist/controllers/tree_view_controller.js +691 -0
- data/dist/index.js +4256 -915
- data/lib/stimeo/ui/version.rb +2 -3
- metadata +28 -2
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/carousel_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/logical_scroll.ts
|
|
6
|
+
function isRtl(element) {
|
|
7
|
+
return window.getComputedStyle(element).direction === "rtl";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
// src/utils/arrow_step.ts
|
|
11
|
+
function logicalArrowStep(key, element) {
|
|
12
|
+
if (key === "ArrowDown") return 1;
|
|
13
|
+
if (key === "ArrowUp") return -1;
|
|
14
|
+
if (key !== "ArrowRight" && key !== "ArrowLeft") return 0;
|
|
15
|
+
const forward = isRtl(element) ? "ArrowLeft" : "ArrowRight";
|
|
16
|
+
return key === forward ? 1 : -1;
|
|
17
|
+
}
|
|
18
|
+
function isReservedArrowChord(event, allow = []) {
|
|
19
|
+
if (!event.key.startsWith("Arrow")) return false;
|
|
20
|
+
return event.altKey && !allow.includes("alt") || event.ctrlKey && !allow.includes("ctrl") || event.metaKey && !allow.includes("meta") || event.shiftKey && !allow.includes("shift");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// src/utils/roving_tabindex.ts
|
|
24
|
+
var RovingTabindex = class {
|
|
25
|
+
/** Returns the current ordered item elements; called on every operation. */
|
|
26
|
+
#getItems;
|
|
27
|
+
/**
|
|
28
|
+
* @param getItems - Returns the current ordered item elements. Called on every
|
|
29
|
+
* operation so the live target list is always used.
|
|
30
|
+
*/
|
|
31
|
+
constructor(getItems) {
|
|
32
|
+
this.#getItems = getItems;
|
|
33
|
+
}
|
|
34
|
+
/** Index of the currently tabbable item (`tabindex="0"`), or `-1` if none. */
|
|
35
|
+
get activeIndex() {
|
|
36
|
+
return this.#getItems().findIndex((item) => item.tabIndex === 0);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Makes exactly the item at `index` tabbable (`tabindex="0"`) and removes every
|
|
40
|
+
* other item from the Tab sequence (`tabindex="-1"`). An out-of-range `index`
|
|
41
|
+
* (e.g. `-1`) leaves all items at `-1`, which a controller can use to express
|
|
42
|
+
* "nothing is currently tabbable".
|
|
43
|
+
*
|
|
44
|
+
* @param index - Position of the item to make tabbable.
|
|
45
|
+
* @param options - Pass `{ focus: true }` to also move DOM focus to that item.
|
|
46
|
+
*/
|
|
47
|
+
setActive(index, { focus = false } = {}) {
|
|
48
|
+
const items = this.#getItems();
|
|
49
|
+
items.forEach((item, i) => {
|
|
50
|
+
item.tabIndex = i === index ? 0 : -1;
|
|
51
|
+
});
|
|
52
|
+
if (focus) items[index]?.focus();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
function rovingMove(current, length, delta, wrap) {
|
|
56
|
+
if (length === 0) return -1;
|
|
57
|
+
const next = current + delta;
|
|
58
|
+
return (next + length) % length;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// src/utils/safe_timeout.ts
|
|
62
|
+
var TimerRegistry = class {
|
|
63
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
64
|
+
ids = /* @__PURE__ */ new Set();
|
|
65
|
+
/**
|
|
66
|
+
* Cancels a single tracked timer.
|
|
67
|
+
*
|
|
68
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
69
|
+
* registry), so callers can clear defensively without guarding.
|
|
70
|
+
*/
|
|
71
|
+
clear(id) {
|
|
72
|
+
if (this.ids.delete(id)) {
|
|
73
|
+
this.cancel(id);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
78
|
+
* to guarantee no timer outlives the element.
|
|
79
|
+
*/
|
|
80
|
+
clearAll() {
|
|
81
|
+
for (const id of this.ids) {
|
|
82
|
+
this.cancel(id);
|
|
83
|
+
}
|
|
84
|
+
this.ids.clear();
|
|
85
|
+
}
|
|
86
|
+
/** Number of timers currently tracked (pending). */
|
|
87
|
+
get size() {
|
|
88
|
+
return this.ids.size;
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
var SafeInterval = class extends TimerRegistry {
|
|
92
|
+
/** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
|
|
93
|
+
set(callback, delay) {
|
|
94
|
+
const id = this.schedule(callback, delay);
|
|
95
|
+
this.ids.add(id);
|
|
96
|
+
return id;
|
|
97
|
+
}
|
|
98
|
+
schedule(callback, delay) {
|
|
99
|
+
return window.setInterval(callback, delay);
|
|
100
|
+
}
|
|
101
|
+
cancel(id) {
|
|
102
|
+
window.clearInterval(id);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
// src/controllers/carousel_controller.ts
|
|
107
|
+
var CarouselController = class extends Controller {
|
|
108
|
+
static targets = ["slide", "viewport", "prev", "next", "picker", "playToggle"];
|
|
109
|
+
static values = {
|
|
110
|
+
autoplay: { type: Boolean, default: false },
|
|
111
|
+
interval: { type: Number, default: 5e3 },
|
|
112
|
+
loop: { type: Boolean, default: true }
|
|
113
|
+
};
|
|
114
|
+
static actions = [
|
|
115
|
+
"goto",
|
|
116
|
+
"next",
|
|
117
|
+
"onPickerKeydown",
|
|
118
|
+
"pause",
|
|
119
|
+
"prev",
|
|
120
|
+
"resume",
|
|
121
|
+
"togglePlay"
|
|
122
|
+
];
|
|
123
|
+
static events = ["change", "pause", "play"];
|
|
124
|
+
#roving = new RovingTabindex(() => this.pickerTargets);
|
|
125
|
+
/** Gates the target callback so it does not repaint once per authored picker on mount. */
|
|
126
|
+
#connected = false;
|
|
127
|
+
#intervals = new SafeInterval();
|
|
128
|
+
/** Index of the visible slide. */
|
|
129
|
+
#index = 0;
|
|
130
|
+
/** User intent to autoplay (toggled by the play button / focus hard-stop). */
|
|
131
|
+
#playing = false;
|
|
132
|
+
/** Pointer is hovering the carousel: a temporary, auto-resuming suspension. */
|
|
133
|
+
#pointerPaused = false;
|
|
134
|
+
/** Id of the live autoplay interval, or null when stopped. */
|
|
135
|
+
#timerId = null;
|
|
136
|
+
/**
|
|
137
|
+
* Renders the initial slide and starts autoplay when requested.
|
|
138
|
+
*
|
|
139
|
+
* `findIndex` makes the authored pre-selection first-wins when several pickers
|
|
140
|
+
* are marked; `#render` then writes an explicit value onto every picker.
|
|
141
|
+
*/
|
|
142
|
+
connect() {
|
|
143
|
+
const preselected = this.pickerTargets.findIndex(
|
|
144
|
+
(picker) => picker.getAttribute("aria-selected") === "true"
|
|
145
|
+
);
|
|
146
|
+
this.#index = preselected === -1 ? 0 : preselected;
|
|
147
|
+
this.#playing = this.#initialPlaying();
|
|
148
|
+
this.#render({ focus: false });
|
|
149
|
+
this.#syncTimer();
|
|
150
|
+
this.#connected = true;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Re-establishes the single selected picker when one is added after connect.
|
|
154
|
+
*
|
|
155
|
+
* Without this an appended picker that arrives `aria-selected="true"` leaves two
|
|
156
|
+
* marked at once — the authored pre-selection is only read on connect, so
|
|
157
|
+
* nothing else ever resolves the conflict. The current slide is kept: the
|
|
158
|
+
* repaint re-derives every picker from `#index`, so a late arrival never steals
|
|
159
|
+
* the selection.
|
|
160
|
+
*/
|
|
161
|
+
pickerTargetConnected() {
|
|
162
|
+
if (!this.#connected) return;
|
|
163
|
+
this.#render({ focus: false });
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Resolves the starting autoplay intent. The play toggle's `aria-pressed` is the
|
|
167
|
+
* source of truth **when present**, so a Turbo Drive cache restore / morph that
|
|
168
|
+
* re-runs `connect()` against existing DOM does not silently resume autoplay the
|
|
169
|
+
* user had stopped (e.g. by focusing into the carousel). Only when no toggle
|
|
170
|
+
* carries `aria-pressed` does it fall back to the declarative `autoplay` value.
|
|
171
|
+
*/
|
|
172
|
+
#initialPlaying() {
|
|
173
|
+
if (this.hasPlayToggleTarget && this.playToggleTarget.hasAttribute("aria-pressed")) {
|
|
174
|
+
return this.playToggleTarget.getAttribute("aria-pressed") === "true";
|
|
175
|
+
}
|
|
176
|
+
return this.autoplayValue;
|
|
177
|
+
}
|
|
178
|
+
/** Clears the autoplay interval so it never fires after teardown. */
|
|
179
|
+
disconnect() {
|
|
180
|
+
this.#connected = false;
|
|
181
|
+
this.#intervals.clearAll();
|
|
182
|
+
this.#timerId = null;
|
|
183
|
+
}
|
|
184
|
+
/** Advances to the next slide. Bound via `data-action`. */
|
|
185
|
+
next() {
|
|
186
|
+
this.#select(this.#step(1), { focus: false });
|
|
187
|
+
}
|
|
188
|
+
/** Returns to the previous slide. Bound via `data-action`. */
|
|
189
|
+
prev() {
|
|
190
|
+
this.#select(this.#step(-1), { focus: false });
|
|
191
|
+
}
|
|
192
|
+
/** Jumps to the slide whose picker was activated (click / Enter / Space). */
|
|
193
|
+
goto(event) {
|
|
194
|
+
const target = event.currentTarget;
|
|
195
|
+
const index = this.pickerTargets.indexOf(target);
|
|
196
|
+
if (index !== -1) this.#select(index, { focus: false });
|
|
197
|
+
}
|
|
198
|
+
/** Toggles autoplay on the user's explicit request and syncs the timer. */
|
|
199
|
+
togglePlay() {
|
|
200
|
+
this.#playing = !this.#playing;
|
|
201
|
+
this.#syncTimer();
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Suspends autoplay. Hover (`mouseenter`) is a temporary suspension that resumes
|
|
205
|
+
* on leave; keyboard focus (`focusin`) is a hard stop that turns autoplay off so
|
|
206
|
+
* it cannot resume without an explicit play (WCAG 2.2.2).
|
|
207
|
+
*/
|
|
208
|
+
pause(event) {
|
|
209
|
+
if (event?.type.startsWith("focus")) {
|
|
210
|
+
this.#playing = false;
|
|
211
|
+
} else {
|
|
212
|
+
this.#pointerPaused = true;
|
|
213
|
+
}
|
|
214
|
+
this.#syncTimer();
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Lifts a hover suspension (`mouseleave`) and resumes autoplay if it is still
|
|
218
|
+
* on. A `focusout` does nothing here: the focus pause was a hard stop, so the
|
|
219
|
+
* user must press play to restart.
|
|
220
|
+
*/
|
|
221
|
+
resume(event) {
|
|
222
|
+
if (event?.type.startsWith("focus")) return;
|
|
223
|
+
this.#pointerPaused = false;
|
|
224
|
+
this.#syncTimer();
|
|
225
|
+
}
|
|
226
|
+
/** Picker roving: arrows move focus only; Home/End activate first/last slide. */
|
|
227
|
+
onPickerKeydown(event) {
|
|
228
|
+
if (event.defaultPrevented) return;
|
|
229
|
+
if (isReservedArrowChord(event)) return;
|
|
230
|
+
const current = this.pickerTargets.indexOf(event.currentTarget);
|
|
231
|
+
if (current === -1) return;
|
|
232
|
+
const length = this.pickerTargets.length;
|
|
233
|
+
switch (event.key) {
|
|
234
|
+
case "ArrowRight":
|
|
235
|
+
case "ArrowDown":
|
|
236
|
+
case "ArrowLeft":
|
|
237
|
+
case "ArrowUp": {
|
|
238
|
+
event.preventDefault();
|
|
239
|
+
const step = logicalArrowStep(event.key, this.element);
|
|
240
|
+
this.#roving.setActive(rovingMove(current, length, step), { focus: true });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
case "Home":
|
|
244
|
+
event.preventDefault();
|
|
245
|
+
this.#select(0, { focus: true });
|
|
246
|
+
return;
|
|
247
|
+
case "End":
|
|
248
|
+
event.preventDefault();
|
|
249
|
+
this.#select(length - 1, { focus: true });
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
/** Resolves the index one step away from the current one, honoring `loop`. */
|
|
254
|
+
#step(delta) {
|
|
255
|
+
const total = this.slideTargets.length;
|
|
256
|
+
if (total === 0) return 0;
|
|
257
|
+
const next = this.#index + delta;
|
|
258
|
+
if (this.loopValue) return (next + total) % total;
|
|
259
|
+
return Math.min(total - 1, Math.max(0, next));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Changes the active slide, updates state hooks, and emits `change` — but only
|
|
263
|
+
* when the index actually changes, so a `next`/`prev` clamped at the end (or an
|
|
264
|
+
* autoplay tick at a non-looping boundary) re-renders without a spurious event
|
|
265
|
+
* (matching the "emit on real change" policy of flash/masonry/bulk-select).
|
|
266
|
+
*/
|
|
267
|
+
#select(index, { focus }) {
|
|
268
|
+
const changed = index !== this.#index;
|
|
269
|
+
this.#index = index;
|
|
270
|
+
this.#render({ focus });
|
|
271
|
+
this.#syncTimer();
|
|
272
|
+
if (changed) this.dispatch("change", { detail: { index, total: this.slideTargets.length } });
|
|
273
|
+
}
|
|
274
|
+
/** Reflects `this.#index` onto slides and pickers (state hooks + roving). */
|
|
275
|
+
#render({ focus }) {
|
|
276
|
+
this.slideTargets.forEach((slide, i) => {
|
|
277
|
+
const active = i === this.#index;
|
|
278
|
+
slide.setAttribute("data-state", active ? "active" : "inactive");
|
|
279
|
+
slide.hidden = !active;
|
|
280
|
+
});
|
|
281
|
+
this.pickerTargets.forEach((picker, i) => {
|
|
282
|
+
picker.setAttribute("aria-selected", i === this.#index ? "true" : "false");
|
|
283
|
+
});
|
|
284
|
+
this.#roving.setActive(this.#index, { focus });
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Drives the autoplay interval toward the desired state. Autoplay should run
|
|
288
|
+
* only when the user wants it (`playing`), the pointer is not hovering, and more
|
|
289
|
+
* than one slide exists. Transitions emit `play`/`pause` and keep the toggle's
|
|
290
|
+
* `aria-pressed` in sync.
|
|
291
|
+
*/
|
|
292
|
+
#syncTimer() {
|
|
293
|
+
if (!this.loopValue && this.#index >= this.slideTargets.length - 1) {
|
|
294
|
+
this.#playing = false;
|
|
295
|
+
}
|
|
296
|
+
const shouldRun = this.#playing && !this.#pointerPaused && this.slideTargets.length > 1;
|
|
297
|
+
if (shouldRun && this.#timerId === null) {
|
|
298
|
+
this.#timerId = this.#intervals.set(() => this.next(), this.intervalValue);
|
|
299
|
+
this.dispatch("play");
|
|
300
|
+
} else if (!shouldRun && this.#timerId !== null) {
|
|
301
|
+
this.#intervals.clear(this.#timerId);
|
|
302
|
+
this.#timerId = null;
|
|
303
|
+
this.dispatch("pause");
|
|
304
|
+
}
|
|
305
|
+
if (this.hasPlayToggleTarget) {
|
|
306
|
+
this.playToggleTarget.setAttribute("aria-pressed", this.#playing ? "true" : "false");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
|
|
311
|
+
export { CarouselController };
|
|
312
|
+
//# sourceMappingURL=carousel_controller.js.map
|
|
313
|
+
//# sourceMappingURL=carousel_controller.js.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus';
|
|
2
|
+
|
|
3
|
+
// src/controllers/clipboard_controller.ts
|
|
4
|
+
|
|
5
|
+
// src/utils/safe_timeout.ts
|
|
6
|
+
var TimerRegistry = class {
|
|
7
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
8
|
+
ids = /* @__PURE__ */ new Set();
|
|
9
|
+
/**
|
|
10
|
+
* Cancels a single tracked timer.
|
|
11
|
+
*
|
|
12
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
13
|
+
* registry), so callers can clear defensively without guarding.
|
|
14
|
+
*/
|
|
15
|
+
clear(id) {
|
|
16
|
+
if (this.ids.delete(id)) {
|
|
17
|
+
this.cancel(id);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
22
|
+
* to guarantee no timer outlives the element.
|
|
23
|
+
*/
|
|
24
|
+
clearAll() {
|
|
25
|
+
for (const id of this.ids) {
|
|
26
|
+
this.cancel(id);
|
|
27
|
+
}
|
|
28
|
+
this.ids.clear();
|
|
29
|
+
}
|
|
30
|
+
/** Number of timers currently tracked (pending). */
|
|
31
|
+
get size() {
|
|
32
|
+
return this.ids.size;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
36
|
+
/**
|
|
37
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
38
|
+
*
|
|
39
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
40
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
41
|
+
*/
|
|
42
|
+
set(callback, delay) {
|
|
43
|
+
const id = this.schedule(() => {
|
|
44
|
+
this.ids.delete(id);
|
|
45
|
+
callback();
|
|
46
|
+
}, delay);
|
|
47
|
+
this.ids.add(id);
|
|
48
|
+
return id;
|
|
49
|
+
}
|
|
50
|
+
schedule(callback, delay) {
|
|
51
|
+
return window.setTimeout(callback, delay);
|
|
52
|
+
}
|
|
53
|
+
cancel(id) {
|
|
54
|
+
window.clearTimeout(id);
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/controllers/clipboard_controller.ts
|
|
59
|
+
var ClipboardController = class extends Controller {
|
|
60
|
+
static targets = ["source", "button", "feedback"];
|
|
61
|
+
static values = {
|
|
62
|
+
text: { type: String, default: "" },
|
|
63
|
+
feedbackDuration: { type: Number, default: 2e3 },
|
|
64
|
+
copiedLabel: { type: String, default: "Copied" },
|
|
65
|
+
errorLabel: { type: String, default: "Copy failed" }
|
|
66
|
+
};
|
|
67
|
+
static actions = ["copy"];
|
|
68
|
+
static events = ["copy"];
|
|
69
|
+
/** Auto-clear timer for the completion notice; torn down on disconnect. */
|
|
70
|
+
#timers = new SafeTimeout();
|
|
71
|
+
/**
|
|
72
|
+
* The pending auto-clear timer id, or `null` when none is scheduled. Tracked so
|
|
73
|
+
* a rapid second copy cancels the first window instead of letting a stale timer
|
|
74
|
+
* reset the freshly-shown notice early.
|
|
75
|
+
*/
|
|
76
|
+
#resetTimerId = null;
|
|
77
|
+
connect() {
|
|
78
|
+
if (!this.element.hasAttribute("data-state")) {
|
|
79
|
+
this.element.setAttribute("data-state", "idle");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
disconnect() {
|
|
83
|
+
this.#timers.clearAll();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Copies the resolved text and reports the outcome. Bound via `data-action`
|
|
87
|
+
* (click). Always dispatches `stimeo--clipboard:copy` with `{ success, text }`
|
|
88
|
+
* — including on failure — so consumers can react either way.
|
|
89
|
+
*/
|
|
90
|
+
async copy() {
|
|
91
|
+
const text = this.#resolveText();
|
|
92
|
+
let success = false;
|
|
93
|
+
try {
|
|
94
|
+
if (!navigator.clipboard?.writeText) throw new Error("Clipboard API unavailable");
|
|
95
|
+
await navigator.clipboard.writeText(text);
|
|
96
|
+
success = true;
|
|
97
|
+
} catch {
|
|
98
|
+
success = false;
|
|
99
|
+
}
|
|
100
|
+
this.#reportResult(success);
|
|
101
|
+
this.dispatch("copy", { detail: { success, text } });
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* The text to copy: the explicit `text` value when set, otherwise the source
|
|
105
|
+
* target's current value (inputs/textareas) or text content.
|
|
106
|
+
*/
|
|
107
|
+
#resolveText() {
|
|
108
|
+
if (this.textValue.length > 0) return this.textValue;
|
|
109
|
+
if (!this.hasSourceTarget) return "";
|
|
110
|
+
const source = this.sourceTarget;
|
|
111
|
+
if (source instanceof HTMLInputElement || source instanceof HTMLTextAreaElement) {
|
|
112
|
+
return source.value;
|
|
113
|
+
}
|
|
114
|
+
return source.textContent ?? "";
|
|
115
|
+
}
|
|
116
|
+
/** Reflects the result on `data-state`, announces it, and schedules a reset. */
|
|
117
|
+
#reportResult(success) {
|
|
118
|
+
this.element.setAttribute("data-state", success ? "copied" : "error");
|
|
119
|
+
if (this.hasFeedbackTarget) {
|
|
120
|
+
this.feedbackTarget.textContent = success ? this.copiedLabelValue : this.errorLabelValue;
|
|
121
|
+
}
|
|
122
|
+
if (this.#resetTimerId !== null) {
|
|
123
|
+
this.#timers.clear(this.#resetTimerId);
|
|
124
|
+
this.#resetTimerId = null;
|
|
125
|
+
}
|
|
126
|
+
if (this.feedbackDurationValue > 0) {
|
|
127
|
+
this.#resetTimerId = this.#timers.set(() => {
|
|
128
|
+
this.#resetTimerId = null;
|
|
129
|
+
this.#reset();
|
|
130
|
+
}, this.feedbackDurationValue);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Returns to the idle state and clears the completion notice. */
|
|
134
|
+
#reset() {
|
|
135
|
+
this.element.setAttribute("data-state", "idle");
|
|
136
|
+
if (this.hasFeedbackTarget) {
|
|
137
|
+
this.feedbackTarget.textContent = "";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
export { ClipboardController };
|
|
143
|
+
//# sourceMappingURL=clipboard_controller.js.map
|
|
144
|
+
//# sourceMappingURL=clipboard_controller.js.map
|