stimeo-ui 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +8 -1
- package/dist/cable/index.d.ts +79 -23
- package/dist/cable/index.js +363 -436
- package/dist/cable/index.js.map +1 -1
- package/dist/controllers/accordion_controller.d.ts +4 -2
- package/dist/controllers/accordion_controller.js +166 -55
- package/dist/controllers/accordion_controller.js.map +1 -1
- package/dist/controllers/alert_dialog_controller.d.ts +12 -2
- package/dist/controllers/alert_dialog_controller.js +862 -221
- package/dist/controllers/alert_dialog_controller.js.map +1 -1
- package/dist/controllers/announcer_controller.d.ts +35 -5
- package/dist/controllers/announcer_controller.js +135 -198
- package/dist/controllers/announcer_controller.js.map +1 -1
- package/dist/controllers/aspect_ratio_controller.js +0 -10
- package/dist/controllers/aspect_ratio_controller.js.map +1 -1
- package/dist/controllers/auto_submit_controller.d.ts +30 -6
- package/dist/controllers/auto_submit_controller.js +271 -127
- package/dist/controllers/auto_submit_controller.js.map +1 -1
- package/dist/controllers/avatar_controller.js +171 -105
- package/dist/controllers/avatar_controller.js.map +1 -1
- package/dist/controllers/breadcrumb_controller.js +0 -130
- package/dist/controllers/breadcrumb_controller.js.map +1 -1
- package/dist/controllers/bulk_select_controller.d.ts +18 -0
- package/dist/controllers/bulk_select_controller.js +262 -52
- package/dist/controllers/bulk_select_controller.js.map +1 -1
- package/dist/controllers/calendar_controller.d.ts +24 -5
- package/dist/controllers/calendar_controller.js +175 -209
- package/dist/controllers/calendar_controller.js.map +1 -1
- package/dist/controllers/carousel_controller.d.ts +34 -7
- package/dist/controllers/carousel_controller.js +461 -399
- package/dist/controllers/carousel_controller.js.map +1 -1
- package/dist/controllers/character_counter_controller.d.ts +17 -1
- package/dist/controllers/character_counter_controller.js +246 -156
- package/dist/controllers/character_counter_controller.js.map +1 -1
- package/dist/controllers/checkbox_controller.d.ts +7 -2
- package/dist/controllers/checkbox_controller.js +32 -74
- package/dist/controllers/checkbox_controller.js.map +1 -1
- package/dist/controllers/clipboard_controller.d.ts +16 -5
- package/dist/controllers/clipboard_controller.js +168 -147
- package/dist/controllers/clipboard_controller.js.map +1 -1
- package/dist/controllers/collapsible_controller.d.ts +16 -4
- package/dist/controllers/collapsible_controller.js +301 -120
- package/dist/controllers/collapsible_controller.js.map +1 -1
- package/dist/controllers/color_picker_controller.d.ts +18 -6
- package/dist/controllers/color_picker_controller.js +149 -139
- package/dist/controllers/color_picker_controller.js.map +1 -1
- package/dist/controllers/combobox_controller.d.ts +23 -5
- package/dist/controllers/combobox_controller.js +360 -133
- package/dist/controllers/combobox_controller.js.map +1 -1
- package/dist/controllers/command_palette_controller.d.ts +45 -14
- package/dist/controllers/command_palette_controller.js +970 -358
- package/dist/controllers/command_palette_controller.js.map +1 -1
- package/dist/controllers/conditional_fields_controller.d.ts +15 -2
- package/dist/controllers/conditional_fields_controller.js +166 -124
- package/dist/controllers/conditional_fields_controller.js.map +1 -1
- package/dist/controllers/confirm_controller.d.ts +49 -12
- package/dist/controllers/confirm_controller.js +939 -248
- package/dist/controllers/confirm_controller.js.map +1 -1
- package/dist/controllers/context_menu_controller.d.ts +31 -4
- package/dist/controllers/context_menu_controller.js +256 -128
- package/dist/controllers/context_menu_controller.js.map +1 -1
- package/dist/controllers/count_up_controller.d.ts +13 -1
- package/dist/controllers/count_up_controller.js +73 -21
- package/dist/controllers/count_up_controller.js.map +1 -1
- package/dist/controllers/countdown_controller.d.ts +45 -1
- package/dist/controllers/countdown_controller.js +183 -148
- package/dist/controllers/countdown_controller.js.map +1 -1
- package/dist/controllers/currency_input_controller.d.ts +21 -2
- package/dist/controllers/currency_input_controller.js +134 -158
- package/dist/controllers/currency_input_controller.js.map +1 -1
- package/dist/controllers/data_grid_controller.d.ts +15 -4
- package/dist/controllers/data_grid_controller.js +114 -186
- package/dist/controllers/data_grid_controller.js.map +1 -1
- package/dist/controllers/date_range_picker_controller.d.ts +30 -5
- package/dist/controllers/date_range_picker_controller.js +288 -206
- package/dist/controllers/date_range_picker_controller.js.map +1 -1
- package/dist/controllers/dialog_controller.d.ts +13 -1
- package/dist/controllers/dialog_controller.js +858 -223
- package/dist/controllers/dialog_controller.js.map +1 -1
- package/dist/controllers/direct_upload_controller.d.ts +24 -7
- package/dist/controllers/direct_upload_controller.js +73 -153
- package/dist/controllers/direct_upload_controller.js.map +1 -1
- package/dist/controllers/dirty_form_controller.js +14 -48
- package/dist/controllers/dirty_form_controller.js.map +1 -1
- package/dist/controllers/dismissible_controller.d.ts +19 -0
- package/dist/controllers/dismissible_controller.js +175 -21
- package/dist/controllers/dismissible_controller.js.map +1 -1
- package/dist/controllers/drawer_controller.d.ts +39 -11
- package/dist/controllers/drawer_controller.js +933 -341
- package/dist/controllers/drawer_controller.js.map +1 -1
- package/dist/controllers/dropdown_controller.d.ts +31 -1
- package/dist/controllers/dropdown_controller.js +312 -82
- package/dist/controllers/dropdown_controller.js.map +1 -1
- package/dist/controllers/editable_controller.d.ts +12 -1
- package/dist/controllers/editable_controller.js +158 -68
- package/dist/controllers/editable_controller.js.map +1 -1
- package/dist/controllers/empty_state_controller.js +67 -53
- package/dist/controllers/empty_state_controller.js.map +1 -1
- package/dist/controllers/file_dropzone_controller.d.ts +60 -18
- package/dist/controllers/file_dropzone_controller.js +311 -207
- package/dist/controllers/file_dropzone_controller.js.map +1 -1
- package/dist/controllers/filter_controller.js +67 -39
- package/dist/controllers/filter_controller.js.map +1 -1
- package/dist/controllers/flash_controller.d.ts +31 -8
- package/dist/controllers/flash_controller.js +172 -314
- package/dist/controllers/flash_controller.js.map +1 -1
- package/dist/controllers/focus_controller.d.ts +12 -3
- package/dist/controllers/focus_controller.js +654 -240
- package/dist/controllers/focus_controller.js.map +1 -1
- package/dist/controllers/form_field_controller.js +147 -132
- package/dist/controllers/form_field_controller.js.map +1 -1
- package/dist/controllers/form_validation_controller.js +13 -86
- package/dist/controllers/form_validation_controller.js.map +1 -1
- package/dist/controllers/frame_loading_controller.d.ts +41 -9
- package/dist/controllers/frame_loading_controller.js +264 -213
- package/dist/controllers/frame_loading_controller.js.map +1 -1
- package/dist/controllers/highlight_controller.d.ts +22 -2
- package/dist/controllers/highlight_controller.js +107 -75
- package/dist/controllers/highlight_controller.js.map +1 -1
- package/dist/controllers/hover_card_controller.d.ts +56 -3
- package/dist/controllers/hover_card_controller.js +309 -118
- package/dist/controllers/hover_card_controller.js.map +1 -1
- package/dist/controllers/idle_controller.d.ts +42 -1
- package/dist/controllers/idle_controller.js +267 -101
- package/dist/controllers/idle_controller.js.map +1 -1
- package/dist/controllers/input_mask_controller.js +71 -72
- package/dist/controllers/input_mask_controller.js.map +1 -1
- package/dist/controllers/intersection_controller.d.ts +32 -13
- package/dist/controllers/intersection_controller.js +147 -106
- package/dist/controllers/intersection_controller.js.map +1 -1
- package/dist/controllers/lazy_frame_controller.d.ts +16 -0
- package/dist/controllers/lazy_frame_controller.js +9 -65
- package/dist/controllers/lazy_frame_controller.js.map +1 -1
- package/dist/controllers/listbox_controller.d.ts +34 -2
- package/dist/controllers/listbox_controller.js +306 -204
- 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 +54 -64
- package/dist/controllers/local_time_controller.js.map +1 -1
- package/dist/controllers/masonry_controller.d.ts +11 -3
- package/dist/controllers/masonry_controller.js +70 -93
- package/dist/controllers/masonry_controller.js.map +1 -1
- package/dist/controllers/menu_controller.d.ts +32 -7
- package/dist/controllers/menu_controller.js +214 -156
- package/dist/controllers/menu_controller.js.map +1 -1
- package/dist/controllers/menubar_controller.d.ts +4 -4
- package/dist/controllers/menubar_controller.js +170 -329
- package/dist/controllers/menubar_controller.js.map +1 -1
- package/dist/controllers/meter_controller.d.ts +20 -0
- package/dist/controllers/meter_controller.js +151 -67
- package/dist/controllers/meter_controller.js.map +1 -1
- package/dist/controllers/multi_select_controller.d.ts +32 -4
- package/dist/controllers/multi_select_controller.js +593 -358
- package/dist/controllers/multi_select_controller.js.map +1 -1
- package/dist/controllers/navigation_menu_controller.d.ts +8 -1
- package/dist/controllers/navigation_menu_controller.js +124 -232
- package/dist/controllers/navigation_menu_controller.js.map +1 -1
- package/dist/controllers/nested_form_controller.d.ts +10 -0
- package/dist/controllers/nested_form_controller.js +352 -182
- package/dist/controllers/nested_form_controller.js.map +1 -1
- package/dist/controllers/network_status_controller.d.ts +23 -0
- package/dist/controllers/network_status_controller.js +242 -54
- package/dist/controllers/network_status_controller.js.map +1 -1
- package/dist/controllers/number_input_controller.d.ts +26 -2
- package/dist/controllers/number_input_controller.js +320 -263
- package/dist/controllers/number_input_controller.js.map +1 -1
- package/dist/controllers/optimistic_controller.js +16 -76
- package/dist/controllers/optimistic_controller.js.map +1 -1
- package/dist/controllers/otp_controller.d.ts +20 -4
- package/dist/controllers/otp_controller.js +198 -193
- package/dist/controllers/otp_controller.js.map +1 -1
- package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
- package/dist/controllers/overflow_indicator_controller.js +116 -113
- package/dist/controllers/overflow_indicator_controller.js.map +1 -1
- package/dist/controllers/overflow_menu_controller.d.ts +48 -11
- package/dist/controllers/overflow_menu_controller.js +343 -283
- package/dist/controllers/overflow_menu_controller.js.map +1 -1
- package/dist/controllers/pagination_controller.d.ts +35 -6
- package/dist/controllers/pagination_controller.js +295 -119
- package/dist/controllers/pagination_controller.js.map +1 -1
- package/dist/controllers/password_reveal_controller.d.ts +45 -8
- package/dist/controllers/password_reveal_controller.js +389 -121
- package/dist/controllers/password_reveal_controller.js.map +1 -1
- package/dist/controllers/password_strength_controller.d.ts +14 -5
- package/dist/controllers/password_strength_controller.js +99 -153
- package/dist/controllers/password_strength_controller.js.map +1 -1
- package/dist/controllers/persist_controller.d.ts +11 -4
- package/dist/controllers/persist_controller.js +237 -103
- package/dist/controllers/persist_controller.js.map +1 -1
- package/dist/controllers/pointer_drag_controller.d.ts +19 -7
- package/dist/controllers/pointer_drag_controller.js +185 -165
- package/dist/controllers/pointer_drag_controller.js.map +1 -1
- package/dist/controllers/popover_controller.d.ts +38 -2
- package/dist/controllers/popover_controller.js +236 -97
- package/dist/controllers/popover_controller.js.map +1 -1
- package/dist/controllers/portal_controller.d.ts +44 -4
- package/dist/controllers/portal_controller.js +209 -76
- package/dist/controllers/portal_controller.js.map +1 -1
- package/dist/controllers/preview_guard_controller.d.ts +10 -4
- package/dist/controllers/preview_guard_controller.js +232 -116
- package/dist/controllers/preview_guard_controller.js.map +1 -1
- package/dist/controllers/progress_controller.d.ts +11 -0
- package/dist/controllers/progress_controller.js +125 -56
- package/dist/controllers/progress_controller.js.map +1 -1
- package/dist/controllers/radio_group_controller.d.ts +14 -5
- package/dist/controllers/radio_group_controller.js +117 -137
- package/dist/controllers/radio_group_controller.js.map +1 -1
- package/dist/controllers/range_slider_controller.d.ts +43 -3
- package/dist/controllers/range_slider_controller.js +192 -105
- package/dist/controllers/range_slider_controller.js.map +1 -1
- package/dist/controllers/rating_controller.d.ts +20 -4
- package/dist/controllers/rating_controller.js +418 -207
- package/dist/controllers/rating_controller.js.map +1 -1
- package/dist/controllers/read_more_controller.d.ts +2 -0
- package/dist/controllers/read_more_controller.js +134 -63
- package/dist/controllers/read_more_controller.js.map +1 -1
- package/dist/controllers/reading_progress_controller.d.ts +1 -1
- package/dist/controllers/reading_progress_controller.js +147 -135
- package/dist/controllers/reading_progress_controller.js.map +1 -1
- package/dist/controllers/relative_time_controller.d.ts +18 -4
- package/dist/controllers/relative_time_controller.js +145 -110
- package/dist/controllers/relative_time_controller.js.map +1 -1
- package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
- package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
- package/dist/controllers/reset_on_restore_controller.js.map +1 -0
- package/dist/controllers/resizable_controller.d.ts +28 -2
- package/dist/controllers/resizable_controller.js +255 -139
- package/dist/controllers/resizable_controller.js.map +1 -1
- package/dist/controllers/roving_controller.js +39 -84
- package/dist/controllers/roving_controller.js.map +1 -1
- package/dist/controllers/scroll_area_controller.d.ts +7 -3
- package/dist/controllers/scroll_area_controller.js +428 -225
- package/dist/controllers/scroll_area_controller.js.map +1 -1
- package/dist/controllers/scroll_restore_controller.js +0 -53
- package/dist/controllers/scroll_restore_controller.js.map +1 -1
- package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
- package/dist/controllers/scroll_visibility_controller.js +395 -145
- package/dist/controllers/scroll_visibility_controller.js.map +1 -1
- package/dist/controllers/scrollspy_controller.d.ts +6 -1
- package/dist/controllers/scrollspy_controller.js +181 -242
- package/dist/controllers/scrollspy_controller.js.map +1 -1
- package/dist/controllers/separator_controller.d.ts +19 -4
- package/dist/controllers/separator_controller.js +267 -140
- package/dist/controllers/separator_controller.js.map +1 -1
- package/dist/controllers/sidebar_controller.d.ts +64 -7
- package/dist/controllers/sidebar_controller.js +1023 -386
- package/dist/controllers/sidebar_controller.js.map +1 -1
- package/dist/controllers/skeleton_controller.d.ts +23 -0
- package/dist/controllers/skeleton_controller.js +248 -97
- package/dist/controllers/skeleton_controller.js.map +1 -1
- package/dist/controllers/slider_controller.d.ts +27 -1
- package/dist/controllers/slider_controller.js +159 -79
- package/dist/controllers/slider_controller.js.map +1 -1
- package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
- package/dist/controllers/smart_sticky_header_controller.js +162 -63
- package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
- package/dist/controllers/sortable_controller.js +0 -112
- package/dist/controllers/sortable_controller.js.map +1 -1
- package/dist/controllers/spinner_controller.d.ts +39 -4
- package/dist/controllers/spinner_controller.js +277 -169
- package/dist/controllers/spinner_controller.js.map +1 -1
- package/dist/controllers/step_indicator_controller.d.ts +5 -0
- package/dist/controllers/step_indicator_controller.js +99 -46
- package/dist/controllers/step_indicator_controller.js.map +1 -1
- package/dist/controllers/stepper_controller.d.ts +12 -3
- package/dist/controllers/stepper_controller.js +114 -73
- package/dist/controllers/stepper_controller.js.map +1 -1
- package/dist/controllers/stick_to_bottom_controller.d.ts +6 -0
- package/dist/controllers/stick_to_bottom_controller.js +262 -138
- package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
- package/dist/controllers/sticky_observer_controller.d.ts +11 -0
- package/dist/controllers/sticky_observer_controller.js +214 -49
- package/dist/controllers/sticky_observer_controller.js.map +1 -1
- package/dist/controllers/submit_once_controller.d.ts +23 -8
- package/dist/controllers/submit_once_controller.js +294 -205
- package/dist/controllers/submit_once_controller.js.map +1 -1
- package/dist/controllers/switch_controller.d.ts +12 -1
- package/dist/controllers/switch_controller.js +33 -60
- package/dist/controllers/switch_controller.js.map +1 -1
- package/dist/controllers/tabs_controller.d.ts +2 -2
- package/dist/controllers/tabs_controller.js +35 -29
- package/dist/controllers/tabs_controller.js.map +1 -1
- package/dist/controllers/tags_input_controller.d.ts +12 -0
- package/dist/controllers/tags_input_controller.js +136 -145
- package/dist/controllers/tags_input_controller.js.map +1 -1
- package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
- package/dist/controllers/textarea_autosize_controller.js +71 -58
- package/dist/controllers/textarea_autosize_controller.js.map +1 -1
- package/dist/controllers/theme_controller.d.ts +21 -9
- package/dist/controllers/theme_controller.js +130 -112
- package/dist/controllers/theme_controller.js.map +1 -1
- package/dist/controllers/time_picker_controller.d.ts +18 -1
- package/dist/controllers/time_picker_controller.js +146 -80
- package/dist/controllers/time_picker_controller.js.map +1 -1
- package/dist/controllers/toast_controller.d.ts +36 -11
- package/dist/controllers/toast_controller.js +186 -297
- package/dist/controllers/toast_controller.js.map +1 -1
- package/dist/controllers/toggle_group_controller.d.ts +12 -3
- package/dist/controllers/toggle_group_controller.js +95 -136
- package/dist/controllers/toggle_group_controller.js.map +1 -1
- package/dist/controllers/toolbar_controller.js +68 -98
- package/dist/controllers/toolbar_controller.js.map +1 -1
- package/dist/controllers/tooltip_controller.d.ts +45 -3
- package/dist/controllers/tooltip_controller.js +158 -124
- package/dist/controllers/tooltip_controller.js.map +1 -1
- package/dist/controllers/transition_controller.d.ts +22 -4
- package/dist/controllers/transition_controller.js +123 -119
- package/dist/controllers/transition_controller.js.map +1 -1
- package/dist/controllers/tree_view_controller.d.ts +21 -11
- package/dist/controllers/tree_view_controller.js +150 -345
- package/dist/controllers/tree_view_controller.js.map +1 -1
- package/dist/index.d.ts +8 -5
- package/dist/index.js +6370 -9346
- package/dist/index.js.map +1 -1
- package/dist/inspector/cli.d.ts +23 -3
- package/dist/inspector/cli.js +92 -37
- package/dist/inspector/cli.js.map +1 -1
- package/dist/inspector/cli_bin.js +92 -56
- package/dist/inspector/cli_bin.js.map +1 -1
- package/dist/inspector/examples.json +8 -8
- package/dist/inspector/manifest.json +787 -100
- package/dist/positioning/index.d.ts +8 -0
- package/dist/positioning/index.js +148 -47
- package/dist/positioning/index.js.map +1 -1
- package/package.json +6 -6
- package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
- package/dist/controllers/reset_before_cache_controller.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/declared_value.ts","../../src/utils/safe_timeout.ts","../../src/utils/template_row.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.\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 * Readers for the String Values whose text can only be checked by handing it to\n * a parser: a CSS selector, a regular-expression source, a JSON object.\n *\n * Stimulus reads each of them as an ordinary string, so the controller connects\n * and the flaw surfaces later, inside the first handler that consumes the value\n * (a `SyntaxError` from `new RegExp`, a `DOMException` from the selector engine),\n * where it takes the whole handler down on every event. Parsing here, once, in\n * the `<name>ValueChanged` callback keeps the failure local to the declaration:\n * an unreadable value falls back to its default, the element stays alive, and\n * the hot path only ever receives a value that parsed.\n *\n * Which default a broken declaration falls back to is the caller's contract, so\n * every reader takes (or returns) the fallback rather than choosing one.\n */\n\n/**\n * The result of `parse(raw)`, or `fallback` when `parse` throws.\n *\n * Any exception counts as \"does not parse\": the parsers these Values feed\n * (`RegExp`, `JSON.parse`, the selector engine) all report a malformed input by\n * throwing, and none of them throws for another reason.\n */\nexport function parseDeclared<T>(raw: string, parse: (raw: string) => T, fallback: T): T {\n try {\n return parse(raw);\n } catch {\n return fallback;\n }\n}\n\n/** What {@link validSelector} needs of the element it probes with. */\nexport interface SelectorProbe {\n matches(selector: string): boolean;\n}\n\n/**\n * `raw` when it is a non-empty selector the DOM accepts, otherwise `fallback`.\n *\n * `element.matches` is the probe: the engine parses the selector and throws for\n * one it cannot read. Whether the selector actually matches `element` plays no\n * part, so a selector aimed at another element still passes.\n *\n * The parameter names the one method the probe uses rather than `Element`, so\n * this module carries no DOM type and the readers below stay importable from\n * the Node-side Inspector, which checks the same declarations statically.\n */\nexport function validSelector(element: SelectorProbe, raw: string, fallback: string): string {\n if (raw.length === 0) return fallback;\n return parseDeclared(\n raw,\n (selector) => {\n element.matches(selector);\n return selector;\n },\n fallback,\n );\n}\n\n/** How `compileRegExp` wraps a source before compiling it. */\nexport type RegExpAnchor = \"none\" | \"exact\";\n\n/**\n * `source` compiled as a `RegExp`, or `null` when it does not compile.\n *\n * `\"exact\"` wraps the source as `^(?:source)$`, so the whole input has to match\n * and an alternation inside the source cannot escape the anchors. The source is\n * compiled on its own first, because an unbalanced source can be made to parse\n * by the wrapper's own parentheses — `0)|(1` becomes `^(?:0)|(1)$` — which would\n * accept a declaration that is not a regular expression and leave half of it\n * outside the anchors. The default that replaces a broken source is the\n * caller's, so `null` is returned rather than a fallback pattern.\n */\nexport function compileRegExp(source: string, anchor: RegExpAnchor = \"none\"): RegExp | null {\n return parseDeclared(\n source,\n (text) => {\n const bare = new RegExp(text);\n return anchor === \"exact\" ? new RegExp(`^(?:${text})$`) : bare;\n },\n null,\n );\n}\n\n/**\n * The JSON object `raw` declares, or `null` when the text does not parse or\n * parses to something other than a plain object (`null`, an array, a scalar).\n * Values are returned as parsed; narrowing them is the caller's contract.\n */\nexport function parseJsonObject(raw: string): Record<string, unknown> | null {\n const parsed = parseDeclared<unknown>(raw, (text) => JSON.parse(text), null);\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n return parsed as Record<string, unknown>;\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 * Largest delay a timer can hold: the platform stores it in a 32-bit signed\n * integer, and anything larger overflows to `1`, so a delay meant to be far in\n * the future fires almost immediately. A declared delay above this bound names\n * no delay at all, and a controller reading one falls back to its default.\n */\nexport const MAX_TIMER_DELAY_MS = 2_147_483_647;\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 { fillTemplate } from \"./announce\";\nimport { matchingPart } from \"./element_part\";\nimport { targetSelector } from \"./target_selector\";\n\n/**\n * Clones the first element of a `<template>`, and nothing else.\n *\n * A template is authored across lines, so its content holds whitespace text nodes\n * around that element — and a second element, where one is written. Cloning the\n * element on its own makes it the whole unit the caller appends, so the same node\n * a later `remove()` takes back is everything that went in, and the parent's child\n * list stays equal to its element list.\n *\n * `null` when the content has no element at all.\n */\nexport function cloneTemplateRoot(template: HTMLTemplateElement): HTMLElement | null {\n const root = template.content.firstElementChild;\n return root instanceof HTMLElement ? (root.cloneNode(true) as HTMLElement) : null;\n}\n\n/** What a consumer declares once about the rows it builds from a `<template>`. */\nexport interface TemplateRowOptions<R extends string, O extends string> {\n /** The controller identifier the row's parts are declared against. */\n readonly identifier: string;\n /** Target name the row itself carries. */\n readonly root: string;\n /** Target names the row must contain; the first one missing is the one reported. */\n readonly required: readonly R[];\n /** Target names the row may contain; a missing one resolves to `null`. */\n readonly optional?: readonly O[];\n /** Target name of the `<button>` whose authored `aria-label` names the row. */\n readonly button: string;\n /** What the widget did not do, e.g. `\"added no tag\"`. */\n readonly outcome: string;\n /** What the author wrote it in, e.g. `\"chip template\"`. */\n readonly noun: string;\n}\n\n/** One row: the element to append, the parts inside it, and its named button. */\nexport interface TemplateRowParts<R extends string, O extends string> {\n readonly root: HTMLElement;\n readonly slots: Readonly<Record<R, HTMLElement> & Record<O, HTMLElement | null>>;\n readonly button: HTMLButtonElement;\n}\n\n/**\n * Builds one row from an authored `<template>`, naming an unusable one once per connection.\n *\n * **The row is the template's only element, and it carries {@link\n * TemplateRowOptions.root} itself.** Cloning it alone ({@link cloneTemplateRoot})\n * makes what the caller appends exactly what a later `remove()` takes back; a row\n * wrapped in another element, or standing beside a second one, would leave that\n * other element behind on every removal — or never be rendered at all — so such a\n * template is refused rather than half-built.\n *\n * Parts resolve through the caller's own namespace, derived from the identifier\n * the controller is registered under, so a registration under any name reaches\n * the attributes that registration renders.\n *\n * A template that cannot produce a row yields `null` and changes nothing: what a\n * refused row means for the selection, the field, or the announcement stays with\n * the caller. The diagnostic names what is wrong — the element count, or the first\n * missing part — because the only other symptom is a widget that silently adds\n * nothing, and the causes a static check cannot see — a server-rendered template,\n * a name that renders empty from a missing translation — would have no diagnostic\n * anywhere. It is written once per connection, so one authoring mistake cannot\n * flood the console.\n *\n * @example\n * ```ts\n * readonly #rows = new TemplateRow({\n * identifier: this.identifier,\n * root: \"tag\",\n * required: [\"label\"],\n * button: \"remove\",\n * outcome: \"added no tag\",\n * noun: \"chip template\",\n * });\n *\n * connect(): void {\n * this.#rows.connect();\n * }\n * ```\n */\nexport class TemplateRow<R extends string, O extends string = never> {\n readonly #options: TemplateRowOptions<R, O>;\n #warned = false;\n\n constructor(options: TemplateRowOptions<R, O>) {\n this.#options = options;\n }\n\n /** The attribute selector for one declared part, in this controller's namespace. */\n selector(name: string): string {\n return targetSelector(this.#options.identifier, name);\n }\n\n /** Re-arms the once-per-connection diagnostic. */\n connect(): void {\n this.#warned = false;\n }\n\n /**\n * Names one missing part on the console, at most once per connection, and\n * returns `null` so a caller can hand it straight back.\n */\n report(missing: string): null {\n return this.#say(`lacks ${missing}`);\n }\n\n /** Writes one diagnostic per connection, saying what the template got wrong. */\n #say(problem: string): null {\n if (this.#warned) return null;\n this.#warned = true;\n const { identifier, outcome, noun } = this.#options;\n console.warn(`Stimeo UI: \"${identifier}\" ${outcome} because its ${noun} ${problem}.`);\n return null;\n }\n\n /**\n * Clones the row and resolves its parts, or names what the template got wrong\n * and returns `null`. `values` fills the button's authored `aria-label`.\n */\n instantiate(\n template: HTMLTemplateElement,\n values: Record<string, string | number>,\n ): TemplateRowParts<R, O> | null {\n const { root: rootName, required, optional, button: buttonName } = this.#options;\n const root = cloneTemplateRoot(template);\n if (!root?.matches(this.selector(rootName))) {\n return this.report(`${this.#article(rootName)} \"${rootName}\" root`);\n }\n // One element, and it is the row: anything beside it would be left behind by\n // a removal that takes the row back, which is the accumulation this contract\n // exists to stop. The row here is the one that was asked for, so the count is\n // what the diagnostic names — a missing part would point at the wrong element.\n const count = template.content.children.length;\n if (count !== 1) {\n return this.#say(`holds ${count} elements, and the row has to be its only one`);\n }\n // Required parts are resolved before they are written, so the declared shape\n // holds for every row returned at all — a missing one leaves through `report`\n // instead. The accumulator is keyed loosely because that ordering is what\n // makes the narrower return type true, and no index signature can say so.\n const slots: Record<string, HTMLElement | null> = {};\n for (const name of required) {\n const part = this.#resolve(root, name);\n if (!part) return this.report(`${this.#article(name)} \"${name}\" target`);\n slots[name] = part;\n }\n for (const name of optional ?? []) slots[name] = this.#resolve(root, name);\n const named = this.#resolve(root, buttonName);\n if (!(named instanceof HTMLButtonElement)) {\n return this.report(`${this.#article(buttonName)} \"${buttonName}\" target <button>`);\n }\n const button = named;\n const name = button.getAttribute(\"aria-label\")?.trim() ?? \"\";\n if (name === \"\") return this.report(`a non-empty aria-label on its \"${buttonName}\" target`);\n button.setAttribute(\"aria-label\", fillTemplate(name, values));\n return { root, slots, button } as TemplateRowParts<R, O>;\n }\n\n /** The row may be its own part; otherwise the first descendant carrying the target. */\n #resolve(root: HTMLElement, name: string): HTMLElement | null {\n return matchingPart(root, this.selector(name));\n }\n\n #article(name: string): string {\n return /^[aeiou]/i.test(name) ? \"an\" : \"a\";\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { validSelector } from \"../utils/declared_value\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { cloneTemplateRoot } from \"../utils/template_row\";\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 FileDropzoneController}, 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 this.#scopeSelector = validSelector(this.element, this.scopeValue, \"\");\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 /**\n * Marks a not-yet-settled row done at 100%, announces it, and emits `done`.\n *\n * @stimeoRuntimeOnly `announceDoneText` words the one announcement of this completion and\n * `removeOnDone` decides whether it arms the row's removal.\n */\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 * @stimeoRuntimeOnly `announceErrorText` words the one announcement of this 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 /**\n * Re-arms `removeOnDone` for completed rows after a reconnect.\n *\n * @stimeoRuntimeOnly `removeOnDone` decides whether reconnecting arms the removal of finished\n * rows again.\n */\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 = cloneTemplateRoot(this.rowTarget);\n if (!clone) 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 * 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 event's file name, or `\"\"` when it carries none. */\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/declared_value.ts","../../src/utils/safe_timeout.ts","../../src/utils/keyed_timers.ts","../../src/utils/template_row.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 * Readers for the String Values whose text can only be checked by handing it to\n * a parser: a CSS selector, a regular-expression source, a JSON object.\n *\n * Stimulus reads each of them as an ordinary string, so the controller connects\n * and the flaw surfaces later, inside the first handler that consumes the value\n * (a `SyntaxError` from `new RegExp`, a `DOMException` from the selector engine),\n * where it takes the whole handler down on every event. Parsing here, once, in\n * the `<name>ValueChanged` callback keeps the failure local to the declaration:\n * an unreadable value falls back to its default, the element stays alive, and\n * the hot path only ever receives a value that parsed.\n *\n * Which default a broken declaration falls back to is the caller's contract, so\n * every reader takes (or returns) the fallback rather than choosing one.\n */\n\n/**\n * The result of `parse(raw)`, or `fallback` when `parse` throws.\n *\n * Any exception counts as \"does not parse\": the parsers these Values feed\n * (`RegExp`, `JSON.parse`, the selector engine) all report a malformed input by\n * throwing, and none of them throws for another reason.\n */\nexport function parseDeclared<T>(raw: string, parse: (raw: string) => T, fallback: T): T {\n try {\n return parse(raw);\n } catch {\n return fallback;\n }\n}\n\n/** What {@link validSelector} needs of the element it probes with. */\nexport interface SelectorProbe {\n matches(selector: string): boolean;\n}\n\n/**\n * `raw` when it is a non-empty selector the DOM accepts, otherwise `fallback`.\n *\n * `element.matches` is the probe: the engine parses the selector and throws for\n * one it cannot read. Whether the selector actually matches `element` plays no\n * part, so a selector aimed at another element still passes.\n *\n * The parameter names the one method the probe uses rather than `Element`, so\n * this module carries no DOM type and the readers below stay importable from\n * the Node-side Inspector, which checks the same declarations statically.\n */\nexport function validSelector(element: SelectorProbe, raw: string, fallback: string): string {\n if (raw.length === 0) return fallback;\n return parseDeclared(\n raw,\n (selector) => {\n element.matches(selector);\n return selector;\n },\n fallback,\n );\n}\n\n/** How `compileRegExp` wraps a source before compiling it. */\nexport type RegExpAnchor = \"none\" | \"exact\";\n\n/**\n * `source` compiled as a `RegExp`, or `null` when it does not compile.\n *\n * `\"exact\"` wraps the source as `^(?:source)$`, so the whole input has to match\n * and an alternation inside the source cannot escape the anchors. The source is\n * compiled on its own first, because an unbalanced source can be made to parse\n * by the wrapper's own parentheses — `0)|(1` becomes `^(?:0)|(1)$` — which would\n * accept a declaration that is not a regular expression and leave half of it\n * outside the anchors. The default that replaces a broken source is the\n * caller's, so `null` is returned rather than a fallback pattern.\n */\nexport function compileRegExp(source: string, anchor: RegExpAnchor = \"none\"): RegExp | null {\n return parseDeclared(\n source,\n (text) => {\n const bare = new RegExp(text);\n return anchor === \"exact\" ? new RegExp(`^(?:${text})$`) : bare;\n },\n null,\n );\n}\n\n/**\n * The JSON object `raw` declares, or `null` when the text does not parse or\n * parses to something other than a plain object (`null`, an array, a scalar).\n * Values are returned as parsed; narrowing them is the caller's contract.\n */\nexport function parseJsonObject(raw: string): Record<string, unknown> | null {\n const parsed = parseDeclared<unknown>(raw, (text) => JSON.parse(text), null);\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) return null;\n return parsed as Record<string, unknown>;\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\nexport { MAX_TIMER_DELAY_MS } from \"./timer_bounds\";\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 { SafeTimeout } from \"./safe_timeout\";\n\n/**\n * Keeps at most one pending timer per key, and releases them all together.\n *\n * A region, a row, a message or a speaker that is armed again while its earlier\n * timer is still pending must end up with exactly one timer deciding when its\n * state ends. Two timers on one key means the earlier deadline wins: it cuts the\n * new emphasis short, clears text written after it, or drops a speaker who is\n * still typing — and then reports an ending for a state that is already gone.\n * Arming through this registry cancels the key's earlier timer in the same step,\n * and no timer id ever reaches the caller.\n *\n * An entry lives exactly as long as its timer: it goes as the timer fires, and\n * the whole ledger goes on `clearAll()`. That is a correctness rule, not\n * tidiness — a platform may hand an id released by `clearTimeout` to the next\n * timer it creates, so an entry that outlived its timer would let a later\n * `clear()` on that key cancel whatever timer now holds the id, on some other\n * key entirely.\n *\n * The ledger is a plain `Map`, and the key may be anything. Weak keys would\n * retain nothing less: while a timer is pending the platform already holds the\n * key through the callback, and the entry is gone the moment the timer is.\n *\n * Scope is the one-per-key timer only. What the timer means, and the roster of\n * keys it is armed for, stay with the consumer, so a second kind of timer on the\n * same keys (a transition wait, an animation frame) is a second registry, and a\n * timer that belongs to no key stays on a plain `SafeTimeout`.\n *\n * @example\n * ```ts\n * readonly #expiry = new KeyedTimers<string>();\n *\n * #onSignal(name: string): void {\n * this.#expiry.set(name, () => this.#drop(name), this.#timeout);\n * }\n *\n * disconnect(): void {\n * this.#expiry.clearAll();\n * }\n * ```\n */\nexport class KeyedTimers<K> {\n readonly #timers = new SafeTimeout();\n\n /** The pending timer of each key; an entry lives exactly as long as its timer. */\n readonly #pending = new Map<K, number>();\n\n /**\n * Arms `callback` after `delay` ms for `key`, cancelling the timer `key` had\n * pending. The entry is dropped before the callback runs, so the callback sees\n * the key unarmed and may arm it again for the next round.\n */\n set(key: K, callback: () => void, delay: number): void {\n this.clear(key);\n const id = this.#timers.set(() => {\n this.#pending.delete(key);\n callback();\n }, delay);\n this.#pending.set(key, id);\n }\n\n /**\n * Cancels `key`'s pending timer, if it has one.\n *\n * A timer id is a positive integer, so `-1` stands for \"nothing pending\" and\n * the registry ignores an id it does not own — the unarmed case needs no\n * branch of its own, and no other key's timer can be reached from here.\n */\n clear(key: K): void {\n this.#timers.clear(this.#pending.get(key) ?? -1);\n this.#pending.delete(key);\n }\n\n /**\n * Cancels every pending timer and forgets every key. Call this from a\n * controller's `disconnect()` so no timer, and no entry, outlives the element.\n */\n clearAll(): void {\n this.#timers.clearAll();\n this.#pending.clear();\n }\n\n /** Whether `key` has a timer pending. */\n has(key: K): boolean {\n return this.#pending.has(key);\n }\n}\n","import { fillTemplate } from \"./announce\";\nimport { matchingPart } from \"./element_part\";\nimport { targetSelector } from \"./target_selector\";\n\n/**\n * Clones the first element of a `<template>`, and nothing else.\n *\n * A template is authored across lines, so its content holds whitespace text nodes\n * around that element — and a second element, where one is written. Cloning the\n * element on its own makes it the whole unit the caller appends, so the same node\n * a later `remove()` takes back is everything that went in, and the parent's child\n * list stays equal to its element list.\n *\n * `null` when the content has no element at all.\n */\nexport function cloneTemplateRoot(template: HTMLTemplateElement): HTMLElement | null {\n const root = template.content.firstElementChild;\n return root instanceof HTMLElement ? (root.cloneNode(true) as HTMLElement) : null;\n}\n\n/** What a consumer declares once about the rows it builds from a `<template>`. */\nexport interface TemplateRowOptions<R extends string, O extends string> {\n /** The controller identifier the row's parts are declared against. */\n readonly identifier: string;\n /** Target name the row itself carries. */\n readonly root: string;\n /** Target names the row must contain; the first one missing is the one reported. */\n readonly required: readonly R[];\n /** Target names the row may contain; a missing one resolves to `null`. */\n readonly optional?: readonly O[];\n /** Target name of the `<button>` whose authored `aria-label` names the row. */\n readonly button: string;\n /** What the widget did not do, e.g. `\"added no tag\"`. */\n readonly outcome: string;\n /** What the author wrote it in, e.g. `\"chip template\"`. */\n readonly noun: string;\n}\n\n/** One row: the element to append, the parts inside it, and its named button. */\nexport interface TemplateRowParts<R extends string, O extends string> {\n readonly root: HTMLElement;\n readonly slots: Readonly<Record<R, HTMLElement> & Record<O, HTMLElement | null>>;\n readonly button: HTMLButtonElement;\n}\n\n/**\n * Builds one row from an authored `<template>`, naming an unusable one once per connection.\n *\n * **The row is the template's only element, and it carries {@link\n * TemplateRowOptions.root} itself.** Cloning it alone ({@link cloneTemplateRoot})\n * makes what the caller appends exactly what a later `remove()` takes back; a row\n * wrapped in another element, or standing beside a second one, would leave that\n * other element behind on every removal — or never be rendered at all — so such a\n * template is refused rather than half-built.\n *\n * Parts resolve through the caller's own namespace, derived from the identifier\n * the controller is registered under, so a registration under any name reaches\n * the attributes that registration renders.\n *\n * A template that cannot produce a row yields `null` and changes nothing: what a\n * refused row means for the selection, the field, or the announcement stays with\n * the caller. The diagnostic names what is wrong — the element count, or the first\n * missing part — because the only other symptom is a widget that silently adds\n * nothing, and the causes a static check cannot see — a server-rendered template,\n * a name that renders empty from a missing translation — would have no diagnostic\n * anywhere. It is written once per connection, so one authoring mistake cannot\n * flood the console.\n *\n * @example\n * ```ts\n * readonly #rows = new TemplateRow({\n * identifier: this.identifier,\n * root: \"tag\",\n * required: [\"label\"],\n * button: \"remove\",\n * outcome: \"added no tag\",\n * noun: \"chip template\",\n * });\n *\n * connect(): void {\n * this.#rows.connect();\n * }\n * ```\n */\nexport class TemplateRow<R extends string, O extends string = never> {\n readonly #options: TemplateRowOptions<R, O>;\n #warned = false;\n\n constructor(options: TemplateRowOptions<R, O>) {\n this.#options = options;\n }\n\n /** The attribute selector for one declared part, in this controller's namespace. */\n selector(name: string): string {\n return targetSelector(this.#options.identifier, name);\n }\n\n /** Re-arms the once-per-connection diagnostic. */\n connect(): void {\n this.#warned = false;\n }\n\n /**\n * Names one missing part on the console, at most once per connection, and\n * returns `null` so a caller can hand it straight back.\n */\n report(missing: string): null {\n return this.#say(`lacks ${missing}`);\n }\n\n /** Writes one diagnostic per connection, saying what the template got wrong. */\n #say(problem: string): null {\n if (this.#warned) return null;\n this.#warned = true;\n const { identifier, outcome, noun } = this.#options;\n console.warn(`Stimeo UI: \"${identifier}\" ${outcome} because its ${noun} ${problem}.`);\n return null;\n }\n\n /**\n * Clones the row and resolves its parts, or names what the template got wrong\n * and returns `null`. `values` fills the button's authored `aria-label`.\n */\n instantiate(\n template: HTMLTemplateElement,\n values: Record<string, string | number>,\n ): TemplateRowParts<R, O> | null {\n const { root: rootName, required, optional, button: buttonName } = this.#options;\n const root = cloneTemplateRoot(template);\n if (!root?.matches(this.selector(rootName))) {\n return this.report(`${this.#article(rootName)} \"${rootName}\" root`);\n }\n // One element, and it is the row: anything beside it would be left behind by\n // a removal that takes the row back, which is the accumulation this contract\n // exists to stop. The row here is the one that was asked for, so the count is\n // what the diagnostic names — a missing part would point at the wrong element.\n const count = template.content.children.length;\n if (count !== 1) {\n return this.#say(`holds ${count} elements, and the row has to be its only one`);\n }\n // Required parts are resolved before they are written, so the declared shape\n // holds for every row returned at all — a missing one leaves through `report`\n // instead. The accumulator is keyed loosely because that ordering is what\n // makes the narrower return type true, and no index signature can say so.\n const slots: Record<string, HTMLElement | null> = {};\n for (const name of required) {\n const part = this.#resolve(root, name);\n if (!part) return this.report(`${this.#article(name)} \"${name}\" target`);\n slots[name] = part;\n }\n for (const name of optional ?? []) slots[name] = this.#resolve(root, name);\n const named = this.#resolve(root, buttonName);\n if (!(named instanceof HTMLButtonElement)) {\n return this.report(`${this.#article(buttonName)} \"${buttonName}\" target <button>`);\n }\n const button = named;\n const name = button.getAttribute(\"aria-label\")?.trim() ?? \"\";\n if (name === \"\") return this.report(`a non-empty aria-label on its \"${buttonName}\" target`);\n button.setAttribute(\"aria-label\", fillTemplate(name, values));\n return { root, slots, button } as TemplateRowParts<R, O>;\n }\n\n /** The row may be its own part; otherwise the first descendant carrying the target. */\n #resolve(root: HTMLElement, name: string): HTMLElement | null {\n return matchingPart(root, this.selector(name));\n }\n\n #article(name: string): string {\n return /^[aeiou]/i.test(name) ? \"an\" : \"a\";\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { validSelector } from \"../utils/declared_value\";\nimport { KeyedTimers } from \"../utils/keyed_timers\";\nimport { cloneTemplateRoot } from \"../utils/template_row\";\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/** Suffix of the mark on every row this controller renders; it holds the upload id. */\nconst GENERATED_ATTRIBUTE = \"generated\";\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 FileDropzoneController}, 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, or a `list`\n * leaves, and 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 * whose rows a page restored from the Turbo cache still showed, none of which can\n * 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. `removeOnDone` follows a\n * change made while connected: turning it off cancels every pending removal, turning\n * it on arms one for each completed row still shown, with the full delay from then,\n * and a row never holds more than one pending removal. Rows are transient\n * UI, and each carries `data-<identifier>-generated` with its upload id. A page Turbo\n * restores from its cache is a copy that still shows them, so a connection holding no\n * row of its own removes the marked rows in its list, withdraws the aggregate and\n * reports the discarded ids as `reconcile` (a dead upload cannot resume after\n * restoration). Nothing is removed on `turbo:before-cache`, which Turbo also\n * dispatches on pages that stay — a promoted frame navigation, a state-less\n * `popstate`, a refresh of a cached URL, a `data-turbo-permanent` element carried to\n * the next page — where the uploads are still running. 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 /** The pending removal of each completed row, keyed by upload id. */\n readonly #removals = new KeyedTimers<string>();\n readonly #rows = new Map<string, HTMLElement>();\n /** Whether the controller is between `connect()` and `disconnect()`. */\n #connected = false;\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 this.#connected = true;\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.#discardInherited();\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.#syncRemovals();\n }\n\n override disconnect(): void {\n this.#connected = false;\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.#removals.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`.\n }\n\n /** The row mark above, in the namespace this controller is registered under. */\n get #generatedAttribute(): string {\n return `data-${this.identifier}-${GENERATED_ATTRIBUTE}`;\n }\n\n /**\n * Removes the marked rows a connection holding no row of its own finds in its list —\n * on a page restored from Turbo's cache, the copies of rows whose uploads died with\n * the page they ran on — withdraws the aggregate, and reports their ids once.\n */\n #discardInherited(): void {\n if (this.#rows.size > 0 || !this.hasListTarget) return;\n const ids: string[] = [];\n for (const row of Array.from(this.listTarget.children)) {\n const id = row.getAttribute(this.#generatedAttribute);\n if (id === null) continue;\n row.remove();\n ids.push(id);\n }\n if (ids.length === 0) return;\n this.#syncAggregate();\n this.dispatch(\"reconcile\", { detail: { ids } });\n }\n\n /** Validates `scope` once so the per-event path never parses or throws. */\n scopeValueChanged(): void {\n this.#scopeSelector = validSelector(this.element, this.scopeValue, \"\");\n }\n\n /**\n * Follows `removeOnDone` changed at runtime: off cancels every pending removal, on\n * arms one for each completed row that has none, with the full delay from now.\n * Stimulus also calls this ahead of `connect()`, which arms the removals itself.\n */\n removeOnDoneValueChanged(): void {\n if (this.#connected) this.#syncRemovals();\n }\n\n /** Recounts the aggregate once a list leaves, retiring rows outside the list that stays. */\n listTargetDisconnected(): void {\n if (this.#connected) this.#syncAggregate();\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 /**\n * Marks a not-yet-settled row done at 100%, announces it, and emits `done`.\n *\n * @stimeoRuntimeOnly `announceDoneText` words the one announcement of this completion and\n * `removeOnDone` decides whether it arms the row's removal.\n */\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) this.#scheduleRemoval(id, row);\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 * @stimeoRuntimeOnly `announceErrorText` words the one announcement of this 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 /**\n * Brings the pending removals in line with `removeOnDone`: with it off none is left\n * pending; with it on every completed row still shown has one, and a row that already\n * has one keeps it and its deadline.\n *\n * @stimeoRuntimeOnly `removeOnDone` decides whether the completed rows have a removal\n * pending.\n */\n #syncRemovals(): void {\n if (!this.removeOnDoneValue) {\n this.#removals.clearAll();\n return;\n }\n this.#prune();\n for (const [id, row] of this.#rows) {\n if (row.getAttribute(\"data-upload-state\") === \"done\" && !this.#removals.has(id)) {\n this.#scheduleRemoval(id, row);\n }\n }\n }\n\n /** Arms the removal of a completed row, replacing one its id already had pending. */\n #scheduleRemoval(id: string, row: HTMLElement): void {\n this.#removals.set(id, () => this.#removeRow(row), REMOVE_DELAY);\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 this.#retire(key, existing);\n }\n if (!this.hasRowTarget || !this.hasListTarget) return null;\n const clone = cloneTemplateRoot(this.rowTarget);\n if (!clone) return null;\n this.#applyName(clone, name);\n clone.setAttribute(this.#generatedAttribute, key);\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(row: HTMLElement): void {\n row.remove();\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`, which is where a restored page's discard looks for them.\n */\n #prune(): void {\n for (const [id, row] of this.#rows) {\n if (this.#tracksRow(row)) continue;\n this.#retire(id, row);\n }\n }\n\n /**\n * Removes and forgets a retired clone, cancelling the removal its id had pending, so a\n * row rebuilt for the same id never inherits that deadline.\n */\n #retire(id: string, row: HTMLElement): void {\n row.remove();\n this.#rows.delete(id);\n this.#removals.clear(id);\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 event's file name, or `\"\"` when it carries none. */\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,68 +1,34 @@
|
|
|
1
1
|
import { Controller } from '@hotwired/stimulus';
|
|
2
2
|
|
|
3
|
-
// src/controllers/dirty_form_controller.ts
|
|
4
|
-
|
|
5
|
-
// src/utils/listener_set.ts
|
|
6
3
|
var ListenerSet = class {
|
|
7
|
-
/** The generation every `add` joins until the next `dispose()`. */
|
|
8
4
|
#abort = new AbortController();
|
|
9
|
-
/**
|
|
10
|
-
* Attaches `handler` to the open generation, exactly as the caller spelled it.
|
|
11
|
-
*
|
|
12
|
-
* The set supplies the signal, so `options` carries everything else the DOM
|
|
13
|
-
* accepts — `capture` included, which has to match at release time and no
|
|
14
|
-
* longer has a second place to drift from.
|
|
15
|
-
*/
|
|
16
5
|
add(target, type, handler, options) {
|
|
17
6
|
target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
|
|
18
7
|
}
|
|
19
|
-
/**
|
|
20
|
-
* Releases every listener of the open generation, synchronously, and opens the
|
|
21
|
-
* next one. Idempotent, and safe before anything has been added.
|
|
22
|
-
*/
|
|
23
8
|
dispose() {
|
|
24
9
|
this.#abort.abort();
|
|
25
10
|
this.#abort = new AbortController();
|
|
26
11
|
}
|
|
27
12
|
};
|
|
28
13
|
|
|
29
|
-
// src/utils/safe_timeout.ts
|
|
30
14
|
var TimerRegistry = class {
|
|
31
|
-
/** Live timer ids that have not yet been cleared (or, for timeouts, fired). */
|
|
32
15
|
ids = /* @__PURE__ */ new Set();
|
|
33
|
-
/**
|
|
34
|
-
* Cancels a single tracked timer.
|
|
35
|
-
*
|
|
36
|
-
* No-ops if the id is unknown (already cleared, fired, or never owned by this
|
|
37
|
-
* registry), so callers can clear defensively without guarding.
|
|
38
|
-
*/
|
|
39
16
|
clear(id) {
|
|
40
17
|
if (this.ids.delete(id)) {
|
|
41
18
|
this.cancel(id);
|
|
42
19
|
}
|
|
43
20
|
}
|
|
44
|
-
/**
|
|
45
|
-
* Cancels every tracked timer. Call this from a controller's `disconnect()`
|
|
46
|
-
* to guarantee no timer outlives the element.
|
|
47
|
-
*/
|
|
48
21
|
clearAll() {
|
|
49
22
|
for (const id of this.ids) {
|
|
50
23
|
this.cancel(id);
|
|
51
24
|
}
|
|
52
25
|
this.ids.clear();
|
|
53
26
|
}
|
|
54
|
-
/** Number of timers currently tracked (pending). */
|
|
55
27
|
get size() {
|
|
56
28
|
return this.ids.size;
|
|
57
29
|
}
|
|
58
30
|
};
|
|
59
31
|
var SafeTimeout = class extends TimerRegistry {
|
|
60
|
-
/**
|
|
61
|
-
* Schedules `callback` after `delay` ms and returns the timer id.
|
|
62
|
-
*
|
|
63
|
-
* The id is removed from the registry automatically when the timeout fires,
|
|
64
|
-
* so {@link TimerRegistry.size | size} reflects only still-pending timers.
|
|
65
|
-
*/
|
|
66
32
|
set(callback, delay) {
|
|
67
33
|
const id = this.schedule(() => {
|
|
68
34
|
this.ids.delete(id);
|
|
@@ -79,21 +45,31 @@ var SafeTimeout = class extends TimerRegistry {
|
|
|
79
45
|
}
|
|
80
46
|
};
|
|
81
47
|
|
|
82
|
-
|
|
48
|
+
function sharedRegistry(key, create) {
|
|
49
|
+
const symbol = Symbol.for(key);
|
|
50
|
+
const scope = globalThis;
|
|
51
|
+
const existing = scope[symbol];
|
|
52
|
+
if (existing !== void 0) return existing;
|
|
53
|
+
const registry = create();
|
|
54
|
+
Object.defineProperty(globalThis, symbol, { value: registry });
|
|
55
|
+
return registry;
|
|
56
|
+
}
|
|
57
|
+
|
|
83
58
|
var TransientHooks = class {
|
|
84
59
|
#attributes;
|
|
85
60
|
constructor(options) {
|
|
86
61
|
this.#attributes = options.attributes;
|
|
87
62
|
}
|
|
88
|
-
/** Drops the declared hooks from `element`; what it carries besides stays. */
|
|
89
63
|
reset(element) {
|
|
90
64
|
for (const attribute of this.#attributes) element.removeAttribute(attribute);
|
|
91
65
|
}
|
|
92
66
|
};
|
|
93
67
|
|
|
94
|
-
// src/controllers/dirty_form_controller.ts
|
|
95
68
|
var TRANSIENT = new TransientHooks({ attributes: ["data-dirty"] });
|
|
96
|
-
var visitCoordinators =
|
|
69
|
+
var visitCoordinators = sharedRegistry(
|
|
70
|
+
"stimeo-ui.dirty-form.registry.v1",
|
|
71
|
+
() => /* @__PURE__ */ new WeakMap()
|
|
72
|
+
);
|
|
97
73
|
var TurboVisitCoordinator = class {
|
|
98
74
|
#document;
|
|
99
75
|
#participants = /* @__PURE__ */ new Set();
|
|
@@ -127,7 +103,6 @@ var TurboVisitCoordinator = class {
|
|
|
127
103
|
}
|
|
128
104
|
this.#participants.add(participant);
|
|
129
105
|
}
|
|
130
|
-
/** Removes a participant and reports whether the coordinator became empty. */
|
|
131
106
|
remove(participant) {
|
|
132
107
|
this.#participants.delete(participant);
|
|
133
108
|
if (this.#participants.size > 0) return false;
|
|
@@ -163,7 +138,6 @@ var DirtyFormController = class extends Controller {
|
|
|
163
138
|
#beforeunloadBound = false;
|
|
164
139
|
#timeouts = new SafeTimeout();
|
|
165
140
|
#listeners = new ListenerSet();
|
|
166
|
-
/** Whether the active submission still represents the form's current values. */
|
|
167
141
|
#submitting = false;
|
|
168
142
|
#submitAttempt = null;
|
|
169
143
|
#submittedBaseline = null;
|
|
@@ -216,7 +190,6 @@ var DirtyFormController = class extends Controller {
|
|
|
216
190
|
event.returnValue = this.messageValue;
|
|
217
191
|
};
|
|
218
192
|
connect() {
|
|
219
|
-
this.#resetSubmission();
|
|
220
193
|
this.#baseline = this.#serialize();
|
|
221
194
|
this.#setDirty(false);
|
|
222
195
|
TRANSIENT.reset(this.element);
|
|
@@ -238,17 +211,14 @@ var DirtyFormController = class extends Controller {
|
|
|
238
211
|
this.#unbindBeforeUnload();
|
|
239
212
|
this.#resetSubmission();
|
|
240
213
|
}
|
|
241
|
-
/** Re-baselines to the current values and clears the dirty state (e.g. after a save). */
|
|
242
214
|
markClean() {
|
|
243
215
|
this.#baseline = this.#serialize();
|
|
244
216
|
this.#setDirty(false);
|
|
245
217
|
}
|
|
246
|
-
/** Adopts restored values without erasing an already-dirty user revision. */
|
|
247
218
|
acceptRestore() {
|
|
248
219
|
if (this.#dirty) return;
|
|
249
220
|
this.markClean();
|
|
250
221
|
}
|
|
251
|
-
/** Recomputes dirty against the baseline and flips state when it changes. */
|
|
252
222
|
#evaluate() {
|
|
253
223
|
this.#setDirty(this.#serialize() !== this.#baseline);
|
|
254
224
|
}
|
|
@@ -264,7 +234,6 @@ var DirtyFormController = class extends Controller {
|
|
|
264
234
|
}
|
|
265
235
|
this.dispatch("dirty", { detail: { dirty } });
|
|
266
236
|
}
|
|
267
|
-
/** Evaluates this form for the document-level Turbo visit coordinator. */
|
|
268
237
|
#evaluateVisit(event) {
|
|
269
238
|
if (!this.#dirty || this.#guardSuppressed()) return null;
|
|
270
239
|
const guard = this.dispatch("guard", { detail: { event }, cancelable: true });
|
|
@@ -293,7 +262,6 @@ var DirtyFormController = class extends Controller {
|
|
|
293
262
|
this.#submittedBaseline = null;
|
|
294
263
|
this.#activeSubmission = null;
|
|
295
264
|
}
|
|
296
|
-
/** Stable serialization of the form's controls for change detection. */
|
|
297
265
|
#serialize() {
|
|
298
266
|
const parts = [];
|
|
299
267
|
for (const el of Array.from(this.element.elements)) {
|
|
@@ -310,7 +278,6 @@ var DirtyFormController = class extends Controller {
|
|
|
310
278
|
}
|
|
311
279
|
return JSON.stringify(parts);
|
|
312
280
|
}
|
|
313
|
-
/** A stable key for a control, or null for elements without value semantics. */
|
|
314
281
|
#nameOf(el) {
|
|
315
282
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
|
|
316
283
|
return el.name || el.id || "";
|
|
@@ -320,5 +287,4 @@ var DirtyFormController = class extends Controller {
|
|
|
320
287
|
};
|
|
321
288
|
|
|
322
289
|
export { DirtyFormController };
|
|
323
|
-
//# sourceMappingURL=dirty_form_controller.js.map
|
|
324
290
|
//# sourceMappingURL=dirty_form_controller.js.map
|