stimeo-ui 0.7.0 → 0.8.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 +62 -0
- package/dist/controllers/auto_submit_controller.d.ts +14 -3
- package/dist/controllers/auto_submit_controller.js +94 -16
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +81 -47
- package/dist/controllers/carousel_controller.js +451 -100
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +39 -5
- package/dist/controllers/currency_input_controller.js +305 -74
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +3 -1
- package/dist/controllers/direct_upload_controller.js +12 -2
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +123 -29
- package/dist/controllers/file_dropzone_controller.js +386 -63
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +3 -1
- package/dist/controllers/flash_controller.js +3 -1
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +4 -2
- package/dist/controllers/frame_loading_controller.js +2 -1
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.d.ts +42 -16
- package/dist/controllers/input_mask_controller.js +251 -76
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +51 -14
- package/dist/controllers/nested_form_controller.js +450 -42
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +61 -25
- package/dist/controllers/otp_controller.js +472 -114
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +3 -2
- package/dist/controllers/spinner_controller.js +7 -5
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +4 -1
- package/dist/controllers/submit_once_controller.js +3 -1
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +30 -10
- package/dist/controllers/textarea_autosize_controller.js +131 -3
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/index.js +1927 -607
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +8 -1
- package/dist/inspector/cli.js +3 -1
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +3 -1
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +4 -4
- package/dist/inspector/manifest.json +182 -23
- package/package.json +2 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/controllers/direct_upload_controller.ts"],"names":[],"mappings":"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","file":"direct_upload_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Detail shapes for the ActiveStorage `direct-upload:*` events. */\ninterface UploadDetail {\n id?: string | number;\n file?: { name?: string };\n progress?: number;\n error?: string;\n}\n\n/** Delay (ms) before a completed row is removed when `removeOnDone` is set. */\nconst REMOVE_DELAY = 4000;\n\n/**\n * Headless progress UI for ActiveStorage Direct Uploads: subscribes to the\n * `direct-upload:*` events and renders a per-file progress row (no dedicated APG\n * pattern; the rows follow the `role=\"progressbar\"` practice). The companion to\n * {@link \"file-dropzone\"}, which leaves transport out of scope.\n *\n * Markup contract (identifier: `stimeo--direct-upload`):\n * <div data-controller=\"stimeo--direct-upload\"\n * data-stimeo--direct-upload-announce-done-text-value=\"{name} uploaded\"\n * data-stimeo--direct-upload-announce-error-text-value=\"{name} failed\">\n * <div data-stimeo--direct-upload-target=\"list\"></div>\n * <template data-stimeo--direct-upload-target=\"row\">\n * <div role=\"progressbar\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n * <span data-field=\"name\"></span><span data-field=\"percent\"></span>\n * </div>\n * </template>\n * </div>\n *\n * For each upload it clones the `row` template into `list`, then keeps\n * `aria-valuenow` / `aria-valuetext`, the `[data-field=\"percent\"]` text, and the\n * `--stimeo--upload-progress` custom property (0–100, rounded and clamped) in\n * sync as `direct-upload:progress` arrives. The file name carried by the events\n * fills `[data-field=\"name\"]` and becomes the row's `aria-label` unless the\n * template already authors one. The aggregate across live rows is mirrored on\n * the controller element as `data-upload-progress` plus the same custom\n * property, recomputed whenever a row is added, updated, or removed, and\n * withdrawn entirely once no rows remain.\n *\n * A row leaves `uploading` exactly once: `direct-upload:error` settles it as\n * `error`, and `direct-upload:end` — which ActiveStorage fires after success\n * *and* failure — completes only rows not already settled, pinning them at 100%\n * and flipping `data-upload-state` to `done`. Later events never rewrite a\n * settled row, so a failure survives the `end` that follows it.\n *\n * Completion and failure are handed to the page's shared `stimeo--announcer` as\n * one polite message each, worded by the consumer via `announceDoneText` /\n * `announceErrorText` (`{name}` expands to the file name; empty templates stay\n * silent, keeping announcements opt-in and i18n-neutral). Per-tick progress is\n * not announced — the progressbar's `aria-valuenow` conveys it — to avoid\n * flooding. A failure this controller has rendered also cancels the\n * `direct-upload:error` default, which suppresses ActiveStorage's native\n * `alert()`; when no row could be rendered the alert stays as the fallback.\n *\n * Events dispatched on the controller element (all bubble):\n * - `stimeo--direct-upload:progress` dispatches `{ id: string, percent: number }`\n * on each progress update, after clamping.\n * - `stimeo--direct-upload:done` dispatches `{ id: string }` when an upload\n * completes successfully.\n * - `stimeo--direct-upload:error` dispatches `{ id: string, error: string }`\n * when an upload fails.\n *\n * @remarks\n * Behavior only — no bars are drawn. The `direct-upload:*` listeners live on\n * `document` (the events bubble there) and are removed on `disconnect()` (Turbo\n * navigation included), along with any pending removal timers, so a callback\n * that arrives after teardown never touches a detached row. Rows are transient\n * UI: just before Turbo caches the page they are removed and the aggregate is\n * withdrawn, so a restored snapshot starts pristine instead of replaying stale\n * rows (a dead upload cannot resume after restoration). The live DOM stays the\n * source of truth — a row removed or replaced outside this controller is\n * forgotten and rebuilt on the next event for its id, and a clone stranded\n * outside the current `list` (a re-pointed target) is removed outright, so\n * generated rows only ever live there.\n *\n * With multiple upload widgets on one page, set `scope` to a selector for the\n * owning form/root so each widget only handles its own uploads. A `scope` that\n * does not parse as a selector falls back to the default (handle all) instead\n * of throwing from the event handlers, so one broken declaration cannot silence\n * the widget.\n */\nexport class DirectUploadController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"row\"];\n static override values = {\n removeOnDone: { type: Boolean, default: false },\n announceDoneText: { type: String, default: \"\" },\n announceErrorText: { type: String, default: \"\" },\n scope: { type: String, default: \"\" },\n };\n static events = [\"progress\", \"done\", \"error\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly rowTarget: HTMLTemplateElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasRowTarget: boolean;\n\n declare removeOnDoneValue: boolean;\n declare announceDoneTextValue: string;\n declare announceErrorTextValue: string;\n declare scopeValue: string;\n\n readonly #timeouts = new SafeTimeout();\n readonly #rows = new Map<string, HTMLElement>();\n readonly #beforeCache = new BeforeCacheReset(() => this.#reset());\n\n /** The validated `scope` selector; a broken declaration falls back to `\"\"`. */\n #scopeSelector = \"\";\n\n readonly #onInitialize = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#rowFor(detail.id, this.#name(detail));\n };\n\n readonly #onProgress = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#updateProgress(this.#key(detail.id), detail.progress ?? 0, this.#name(detail));\n };\n\n readonly #onError = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n const rendered = this.#fail(this.#key(detail.id), detail.error ?? \"\", this.#name(detail));\n // ActiveStorage alert()s the raw error unless the event is cancelled. A\n // failure this widget displays is handled, so the blocking duplicate is\n // suppressed; an unrendered failure keeps the alert as its only signal.\n if (rendered) event.preventDefault();\n };\n\n readonly #onEnd = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#complete(this.#key(detail.id), this.#name(detail));\n };\n\n override connect(): void {\n document.addEventListener(\"direct-upload:initialize\", this.#onInitialize);\n document.addEventListener(\"direct-upload:progress\", this.#onProgress);\n document.addEventListener(\"direct-upload:error\", this.#onError);\n document.addEventListener(\"direct-upload:end\", this.#onEnd);\n this.#beforeCache.activate();\n // An in-page move runs disconnect() → connect() on the same instance, and\n // teardown cancelled any pending removals; completed rows re-earn theirs.\n this.#rescheduleRemovals();\n }\n\n override disconnect(): void {\n document.removeEventListener(\"direct-upload:initialize\", this.#onInitialize);\n document.removeEventListener(\"direct-upload:progress\", this.#onProgress);\n document.removeEventListener(\"direct-upload:error\", this.#onError);\n document.removeEventListener(\"direct-upload:end\", this.#onEnd);\n this.#beforeCache.deactivate();\n this.#timeouts.clearAll();\n // `#rows` is kept: `disconnect()` also fires on an in-page move, where the\n // rows travel with the element and the next event should keep updating\n // them. Stale entries self-heal via `#prune`, and the Turbo-cache path\n // clears everything in `#reset()`.\n }\n\n /** Validates `scope` once so the per-event path never parses or throws. */\n scopeValueChanged(): void {\n const selector = this.scopeValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#scopeSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#scopeSelector = \"\";\n }\n\n /** Updates a row's progress and the aggregate, emitting `progress`. */\n #updateProgress(id: string, percent: number, name: string): void {\n const row = this.#rowFor(id, name);\n if (row === null || this.#isSettled(row)) return;\n const clamped = this.#applyProgress(row, percent);\n this.#syncAggregate();\n this.dispatch(\"progress\", { detail: { id, percent: clamped } });\n }\n\n /** Marks a not-yet-settled row done at 100%, announces it, and emits `done`. */\n #complete(id: string, name: string): void {\n // Resolve lazily like `#fail`/`#updateProgress` so an `end` that arrives\n // without a prior `initialize`/`progress` (no row yet) still records the\n // completion instead of silently dropping it.\n const row = this.#rowFor(id, name);\n if (row === null || this.#isSettled(row)) return;\n row.setAttribute(\"data-upload-state\", \"done\");\n this.#applyProgress(row, 100);\n this.#syncAggregate();\n this.#announce(this.announceDoneTextValue, name, row);\n this.dispatch(\"done\", { detail: { id } });\n if (this.removeOnDoneValue) {\n this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);\n }\n }\n\n /**\n * Marks a not-yet-settled row failed, announces it, and emits `error`.\n * Returns whether the failure is rendered by this widget (used to decide the\n * `direct-upload:error` default), which also holds when the row already\n * displays an earlier failure.\n */\n #fail(id: string, error: string, name: string): boolean {\n const row = this.#rowFor(id, name);\n if (row === null) return false;\n if (this.#isSettled(row)) return row.getAttribute(\"data-upload-state\") === \"error\";\n row.setAttribute(\"data-upload-state\", \"error\");\n this.#announce(this.announceErrorTextValue, name, row);\n this.dispatch(\"error\", { detail: { id, error } });\n return true;\n }\n\n /** Whether the row reached a terminal state; settled rows are never rewritten. */\n #isSettled(row: HTMLElement): boolean {\n const state = row.getAttribute(\"data-upload-state\");\n return state === \"done\" || state === \"error\";\n }\n\n /** Re-arms `removeOnDone` for completed rows after a reconnect. */\n #rescheduleRemovals(): void {\n if (!this.removeOnDoneValue) return;\n this.#prune();\n for (const [id, row] of this.#rows) {\n if (row.getAttribute(\"data-upload-state\") === \"done\") {\n this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);\n }\n }\n }\n\n /** Returns the live row for `id`, creating (and labeling) one on first sight. */\n #rowFor(id: string | number | undefined, name: string): HTMLElement | null {\n const key = this.#key(id);\n const existing = this.#rows.get(key);\n if (existing !== undefined) {\n if (this.#tracksRow(existing)) {\n this.#applyName(existing, name);\n return existing;\n }\n // Removed, replaced, or no longer inside the current list: retire the\n // clone (removing it is a no-op when something else already did) and\n // rebuild from the live DOM.\n existing.remove();\n this.#rows.delete(key);\n }\n if (!this.hasRowTarget || !this.hasListTarget) return null;\n const clone = this.rowTarget.content.firstElementChild?.cloneNode(true);\n if (!(clone instanceof HTMLElement)) return null;\n this.#applyName(clone, name);\n clone.setAttribute(\"data-upload-state\", \"uploading\");\n this.#applyProgress(clone, 0);\n this.listTarget.appendChild(clone);\n this.#rows.set(key, clone);\n this.#syncAggregate();\n return clone;\n }\n\n /**\n * Writes the event's file name into `[data-field=\"name\"]` and, unless the row\n * already carries a non-blank `aria-label` — authored on the template or\n * applied by an earlier event — makes it the accessible name too. The visible\n * name never depends on the label: an authored label keeps its wording while\n * the field still shows which file this row tracks.\n */\n #applyName(row: HTMLElement, name: string): void {\n if (name.length === 0) return;\n this.#setField(row, \"name\", name);\n // A whitespace-only label computes to no accessible name, so only a\n // non-blank one counts as authored.\n if ((row.getAttribute(\"aria-label\") ?? \"\").trim().length > 0) return;\n row.setAttribute(\"aria-label\", name);\n }\n\n /** Writes one progress value to every per-row hook; returns the clamped percent. */\n #applyProgress(row: HTMLElement, percent: number): number {\n const clamped = Math.max(0, Math.min(100, Math.round(percent)));\n row.setAttribute(\"aria-valuenow\", String(clamped));\n row.setAttribute(\"aria-valuetext\", `${clamped}%`);\n row.style.setProperty(\"--stimeo--upload-progress\", `${clamped}%`);\n this.#setField(row, \"percent\", `${clamped}%`);\n return clamped;\n }\n\n #removeRow(id: string): void {\n const row = this.#rows.get(id);\n if (row === undefined) return;\n row.remove();\n this.#rows.delete(id);\n this.#syncAggregate();\n }\n\n /**\n * Reflects the average progress across live rows on the controller element,\n * withdrawing both hooks once no rows remain.\n */\n #syncAggregate(): void {\n this.#prune();\n if (this.#rows.size === 0) {\n this.element.removeAttribute(\"data-upload-progress\");\n this.element.style.removeProperty(\"--stimeo--upload-progress\");\n return;\n }\n let total = 0;\n for (const row of this.#rows.values()) {\n total += Number(row.getAttribute(\"aria-valuenow\") ?? \"0\");\n }\n const overall = Math.round(total / this.#rows.size);\n this.element.setAttribute(\"data-upload-progress\", String(overall));\n this.element.style.setProperty(\"--stimeo--upload-progress\", `${overall}%`);\n }\n\n /**\n * Whether a bookkept row is still this widget's live UI: connected, and — when\n * a `list` target is present — inside the *current* one, so re-pointing the\n * target attribute at a new element retires rows kept alive in the old list.\n */\n #tracksRow(row: HTMLElement): boolean {\n if (!row.isConnected) return false;\n return !this.hasListTarget || this.listTarget.contains(row);\n }\n\n /**\n * Retires rows that left the live UI (list swap, external removal). A retired\n * clone is removed outright — generated rows only ever live under the current\n * `list`, so the before-cache rewind never has an untracked leftover to miss.\n */\n #prune(): void {\n for (const [id, row] of this.#rows) {\n if (this.#tracksRow(row)) continue;\n row.remove();\n this.#rows.delete(id);\n }\n }\n\n /**\n * Returns the widget to its pre-upload state just before Turbo caches the\n * page, so the snapshot never replays rows for uploads that cannot resume.\n */\n #reset(): void {\n for (const row of this.#rows.values()) row.remove();\n this.#rows.clear();\n this.#timeouts.clearAll();\n this.#syncAggregate();\n }\n\n /**\n * Sends one consumer-worded message to the page's shared announcer. `{name}`\n * resolves to the row's displayed name first — the event that settles an\n * upload may omit the file although an earlier event already named the row —\n * then the event's own name, then the accessible name (which an authored\n * label owns, so it is the last resort, not the primary source).\n */\n #announce(template: string, name: string, row: HTMLElement): void {\n const stored = this.#field(row, \"name\")?.textContent ?? \"\";\n const label =\n stored.length > 0 ? stored : name.length > 0 ? name : (row.getAttribute(\"aria-label\") ?? \"\");\n announce(fillTemplate(template, { name: label }));\n }\n\n #field(row: HTMLElement, name: string): HTMLElement | null {\n return row.querySelector<HTMLElement>(`[data-field=\"${name}\"]`);\n }\n\n #setField(row: HTMLElement, name: string, text: string): void {\n const field = this.#field(row, name);\n if (field !== null) field.textContent = text;\n }\n\n #detail(event: Event): UploadDetail {\n return (event as CustomEvent<UploadDetail>).detail ?? {};\n }\n\n /** The file name every ActiveStorage `direct-upload:*` event carries. */\n #name(detail: UploadDetail): string {\n return detail.file?.name ?? \"\";\n }\n\n /**\n * Whether an event belongs to this controller. With `scope` set, only events\n * whose target (the file input) sits inside an element matching `scope` are\n * handled, so several upload widgets on one page do not cross-populate.\n * Resolved with `closest()` from the target itself, so the chatty `progress`\n * stream never pays a document-wide query. Empty `scope` handles all.\n */\n #inScope(event: Event): boolean {\n if (this.#scopeSelector.length === 0) return true;\n const target = event.target;\n return target instanceof Element && target.closest(this.#scopeSelector) !== null;\n }\n\n #key(id: string | number | undefined): string {\n return String(id ?? \"\");\n }\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/controllers/direct_upload_controller.ts"],"names":[],"mappings":"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","file":"direct_upload_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way (the `MicrotaskCoalescer` split).\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Detail shapes for the ActiveStorage `direct-upload:*` events. */\ninterface UploadDetail {\n id?: string | number;\n file?: { name?: string };\n progress?: number;\n error?: string;\n}\n\n/** Delay (ms) before a completed row is removed when `removeOnDone` is set. */\nconst REMOVE_DELAY = 4000;\n\n/**\n * Headless progress UI for ActiveStorage Direct Uploads: subscribes to the\n * `direct-upload:*` events and renders a per-file progress row (no dedicated APG\n * pattern; the rows follow the `role=\"progressbar\"` practice). The companion to\n * {@link \"file-dropzone\"}, which leaves transport out of scope.\n *\n * Markup contract (identifier: `stimeo--direct-upload`):\n * <div data-controller=\"stimeo--direct-upload\"\n * data-stimeo--direct-upload-announce-done-text-value=\"{name} uploaded\"\n * data-stimeo--direct-upload-announce-error-text-value=\"{name} failed\">\n * <div data-stimeo--direct-upload-target=\"list\"></div>\n * <template data-stimeo--direct-upload-target=\"row\">\n * <div role=\"progressbar\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n * <span data-field=\"name\"></span><span data-field=\"percent\"></span>\n * </div>\n * </template>\n * </div>\n *\n * For each upload it clones the `row` template into `list`, then keeps\n * `aria-valuenow` / `aria-valuetext`, the `[data-field=\"percent\"]` text, and the\n * `--stimeo--upload-progress` custom property (0–100, rounded and clamped) in\n * sync as `direct-upload:progress` arrives. The file name carried by the events\n * fills `[data-field=\"name\"]` and becomes the row's `aria-label` unless the\n * template already authors one. The aggregate across live rows is mirrored on\n * the controller element as `data-upload-progress` plus the same custom\n * property, recomputed whenever a row is added, updated, or removed, and\n * withdrawn entirely once no rows remain.\n *\n * A row leaves `uploading` exactly once: `direct-upload:error` settles it as\n * `error`, and `direct-upload:end` — which ActiveStorage fires after success\n * *and* failure — completes only rows not already settled, pinning them at 100%\n * and flipping `data-upload-state` to `done`. Later events never rewrite a\n * settled row, so a failure survives the `end` that follows it.\n *\n * Completion and failure are handed to the page's shared `stimeo--announcer` as\n * one polite message each, worded by the consumer via `announceDoneText` /\n * `announceErrorText` (`{name}` expands to the file name; empty templates stay\n * silent, keeping announcements opt-in and i18n-neutral). Per-tick progress is\n * not announced — the progressbar's `aria-valuenow` conveys it — to avoid\n * flooding. A failure this controller has rendered also cancels the\n * `direct-upload:error` default, which suppresses ActiveStorage's native\n * `alert()`; when no row could be rendered the alert stays as the fallback.\n *\n * Events dispatched on the controller element (all bubble):\n * - `stimeo--direct-upload:progress` dispatches `{ id: string, percent: number }`\n * on each progress update, after clamping.\n * - `stimeo--direct-upload:done` dispatches `{ id: string }` when an upload\n * completes successfully.\n * - `stimeo--direct-upload:reconcile` dispatches `{ ids: string[] }` — the uploads\n * the Turbo cache rewind discarded, none of which can still finish\n * - `stimeo--direct-upload:error` dispatches `{ id: string, error: string }`\n * when an upload fails.\n *\n * @remarks\n * Behavior only — no bars are drawn. The `direct-upload:*` listeners live on\n * `document` (the events bubble there) and are removed on `disconnect()` (Turbo\n * navigation included), along with any pending removal timers, so a callback\n * that arrives after teardown never touches a detached row. Rows are transient\n * UI: just before Turbo caches the page they are removed and the aggregate is\n * withdrawn, so a restored snapshot starts pristine instead of replaying stale\n * rows (a dead upload cannot resume after restoration). The live DOM stays the\n * source of truth — a row removed or replaced outside this controller is\n * forgotten and rebuilt on the next event for its id, and a clone stranded\n * outside the current `list` (a re-pointed target) is removed outright, so\n * generated rows only ever live there.\n *\n * With multiple upload widgets on one page, set `scope` to a selector for the\n * owning form/root so each widget only handles its own uploads. A `scope` that\n * does not parse as a selector falls back to the default (handle all) instead\n * of throwing from the event handlers, so one broken declaration cannot silence\n * the widget.\n */\nexport class DirectUploadController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"row\"];\n static override values = {\n removeOnDone: { type: Boolean, default: false },\n announceDoneText: { type: String, default: \"\" },\n announceErrorText: { type: String, default: \"\" },\n scope: { type: String, default: \"\" },\n };\n static events = [\"progress\", \"done\", \"error\", \"reconcile\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly rowTarget: HTMLTemplateElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasRowTarget: boolean;\n\n declare removeOnDoneValue: boolean;\n declare announceDoneTextValue: string;\n declare announceErrorTextValue: string;\n declare scopeValue: string;\n\n readonly #timeouts = new SafeTimeout();\n readonly #rows = new Map<string, HTMLElement>();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** The validated `scope` selector; a broken declaration falls back to `\"\"`. */\n #scopeSelector = \"\";\n\n readonly #onInitialize = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#rowFor(detail.id, this.#name(detail));\n };\n\n readonly #onProgress = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#updateProgress(this.#key(detail.id), detail.progress ?? 0, this.#name(detail));\n };\n\n readonly #onError = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n const rendered = this.#fail(this.#key(detail.id), detail.error ?? \"\", this.#name(detail));\n // ActiveStorage alert()s the raw error unless the event is cancelled. A\n // failure this widget displays is handled, so the blocking duplicate is\n // suppressed; an unrendered failure keeps the alert as its only signal.\n if (rendered) event.preventDefault();\n };\n\n readonly #onEnd = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#complete(this.#key(detail.id), this.#name(detail));\n };\n\n override connect(): void {\n document.addEventListener(\"direct-upload:initialize\", this.#onInitialize);\n document.addEventListener(\"direct-upload:progress\", this.#onProgress);\n document.addEventListener(\"direct-upload:error\", this.#onError);\n document.addEventListener(\"direct-upload:end\", this.#onEnd);\n this.#beforeCache.activate();\n // An in-page move runs disconnect() → connect() on the same instance, and\n // teardown cancelled any pending removals; completed rows re-earn theirs.\n this.#rescheduleRemovals();\n }\n\n override disconnect(): void {\n document.removeEventListener(\"direct-upload:initialize\", this.#onInitialize);\n document.removeEventListener(\"direct-upload:progress\", this.#onProgress);\n document.removeEventListener(\"direct-upload:error\", this.#onError);\n document.removeEventListener(\"direct-upload:end\", this.#onEnd);\n this.#beforeCache.deactivate();\n this.#timeouts.clearAll();\n // `#rows` is kept: `disconnect()` also fires on an in-page move, where the\n // rows travel with the element and the next event should keep updating\n // them. Stale entries self-heal via `#prune`, and the Turbo-cache path\n // clears everything in `#reset()`.\n }\n\n /** Validates `scope` once so the per-event path never parses or throws. */\n scopeValueChanged(): void {\n const selector = this.scopeValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#scopeSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#scopeSelector = \"\";\n }\n\n /** Updates a row's progress and the aggregate, emitting `progress`. */\n #updateProgress(id: string, percent: number, name: string): void {\n const row = this.#rowFor(id, name);\n if (row === null || this.#isSettled(row)) return;\n const clamped = this.#applyProgress(row, percent);\n this.#syncAggregate();\n this.dispatch(\"progress\", { detail: { id, percent: clamped } });\n }\n\n /** Marks a not-yet-settled row done at 100%, announces it, and emits `done`. */\n #complete(id: string, name: string): void {\n // Resolve lazily like `#fail`/`#updateProgress` so an `end` that arrives\n // without a prior `initialize`/`progress` (no row yet) still records the\n // completion instead of silently dropping it.\n const row = this.#rowFor(id, name);\n if (row === null || this.#isSettled(row)) return;\n row.setAttribute(\"data-upload-state\", \"done\");\n this.#applyProgress(row, 100);\n this.#syncAggregate();\n this.#announce(this.announceDoneTextValue, name, row);\n this.dispatch(\"done\", { detail: { id } });\n if (this.removeOnDoneValue) {\n this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);\n }\n }\n\n /**\n * Marks a not-yet-settled row failed, announces it, and emits `error`.\n * Returns whether the failure is rendered by this widget (used to decide the\n * `direct-upload:error` default), which also holds when the row already\n * displays an earlier failure.\n */\n #fail(id: string, error: string, name: string): boolean {\n const row = this.#rowFor(id, name);\n if (row === null) return false;\n if (this.#isSettled(row)) return row.getAttribute(\"data-upload-state\") === \"error\";\n row.setAttribute(\"data-upload-state\", \"error\");\n this.#announce(this.announceErrorTextValue, name, row);\n this.dispatch(\"error\", { detail: { id, error } });\n return true;\n }\n\n /** Whether the row reached a terminal state; settled rows are never rewritten. */\n #isSettled(row: HTMLElement): boolean {\n const state = row.getAttribute(\"data-upload-state\");\n return state === \"done\" || state === \"error\";\n }\n\n /** Re-arms `removeOnDone` for completed rows after a reconnect. */\n #rescheduleRemovals(): void {\n if (!this.removeOnDoneValue) return;\n this.#prune();\n for (const [id, row] of this.#rows) {\n if (row.getAttribute(\"data-upload-state\") === \"done\") {\n this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);\n }\n }\n }\n\n /** Returns the live row for `id`, creating (and labeling) one on first sight. */\n #rowFor(id: string | number | undefined, name: string): HTMLElement | null {\n const key = this.#key(id);\n const existing = this.#rows.get(key);\n if (existing !== undefined) {\n if (this.#tracksRow(existing)) {\n this.#applyName(existing, name);\n return existing;\n }\n // Removed, replaced, or no longer inside the current list: retire the\n // clone (removing it is a no-op when something else already did) and\n // rebuild from the live DOM.\n existing.remove();\n this.#rows.delete(key);\n }\n if (!this.hasRowTarget || !this.hasListTarget) return null;\n const clone = this.rowTarget.content.firstElementChild?.cloneNode(true);\n if (!(clone instanceof HTMLElement)) return null;\n this.#applyName(clone, name);\n clone.setAttribute(\"data-upload-state\", \"uploading\");\n this.#applyProgress(clone, 0);\n this.listTarget.appendChild(clone);\n this.#rows.set(key, clone);\n this.#syncAggregate();\n return clone;\n }\n\n /**\n * Writes the event's file name into `[data-field=\"name\"]` and, unless the row\n * already carries a non-blank `aria-label` — authored on the template or\n * applied by an earlier event — makes it the accessible name too. The visible\n * name never depends on the label: an authored label keeps its wording while\n * the field still shows which file this row tracks.\n */\n #applyName(row: HTMLElement, name: string): void {\n if (name.length === 0) return;\n this.#setField(row, \"name\", name);\n // A whitespace-only label computes to no accessible name, so only a\n // non-blank one counts as authored.\n if ((row.getAttribute(\"aria-label\") ?? \"\").trim().length > 0) return;\n row.setAttribute(\"aria-label\", name);\n }\n\n /** Writes one progress value to every per-row hook; returns the clamped percent. */\n #applyProgress(row: HTMLElement, percent: number): number {\n const clamped = Math.max(0, Math.min(100, Math.round(percent)));\n row.setAttribute(\"aria-valuenow\", String(clamped));\n row.setAttribute(\"aria-valuetext\", `${clamped}%`);\n row.style.setProperty(\"--stimeo--upload-progress\", `${clamped}%`);\n this.#setField(row, \"percent\", `${clamped}%`);\n return clamped;\n }\n\n #removeRow(id: string): void {\n const row = this.#rows.get(id);\n if (row === undefined) return;\n row.remove();\n this.#rows.delete(id);\n this.#syncAggregate();\n }\n\n /**\n * Reflects the average progress across live rows on the controller element,\n * withdrawing both hooks once no rows remain.\n */\n #syncAggregate(): void {\n this.#prune();\n if (this.#rows.size === 0) {\n this.element.removeAttribute(\"data-upload-progress\");\n this.element.style.removeProperty(\"--stimeo--upload-progress\");\n return;\n }\n let total = 0;\n for (const row of this.#rows.values()) {\n total += Number(row.getAttribute(\"aria-valuenow\") ?? \"0\");\n }\n const overall = Math.round(total / this.#rows.size);\n this.element.setAttribute(\"data-upload-progress\", String(overall));\n this.element.style.setProperty(\"--stimeo--upload-progress\", `${overall}%`);\n }\n\n /**\n * Whether a bookkept row is still this widget's live UI: connected, and — when\n * a `list` target is present — inside the *current* one, so re-pointing the\n * target attribute at a new element retires rows kept alive in the old list.\n */\n #tracksRow(row: HTMLElement): boolean {\n if (!row.isConnected) return false;\n return !this.hasListTarget || this.listTarget.contains(row);\n }\n\n /**\n * Retires rows that left the live UI (list swap, external removal). A retired\n * clone is removed outright — generated rows only ever live under the current\n * `list`, so the before-cache rewind never has an untracked leftover to miss.\n */\n #prune(): void {\n for (const [id, row] of this.#rows) {\n if (this.#tracksRow(row)) continue;\n row.remove();\n this.#rows.delete(id);\n }\n }\n\n /**\n * Returns the widget to its pre-upload state just before Turbo caches the\n * page, so the snapshot never replays rows for uploads that cannot resume.\n */\n /**\n * Rewinds for the snapshot and reports what that discarded. An upload in flight\n * cannot survive the navigation, so a consumer mirroring the rows would keep a\n * progress bar that never resolves.\n */\n #rewindForCache(): void {\n const ids = [...this.#rows.keys()];\n this.#reset();\n if (ids.length > 0) this.dispatch(\"reconcile\", { detail: { ids } });\n }\n\n #reset(): void {\n for (const row of this.#rows.values()) row.remove();\n this.#rows.clear();\n this.#timeouts.clearAll();\n this.#syncAggregate();\n }\n\n /**\n * Sends one consumer-worded message to the page's shared announcer. `{name}`\n * resolves to the row's displayed name first — the event that settles an\n * upload may omit the file although an earlier event already named the row —\n * then the event's own name, then the accessible name (which an authored\n * label owns, so it is the last resort, not the primary source).\n */\n #announce(template: string, name: string, row: HTMLElement): void {\n const stored = this.#field(row, \"name\")?.textContent ?? \"\";\n const label =\n stored.length > 0 ? stored : name.length > 0 ? name : (row.getAttribute(\"aria-label\") ?? \"\");\n announce(fillTemplate(template, { name: label }));\n }\n\n #field(row: HTMLElement, name: string): HTMLElement | null {\n return row.querySelector<HTMLElement>(`[data-field=\"${name}\"]`);\n }\n\n #setField(row: HTMLElement, name: string, text: string): void {\n const field = this.#field(row, name);\n if (field !== null) field.textContent = text;\n }\n\n #detail(event: Event): UploadDetail {\n return (event as CustomEvent<UploadDetail>).detail ?? {};\n }\n\n /** The file name every ActiveStorage `direct-upload:*` event carries. */\n #name(detail: UploadDetail): string {\n return detail.file?.name ?? \"\";\n }\n\n /**\n * Whether an event belongs to this controller. With `scope` set, only events\n * whose target (the file input) sits inside an element matching `scope` are\n * handled, so several upload widgets on one page do not cross-populate.\n * Resolved with `closest()` from the target itself, so the chatty `progress`\n * stream never pays a document-wide query. Empty `scope` handles all.\n */\n #inScope(event: Event): boolean {\n if (this.#scopeSelector.length === 0) return true;\n const target = event.target;\n return target instanceof Element && target.closest(this.#scopeSelector) !== null;\n }\n\n #key(id: string | number | undefined): string {\n return String(id ?? \"\");\n }\n}\n"]}
|
|
@@ -5,7 +5,8 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
5
5
|
*
|
|
6
6
|
* Markup contract (identifier: `stimeo--file-dropzone`):
|
|
7
7
|
* <div data-controller="stimeo--file-dropzone"
|
|
8
|
-
* data-stimeo--file-dropzone-max-size-value="5242880"
|
|
8
|
+
* data-stimeo--file-dropzone-max-size-value="5242880"
|
|
9
|
+
* data-stimeo--file-dropzone-announce-added-text-value="{name} added; {total} selected">
|
|
9
10
|
* <div data-stimeo--file-dropzone-target="zone"
|
|
10
11
|
* data-action="dragover->stimeo--file-dropzone#onDragOver
|
|
11
12
|
* dragleave->stimeo--file-dropzone#onDragLeave
|
|
@@ -17,9 +18,14 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
17
18
|
* data-action="change->stimeo--file-dropzone#onChange" />
|
|
18
19
|
* </div>
|
|
19
20
|
* <ul data-stimeo--file-dropzone-target="list" aria-label="Selected files"></ul>
|
|
20
|
-
* <
|
|
21
|
-
*
|
|
22
|
-
*
|
|
21
|
+
* <template data-stimeo--file-dropzone-target="itemTemplate">
|
|
22
|
+
* <li data-stimeo--file-dropzone-target="item">
|
|
23
|
+
* <img data-stimeo--file-dropzone-target="thumb" alt="" hidden />
|
|
24
|
+
* <span data-stimeo--file-dropzone-target="name"></span>
|
|
25
|
+
* <button type="button" aria-label="Remove {name}"
|
|
26
|
+
* data-stimeo--file-dropzone-target="remove">×</button>
|
|
27
|
+
* </li>
|
|
28
|
+
* </template>
|
|
23
29
|
* </div>
|
|
24
30
|
*
|
|
25
31
|
* There is no single APG pattern; the native `<input type="file">` stays the
|
|
@@ -29,19 +35,50 @@ import { Controller } from '@hotwired/stimulus';
|
|
|
29
35
|
*
|
|
30
36
|
* Behavior provided:
|
|
31
37
|
* - Click / keyboard via the `trigger` opens the native file dialog; drag-and-drop
|
|
32
|
-
* over the `zone` adds files
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* -
|
|
37
|
-
*
|
|
38
|
-
*
|
|
38
|
+
* over the `zone` adds files. The zone carries `data-dragover` for as long as the
|
|
39
|
+
* pointer is anywhere inside it, descendants included — a `dragleave` whose
|
|
40
|
+
* `relatedTarget` is still in the zone is the pointer crossing an inner element,
|
|
41
|
+
* not leaving.
|
|
42
|
+
* - Each file is validated in order against `accept`, `maxSize`, duplication, and
|
|
43
|
+
* the file count (`maxFiles`, or 1 when the input is not `multiple`). The reason
|
|
44
|
+
* is therefore the most specific one: a file the zone would refuse whatever the
|
|
45
|
+
* count says reports its own defect rather than `count`. Rejects fire
|
|
46
|
+
* `stimeo--file-dropzone:reject` and set `data-…-invalid`, which the next batch
|
|
47
|
+
* clears.
|
|
48
|
+
* - Two files count as the same when name, size, and last-modified time all match;
|
|
49
|
+
* `allowDuplicates` turns the check off.
|
|
50
|
+
* - Accepted files render from `itemTemplate`; the template's own remove-button
|
|
51
|
+
* `aria-label` is kept and its `{name}` expanded, so the accessible name stays in
|
|
52
|
+
* the consumer's language. A template missing a part renders nothing, changes no
|
|
53
|
+
* state, and reports itself once per connection.
|
|
54
|
+
* - The accepted set is mirrored onto the native input, so a plain form submit
|
|
55
|
+
* carries the dropped files with no JavaScript from the consumer. Where
|
|
56
|
+
* `DataTransfer` cannot be constructed the previews and events still work and the
|
|
57
|
+
* input keeps whatever the native dialog last put there.
|
|
39
58
|
* - Removing a file revokes its `objectURL` and moves focus to the next (else
|
|
40
|
-
* previous) remove button, falling back to the trigger
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* `
|
|
44
|
-
* `
|
|
59
|
+
* previous) remove button, falling back to the trigger.
|
|
60
|
+
* - Additions, rejections, removals, and the drag affordance are handed to the
|
|
61
|
+
* page's shared `stimeo--announcer` as one polite message each, worded by the
|
|
62
|
+
* consumer (`{name}` is the file this message is about, `{count}` how many files
|
|
63
|
+
* it covers, `{total}` how many are selected afterwards). Empty templates stay
|
|
64
|
+
* silent, keeping announcements opt-in and i18n-neutral. One batch produces at
|
|
65
|
+
* most one message per outcome, so dropping twenty files never reads twenty lines.
|
|
66
|
+
* - A drop into a disabled field is refused: with the input `disabled` or inside a
|
|
67
|
+
* disabled `fieldset` the zone never becomes a drop target, matching what the
|
|
68
|
+
* native click and keyboard paths already do.
|
|
69
|
+
* - Replacing the `list` target rebinds delegated removal and moves the
|
|
70
|
+
* client-only previews into the replacement; a morph that empties the list in
|
|
71
|
+
* place moves them back on the next interaction.
|
|
72
|
+
* `reject` dispatches `{ file: File, reason: "type" | "size" | "duplicate" | "count" }`.
|
|
73
|
+
* `change` and `reconcile` dispatch `{ files: File[] }`.
|
|
74
|
+
*
|
|
75
|
+
* @remarks
|
|
76
|
+
* Previews are client-only state that no restored snapshot can revive — the `File`
|
|
77
|
+
* objects and their `blob:` URLs die with the page. Just before Turbo caches the
|
|
78
|
+
* page the generated items are removed, every `objectURL` revoked, and both state
|
|
79
|
+
* attributes withdrawn, so a restored snapshot starts pristine instead of showing
|
|
80
|
+
* rows that cannot be removed and do not count towards `maxFiles`. A `disconnect()`
|
|
81
|
+
* that turns out to be an in-page move keeps the selection intact.
|
|
45
82
|
*/
|
|
46
83
|
declare class FileDropzoneController extends Controller<HTMLElement> {
|
|
47
84
|
#private;
|
|
@@ -55,45 +92,102 @@ declare class FileDropzoneController extends Controller<HTMLElement> {
|
|
|
55
92
|
type: NumberConstructor;
|
|
56
93
|
default: number;
|
|
57
94
|
};
|
|
58
|
-
|
|
95
|
+
allowDuplicates: {
|
|
96
|
+
type: BooleanConstructor;
|
|
97
|
+
default: boolean;
|
|
98
|
+
};
|
|
99
|
+
announceDragText: {
|
|
100
|
+
type: StringConstructor;
|
|
101
|
+
default: string;
|
|
102
|
+
};
|
|
103
|
+
announceAddedText: {
|
|
104
|
+
type: StringConstructor;
|
|
105
|
+
default: string;
|
|
106
|
+
};
|
|
107
|
+
announceRemovedText: {
|
|
108
|
+
type: StringConstructor;
|
|
109
|
+
default: string;
|
|
110
|
+
};
|
|
111
|
+
announceRejectedTypeText: {
|
|
112
|
+
type: StringConstructor;
|
|
113
|
+
default: string;
|
|
114
|
+
};
|
|
115
|
+
announceRejectedSizeText: {
|
|
116
|
+
type: StringConstructor;
|
|
117
|
+
default: string;
|
|
118
|
+
};
|
|
119
|
+
announceRejectedDuplicateText: {
|
|
120
|
+
type: StringConstructor;
|
|
121
|
+
default: string;
|
|
122
|
+
};
|
|
123
|
+
announceRejectedCountText: {
|
|
59
124
|
type: StringConstructor;
|
|
60
125
|
default: string;
|
|
61
126
|
};
|
|
62
127
|
};
|
|
63
128
|
static actions: readonly ["onChange", "onDragLeave", "onDragOver", "onDrop", "openDialog"];
|
|
64
|
-
static events: readonly ["change", "reject"];
|
|
129
|
+
static events: readonly ["change", "reject", "reconcile"];
|
|
65
130
|
readonly inputTarget: HTMLInputElement;
|
|
66
131
|
readonly listTarget: HTMLElement;
|
|
67
132
|
readonly listTargets: HTMLElement[];
|
|
68
133
|
readonly triggerTarget: HTMLElement;
|
|
69
134
|
readonly itemTemplateTarget: HTMLTemplateElement;
|
|
70
|
-
readonly
|
|
135
|
+
readonly hasInputTarget: boolean;
|
|
71
136
|
readonly hasListTarget: boolean;
|
|
72
137
|
readonly hasTriggerTarget: boolean;
|
|
73
138
|
readonly hasItemTemplateTarget: boolean;
|
|
74
|
-
readonly hasStatusTarget: boolean;
|
|
75
139
|
readonly hasZoneTarget: boolean;
|
|
76
140
|
readonly zoneTarget: HTMLElement;
|
|
77
141
|
maxSizeValue: number;
|
|
78
142
|
maxFilesValue: number;
|
|
79
|
-
|
|
80
|
-
|
|
143
|
+
allowDuplicatesValue: boolean;
|
|
144
|
+
announceDragTextValue: string;
|
|
145
|
+
announceAddedTextValue: string;
|
|
146
|
+
announceRemovedTextValue: string;
|
|
147
|
+
announceRejectedTypeTextValue: string;
|
|
148
|
+
announceRejectedSizeTextValue: string;
|
|
149
|
+
announceRejectedDuplicateTextValue: string;
|
|
150
|
+
announceRejectedCountTextValue: string;
|
|
151
|
+
/** Subscribes to the Turbo cache rewind and re-arms the template diagnostic. */
|
|
81
152
|
connect(): void;
|
|
82
|
-
/**
|
|
153
|
+
/**
|
|
154
|
+
* Releases the delegated listeners. The selection itself survives an in-page
|
|
155
|
+
* move and is released only once the gate proves a real detach — revoking a
|
|
156
|
+
* preview URL on a move would leave a live item pointing at a dead `blob:`.
|
|
157
|
+
*/
|
|
83
158
|
disconnect(): void;
|
|
84
|
-
/**
|
|
159
|
+
/**
|
|
160
|
+
* Binds removal and restores client-only previews for every list this controller
|
|
161
|
+
* renders into — the one present at connect and any Turbo puts in its place.
|
|
162
|
+
* This is the only place the listener is attached, so the pair with
|
|
163
|
+
* {@link listTargetDisconnected} keeps it from outliving the element it is on.
|
|
164
|
+
*/
|
|
85
165
|
listTargetConnected(list: HTMLElement): void;
|
|
86
166
|
/** Releases only the list target that actually disconnected. */
|
|
87
167
|
listTargetDisconnected(list: HTMLElement): void;
|
|
88
168
|
/** Opens the native file dialog. Bound via `data-action` (trigger click). */
|
|
89
169
|
openDialog(): void;
|
|
90
|
-
/**
|
|
170
|
+
/**
|
|
171
|
+
* Adds the files chosen through the native dialog. The input holds only what
|
|
172
|
+
* the dialog just returned, so the accepted set is written back over it once
|
|
173
|
+
* the batch is validated.
|
|
174
|
+
*/
|
|
91
175
|
onChange(): void;
|
|
92
|
-
/**
|
|
176
|
+
/**
|
|
177
|
+
* Marks the zone as a drop target and announces the affordance once per drag.
|
|
178
|
+
* Bound via `data-action` (dragover). Leaving the default alone is what refuses
|
|
179
|
+
* the drop, so a disabled field and a drag an inner dropzone already claimed
|
|
180
|
+
* both fall through untouched.
|
|
181
|
+
*/
|
|
93
182
|
onDragOver(event: DragEvent): void;
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
-
|
|
183
|
+
/**
|
|
184
|
+
* Clears the drag-over flag when the pointer leaves the zone. Bound via
|
|
185
|
+
* `data-action` (dragleave). `dragleave` bubbles from every descendant the
|
|
186
|
+
* pointer crosses, so the flag only drops when the element being entered is
|
|
187
|
+
* outside the zone (or there is none, the pointer having left the window).
|
|
188
|
+
*/
|
|
189
|
+
onDragLeave(event: DragEvent): void;
|
|
190
|
+
/** Accepts dropped files, clearing the drag-over state. Bound via `data-action` (drop). */
|
|
97
191
|
onDrop(event: DragEvent): void;
|
|
98
192
|
}
|
|
99
193
|
|