stimeo-ui 0.12.0 → 0.13.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.
- package/CHANGELOG.md +37 -0
- package/dist/cable/index.d.ts +67 -13
- package/dist/cable/index.js +438 -152
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +1 -1
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +1 -1
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.d.ts +1 -1
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +2 -1
- package/dist/controllers/calendar_controller.js +10 -2
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.js +1 -1
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +1 -1
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +1 -1
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +3 -3
- package/dist/controllers/command_palette_controller.js +2 -2
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +2 -2
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +1 -1
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +1 -1
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +1 -1
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.js +10 -10
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +1 -1
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +2 -2
- package/dist/controllers/drawer_controller.js +2 -2
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +1 -1
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +2 -2
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +3 -3
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +1 -1
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +1 -1
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.js.map +1 -1
- package/dist/controllers/local_time_controller.d.ts +1 -1
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +1 -1
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +1 -1
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +2 -2
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +1 -1
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.d.ts +23 -10
- package/dist/controllers/optimistic_controller.js +141 -20
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +1 -1
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +2 -1
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +1 -1
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +2 -2
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +1 -1
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +3 -3
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/roving_controller.d.ts +1 -1
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +1 -1
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +2 -2
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +3 -3
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +3 -3
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +3 -2
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/toast_controller.js +1 -1
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.d.ts +1 -1
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +1 -1
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +1 -1
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +4 -3
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.js +101 -38
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +1 -1
- package/dist/inspector/cli.js +1 -1
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +1 -1
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +1 -1
- package/dist/inspector/manifest.json +6 -3
- package/package.json +1 -1
package/dist/cable/index.js
CHANGED
|
@@ -3,6 +3,23 @@ import { createConsumer } from '@rails/actioncable';
|
|
|
3
3
|
|
|
4
4
|
// src/cable/live_counter_controller.ts
|
|
5
5
|
|
|
6
|
+
// src/utils/announce.ts
|
|
7
|
+
function announce(message, options = {}) {
|
|
8
|
+
const text = message.trim();
|
|
9
|
+
if (text.length === 0) return;
|
|
10
|
+
window.dispatchEvent(
|
|
11
|
+
new CustomEvent("stimeo--announcer:announce", {
|
|
12
|
+
detail: { message: text, assertive: options.assertive === true }
|
|
13
|
+
})
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
function fillTemplate(template, values) {
|
|
17
|
+
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
18
|
+
const replacement = values[name];
|
|
19
|
+
return replacement === void 0 ? match : String(replacement);
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
6
23
|
// src/utils/authored_integer.ts
|
|
7
24
|
var TOKEN_BODY = /\d[\d.,'’’ _]*\d|\d/;
|
|
8
25
|
var SIGN_OPENER = /[\s([]/;
|
|
@@ -24,6 +41,129 @@ function authoredInteger(text) {
|
|
|
24
41
|
const magnitude = Number.parseInt(digits, 10);
|
|
25
42
|
return signed ? -magnitude || 0 : magnitude;
|
|
26
43
|
}
|
|
44
|
+
|
|
45
|
+
// src/utils/blur_deferral.ts
|
|
46
|
+
var BlurDeferral = class {
|
|
47
|
+
/** Elements currently holding an update back, mapped to their `blur` listener. */
|
|
48
|
+
#pending = /* @__PURE__ */ new Map();
|
|
49
|
+
/** Called after a pending element blurs and has been detached. */
|
|
50
|
+
#onRelease;
|
|
51
|
+
/** @param onRelease - Invoked once `element` actually blurs; never on `release`. */
|
|
52
|
+
constructor(onRelease) {
|
|
53
|
+
this.#onRelease = onRelease;
|
|
54
|
+
}
|
|
55
|
+
/** Number of elements currently holding an update back. */
|
|
56
|
+
get size() {
|
|
57
|
+
return this.#pending.size;
|
|
58
|
+
}
|
|
59
|
+
/** Snapshot of the pending elements, safe to iterate while releasing them. */
|
|
60
|
+
get elements() {
|
|
61
|
+
return [...this.#pending.keys()];
|
|
62
|
+
}
|
|
63
|
+
/** Whether `element` is currently holding an update back. */
|
|
64
|
+
has(element) {
|
|
65
|
+
return this.#pending.has(element);
|
|
66
|
+
}
|
|
67
|
+
/** Holds an update back until `element` blurs. Idempotent (no stacked listeners). */
|
|
68
|
+
defer(element) {
|
|
69
|
+
if (this.#pending.has(element)) return;
|
|
70
|
+
const onBlur = () => {
|
|
71
|
+
this.#detach(element);
|
|
72
|
+
this.#onRelease(element);
|
|
73
|
+
};
|
|
74
|
+
this.#pending.set(element, onBlur);
|
|
75
|
+
element.addEventListener("blur", onBlur);
|
|
76
|
+
}
|
|
77
|
+
/** Defers `element` as the only pending entry, cancelling any others. */
|
|
78
|
+
deferOnly(element) {
|
|
79
|
+
for (const pending of this.elements) {
|
|
80
|
+
if (pending !== element) this.#detach(pending);
|
|
81
|
+
}
|
|
82
|
+
this.defer(element);
|
|
83
|
+
}
|
|
84
|
+
/** Cancels `element`'s deferral without completing it; no-ops when not pending. */
|
|
85
|
+
release(element) {
|
|
86
|
+
this.#detach(element);
|
|
87
|
+
}
|
|
88
|
+
/** Cancels every deferral without completing any of them. */
|
|
89
|
+
releaseAll() {
|
|
90
|
+
for (const element of this.elements) this.#detach(element);
|
|
91
|
+
}
|
|
92
|
+
/** Removes the `blur` listener for `element` and forgets it. */
|
|
93
|
+
#detach(element) {
|
|
94
|
+
const onBlur = this.#pending.get(element);
|
|
95
|
+
if (onBlur) element.removeEventListener("blur", onBlur);
|
|
96
|
+
this.#pending.delete(element);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/utils/safe_timeout.ts
|
|
101
|
+
var MAX_TIMER_DELAY_MS = 2147483647;
|
|
102
|
+
var TimerRegistry = class {
|
|
103
|
+
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
104
|
+
ids = /* @__PURE__ */ new Set();
|
|
105
|
+
/**
|
|
106
|
+
* Cancels a single tracked timer.
|
|
107
|
+
*
|
|
108
|
+
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
109
|
+
* registry), so callers can clear defensively without guarding.
|
|
110
|
+
*/
|
|
111
|
+
clear(id) {
|
|
112
|
+
if (this.ids.delete(id)) {
|
|
113
|
+
this.cancel(id);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
118
|
+
* to guarantee no timer outlives the element.
|
|
119
|
+
*/
|
|
120
|
+
clearAll() {
|
|
121
|
+
for (const id of this.ids) {
|
|
122
|
+
this.cancel(id);
|
|
123
|
+
}
|
|
124
|
+
this.ids.clear();
|
|
125
|
+
}
|
|
126
|
+
/** Number of timers currently tracked (pending). */
|
|
127
|
+
get size() {
|
|
128
|
+
return this.ids.size;
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
var SafeTimeout = class extends TimerRegistry {
|
|
132
|
+
/**
|
|
133
|
+
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
134
|
+
*
|
|
135
|
+
* The id is removed from the registry automatically when the timeout fires,
|
|
136
|
+
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
137
|
+
*/
|
|
138
|
+
set(callback, delay) {
|
|
139
|
+
const id = this.schedule(() => {
|
|
140
|
+
this.ids.delete(id);
|
|
141
|
+
callback();
|
|
142
|
+
}, delay);
|
|
143
|
+
this.ids.add(id);
|
|
144
|
+
return id;
|
|
145
|
+
}
|
|
146
|
+
schedule(callback, delay) {
|
|
147
|
+
return window.setTimeout(callback, delay);
|
|
148
|
+
}
|
|
149
|
+
cancel(id) {
|
|
150
|
+
window.clearTimeout(id);
|
|
151
|
+
}
|
|
152
|
+
};
|
|
153
|
+
var SafeInterval = class extends TimerRegistry {
|
|
154
|
+
/** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
|
|
155
|
+
set(callback, delay) {
|
|
156
|
+
const id = this.schedule(callback, delay);
|
|
157
|
+
this.ids.add(id);
|
|
158
|
+
return id;
|
|
159
|
+
}
|
|
160
|
+
schedule(callback, delay) {
|
|
161
|
+
return window.setInterval(callback, delay);
|
|
162
|
+
}
|
|
163
|
+
cancel(id) {
|
|
164
|
+
window.clearInterval(id);
|
|
165
|
+
}
|
|
166
|
+
};
|
|
27
167
|
function parseSubscriptionParams(raw) {
|
|
28
168
|
try {
|
|
29
169
|
const parsed = JSON.parse(raw);
|
|
@@ -131,92 +271,186 @@ function openSharedSubscription(consumer, channel) {
|
|
|
131
271
|
|
|
132
272
|
// src/cable/live_counter_controller.ts
|
|
133
273
|
var DISABLED_MARKER = "data-live-counter-disabled";
|
|
134
|
-
var LiveCounterController = class extends Controller {
|
|
274
|
+
var LiveCounterController = class _LiveCounterController extends Controller {
|
|
135
275
|
static targets = ["value", "trigger"];
|
|
136
276
|
static values = {
|
|
137
277
|
channel: { type: String, default: "" },
|
|
138
278
|
params: { type: String, default: "" },
|
|
139
|
-
id: { type: String, default: "" }
|
|
279
|
+
id: { type: String, default: "" },
|
|
280
|
+
announceText: { type: String, default: "" }
|
|
140
281
|
};
|
|
282
|
+
/** Collapses a burst of broadcasts into one announcement. */
|
|
283
|
+
static #announceDelay = 200;
|
|
141
284
|
static actions = ["increment"];
|
|
142
285
|
static events = ["change"];
|
|
143
286
|
/** Identifier parameters parsed once from their declaration, never in the hot path. */
|
|
144
287
|
#params = {};
|
|
145
288
|
#subscription = null;
|
|
289
|
+
#connected = false;
|
|
146
290
|
/**
|
|
147
|
-
*
|
|
291
|
+
* The optimistic bumps this client has yet to see echoed back, in the order it
|
|
292
|
+
* sent them. An echo belongs to the send that caused it, and the amount is what
|
|
293
|
+
* names that send: a counter that guessed nothing — or guessed a different
|
|
294
|
+
* amount — applies the delta like any other, so siblings and other tabs
|
|
295
|
+
* converge whatever step each of them uses.
|
|
296
|
+
*/
|
|
297
|
+
#outstanding = [];
|
|
298
|
+
/** Holds a disable back while the trigger owns focus, so focus is never taken away. */
|
|
299
|
+
#focusedTriggers = new BlurDeferral((trigger) => this.#syncTrigger(trigger));
|
|
300
|
+
#timers = new SafeTimeout();
|
|
301
|
+
/**
|
|
302
|
+
* Re-parses the identifier parameters and moves the subscription to the
|
|
303
|
+
* identifier they now name.
|
|
148
304
|
*
|
|
149
305
|
* A malformed declaration falls back to no parameters, so the identifier keeps
|
|
150
306
|
* naming the channel instead of the subscription never being created at all.
|
|
151
307
|
*/
|
|
152
308
|
paramsValueChanged() {
|
|
153
309
|
this.#params = parseSubscriptionParams(this.paramsValue);
|
|
310
|
+
this.#resubscribe();
|
|
311
|
+
}
|
|
312
|
+
/** Moves the subscription when the declaration names a different channel. */
|
|
313
|
+
channelValueChanged() {
|
|
314
|
+
this.#resubscribe();
|
|
154
315
|
}
|
|
155
316
|
connect() {
|
|
317
|
+
this.#connected = true;
|
|
318
|
+
this.#subscribe();
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Opens the subscription the declaration names, and reflects the gate.
|
|
322
|
+
*
|
|
323
|
+
* The rejected hook is transient server state: a Turbo cache snapshot must not
|
|
324
|
+
* resurrect it — the fresh subscription re-decides it. An outstanding guess
|
|
325
|
+
* belongs to the identifier that is going away, so it is dropped with it.
|
|
326
|
+
*/
|
|
327
|
+
#subscribe() {
|
|
156
328
|
this.element.removeAttribute("data-live-counter-rejected");
|
|
329
|
+
this.#outstanding.length = 0;
|
|
157
330
|
if (this.channelValue) {
|
|
158
|
-
this.#subscription =
|
|
159
|
-
{ channel: this.channelValue, ...this.#params },
|
|
160
|
-
{
|
|
161
|
-
connected: () => this.#syncTriggers(),
|
|
162
|
-
// A drop closes the send window (the shared subscription tracks it)
|
|
163
|
-
// until Action Cable reconnects and re-confirms — an increment during
|
|
164
|
-
// the outage would bump the display while its perform() is silently
|
|
165
|
-
// discarded by the closed socket.
|
|
166
|
-
disconnected: () => this.#syncTriggers(),
|
|
167
|
-
// The server refused the subscription (auth, bad params): the gate
|
|
168
|
-
// stays shut for good, and the hook lets the consumer's CSS disable
|
|
169
|
-
// or hide the trigger instead of leaving a silently dead button.
|
|
170
|
-
rejected: () => {
|
|
171
|
-
this.element.setAttribute("data-live-counter-rejected", "true");
|
|
172
|
-
this.#syncTriggers();
|
|
173
|
-
},
|
|
174
|
-
received: (data) => this.#onReceived(data)
|
|
175
|
-
}
|
|
176
|
-
);
|
|
331
|
+
this.#subscription = this.#open();
|
|
177
332
|
}
|
|
178
333
|
this.#syncTriggers();
|
|
179
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Opens the wire subscription, or returns null when it cannot be opened.
|
|
337
|
+
*
|
|
338
|
+
* A consumer that cannot be built (no Action Cable meta tag, a boot-order
|
|
339
|
+
* mistake) would otherwise throw out of `connect()` and leave the element
|
|
340
|
+
* half-wired: no gate reflected, and a trigger that looks live. The gate reads
|
|
341
|
+
* the declaration, so a null subscription keeps it shut and the display never
|
|
342
|
+
* moves past what the server can receive.
|
|
343
|
+
*/
|
|
344
|
+
#open() {
|
|
345
|
+
try {
|
|
346
|
+
return createConfirmedSubscription(this.#descriptor, {
|
|
347
|
+
connected: () => this.#syncTriggers(),
|
|
348
|
+
// A drop closes the send window (the shared subscription tracks it)
|
|
349
|
+
// until Action Cable reconnects and re-confirms — an increment during
|
|
350
|
+
// the outage would bump the display while its perform() is silently
|
|
351
|
+
// discarded by the closed socket.
|
|
352
|
+
disconnected: () => this.#syncTriggers(),
|
|
353
|
+
// The server refused the subscription (auth, bad params): the gate
|
|
354
|
+
// stays shut for good, and the hook lets the consumer's CSS disable
|
|
355
|
+
// or hide the trigger instead of leaving a silently dead button.
|
|
356
|
+
rejected: () => {
|
|
357
|
+
this.element.setAttribute("data-live-counter-rejected", "true");
|
|
358
|
+
this.#syncTriggers();
|
|
359
|
+
},
|
|
360
|
+
received: (data) => this.#onReceived(data)
|
|
361
|
+
});
|
|
362
|
+
} catch (error) {
|
|
363
|
+
console.warn("Stimeo UI: the live counter could not open its subscription.", error);
|
|
364
|
+
return null;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* The identifier this declaration names.
|
|
369
|
+
*
|
|
370
|
+
* `channel` comes first so the identifier text matches what Action Cable
|
|
371
|
+
* builds, and a `channel` key inside `params` cannot displace the declared
|
|
372
|
+
* one — `params` names *additional* identifier parameters.
|
|
373
|
+
*/
|
|
374
|
+
get #descriptor() {
|
|
375
|
+
const { channel: _ignored, ...rest } = this.#params;
|
|
376
|
+
return { channel: this.channelValue, ...rest };
|
|
377
|
+
}
|
|
378
|
+
/** Moves to the identifier the declaration now names. */
|
|
379
|
+
#resubscribe() {
|
|
380
|
+
if (!this.#connected) return;
|
|
381
|
+
this.#subscription?.unsubscribe();
|
|
382
|
+
this.#subscription = null;
|
|
383
|
+
this.#subscribe();
|
|
384
|
+
}
|
|
180
385
|
disconnect() {
|
|
386
|
+
this.#connected = false;
|
|
387
|
+
this.#timers.clearAll();
|
|
388
|
+
this.#focusedTriggers.releaseAll();
|
|
181
389
|
this.#subscription?.unsubscribe();
|
|
182
390
|
this.#subscription = null;
|
|
183
391
|
this.element.removeAttribute("data-live-counter-rejected");
|
|
392
|
+
for (const trigger of this.triggerTargets) this.#release(trigger);
|
|
184
393
|
}
|
|
185
394
|
/** Late-added triggers (e.g. via a Turbo Stream) pick up the current gate. */
|
|
186
395
|
triggerTargetConnected(target) {
|
|
187
396
|
this.#syncTrigger(target);
|
|
188
397
|
}
|
|
398
|
+
/** A trigger that stops being one keeps nothing this controller lent it. */
|
|
399
|
+
triggerTargetDisconnected(target) {
|
|
400
|
+
this.#focusedTriggers.release(target);
|
|
401
|
+
this.#release(target);
|
|
402
|
+
}
|
|
189
403
|
/**
|
|
190
404
|
* Optimistic local increment: bumps the display immediately, then asks the
|
|
191
405
|
* server to persist and broadcast. The step comes from the action param
|
|
192
406
|
* (`data-stimeo--live-counter-delta-param`), default 1. Bound via `data-action`.
|
|
193
407
|
*/
|
|
194
408
|
increment(event) {
|
|
195
|
-
if (
|
|
409
|
+
if (!this.#ready) return;
|
|
196
410
|
const raw = Number(event?.params?.delta ?? 1);
|
|
197
|
-
const delta = Number.
|
|
198
|
-
if (
|
|
411
|
+
const delta = Number.isSafeInteger(raw) ? raw : 1;
|
|
412
|
+
if (this.#local) {
|
|
413
|
+
this.#write(this.#current + delta);
|
|
414
|
+
} else if (this.idValue !== "") {
|
|
415
|
+
this.#outstanding.push(delta);
|
|
199
416
|
this.#write(this.#current + delta);
|
|
200
417
|
}
|
|
201
418
|
this.#subscription?.perform("increment", { id: this.idValue, delta });
|
|
202
419
|
}
|
|
203
|
-
/**
|
|
420
|
+
/**
|
|
421
|
+
* Reconciles a broadcast: absolute `count` wins; the echo of this client's own
|
|
422
|
+
* guess is skipped.
|
|
423
|
+
*
|
|
424
|
+
* Only whole numbers reach the display. The wire carries values another client
|
|
425
|
+
* named and the server relayed, and the display is the state this controller
|
|
426
|
+
* reads back — a fraction, an infinity or a NaN would be written as text that
|
|
427
|
+
* reads back as a different number, or as none at all.
|
|
428
|
+
*/
|
|
204
429
|
#onReceived(data) {
|
|
205
430
|
const message = data;
|
|
206
|
-
if (
|
|
207
|
-
this.#
|
|
431
|
+
if (Number.isSafeInteger(message?.count)) {
|
|
432
|
+
this.#outstanding.length = 0;
|
|
433
|
+
this.#reconcile(message?.count);
|
|
208
434
|
return;
|
|
209
435
|
}
|
|
210
|
-
if (
|
|
211
|
-
|
|
436
|
+
if (Number.isSafeInteger(message?.delta)) {
|
|
437
|
+
const delta = message?.delta;
|
|
438
|
+
const own = typeof message?.by === "string" && message.by !== "" && message.by === this.idValue;
|
|
439
|
+
const guess = own ? this.#outstanding.indexOf(delta) : -1;
|
|
440
|
+
if (guess !== -1) {
|
|
441
|
+
this.#outstanding.splice(guess, 1);
|
|
212
442
|
return;
|
|
213
443
|
}
|
|
214
|
-
this.#
|
|
444
|
+
this.#reconcile(this.#current + delta);
|
|
215
445
|
}
|
|
216
446
|
}
|
|
217
|
-
/** True
|
|
447
|
+
/** True when no channel is declared: the local-only mode, with no server to diverge from. */
|
|
448
|
+
get #local() {
|
|
449
|
+
return this.channelValue === "";
|
|
450
|
+
}
|
|
451
|
+
/** True while an increment would go through (local-only counters always are). */
|
|
218
452
|
get #ready() {
|
|
219
|
-
return
|
|
453
|
+
return this.#local || (this.#subscription?.confirmed ?? false);
|
|
220
454
|
}
|
|
221
455
|
/**
|
|
222
456
|
* Reflects the send gate onto the optional `trigger` targets as the real
|
|
@@ -236,14 +470,23 @@ var LiveCounterController = class extends Controller {
|
|
|
236
470
|
*/
|
|
237
471
|
#syncTrigger(trigger) {
|
|
238
472
|
if (this.#ready) {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
473
|
+
this.#focusedTriggers.release(trigger);
|
|
474
|
+
this.#release(trigger);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (trigger.hasAttribute("disabled")) return;
|
|
478
|
+
if (trigger.contains(document.activeElement)) {
|
|
479
|
+
this.#focusedTriggers.defer(trigger);
|
|
480
|
+
return;
|
|
246
481
|
}
|
|
482
|
+
trigger.setAttribute("disabled", "");
|
|
483
|
+
trigger.setAttribute(DISABLED_MARKER, "");
|
|
484
|
+
}
|
|
485
|
+
/** Gives back a `disabled` this controller lent, leaving an authored one alone. */
|
|
486
|
+
#release(trigger) {
|
|
487
|
+
if (!trigger.hasAttribute(DISABLED_MARKER)) return;
|
|
488
|
+
trigger.removeAttribute("disabled");
|
|
489
|
+
trigger.removeAttribute(DISABLED_MARKER);
|
|
247
490
|
}
|
|
248
491
|
/** The displayed element: the `value` target, else the controller element. */
|
|
249
492
|
get #display() {
|
|
@@ -253,93 +496,59 @@ var LiveCounterController = class extends Controller {
|
|
|
253
496
|
get #current() {
|
|
254
497
|
return authoredInteger(this.#display.textContent ?? "") ?? 0;
|
|
255
498
|
}
|
|
499
|
+
/**
|
|
500
|
+
* Writes a whole count — every caller has already established that it is one —
|
|
501
|
+
* and reports whether the display actually moved.
|
|
502
|
+
*/
|
|
256
503
|
#write(count) {
|
|
257
|
-
if (count === this.#current) return;
|
|
504
|
+
if (count === this.#current) return false;
|
|
258
505
|
this.#display.textContent = String(count);
|
|
259
506
|
this.dispatch("change", { detail: { count } });
|
|
507
|
+
return true;
|
|
260
508
|
}
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
// src/utils/safe_timeout.ts
|
|
264
|
-
var TimerRegistry = class {
|
|
265
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
266
|
-
ids = /* @__PURE__ */ new Set();
|
|
267
509
|
/**
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
*
|
|
271
|
-
* registry), so callers can clear defensively without guarding.
|
|
510
|
+
* Settles the display on a count the server stated, announcing what it settled
|
|
511
|
+
* on. Announcing the optimistic bump instead would read a guess out and then
|
|
512
|
+
* correct it, so the announcement rides this path only.
|
|
272
513
|
*/
|
|
273
|
-
|
|
274
|
-
if (this
|
|
275
|
-
this.cancel(id);
|
|
276
|
-
}
|
|
514
|
+
#reconcile(count) {
|
|
515
|
+
if (this.#write(count)) this.#announce(count);
|
|
277
516
|
}
|
|
278
517
|
/**
|
|
279
|
-
*
|
|
280
|
-
* to guarantee no timer outlives the element.
|
|
281
|
-
*/
|
|
282
|
-
clearAll() {
|
|
283
|
-
for (const id of this.ids) {
|
|
284
|
-
this.cancel(id);
|
|
285
|
-
}
|
|
286
|
-
this.ids.clear();
|
|
287
|
-
}
|
|
288
|
-
/** Number of timers currently tracked (pending). */
|
|
289
|
-
get size() {
|
|
290
|
-
return this.ids.size;
|
|
291
|
-
}
|
|
292
|
-
};
|
|
293
|
-
var SafeTimeout = class extends TimerRegistry {
|
|
294
|
-
/**
|
|
295
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
518
|
+
* Hands a reconciled count to the page's shared announcer.
|
|
296
519
|
*
|
|
297
|
-
*
|
|
298
|
-
*
|
|
520
|
+
* Debounced: a burst of broadcasts is one announcement, not one per message,
|
|
521
|
+
* and only the last one is ever sent. Wording comes from the consumer and
|
|
522
|
+
* {@link announce} drops an empty message, so an undeclared template announces
|
|
523
|
+
* nothing at all — a count that ticks constantly should not interrupt a reader
|
|
524
|
+
* unless the page asked for it.
|
|
299
525
|
*/
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
},
|
|
305
|
-
this.ids.add(id);
|
|
306
|
-
return id;
|
|
307
|
-
}
|
|
308
|
-
schedule(callback, delay) {
|
|
309
|
-
return window.setTimeout(callback, delay);
|
|
310
|
-
}
|
|
311
|
-
cancel(id) {
|
|
312
|
-
window.clearTimeout(id);
|
|
313
|
-
}
|
|
314
|
-
};
|
|
315
|
-
var SafeInterval = class extends TimerRegistry {
|
|
316
|
-
/** Schedules a repeating `callback` every `delay` ms and returns the timer id. */
|
|
317
|
-
set(callback, delay) {
|
|
318
|
-
const id = this.schedule(callback, delay);
|
|
319
|
-
this.ids.add(id);
|
|
320
|
-
return id;
|
|
321
|
-
}
|
|
322
|
-
schedule(callback, delay) {
|
|
323
|
-
return window.setInterval(callback, delay);
|
|
324
|
-
}
|
|
325
|
-
cancel(id) {
|
|
326
|
-
window.clearInterval(id);
|
|
526
|
+
#announce(count) {
|
|
527
|
+
this.#timers.clearAll();
|
|
528
|
+
this.#timers.set(() => {
|
|
529
|
+
announce(fillTemplate(this.announceTextValue, { count }));
|
|
530
|
+
}, _LiveCounterController.#announceDelay);
|
|
327
531
|
}
|
|
328
532
|
};
|
|
329
|
-
|
|
330
|
-
// src/cable/presence_controller.ts
|
|
331
533
|
var BEACON_THROTTLE_MS = 2e3;
|
|
332
|
-
var
|
|
534
|
+
var DEFAULT_HEARTBEAT_MS = 15e3;
|
|
535
|
+
var DEFAULT_TIMEOUT_MS = 4e4;
|
|
536
|
+
var speakers = /* @__PURE__ */ new Map();
|
|
537
|
+
var PresenceController = class _PresenceController extends Controller {
|
|
333
538
|
static targets = ["count", "list", "template"];
|
|
334
539
|
static values = {
|
|
335
540
|
channel: { type: String, default: "" },
|
|
336
541
|
params: { type: String, default: "" },
|
|
337
542
|
id: { type: String, default: "" },
|
|
338
543
|
name: { type: String, default: "" },
|
|
339
|
-
heartbeat: { type: Number, default:
|
|
340
|
-
timeout: { type: Number, default:
|
|
544
|
+
heartbeat: { type: Number, default: DEFAULT_HEARTBEAT_MS },
|
|
545
|
+
timeout: { type: Number, default: DEFAULT_TIMEOUT_MS },
|
|
546
|
+
announceJoinText: { type: String, default: "" },
|
|
547
|
+
announceLeaveText: { type: String, default: "" }
|
|
341
548
|
};
|
|
342
549
|
static events = ["join", "leave", "change"];
|
|
550
|
+
/** Delay (ms) before one roster change is sent to the shared announcer. */
|
|
551
|
+
static #announceDelay = 200;
|
|
343
552
|
/** Identifier parameters parsed once from their declaration, never in the hot path. */
|
|
344
553
|
#params = {};
|
|
345
554
|
#subscription = null;
|
|
@@ -351,6 +560,10 @@ var PresenceController = class extends Controller {
|
|
|
351
560
|
#lastBeaconAt = 0;
|
|
352
561
|
/** Pending trailing-edge convergence beacon (at most one queued). */
|
|
353
562
|
#pendingBeacon = null;
|
|
563
|
+
/** The one outstanding announcement, so a newer roster change supersedes it. */
|
|
564
|
+
#announceId = null;
|
|
565
|
+
/** The `speakers` key this element holds, recomputed on every `connect()`. */
|
|
566
|
+
#speakerKey = "";
|
|
354
567
|
/**
|
|
355
568
|
* Re-parses the identifier parameters when the declaration changes.
|
|
356
569
|
*
|
|
@@ -360,38 +573,108 @@ var PresenceController = class extends Controller {
|
|
|
360
573
|
paramsValueChanged() {
|
|
361
574
|
this.#params = parseSubscriptionParams(this.paramsValue);
|
|
362
575
|
}
|
|
576
|
+
/**
|
|
577
|
+
* The identifier this declaration names. `params` *adds* to the identifier, so a
|
|
578
|
+
* `channel` key inside it names an extra parameter and never replaces the channel
|
|
579
|
+
* the element declares.
|
|
580
|
+
*/
|
|
581
|
+
get #descriptor() {
|
|
582
|
+
const { channel: _declared, ...rest } = this.#params;
|
|
583
|
+
return { channel: this.channelValue, ...rest };
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* The beacon period, in ms: a finite, positive number a timer can hold. Anything
|
|
587
|
+
* else names no interval — `setInterval` reads `NaN`, a negative value, `Infinity`
|
|
588
|
+
* and a value past {@link MAX_TIMER_DELAY_MS} alike as "as often as possible", so
|
|
589
|
+
* the declaration would flood the channel rather than heartbeat on it. Zero is out
|
|
590
|
+
* for the same reason, which is why this bound is tighter than the non-negative one
|
|
591
|
+
* a throttle or a plain delay can use. Such a declaration falls back to the default.
|
|
592
|
+
*/
|
|
593
|
+
get #heartbeat() {
|
|
594
|
+
const declared = this.heartbeatValue;
|
|
595
|
+
return Number.isFinite(declared) && declared > 0 && declared <= MAX_TIMER_DELAY_MS ? declared : DEFAULT_HEARTBEAT_MS;
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* The silence after which a peer is dropped, in ms: a finite, positive number a
|
|
599
|
+
* timer can hold. Anything else names no delay and every peer expires in the task
|
|
600
|
+
* that records it, so the roster could never hold anyone. Such a declaration falls
|
|
601
|
+
* back to the default.
|
|
602
|
+
*/
|
|
603
|
+
get #timeout() {
|
|
604
|
+
const declared = this.timeoutValue;
|
|
605
|
+
return Number.isFinite(declared) && declared > 0 && declared <= MAX_TIMER_DELAY_MS ? declared : DEFAULT_TIMEOUT_MS;
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Draws the roster into a list target, which arrives empty from a Turbo Stream
|
|
609
|
+
* replacement or a morph. Stimulus runs this before `connect()` on the first mount
|
|
610
|
+
* too, where the roster is still empty and clearing whatever a cache snapshot
|
|
611
|
+
* preserved is what `connect()` goes on to do anyway.
|
|
612
|
+
*/
|
|
613
|
+
listTargetConnected() {
|
|
614
|
+
for (const child of this.listTarget.querySelectorAll("[data-presence-id]")) {
|
|
615
|
+
child.remove();
|
|
616
|
+
}
|
|
617
|
+
for (const [id, peer] of this.#peers) this.#appendClone(id, peer.name);
|
|
618
|
+
}
|
|
619
|
+
/** Paints the roster size into a count target that arrives blank, for the same reason. */
|
|
620
|
+
countTargetConnected() {
|
|
621
|
+
this.countTarget.textContent = this.#countMessage(this.#peers.size);
|
|
622
|
+
}
|
|
363
623
|
connect() {
|
|
364
624
|
this.#reset();
|
|
365
625
|
this.element.removeAttribute("data-presence-rejected");
|
|
366
626
|
if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(0);
|
|
367
627
|
if (!this.channelValue) return;
|
|
368
|
-
this.#
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
rejected
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
);
|
|
383
|
-
this.#intervals.set(() => this.#beacon(true), this.heartbeatValue);
|
|
628
|
+
this.#claimSpeaker();
|
|
629
|
+
this.#subscription = createConfirmedSubscription(this.#descriptor, {
|
|
630
|
+
// The first beacon must wait for the confirmed subscription — a
|
|
631
|
+
// perform() before that is silently dropped by Action Cable. Fires
|
|
632
|
+
// again on every reconnect, so the roster self-heals after an outage.
|
|
633
|
+
connected: () => this.#beacon(true),
|
|
634
|
+
// The server refused the subscription: no beacon will ever go through,
|
|
635
|
+
// and the hook lets the consumer's CSS reflect the dead stream.
|
|
636
|
+
rejected: () => {
|
|
637
|
+
this.element.setAttribute("data-presence-rejected", "true");
|
|
638
|
+
},
|
|
639
|
+
received: (data) => this.#onReceived(data)
|
|
640
|
+
});
|
|
641
|
+
this.#intervals.set(() => this.#beacon(true), this.#heartbeat);
|
|
384
642
|
window.addEventListener("pagehide", this.#onPageHide);
|
|
385
643
|
}
|
|
386
644
|
disconnect() {
|
|
387
645
|
window.removeEventListener("pagehide", this.#onPageHide);
|
|
388
|
-
this.#sendLeaveNotice();
|
|
646
|
+
if (this.#releaseSpeaker()) this.#sendLeaveNotice();
|
|
389
647
|
this.#subscription?.unsubscribe();
|
|
390
648
|
this.#subscription = null;
|
|
391
649
|
this.#intervals.clearAll();
|
|
392
650
|
this.#reset();
|
|
393
651
|
this.element.removeAttribute("data-presence-rejected");
|
|
394
652
|
}
|
|
653
|
+
/**
|
|
654
|
+
* Registers this element as one of the voices for its peer, so a sibling on the
|
|
655
|
+
* same channel claiming the same `id` keeps the peer present until they all go.
|
|
656
|
+
* Whether the peer is worth announcing at all is {@link #sendLeaveNotice}'s
|
|
657
|
+
* question, so an element with no own `id` claims a voice like any other and is
|
|
658
|
+
* simply never heard.
|
|
659
|
+
*/
|
|
660
|
+
#claimSpeaker() {
|
|
661
|
+
this.#speakerKey = `${identifierOf(this.#descriptor)}
|
|
662
|
+
${this.idValue}`;
|
|
663
|
+
speakers.set(this.#speakerKey, (speakers.get(this.#speakerKey) ?? 0) + 1);
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Gives up the claim, reporting whether this element was the last voice for the
|
|
667
|
+
* peer. The key is spent here: an element that goes on to connect without a
|
|
668
|
+
* channel claims nothing, and a stale key would spend a voice its siblings own.
|
|
669
|
+
*/
|
|
670
|
+
#releaseSpeaker() {
|
|
671
|
+
const key = this.#speakerKey;
|
|
672
|
+
this.#speakerKey = "";
|
|
673
|
+
const remaining = (speakers.get(key) ?? 1) - 1;
|
|
674
|
+
if (remaining > 0) speakers.set(key, remaining);
|
|
675
|
+
else speakers.delete(key);
|
|
676
|
+
return remaining <= 0;
|
|
677
|
+
}
|
|
395
678
|
/**
|
|
396
679
|
* Sends the best-effort leaving notice (skipped without an own `id`, and
|
|
397
680
|
* outside the confirmed window, where Action Cable would discard it anyway).
|
|
@@ -457,11 +740,12 @@ var PresenceController = class extends Controller {
|
|
|
457
740
|
const name = typeof beacon?.name === "string" ? beacon.name : "";
|
|
458
741
|
const existing = this.#peers.get(id);
|
|
459
742
|
if (existing !== void 0) this.#timers.clear(existing.timer);
|
|
460
|
-
const timer = this.#timers.set(() => this.#drop(id), this
|
|
743
|
+
const timer = this.#timers.set(() => this.#drop(id), this.#timeout);
|
|
461
744
|
this.#peers.set(id, { name, timer });
|
|
462
745
|
if (existing === void 0) {
|
|
463
746
|
this.#appendClone(id, name);
|
|
464
747
|
this.#render();
|
|
748
|
+
this.#announce(this.announceJoinTextValue, name);
|
|
465
749
|
this.dispatch("join", { detail: { id, name } });
|
|
466
750
|
this.#beacon(false);
|
|
467
751
|
} else if (existing.name !== name) {
|
|
@@ -477,6 +761,7 @@ var PresenceController = class extends Controller {
|
|
|
477
761
|
this.#peers.delete(id);
|
|
478
762
|
this.#removeClone(id);
|
|
479
763
|
this.#render();
|
|
764
|
+
this.#announce(this.announceLeaveTextValue, peer.name);
|
|
480
765
|
this.dispatch("leave", { detail: { id } });
|
|
481
766
|
}
|
|
482
767
|
/** Reflects the roster onto the hooks + count target and emits `change`. */
|
|
@@ -487,6 +772,24 @@ var PresenceController = class extends Controller {
|
|
|
487
772
|
if (this.hasCountTarget) this.countTarget.textContent = this.#countMessage(users.length);
|
|
488
773
|
this.dispatch("change", { detail: { users } });
|
|
489
774
|
}
|
|
775
|
+
/**
|
|
776
|
+
* Hands one roster change to the page's shared announcer.
|
|
777
|
+
*
|
|
778
|
+
* Debounced: a burst of arrivals is one announcement, not one per peer — reading
|
|
779
|
+
* out every entry and exit of a busy room is the noise the default avoids. Wording
|
|
780
|
+
* comes from the consumer (`{name}` / `{count}`, where the count is the roster
|
|
781
|
+
* after the change), and an undeclared template does nothing at all — not even
|
|
782
|
+
* cancel an announcement already pending for the other direction.
|
|
783
|
+
*/
|
|
784
|
+
#announce(template, name) {
|
|
785
|
+
const message = fillTemplate(template, { name, count: this.#peers.size });
|
|
786
|
+
if (message.trim() === "") return;
|
|
787
|
+
if (this.#announceId !== null) this.#timers.clear(this.#announceId);
|
|
788
|
+
this.#announceId = this.#timers.set(() => {
|
|
789
|
+
announce(message);
|
|
790
|
+
this.#announceId = null;
|
|
791
|
+
}, _PresenceController.#announceDelay);
|
|
792
|
+
}
|
|
490
793
|
/**
|
|
491
794
|
* Builds the count copy. Localizable through `data-zero` / `data-one` /
|
|
492
795
|
* `data-other` templates on the count target (`%{count}`); the bare number is
|
|
@@ -500,12 +803,11 @@ var PresenceController = class extends Controller {
|
|
|
500
803
|
/** Appends one template clone for a newly present peer (list + template only). */
|
|
501
804
|
#appendClone(id, name) {
|
|
502
805
|
if (!this.hasListTarget || !this.hasTemplateTarget) return;
|
|
503
|
-
const
|
|
504
|
-
|
|
505
|
-
if (!root) return;
|
|
806
|
+
const root = this.templateTarget.content.firstElementChild?.cloneNode(true);
|
|
807
|
+
if (!(root instanceof Element)) return;
|
|
506
808
|
root.setAttribute("data-presence-id", id);
|
|
507
809
|
this.#fillName(root, name);
|
|
508
|
-
this.listTarget.appendChild(
|
|
810
|
+
this.listTarget.appendChild(root);
|
|
509
811
|
}
|
|
510
812
|
#updateClone(id, name) {
|
|
511
813
|
const root = this.#cloneFor(id);
|
|
@@ -533,6 +835,7 @@ var PresenceController = class extends Controller {
|
|
|
533
835
|
#reset() {
|
|
534
836
|
this.#timers.clearAll();
|
|
535
837
|
this.#pendingBeacon = null;
|
|
838
|
+
this.#announceId = null;
|
|
536
839
|
this.#peers.clear();
|
|
537
840
|
this.#lastBeaconAt = 0;
|
|
538
841
|
this.element.removeAttribute("data-present");
|
|
@@ -545,25 +848,6 @@ var PresenceController = class extends Controller {
|
|
|
545
848
|
}
|
|
546
849
|
}
|
|
547
850
|
};
|
|
548
|
-
|
|
549
|
-
// src/utils/announce.ts
|
|
550
|
-
function announce(message, options = {}) {
|
|
551
|
-
const text = message.trim();
|
|
552
|
-
if (text.length === 0) return;
|
|
553
|
-
window.dispatchEvent(
|
|
554
|
-
new CustomEvent("stimeo--announcer:announce", {
|
|
555
|
-
detail: { message: text, assertive: options.assertive === true }
|
|
556
|
-
})
|
|
557
|
-
);
|
|
558
|
-
}
|
|
559
|
-
function fillTemplate(template, values) {
|
|
560
|
-
return template.replace(/\{([a-zA-Z][a-zA-Z0-9]*)\}/g, (match, name) => {
|
|
561
|
-
const replacement = values[name];
|
|
562
|
-
return replacement === void 0 ? match : String(replacement);
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// src/cable/typing_indicator_controller.ts
|
|
567
851
|
var DEFAULT_TIMEOUT = 3e3;
|
|
568
852
|
var DEFAULT_THROTTLE = 2e3;
|
|
569
853
|
var fillTokens = (template, values) => template.replace(/%\{(name|names|count)\}/g, (match, token) => values[token] ?? match);
|
|
@@ -671,14 +955,16 @@ var TypingIndicatorController = class _TypingIndicatorController extends Control
|
|
|
671
955
|
if (added) this.#render();
|
|
672
956
|
}
|
|
673
957
|
/**
|
|
674
|
-
* The silence after which a typer is dropped, in ms: a finite, non-negative number
|
|
675
|
-
* Anything else names no delay — `setTimeout` reads `NaN`, a
|
|
676
|
-
* `Infinity` alike as "now",
|
|
677
|
-
*
|
|
958
|
+
* The silence after which a typer is dropped, in ms: a finite, non-negative number
|
|
959
|
+
* a timer can hold. Anything else names no delay — `setTimeout` reads `NaN`, a
|
|
960
|
+
* negative value and `Infinity` alike as "now", and a value past
|
|
961
|
+
* {@link MAX_TIMER_DELAY_MS} overflows to the same place — so the typer would
|
|
962
|
+
* vanish in the same task it appeared and the indicator could never be seen. Such
|
|
963
|
+
* a declaration falls back to the default.
|
|
678
964
|
*/
|
|
679
965
|
get #timeout() {
|
|
680
966
|
const declared = this.timeoutValue;
|
|
681
|
-
return Number.isFinite(declared) && declared >= 0 ? declared : DEFAULT_TIMEOUT;
|
|
967
|
+
return Number.isFinite(declared) && declared >= 0 && declared <= MAX_TIMER_DELAY_MS ? declared : DEFAULT_TIMEOUT;
|
|
682
968
|
}
|
|
683
969
|
/**
|
|
684
970
|
* The minimum gap between outgoing signals, in ms: a finite, non-negative number.
|