stimeo-ui 0.15.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.
Files changed (332) hide show
  1. package/CHANGELOG.md +209 -0
  2. package/README.md +128 -1
  3. package/dist/cable/index.d.ts +79 -23
  4. package/dist/cable/index.js +392 -417
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/controllers/accordion_controller.d.ts +34 -5
  7. package/dist/controllers/accordion_controller.js +228 -28
  8. package/dist/controllers/accordion_controller.js.map +1 -1
  9. package/dist/controllers/alert_dialog_controller.d.ts +20 -9
  10. package/dist/controllers/alert_dialog_controller.js +862 -221
  11. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  12. package/dist/controllers/announcer_controller.d.ts +38 -7
  13. package/dist/controllers/announcer_controller.js +135 -191
  14. package/dist/controllers/announcer_controller.js.map +1 -1
  15. package/dist/controllers/aspect_ratio_controller.js +0 -10
  16. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  17. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  18. package/dist/controllers/auto_submit_controller.js +295 -110
  19. package/dist/controllers/auto_submit_controller.js.map +1 -1
  20. package/dist/controllers/avatar_controller.js +172 -106
  21. package/dist/controllers/avatar_controller.js.map +1 -1
  22. package/dist/controllers/breadcrumb_controller.d.ts +10 -7
  23. package/dist/controllers/breadcrumb_controller.js +0 -129
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +22 -4
  26. package/dist/controllers/bulk_select_controller.js +264 -53
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +109 -28
  29. package/dist/controllers/calendar_controller.js +346 -180
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +99 -27
  32. package/dist/controllers/carousel_controller.js +587 -300
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +18 -2
  35. package/dist/controllers/character_counter_controller.js +271 -143
  36. package/dist/controllers/character_counter_controller.js.map +1 -1
  37. package/dist/controllers/checkbox_controller.d.ts +7 -2
  38. package/dist/controllers/checkbox_controller.js +77 -75
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +44 -7
  41. package/dist/controllers/clipboard_controller.js +187 -108
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +41 -5
  44. package/dist/controllers/collapsible_controller.js +374 -108
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +47 -14
  47. package/dist/controllers/color_picker_controller.js +183 -127
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +33 -6
  50. package/dist/controllers/combobox_controller.js +426 -109
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +45 -14
  53. package/dist/controllers/command_palette_controller.js +994 -350
  54. package/dist/controllers/command_palette_controller.js.map +1 -1
  55. package/dist/controllers/conditional_fields_controller.d.ts +15 -2
  56. package/dist/controllers/conditional_fields_controller.js +212 -125
  57. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  58. package/dist/controllers/confirm_controller.d.ts +49 -12
  59. package/dist/controllers/confirm_controller.js +939 -245
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +36 -3
  62. package/dist/controllers/context_menu_controller.js +275 -127
  63. package/dist/controllers/context_menu_controller.js.map +1 -1
  64. package/dist/controllers/count_up_controller.d.ts +13 -1
  65. package/dist/controllers/count_up_controller.js +73 -21
  66. package/dist/controllers/count_up_controller.js.map +1 -1
  67. package/dist/controllers/countdown_controller.d.ts +75 -2
  68. package/dist/controllers/countdown_controller.js +235 -97
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -15
  71. package/dist/controllers/currency_input_controller.js +241 -118
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +78 -22
  74. package/dist/controllers/data_grid_controller.js +229 -135
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +64 -11
  77. package/dist/controllers/date_range_picker_controller.js +359 -165
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +22 -3
  80. package/dist/controllers/dialog_controller.js +887 -225
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +24 -7
  83. package/dist/controllers/direct_upload_controller.js +80 -142
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +2 -2
  86. package/dist/controllers/dirty_form_controller.js +26 -47
  87. package/dist/controllers/dirty_form_controller.js.map +1 -1
  88. package/dist/controllers/dismissible_controller.d.ts +19 -0
  89. package/dist/controllers/dismissible_controller.js +175 -20
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +52 -16
  92. package/dist/controllers/drawer_controller.js +963 -336
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +39 -3
  95. package/dist/controllers/dropdown_controller.js +342 -85
  96. package/dist/controllers/dropdown_controller.js.map +1 -1
  97. package/dist/controllers/editable_controller.d.ts +12 -1
  98. package/dist/controllers/editable_controller.js +182 -58
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +67 -53
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +60 -18
  103. package/dist/controllers/file_dropzone_controller.js +426 -229
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.d.ts +10 -4
  106. package/dist/controllers/filter_controller.js +78 -36
  107. package/dist/controllers/filter_controller.js.map +1 -1
  108. package/dist/controllers/flash_controller.d.ts +57 -14
  109. package/dist/controllers/flash_controller.js +328 -211
  110. package/dist/controllers/flash_controller.js.map +1 -1
  111. package/dist/controllers/focus_controller.d.ts +12 -3
  112. package/dist/controllers/focus_controller.js +654 -239
  113. package/dist/controllers/focus_controller.js.map +1 -1
  114. package/dist/controllers/form_field_controller.js +153 -136
  115. package/dist/controllers/form_field_controller.js.map +1 -1
  116. package/dist/controllers/form_validation_controller.js +29 -96
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +41 -9
  119. package/dist/controllers/frame_loading_controller.js +263 -198
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +22 -2
  122. package/dist/controllers/highlight_controller.js +119 -70
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +65 -4
  125. package/dist/controllers/hover_card_controller.js +335 -118
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +58 -4
  128. package/dist/controllers/idle_controller.js +316 -65
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +5 -2
  131. package/dist/controllers/input_mask_controller.js +116 -61
  132. package/dist/controllers/input_mask_controller.js.map +1 -1
  133. package/dist/controllers/intersection_controller.d.ts +32 -13
  134. package/dist/controllers/intersection_controller.js +147 -103
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +16 -0
  137. package/dist/controllers/lazy_frame_controller.js +9 -56
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +84 -10
  140. package/dist/controllers/listbox_controller.js +413 -153
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +1 -1
  143. package/dist/controllers/local_time_controller.js +54 -64
  144. package/dist/controllers/local_time_controller.js.map +1 -1
  145. package/dist/controllers/masonry_controller.d.ts +11 -3
  146. package/dist/controllers/masonry_controller.js +70 -95
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +40 -9
  149. package/dist/controllers/menu_controller.js +254 -167
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +15 -4
  152. package/dist/controllers/menubar_controller.js +206 -331
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +20 -0
  155. package/dist/controllers/meter_controller.js +159 -71
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +50 -7
  158. package/dist/controllers/multi_select_controller.js +797 -388
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +19 -1
  161. package/dist/controllers/navigation_menu_controller.js +156 -231
  162. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  163. package/dist/controllers/nested_form_controller.d.ts +10 -0
  164. package/dist/controllers/nested_form_controller.js +373 -174
  165. package/dist/controllers/nested_form_controller.js.map +1 -1
  166. package/dist/controllers/network_status_controller.d.ts +23 -0
  167. package/dist/controllers/network_status_controller.js +241 -45
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +62 -10
  170. package/dist/controllers/number_input_controller.js +387 -227
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.js +28 -75
  173. package/dist/controllers/optimistic_controller.js.map +1 -1
  174. package/dist/controllers/otp_controller.d.ts +38 -7
  175. package/dist/controllers/otp_controller.js +344 -196
  176. package/dist/controllers/otp_controller.js.map +1 -1
  177. package/dist/controllers/overflow_indicator_controller.d.ts +13 -12
  178. package/dist/controllers/overflow_indicator_controller.js +167 -93
  179. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  180. package/dist/controllers/overflow_menu_controller.d.ts +66 -9
  181. package/dist/controllers/overflow_menu_controller.js +408 -277
  182. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  183. package/dist/controllers/pagination_controller.d.ts +52 -15
  184. package/dist/controllers/pagination_controller.js +342 -120
  185. package/dist/controllers/pagination_controller.js.map +1 -1
  186. package/dist/controllers/password_reveal_controller.d.ts +60 -9
  187. package/dist/controllers/password_reveal_controller.js +424 -99
  188. package/dist/controllers/password_reveal_controller.js.map +1 -1
  189. package/dist/controllers/password_strength_controller.d.ts +14 -5
  190. package/dist/controllers/password_strength_controller.js +99 -153
  191. package/dist/controllers/password_strength_controller.js.map +1 -1
  192. package/dist/controllers/persist_controller.d.ts +11 -4
  193. package/dist/controllers/persist_controller.js +252 -96
  194. package/dist/controllers/persist_controller.js.map +1 -1
  195. package/dist/controllers/pointer_drag_controller.d.ts +19 -7
  196. package/dist/controllers/pointer_drag_controller.js +263 -164
  197. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  198. package/dist/controllers/popover_controller.d.ts +46 -4
  199. package/dist/controllers/popover_controller.js +267 -94
  200. package/dist/controllers/popover_controller.js.map +1 -1
  201. package/dist/controllers/portal_controller.d.ts +44 -4
  202. package/dist/controllers/portal_controller.js +209 -72
  203. package/dist/controllers/portal_controller.js.map +1 -1
  204. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  205. package/dist/controllers/preview_guard_controller.js +243 -112
  206. package/dist/controllers/preview_guard_controller.js.map +1 -1
  207. package/dist/controllers/progress_controller.d.ts +11 -0
  208. package/dist/controllers/progress_controller.js +132 -59
  209. package/dist/controllers/progress_controller.js.map +1 -1
  210. package/dist/controllers/radio_group_controller.d.ts +19 -8
  211. package/dist/controllers/radio_group_controller.js +138 -133
  212. package/dist/controllers/radio_group_controller.js.map +1 -1
  213. package/dist/controllers/range_slider_controller.d.ts +92 -4
  214. package/dist/controllers/range_slider_controller.js +245 -112
  215. package/dist/controllers/range_slider_controller.js.map +1 -1
  216. package/dist/controllers/rating_controller.d.ts +33 -6
  217. package/dist/controllers/rating_controller.js +429 -194
  218. package/dist/controllers/rating_controller.js.map +1 -1
  219. package/dist/controllers/read_more_controller.d.ts +26 -2
  220. package/dist/controllers/read_more_controller.js +185 -47
  221. package/dist/controllers/read_more_controller.js.map +1 -1
  222. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  223. package/dist/controllers/reading_progress_controller.js +147 -135
  224. package/dist/controllers/reading_progress_controller.js.map +1 -1
  225. package/dist/controllers/relative_time_controller.d.ts +18 -4
  226. package/dist/controllers/relative_time_controller.js +145 -110
  227. package/dist/controllers/relative_time_controller.js.map +1 -1
  228. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  229. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +97 -38
  230. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  231. package/dist/controllers/resizable_controller.d.ts +44 -4
  232. package/dist/controllers/resizable_controller.js +294 -118
  233. package/dist/controllers/resizable_controller.js.map +1 -1
  234. package/dist/controllers/roving_controller.js +39 -84
  235. package/dist/controllers/roving_controller.js.map +1 -1
  236. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  237. package/dist/controllers/scroll_area_controller.js +428 -221
  238. package/dist/controllers/scroll_area_controller.js.map +1 -1
  239. package/dist/controllers/scroll_restore_controller.js +0 -53
  240. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  241. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  242. package/dist/controllers/scroll_visibility_controller.js +395 -146
  243. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  244. package/dist/controllers/scrollspy_controller.d.ts +9 -3
  245. package/dist/controllers/scrollspy_controller.js +181 -236
  246. package/dist/controllers/scrollspy_controller.js.map +1 -1
  247. package/dist/controllers/separator_controller.d.ts +59 -17
  248. package/dist/controllers/separator_controller.js +288 -132
  249. package/dist/controllers/separator_controller.js.map +1 -1
  250. package/dist/controllers/sidebar_controller.d.ts +81 -7
  251. package/dist/controllers/sidebar_controller.js +1071 -375
  252. package/dist/controllers/sidebar_controller.js.map +1 -1
  253. package/dist/controllers/skeleton_controller.d.ts +23 -0
  254. package/dist/controllers/skeleton_controller.js +248 -92
  255. package/dist/controllers/skeleton_controller.js.map +1 -1
  256. package/dist/controllers/slider_controller.d.ts +72 -8
  257. package/dist/controllers/slider_controller.js +211 -96
  258. package/dist/controllers/slider_controller.js.map +1 -1
  259. package/dist/controllers/smart_sticky_header_controller.d.ts +21 -3
  260. package/dist/controllers/smart_sticky_header_controller.js +163 -54
  261. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  262. package/dist/controllers/sortable_controller.js +13 -110
  263. package/dist/controllers/sortable_controller.js.map +1 -1
  264. package/dist/controllers/spinner_controller.d.ts +39 -4
  265. package/dist/controllers/spinner_controller.js +277 -161
  266. package/dist/controllers/spinner_controller.js.map +1 -1
  267. package/dist/controllers/step_indicator_controller.d.ts +24 -17
  268. package/dist/controllers/step_indicator_controller.js +107 -53
  269. package/dist/controllers/step_indicator_controller.js.map +1 -1
  270. package/dist/controllers/stepper_controller.d.ts +43 -9
  271. package/dist/controllers/stepper_controller.js +171 -48
  272. package/dist/controllers/stepper_controller.js.map +1 -1
  273. package/dist/controllers/stick_to_bottom_controller.d.ts +25 -0
  274. package/dist/controllers/stick_to_bottom_controller.js +310 -116
  275. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  276. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  277. package/dist/controllers/sticky_observer_controller.js +214 -49
  278. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  279. package/dist/controllers/submit_once_controller.d.ts +22 -6
  280. package/dist/controllers/submit_once_controller.js +294 -192
  281. package/dist/controllers/submit_once_controller.js.map +1 -1
  282. package/dist/controllers/switch_controller.d.ts +40 -7
  283. package/dist/controllers/switch_controller.js +107 -43
  284. package/dist/controllers/switch_controller.js.map +1 -1
  285. package/dist/controllers/tabs_controller.d.ts +13 -1
  286. package/dist/controllers/tabs_controller.js +50 -25
  287. package/dist/controllers/tabs_controller.js.map +1 -1
  288. package/dist/controllers/tags_input_controller.d.ts +27 -3
  289. package/dist/controllers/tags_input_controller.js +290 -149
  290. package/dist/controllers/tags_input_controller.js.map +1 -1
  291. package/dist/controllers/textarea_autosize_controller.d.ts +10 -0
  292. package/dist/controllers/textarea_autosize_controller.js +71 -58
  293. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  294. package/dist/controllers/theme_controller.d.ts +37 -8
  295. package/dist/controllers/theme_controller.js +148 -80
  296. package/dist/controllers/theme_controller.js.map +1 -1
  297. package/dist/controllers/time_picker_controller.d.ts +18 -1
  298. package/dist/controllers/time_picker_controller.js +162 -81
  299. package/dist/controllers/time_picker_controller.js.map +1 -1
  300. package/dist/controllers/toast_controller.d.ts +85 -23
  301. package/dist/controllers/toast_controller.js +365 -193
  302. package/dist/controllers/toast_controller.js.map +1 -1
  303. package/dist/controllers/toggle_group_controller.d.ts +60 -9
  304. package/dist/controllers/toggle_group_controller.js +194 -99
  305. package/dist/controllers/toggle_group_controller.js.map +1 -1
  306. package/dist/controllers/toolbar_controller.js +92 -90
  307. package/dist/controllers/toolbar_controller.js.map +1 -1
  308. package/dist/controllers/tooltip_controller.d.ts +52 -2
  309. package/dist/controllers/tooltip_controller.js +185 -125
  310. package/dist/controllers/tooltip_controller.js.map +1 -1
  311. package/dist/controllers/transition_controller.d.ts +22 -4
  312. package/dist/controllers/transition_controller.js +123 -115
  313. package/dist/controllers/transition_controller.js.map +1 -1
  314. package/dist/controllers/tree_view_controller.d.ts +59 -18
  315. package/dist/controllers/tree_view_controller.js +248 -290
  316. package/dist/controllers/tree_view_controller.js.map +1 -1
  317. package/dist/index.d.ts +33 -3
  318. package/dist/index.js +8649 -8572
  319. package/dist/index.js.map +1 -1
  320. package/dist/inspector/cli.d.ts +95 -9
  321. package/dist/inspector/cli.js +343 -77
  322. package/dist/inspector/cli.js.map +1 -1
  323. package/dist/inspector/cli_bin.js +390 -96
  324. package/dist/inspector/cli_bin.js.map +1 -1
  325. package/dist/inspector/examples.json +45 -45
  326. package/dist/inspector/manifest.json +1308 -155
  327. package/dist/positioning/index.d.ts +8 -0
  328. package/dist/positioning/index.js +148 -45
  329. package/dist/positioning/index.js.map +1 -1
  330. package/package.json +7 -7
  331. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  332. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -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,8 +45,31 @@ var SafeTimeout = class extends TimerRegistry {
79
45
  }
80
46
  };
81
47
 
82
- // src/controllers/dirty_form_controller.ts
83
- var visitCoordinators = /* @__PURE__ */ new WeakMap();
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
+
58
+ var TransientHooks = class {
59
+ #attributes;
60
+ constructor(options) {
61
+ this.#attributes = options.attributes;
62
+ }
63
+ reset(element) {
64
+ for (const attribute of this.#attributes) element.removeAttribute(attribute);
65
+ }
66
+ };
67
+
68
+ var TRANSIENT = new TransientHooks({ attributes: ["data-dirty"] });
69
+ var visitCoordinators = sharedRegistry(
70
+ "stimeo-ui.dirty-form.registry.v1",
71
+ () => /* @__PURE__ */ new WeakMap()
72
+ );
84
73
  var TurboVisitCoordinator = class {
85
74
  #document;
86
75
  #participants = /* @__PURE__ */ new Set();
@@ -114,7 +103,6 @@ var TurboVisitCoordinator = class {
114
103
  }
115
104
  this.#participants.add(participant);
116
105
  }
117
- /** Removes a participant and reports whether the coordinator became empty. */
118
106
  remove(participant) {
119
107
  this.#participants.delete(participant);
120
108
  if (this.#participants.size > 0) return false;
@@ -150,7 +138,6 @@ var DirtyFormController = class extends Controller {
150
138
  #beforeunloadBound = false;
151
139
  #timeouts = new SafeTimeout();
152
140
  #listeners = new ListenerSet();
153
- /** Whether the active submission still represents the form's current values. */
154
141
  #submitting = false;
155
142
  #submitAttempt = null;
156
143
  #submittedBaseline = null;
@@ -203,10 +190,9 @@ var DirtyFormController = class extends Controller {
203
190
  event.returnValue = this.messageValue;
204
191
  };
205
192
  connect() {
206
- this.#resetSubmission();
207
193
  this.#baseline = this.#serialize();
208
194
  this.#setDirty(false);
209
- this.element.removeAttribute("data-dirty");
195
+ TRANSIENT.reset(this.element);
210
196
  this.#listeners.add(this.element, "input", this.#onFieldChange);
211
197
  this.#listeners.add(this.element, "change", this.#onFieldChange);
212
198
  this.#listeners.add(this.element, "submit", this.#onSubmit);
@@ -225,17 +211,14 @@ var DirtyFormController = class extends Controller {
225
211
  this.#unbindBeforeUnload();
226
212
  this.#resetSubmission();
227
213
  }
228
- /** Re-baselines to the current values and clears the dirty state (e.g. after a save). */
229
214
  markClean() {
230
215
  this.#baseline = this.#serialize();
231
216
  this.#setDirty(false);
232
217
  }
233
- /** Adopts restored values without erasing an already-dirty user revision. */
234
218
  acceptRestore() {
235
219
  if (this.#dirty) return;
236
220
  this.markClean();
237
221
  }
238
- /** Recomputes dirty against the baseline and flips state when it changes. */
239
222
  #evaluate() {
240
223
  this.#setDirty(this.#serialize() !== this.#baseline);
241
224
  }
@@ -251,7 +234,6 @@ var DirtyFormController = class extends Controller {
251
234
  }
252
235
  this.dispatch("dirty", { detail: { dirty } });
253
236
  }
254
- /** Evaluates this form for the document-level Turbo visit coordinator. */
255
237
  #evaluateVisit(event) {
256
238
  if (!this.#dirty || this.#guardSuppressed()) return null;
257
239
  const guard = this.dispatch("guard", { detail: { event }, cancelable: true });
@@ -280,7 +262,6 @@ var DirtyFormController = class extends Controller {
280
262
  this.#submittedBaseline = null;
281
263
  this.#activeSubmission = null;
282
264
  }
283
- /** Stable serialization of the form's controls for change detection. */
284
265
  #serialize() {
285
266
  const parts = [];
286
267
  for (const el of Array.from(this.element.elements)) {
@@ -297,7 +278,6 @@ var DirtyFormController = class extends Controller {
297
278
  }
298
279
  return JSON.stringify(parts);
299
280
  }
300
- /** A stable key for a control, or null for elements without value semantics. */
301
281
  #nameOf(el) {
302
282
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement || el instanceof HTMLSelectElement) {
303
283
  return el.name || el.id || "";
@@ -307,5 +287,4 @@ var DirtyFormController = class extends Controller {
307
287
  };
308
288
 
309
289
  export { DirtyFormController };
310
- //# sourceMappingURL=dirty_form_controller.js.map
311
290
  //# sourceMappingURL=dirty_form_controller.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/listener_set.ts","../../src/utils/safe_timeout.ts","../../src/controllers/dirty_form_controller.ts"],"names":[],"mappings":"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","file":"dirty_form_controller.js","sourcesContent":["/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, so `dispose()` belongs exactly where\n * the `removeEventListener` block stood. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\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 #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * 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 { Controller } from \"@hotwired/stimulus\";\nimport { ListenerSet } from \"../utils/listener_set\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\ntype VisitDecision =\n | { readonly kind: \"block\" }\n | { readonly kind: \"confirm\"; readonly message: string }\n | null;\n\ninterface VisitParticipant {\n readonly form: HTMLFormElement;\n readonly evaluate: (event: Event) => VisitDecision;\n}\n\ninterface SubmitAttempt {\n readonly event: Event;\n snapshot: string;\n}\n\ninterface TurboSubmitDetail {\n readonly formSubmission?: unknown;\n readonly success?: boolean;\n}\n\ntype SerializedControl = readonly [\n type: string,\n name: string,\n value: string | readonly string[],\n checked?: 0 | 1,\n];\n\n/**\n * Coordinates native Turbo confirmations for every dirty form in one document.\n *\n * The weak document registry owns no dirty baseline or durable form state. Each\n * controller remains the source of truth for its own state and unregisters on\n * disconnect; the shared coordinator only gives one navigation event a single,\n * deterministic decision point.\n */\nconst visitCoordinators = new WeakMap<Document, TurboVisitCoordinator>();\n\nclass TurboVisitCoordinator {\n readonly #document: Document;\n readonly #participants = new Set<VisitParticipant>();\n\n readonly #onBeforeVisit = (event: Event): void => {\n let blocked = event.defaultPrevented;\n let message: string | undefined;\n\n // Every eligible form gets its public guard event before a native prompt.\n // A later consumer can therefore block the visit without an earlier form\n // opening a redundant confirm first.\n for (const participant of this.#participantsInDomOrder()) {\n if (!participant.form.isConnected) continue;\n const decision = participant.evaluate(event);\n blocked ||= event.defaultPrevented;\n if (decision === null) continue;\n if (decision.kind === \"block\") {\n blocked = true;\n } else {\n message ??= decision.message;\n }\n }\n\n if (blocked) {\n event.preventDefault();\n return;\n }\n if (message === undefined) return;\n if (!this.#document.defaultView?.confirm(message)) event.preventDefault();\n };\n\n constructor(document: Document) {\n this.#document = document;\n }\n\n add(participant: VisitParticipant): void {\n if (this.#participants.size === 0) {\n this.#document.addEventListener(\"turbo:before-visit\", this.#onBeforeVisit);\n }\n this.#participants.add(participant);\n }\n\n /** Removes a participant and reports whether the coordinator became empty. */\n remove(participant: VisitParticipant): boolean {\n this.#participants.delete(participant);\n if (this.#participants.size > 0) return false;\n this.#document.removeEventListener(\"turbo:before-visit\", this.#onBeforeVisit);\n return true;\n }\n\n #participantsInDomOrder(): VisitParticipant[] {\n return Array.from(this.#participants)\n .filter(\n (participant) =>\n participant.form.isConnected && participant.form.ownerDocument === this.#document,\n )\n .sort((left, right) => {\n const position = left.form.compareDocumentPosition(right.form);\n return (\n Number(Boolean(position & Node.DOCUMENT_POSITION_PRECEDING)) -\n Number(Boolean(position & Node.DOCUMENT_POSITION_FOLLOWING))\n );\n });\n }\n}\n\nfunction registerVisitParticipant(document: Document, participant: VisitParticipant): () => void {\n const coordinator = visitCoordinators.get(document) ?? new TurboVisitCoordinator(document);\n visitCoordinators.set(document, coordinator);\n coordinator.add(participant);\n\n return () => {\n if (coordinator.remove(participant)) visitCoordinators.delete(document);\n };\n}\n\n/**\n * Headless \"unsaved changes\" guard for a form (no dedicated APG pattern; supports\n * WCAG 2.2 error-prevention 3.3.4 / 3.3.6 by preventing accidental data loss).\n *\n * Markup contract (identifier: `stimeo--dirty-form`):\n * <form data-controller=\"stimeo--dirty-form\">\n * …fields…\n * </form>\n *\n * Snapshots the form's field values on connect, marks the form `data-dirty` once a\n * value changes, and — while dirty — guards both a full unload (`beforeunload`) and\n * a Turbo visit (`turbo:before-visit`). On a Turbo visit it dispatches a cancelable\n * `guard` event for each eligible form; if a consumer cancels one (or any form uses\n * `confirmBridge`) the visit is blocked, otherwise the document falls back to one\n * native `confirm(message)`, using the first eligible form in live DOM order. While a\n * submit is in flight the guard is suppressed until another edit occurs (so a\n * legitimate submit never prompts); the browser's `formdata` event captures the exact\n * revision Turbo materializes, and a successful `turbo:submit-end` accepts only that\n * revision, preserving any later edits as dirty. A failed submit re-arms the guard;\n * `markClean` clears it manually (e.g. after a custom save), while `acceptRestore`\n * adopts a restored draft only while no user edit is already dirty.\n *\n * `dirty` dispatches `{ dirty }`; `guard` dispatches `{ event }`.\n *\n * @remarks\n * Behavior only — it renders no confirmation UI (pair with a Confirm Bridge) and\n * does not persist input (pair with Persist). The dirty baseline is read from the\n * DOM on `connect()` (no module-scope state) — which also clears a stale\n * `data-dirty` left in a Turbo cache snapshot, since the restored values are the\n * new baseline — `beforeunload` is wired only while dirty, and every listener is\n * removed on `disconnect()` (Turbo navigation included) so a stale guard never\n * outlives the form.\n */\nexport class DirtyFormController extends Controller<HTMLFormElement> {\n static override values = {\n message: { type: String, default: \"You have unsaved changes that will be lost.\" },\n confirmBridge: { type: Boolean, default: false },\n };\n static actions = [\"markClean\", \"acceptRestore\"] as const;\n static events = [\"dirty\", \"guard\"] as const;\n\n declare messageValue: string;\n declare confirmBridgeValue: boolean;\n\n #baseline = \"\";\n #dirty = false;\n #beforeunloadBound = false;\n readonly #timeouts = new SafeTimeout();\n readonly #listeners = new ListenerSet();\n /** Whether the active submission still represents the form's current values. */\n #submitting = false;\n #submitAttempt: SubmitAttempt | null = null;\n #submittedBaseline: string | null = null;\n #activeSubmission: unknown | null = null;\n #unregisterVisit: (() => void) | null = null;\n\n readonly #onFieldChange = (): void => {\n // An edit after submission belongs to a new unsaved revision. Keep the\n // submitted snapshot for submit-end, but stop suppressing navigation now.\n this.#submitting = false;\n this.#evaluate();\n };\n\n readonly #onSubmit = (event: Event): void => {\n const attempt: SubmitAttempt = { event, snapshot: this.#serialize() };\n this.#submitAttempt = attempt;\n this.#submitting = true;\n // Native navigation runs before the next task. If it does not navigate and\n // Turbo never claims the attempt, the form must not remain suppressed.\n this.#timeouts.set(() => {\n if (this.#submitAttempt !== attempt) return;\n this.#submitting = false;\n }, 0);\n };\n\n readonly #onFormData = (): void => {\n if (this.#submitAttempt === null) return;\n // Turbo constructs FormData after target-level submit consumers have run.\n // Capturing here reflects exactly that revision, even when an asynchronous\n // confirmation delays turbo:submit-start and the user edits in the meantime.\n this.#submitAttempt.snapshot = this.#serialize();\n };\n\n readonly #onSubmitStart = (event: Event): void => {\n const detail = (event as CustomEvent<TurboSubmitDetail>).detail;\n this.#activeSubmission = detail?.formSubmission ?? event;\n this.#submittedBaseline = this.#submitAttempt?.snapshot ?? this.#serialize();\n this.#submitAttempt = null;\n this.#submitting = this.#serialize() === this.#submittedBaseline;\n };\n\n readonly #onSubmitEnd = (event: Event): void => {\n const detail = (event as CustomEvent<TurboSubmitDetail>).detail;\n if (\n detail?.formSubmission !== undefined &&\n (this.#activeSubmission === null || detail.formSubmission !== this.#activeSubmission)\n ) {\n return;\n }\n const submittedBaseline = this.#submittedBaseline;\n this.#submitting = false;\n this.#submittedBaseline = null;\n this.#activeSubmission = null;\n if (detail?.success === false) return;\n if (submittedBaseline === null) {\n this.markClean();\n return;\n }\n // The server accepted the submitted revision, not edits made after it left.\n this.#baseline = submittedBaseline;\n this.#evaluate();\n };\n\n readonly #onBeforeUnload = (event: BeforeUnloadEvent): void => {\n if (!this.#dirty || this.#guardSuppressed()) return;\n event.preventDefault();\n // Legacy requirement: a non-empty returnValue triggers the native prompt.\n event.returnValue = this.messageValue;\n };\n\n override connect(): void {\n this.#resetSubmission();\n this.#baseline = this.#serialize();\n this.#setDirty(false);\n // Re-baselining means the restored values ARE the clean state, so a stale\n // data-dirty captured in a Turbo cache snapshot mid-edit must not linger\n // (the guard would not fire, but consumer CSS would keep claiming \"unsaved\").\n this.element.removeAttribute(\"data-dirty\");\n this.#listeners.add(this.element, \"input\", this.#onFieldChange);\n this.#listeners.add(this.element, \"change\", this.#onFieldChange);\n this.#listeners.add(this.element, \"submit\", this.#onSubmit);\n this.#listeners.add(this.element, \"formdata\", this.#onFormData);\n this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n this.#unregisterVisit = registerVisitParticipant(this.element.ownerDocument, {\n form: this.element,\n evaluate: (event) => this.#evaluateVisit(event),\n });\n }\n\n override disconnect(): void {\n this.#listeners.dispose();\n this.#unregisterVisit?.();\n this.#unregisterVisit = null;\n this.#unbindBeforeUnload();\n this.#resetSubmission();\n }\n\n /** Re-baselines to the current values and clears the dirty state (e.g. after a save). */\n markClean(): void {\n this.#baseline = this.#serialize();\n this.#setDirty(false);\n }\n\n /** Adopts restored values without erasing an already-dirty user revision. */\n acceptRestore(): void {\n if (this.#dirty) return;\n this.markClean();\n }\n\n /** Recomputes dirty against the baseline and flips state when it changes. */\n #evaluate(): void {\n this.#setDirty(this.#serialize() !== this.#baseline);\n }\n\n #setDirty(dirty: boolean): void {\n if (dirty === this.#dirty) return;\n this.#dirty = dirty;\n if (dirty) {\n this.element.setAttribute(\"data-dirty\", \"true\");\n this.#bindBeforeUnload();\n } else {\n this.element.removeAttribute(\"data-dirty\");\n this.#unbindBeforeUnload();\n }\n this.dispatch(\"dirty\", { detail: { dirty } });\n }\n\n /** Evaluates this form for the document-level Turbo visit coordinator. */\n #evaluateVisit(event: Event): VisitDecision {\n if (!this.#dirty || this.#guardSuppressed()) return null;\n const guard = this.dispatch(\"guard\", { detail: { event }, cancelable: true });\n if (guard.defaultPrevented) return { kind: \"block\" };\n // A guard consumer may have saved, submitted, or removed the form synchronously.\n if (!this.element.isConnected || !this.#dirty || this.#guardSuppressed()) return null;\n if (this.confirmBridgeValue) return { kind: \"block\" };\n return { kind: \"confirm\", message: this.messageValue };\n }\n\n #bindBeforeUnload(): void {\n if (this.#beforeunloadBound) return;\n window.addEventListener(\"beforeunload\", this.#onBeforeUnload);\n this.#beforeunloadBound = true;\n }\n\n #unbindBeforeUnload(): void {\n if (!this.#beforeunloadBound) return;\n window.removeEventListener(\"beforeunload\", this.#onBeforeUnload);\n this.#beforeunloadBound = false;\n }\n\n #guardSuppressed(): boolean {\n return (\n this.#submitting &&\n (this.#submitAttempt === null || !this.#submitAttempt.event.defaultPrevented)\n );\n }\n\n #resetSubmission(): void {\n this.#timeouts.clearAll();\n this.#submitting = false;\n this.#submitAttempt = null;\n this.#submittedBaseline = null;\n this.#activeSubmission = null;\n }\n\n /** Stable serialization of the form's controls for change detection. */\n #serialize(): string {\n const parts: SerializedControl[] = [];\n for (const el of Array.from(this.element.elements)) {\n const name = this.#nameOf(el);\n if (name === null) continue;\n if (el instanceof HTMLInputElement && (el.type === \"checkbox\" || el.type === \"radio\")) {\n parts.push([el.type, name, el.value, el.checked ? 1 : 0]);\n } else if (el instanceof HTMLSelectElement) {\n // Keep multi-select values structural so option values containing commas cannot collide.\n const value = el.multiple\n ? Array.from(el.selectedOptions).map((option) => option.value)\n : el.value;\n parts.push([el.type, name, value]);\n } else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {\n parts.push([el.type, name, el.value]);\n }\n }\n return JSON.stringify(parts);\n }\n\n /** A stable key for a control, or null for elements without value semantics. */\n #nameOf(el: Element): string | null {\n if (\n el instanceof HTMLInputElement ||\n el instanceof HTMLTextAreaElement ||\n el instanceof HTMLSelectElement\n ) {\n return el.name || el.id || \"\";\n }\n return null;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/listener_set.ts","../../src/utils/safe_timeout.ts","../../src/utils/shared_registry.ts","../../src/utils/transient_hooks.ts","../../src/controllers/dirty_form_controller.ts"],"names":[],"mappings":"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","file":"dirty_form_controller.js","sourcesContent":["/**\n * Owns every listener a controller attaches for one connected lifetime and\n * releases them together, so an event name is spelled once and the release\n * cannot drift from the registration.\n *\n * Detaching by hand repeats three things at the release site — the type, the\n * handler reference and the capture flag — and any one of them drifting leaves\n * a listener alive on an element the controller no longer drives. Aborting the\n * signal a registration was created with releases it whatever it was made of,\n * so the release site names nothing.\n *\n * One set per lifetime, held as a field and never rebuilt. {@link\n * ListenerSet.dispose} is the only thing that closes a generation, and it opens\n * the next one in the same step, so the lifecycle hooks can call `add` and\n * `dispose` in any order any number of times, and no `add` ever meets an\n * already aborted signal — a DOM that registers a listener on such a signal\n * would keep it forever. Rebuilding the set instead would leak:\n * `addEventListener` matches an existing registration on target, type, callback\n * and capture alone, so re-adding the same handler while the first registration\n * is live is discarded, and that live registration keeps the signal it was\n * attached with — a set built by a second `connect()` can never release what it\n * appears to own.\n *\n * Scope is the registration only. Timers, observers, leases and the order they\n * are unwound in stay with the controller, which places `dispose()` in its\n * cleanup sequence. A receiver that changes at runtime (a\n * swapped target) is rebound by calling `dispose()` and then `add()` again from\n * the rebind, not by a second set.\n *\n * The set must own every registration of a tuple it holds: a listener the same\n * target already carries for the same type, callback and capture flag keeps the\n * right to remove it, and this set's `add` for that tuple is discarded. A\n * subscription shared between instances behind a participant count, or one that\n * lives for less than the connection, keeps its own pair of calls.\n *\n * @example\n * ```ts\n * readonly #listeners = new ListenerSet();\n *\n * connect(): void {\n * this.#listeners.add(this.element, \"submit\", this.#onNativeSubmit, { capture: true });\n * this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n * this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n * }\n *\n * disconnect(): void {\n * this.#listeners.dispose();\n * }\n * ```\n */\nexport class ListenerSet {\n /** The generation every `add` joins until the next `dispose()`. */\n #abort = new AbortController();\n\n /**\n * Attaches `handler` to the open generation, exactly as the caller spelled it.\n *\n * The set supplies the signal, so `options` carries everything else the DOM\n * accepts — `capture` included, which has to match at release time and no\n * longer has a second place to drift from.\n */\n add(\n target: EventTarget,\n type: string,\n handler: EventListener,\n options?: Omit<AddEventListenerOptions, \"signal\">,\n ): void {\n target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });\n }\n\n /**\n * Releases every listener of the open generation, synchronously, and opens the\n * next one. Idempotent, and safe before anything has been added.\n */\n dispose(): void {\n this.#abort.abort();\n this.#abort = new AbortController();\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\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","/**\n * One registry per page for every copy of the library.\n *\n * Each `stimeo-ui/controllers/*` file, and each opt-in entry, is built on its own and carries\n * its own copy of every utility it uses, so a page that imports controllers one by one, or one\n * of them next to the barrel, evaluates a utility several times. A registry that coordinates\n * instances of different controllers — the stacks of leased values, the loans of a\n * `tabindex`, the Escape layers, the focus traps — must not be split by that: a dialog inside\n * a drawer, or a character counter and a form field on one input, coordinate whichever files\n * they came from. So such a registry lives once on `globalThis`, under a `Symbol.for` key the\n * copies agree on, and the first copy to ask for it creates it.\n *\n * The key names the registry and the version of its shape\n * (`stimeo-ui.<utility>.registry.v<n>`). Copies whose registries have the same shape share\n * one; a release that changes the shape changes the version, so a copy of another release\n * keeps a registry of its own instead of reading entries it does not understand — the two\n * then coordinate within each release only. An entry another copy put in a registry is read\n * through its fields alone, never through `#private` members, which belong to the class of\n * the copy that created it.\n */\n\n/**\n * The registry stored under `key`, created by `create` on first use.\n *\n * @param key - `stimeo-ui.<utility>.registry.v<n>`: the version is that of the shape `create`\n * returns.\n * @param create - Builds the empty registry.\n */\nexport function sharedRegistry<T extends object>(key: string, create: () => T): T {\n const symbol = Symbol.for(key);\n const scope = globalThis as unknown as Record<symbol, T | undefined>;\n const existing = scope[symbol];\n if (existing !== undefined) return existing;\n const registry = create();\n Object.defineProperty(globalThis, symbol, { value: registry });\n return registry;\n}\n","/**\n * Drops the transient state hooks a connection inherited, so the cycle it opens\n * starts from a DOM that claims nothing.\n *\n * A transient hook is an attribute a controller writes to say \"this is happening\n * right now\" — a drag in progress, a form with unsaved edits, a refused\n * subscription. Its lifetime belongs to the interaction, not to the element, and\n * the two come apart whenever the element outlives the instance that wrote it: a\n * Turbo cache snapshot taken mid-interaction restores the hook with the markup,\n * and an element re-inserted elsewhere in the page carries it along. Nothing\n * takes such a hook off, because the instance that owned it is gone; consumer\n * CSS then keeps claiming a state no one is in.\n *\n * **`turbo:before-cache` cannot do this job**, which is why the pass belongs in\n * `connect()`: Turbo also dispatches that event on pages that stay, where the hook\n * is still true, and an in-page move — the other half of the population — never\n * reaches it, so a hook that arrived with a moved element would survive.\n * `connect()` covers both, because both end in a connection.\n *\n * Scope is the **drop** only. What the fresh cycle should say instead is the\n * consumer's: a value re-read from the live DOM, a roster refilled by the\n * stream, a measurement retaken from the current scroll position, a slot emptied\n * of the last result. Those answers differ per consumer and none of them is a\n * hook removal, so the pass stops where every consumer agrees.\n *\n * A declaration names one group of hooks and carries no per-instance state, so\n * it belongs at module scope, shared by every instance the page connects. A\n * consumer writes as many as it has groups — hooks it drops under different\n * conditions cannot share one. The exception is a hook whose name is only known\n * at runtime, such as one built from the registered identifier: that\n * declaration is a field, because the name is.\n *\n * **A drop is not always right.** The population splits on one question: can the\n * state a hook describes outlive the instance that wrote it? Where it cannot — a\n * drag ends with the pointer, a subscription dies with its socket — dropping on\n * every connection is correct. Where it can — a submission in flight is not\n * interrupted by an in-page move — only a connection that follows a real restore\n * may drop, and the consumer answers \"did a session survive?\" with the signal it\n * already holds for its own teardown (a live-session field, a `DetachGate`\n * probe). The drop then sits behind that answer, as the second example shows.\n *\n * @example Unconditional — the state cannot outlive the instance.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-sortable-dragging\"] });\n *\n * connect(): void {\n * TRANSIENT.reset(this.element);\n * }\n * ```\n *\n * @example Gated — an in-page move must not drop a live state.\n * ```ts\n * const TRANSIENT = new TransientHooks({ attributes: [\"data-optimistic\"] });\n *\n * connect(): void {\n * const restored = !this.#connected && !this.#gate.pending;\n * this.#gate.cancel();\n * this.#connected = true;\n * if (restored) TRANSIENT.reset(this.element);\n * }\n * ```\n */\n\n/** What a consumer declares once about the hooks its interaction owns. */\nexport interface TransientHooksOptions {\n /**\n * The attribute names a connection may find written by an earlier one. Hooks\n * a consumer drops under different conditions belong to different\n * declarations, so each one can be reset on its own.\n */\n readonly attributes: readonly string[];\n}\n\n/**\n * One consumer's declaration of the hooks its interaction owns, dropped from an\n * element by {@link TransientHooks.reset}.\n */\nexport class TransientHooks {\n readonly #attributes: readonly string[];\n\n constructor(options: TransientHooksOptions) {\n this.#attributes = options.attributes;\n }\n\n /** Drops the declared hooks from `element`; what it carries besides stays. */\n reset(element: Element): void {\n for (const attribute of this.#attributes) element.removeAttribute(attribute);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { ListenerSet } from \"../utils/listener_set\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\nimport { sharedRegistry } from \"../utils/shared_registry\";\nimport { TransientHooks } from \"../utils/transient_hooks\";\n\n/** The hook a connection may find written by an earlier, now-gone one. */\nconst TRANSIENT = new TransientHooks({ attributes: [\"data-dirty\"] });\n\ntype VisitDecision =\n | { readonly kind: \"block\" }\n | { readonly kind: \"confirm\"; readonly message: string }\n | null;\n\ninterface VisitParticipant {\n readonly form: HTMLFormElement;\n readonly evaluate: (event: Event) => VisitDecision;\n}\n\ninterface SubmitAttempt {\n readonly event: Event;\n snapshot: string;\n}\n\ninterface TurboSubmitDetail {\n readonly formSubmission?: unknown;\n readonly success?: boolean;\n}\n\ntype SerializedControl = readonly [\n type: string,\n name: string,\n value: string | readonly string[],\n checked?: 0 | 1,\n];\n\n/** The registration surface shared by every entry copy. */\ninterface VisitCoordinator {\n add(participant: VisitParticipant): void;\n remove(participant: VisitParticipant): boolean;\n}\n\n/** The coordinator of each document, dropped once its last participant leaves. */\nconst visitCoordinators = sharedRegistry(\n \"stimeo-ui.dirty-form.registry.v1\",\n () => new WeakMap<Document, VisitCoordinator>(),\n);\n\n/**\n * Coordinates native Turbo confirmations for every dirty form in one document.\n *\n * The weak document registry owns no dirty baseline or durable form state. Each\n * controller remains the source of truth for its own state and unregisters on\n * disconnect; the shared coordinator only gives one navigation event a single,\n * deterministic decision point.\n */\nclass TurboVisitCoordinator {\n readonly #document: Document;\n readonly #participants = new Set<VisitParticipant>();\n\n readonly #onBeforeVisit = (event: Event): void => {\n let blocked = event.defaultPrevented;\n let message: string | undefined;\n\n // Every eligible form gets its public guard event before a native prompt.\n // A later consumer can therefore block the visit without an earlier form\n // opening a redundant confirm first.\n for (const participant of this.#participantsInDomOrder()) {\n if (!participant.form.isConnected) continue;\n const decision = participant.evaluate(event);\n blocked ||= event.defaultPrevented;\n if (decision === null) continue;\n if (decision.kind === \"block\") {\n blocked = true;\n } else {\n message ??= decision.message;\n }\n }\n\n if (blocked) {\n event.preventDefault();\n return;\n }\n if (message === undefined) return;\n if (!this.#document.defaultView?.confirm(message)) event.preventDefault();\n };\n\n constructor(document: Document) {\n this.#document = document;\n }\n\n add(participant: VisitParticipant): void {\n if (this.#participants.size === 0) {\n this.#document.addEventListener(\"turbo:before-visit\", this.#onBeforeVisit);\n }\n this.#participants.add(participant);\n }\n\n /** Removes a participant and reports whether the coordinator became empty. */\n remove(participant: VisitParticipant): boolean {\n this.#participants.delete(participant);\n if (this.#participants.size > 0) return false;\n this.#document.removeEventListener(\"turbo:before-visit\", this.#onBeforeVisit);\n return true;\n }\n\n #participantsInDomOrder(): VisitParticipant[] {\n return Array.from(this.#participants)\n .filter(\n (participant) =>\n participant.form.isConnected && participant.form.ownerDocument === this.#document,\n )\n .sort((left, right) => {\n const position = left.form.compareDocumentPosition(right.form);\n return (\n Number(Boolean(position & Node.DOCUMENT_POSITION_PRECEDING)) -\n Number(Boolean(position & Node.DOCUMENT_POSITION_FOLLOWING))\n );\n });\n }\n}\n\nfunction registerVisitParticipant(document: Document, participant: VisitParticipant): () => void {\n const coordinator = visitCoordinators.get(document) ?? new TurboVisitCoordinator(document);\n visitCoordinators.set(document, coordinator);\n coordinator.add(participant);\n\n return () => {\n if (coordinator.remove(participant)) visitCoordinators.delete(document);\n };\n}\n\n/**\n * Headless \"unsaved changes\" guard for a form (no dedicated APG pattern; supports\n * WCAG 2.2 error-prevention 3.3.4 / 3.3.6 by preventing accidental data loss).\n *\n * Markup contract (identifier: `stimeo--dirty-form`):\n * <form data-controller=\"stimeo--dirty-form\">\n * …fields…\n * </form>\n *\n * Snapshots the form's field values on connect, marks the form `data-dirty` once a\n * value changes, and — while dirty — guards both a full unload (`beforeunload`) and\n * a Turbo visit (`turbo:before-visit`). On a Turbo visit it dispatches a cancelable\n * `guard` event for each eligible form; if a consumer cancels one (or any form uses\n * `confirmBridge`) the visit is blocked, otherwise the document falls back to one\n * native `confirm(message)`, using the first eligible form in live DOM order. While a\n * submit is in flight the guard is suppressed until another edit occurs (so a\n * legitimate submit never prompts); the browser's `formdata` event captures the exact\n * revision Turbo materializes, and a successful `turbo:submit-end` accepts only that\n * revision, preserving any later edits as dirty. A failed submit re-arms the guard;\n * `markClean` clears it manually (e.g. after a custom save), while `acceptRestore`\n * adopts a restored draft only while no user edit is already dirty.\n *\n * `dirty` dispatches `{ dirty }`; `guard` dispatches `{ event }`.\n *\n * @remarks\n * Behavior only — it renders no confirmation UI (pair with `stimeo--confirm`) and\n * does not persist input (pair with `stimeo--persist`). The dirty baseline is read from the\n * DOM on `connect()` (no module-scope state) — which also clears a stale\n * `data-dirty` left in a Turbo cache snapshot, since the restored values are the\n * new baseline — `beforeunload` is wired only while dirty, and every listener is\n * removed on `disconnect()` (Turbo navigation included) so a stale guard never\n * outlives the form.\n */\nexport class DirtyFormController extends Controller<HTMLFormElement> {\n static override values = {\n message: { type: String, default: \"You have unsaved changes that will be lost.\" },\n confirmBridge: { type: Boolean, default: false },\n };\n static actions = [\"markClean\", \"acceptRestore\"] as const;\n static events = [\"dirty\", \"guard\"] as const;\n\n declare messageValue: string;\n declare confirmBridgeValue: boolean;\n\n #baseline = \"\";\n #dirty = false;\n #beforeunloadBound = false;\n readonly #timeouts = new SafeTimeout();\n readonly #listeners = new ListenerSet();\n /** Whether the active submission still represents the form's current values. */\n #submitting = false;\n #submitAttempt: SubmitAttempt | null = null;\n #submittedBaseline: string | null = null;\n #activeSubmission: unknown | null = null;\n #unregisterVisit: (() => void) | null = null;\n\n readonly #onFieldChange = (): void => {\n // An edit after submission belongs to a new unsaved revision. Keep the\n // submitted snapshot for submit-end, but stop suppressing navigation now.\n this.#submitting = false;\n this.#evaluate();\n };\n\n readonly #onSubmit = (event: Event): void => {\n const attempt: SubmitAttempt = { event, snapshot: this.#serialize() };\n this.#submitAttempt = attempt;\n this.#submitting = true;\n // Native navigation runs before the next task. If it does not navigate and\n // Turbo never claims the attempt, the form must not remain suppressed.\n this.#timeouts.set(() => {\n if (this.#submitAttempt !== attempt) return;\n this.#submitting = false;\n }, 0);\n };\n\n readonly #onFormData = (): void => {\n if (this.#submitAttempt === null) return;\n // Turbo constructs FormData after target-level submit consumers have run.\n // Capturing here reflects exactly that revision, even when an asynchronous\n // confirmation delays turbo:submit-start and the user edits in the meantime.\n this.#submitAttempt.snapshot = this.#serialize();\n };\n\n readonly #onSubmitStart = (event: Event): void => {\n const detail = (event as CustomEvent<TurboSubmitDetail>).detail;\n this.#activeSubmission = detail?.formSubmission ?? event;\n this.#submittedBaseline = this.#submitAttempt?.snapshot ?? this.#serialize();\n this.#submitAttempt = null;\n this.#submitting = this.#serialize() === this.#submittedBaseline;\n };\n\n readonly #onSubmitEnd = (event: Event): void => {\n const detail = (event as CustomEvent<TurboSubmitDetail>).detail;\n if (\n detail?.formSubmission !== undefined &&\n (this.#activeSubmission === null || detail.formSubmission !== this.#activeSubmission)\n ) {\n return;\n }\n const submittedBaseline = this.#submittedBaseline;\n this.#submitting = false;\n this.#submittedBaseline = null;\n this.#activeSubmission = null;\n if (detail?.success === false) return;\n if (submittedBaseline === null) {\n this.markClean();\n return;\n }\n // The server accepted the submitted revision, not edits made after it left.\n this.#baseline = submittedBaseline;\n this.#evaluate();\n };\n\n readonly #onBeforeUnload = (event: BeforeUnloadEvent): void => {\n if (!this.#dirty || this.#guardSuppressed()) return;\n event.preventDefault();\n // Legacy requirement: a non-empty returnValue triggers the native prompt.\n event.returnValue = this.messageValue;\n };\n\n override connect(): void {\n this.#baseline = this.#serialize();\n this.#setDirty(false);\n // Re-baselining means the restored values ARE the clean state, so a hook a\n // snapshot preserved mid-edit must not linger: the guard would not fire, but\n // consumer CSS would keep claiming \"unsaved\".\n TRANSIENT.reset(this.element);\n this.#listeners.add(this.element, \"input\", this.#onFieldChange);\n this.#listeners.add(this.element, \"change\", this.#onFieldChange);\n this.#listeners.add(this.element, \"submit\", this.#onSubmit);\n this.#listeners.add(this.element, \"formdata\", this.#onFormData);\n this.#listeners.add(this.element, \"turbo:submit-start\", this.#onSubmitStart);\n this.#listeners.add(this.element, \"turbo:submit-end\", this.#onSubmitEnd);\n this.#unregisterVisit = registerVisitParticipant(this.element.ownerDocument, {\n form: this.element,\n evaluate: (event) => this.#evaluateVisit(event),\n });\n }\n\n override disconnect(): void {\n this.#listeners.dispose();\n this.#unregisterVisit?.();\n this.#unregisterVisit = null;\n this.#unbindBeforeUnload();\n this.#resetSubmission();\n }\n\n /** Re-baselines to the current values and clears the dirty state (e.g. after a save). */\n markClean(): void {\n this.#baseline = this.#serialize();\n this.#setDirty(false);\n }\n\n /** Adopts restored values without erasing an already-dirty user revision. */\n acceptRestore(): void {\n if (this.#dirty) return;\n this.markClean();\n }\n\n /** Recomputes dirty against the baseline and flips state when it changes. */\n #evaluate(): void {\n this.#setDirty(this.#serialize() !== this.#baseline);\n }\n\n #setDirty(dirty: boolean): void {\n if (dirty === this.#dirty) return;\n this.#dirty = dirty;\n if (dirty) {\n this.element.setAttribute(\"data-dirty\", \"true\");\n this.#bindBeforeUnload();\n } else {\n this.element.removeAttribute(\"data-dirty\");\n this.#unbindBeforeUnload();\n }\n this.dispatch(\"dirty\", { detail: { dirty } });\n }\n\n /** Evaluates this form for the document-level Turbo visit coordinator. */\n #evaluateVisit(event: Event): VisitDecision {\n if (!this.#dirty || this.#guardSuppressed()) return null;\n const guard = this.dispatch(\"guard\", { detail: { event }, cancelable: true });\n if (guard.defaultPrevented) return { kind: \"block\" };\n // A guard consumer may have saved, submitted, or removed the form synchronously.\n if (!this.element.isConnected || !this.#dirty || this.#guardSuppressed()) return null;\n if (this.confirmBridgeValue) return { kind: \"block\" };\n return { kind: \"confirm\", message: this.messageValue };\n }\n\n #bindBeforeUnload(): void {\n if (this.#beforeunloadBound) return;\n window.addEventListener(\"beforeunload\", this.#onBeforeUnload);\n this.#beforeunloadBound = true;\n }\n\n #unbindBeforeUnload(): void {\n if (!this.#beforeunloadBound) return;\n window.removeEventListener(\"beforeunload\", this.#onBeforeUnload);\n this.#beforeunloadBound = false;\n }\n\n #guardSuppressed(): boolean {\n return (\n this.#submitting &&\n (this.#submitAttempt === null || !this.#submitAttempt.event.defaultPrevented)\n );\n }\n\n #resetSubmission(): void {\n this.#timeouts.clearAll();\n this.#submitting = false;\n this.#submitAttempt = null;\n this.#submittedBaseline = null;\n this.#activeSubmission = null;\n }\n\n /** Stable serialization of the form's controls for change detection. */\n #serialize(): string {\n const parts: SerializedControl[] = [];\n for (const el of Array.from(this.element.elements)) {\n const name = this.#nameOf(el);\n if (name === null) continue;\n if (el instanceof HTMLInputElement && (el.type === \"checkbox\" || el.type === \"radio\")) {\n parts.push([el.type, name, el.value, el.checked ? 1 : 0]);\n } else if (el instanceof HTMLSelectElement) {\n // Keep multi-select values structural so option values containing commas cannot collide.\n const value = el.multiple\n ? Array.from(el.selectedOptions).map((option) => option.value)\n : el.value;\n parts.push([el.type, name, value]);\n } else if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {\n parts.push([el.type, name, el.value]);\n }\n }\n return JSON.stringify(parts);\n }\n\n /** A stable key for a control, or null for elements without value semantics. */\n #nameOf(el: Element): string | null {\n if (\n el instanceof HTMLInputElement ||\n el instanceof HTMLTextAreaElement ||\n el instanceof HTMLSelectElement\n ) {\n return el.name || el.id || \"\";\n }\n return null;\n }\n}\n"]}
@@ -26,6 +26,13 @@ import { Controller } from '@hotwired/stimulus';
26
26
  * `role="alert"`. Focus retreats to, in order: the `fallback` target → the next
27
27
  * focusable element after the root → the previous one → `document.body` as a
28
28
  * last resort (weak focus; prefer providing a `fallback`).
29
+ *
30
+ * A dismissal acts once, on the root present then. A root that arrives later — in
31
+ * one task, or after an earlier one leaves in a later task — is open: it gets
32
+ * `data-state="open"` when it carries no `data-state`, silently. A root that stops
33
+ * resolving as the target gets that default back; what a dismissal wrote stays, and so
34
+ * does it on a root of a page Turbo restores, which carries the default an earlier
35
+ * connection wrote, the first root or not.
29
36
  */
30
37
  declare class DismissibleController extends Controller<HTMLElement> {
31
38
  #private;
@@ -43,6 +50,7 @@ declare class DismissibleController extends Controller<HTMLElement> {
43
50
  static actions: readonly ["dismiss"];
44
51
  static events: readonly ["dismiss"];
45
52
  readonly rootTarget: HTMLElement;
53
+ readonly rootTargets: HTMLElement[];
46
54
  readonly fallbackTarget: HTMLElement;
47
55
  readonly hasRootTarget: boolean;
48
56
  readonly hasFallbackTarget: boolean;
@@ -50,6 +58,17 @@ declare class DismissibleController extends Controller<HTMLElement> {
50
58
  closeOnEscapeValue: boolean;
51
59
  connect(): void;
52
60
  disconnect(): void;
61
+ /**
62
+ * Writes the `open` default onto a root that arrives after connect, and gives the
63
+ * controller element, the root while there was none, its own `data-state` back.
64
+ */
65
+ rootTargetConnected(root: HTMLElement): void;
66
+ /**
67
+ * Gives a root that no longer resolves as the target its own `data-state` back — after
68
+ * `disconnect()` too, since dropping the identifier leaves the element on the page — and,
69
+ * while connected, writes the `open` default onto the root left.
70
+ */
71
+ rootTargetDisconnected(root: HTMLElement): void;
53
72
  /** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */
54
73
  closeOnEscapeValueChanged(): void;
55
74
  /** Dismisses the element. Bound via `data-action` (click on the close button). */
@@ -1,8 +1,152 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/dismissible_controller.ts
3
+ function sharedRegistry(key, create) {
4
+ const symbol = Symbol.for(key);
5
+ const scope = globalThis;
6
+ const existing = scope[symbol];
7
+ if (existing !== void 0) return existing;
8
+ const registry = create();
9
+ Object.defineProperty(globalThis, symbol, { value: registry });
10
+ return registry;
11
+ }
12
+
13
+ function isAttributeValue(value) {
14
+ return value === null || typeof value === "string";
15
+ }
16
+ function readLeaseRecord(element, record) {
17
+ try {
18
+ const parsed = JSON.parse(element.getAttribute(record) ?? "null");
19
+ if (!Array.isArray(parsed)) return void 0;
20
+ const [original, written, ...rest] = parsed;
21
+ const linked = rest[0] !== false;
22
+ const beneath = linked ? rest : rest.slice(1);
23
+ if (!beneath.every((under) => typeof under === "string")) return void 0;
24
+ return isAttributeValue(original) && isAttributeValue(written) ? { original, written, linked, beneath } : void 0;
25
+ } catch {
26
+ return void 0;
27
+ }
28
+ }
29
+ var AttributeLease = class _AttributeLease {
30
+ static #registry = sharedRegistry(
31
+ "stimeo-ui.attribute-lease.registry.v1",
32
+ () => ({ stacks: /* @__PURE__ */ new WeakMap(), names: /* @__PURE__ */ new Map() })
33
+ );
34
+ #attribute;
35
+ #record;
36
+ #base;
37
+ #held = /* @__PURE__ */ new Set();
38
+ constructor(attribute, owner, options = {}) {
39
+ this.#attribute = attribute;
40
+ this.#record = `data-${owner}-${attribute}-lease`;
41
+ this.#base = options.base === true;
42
+ const names = _AttributeLease.#registry.names;
43
+ names.set(attribute, (names.get(attribute) ?? /* @__PURE__ */ new Set()).add(this.#record));
44
+ }
45
+ write(element, value) {
46
+ const stack = this.#stack(element);
47
+ const hold = this.#take(stack) ?? this.#acquire(element, stack);
48
+ this.#held.add(element);
49
+ const shows = !this.#covered(element, stack, hold);
50
+ hold.written = value;
51
+ this.#mark(element, stack);
52
+ if (shows) this.#reflect(element, value);
53
+ }
54
+ return(element) {
55
+ const held = this.#held.delete(element);
56
+ const known = _AttributeLease.#registry.stacks.get(element)?.has(this.#attribute);
57
+ const stack = held || known || element.hasAttribute(this.#record) ? this.#stack(element) : [];
58
+ const hold = this.#take(stack);
59
+ if (!hold) return false;
60
+ const carries = element.getAttribute(this.#attribute) === hold.written;
61
+ const owns = carries && !this.#covered(element, stack, hold);
62
+ const index = stack.indexOf(hold);
63
+ stack.splice(index, 1);
64
+ const above = stack[index];
65
+ if (above) above.linked &&= hold.linked;
66
+ this.#mark(element, stack);
67
+ if (!owns) return true;
68
+ const below = hold.linked ? stack[index - 1] : void 0;
69
+ this.#reflect(element, below ? below.written : hold.original);
70
+ return true;
71
+ }
72
+ returnAll() {
73
+ for (const element of Array.from(this.#held)) this.return(element);
74
+ }
75
+ #stack(element) {
76
+ const stacks = _AttributeLease.#registry.stacks;
77
+ const byAttribute = stacks.get(element) ?? /* @__PURE__ */ new Map();
78
+ stacks.set(element, byAttribute);
79
+ const stack = byAttribute.get(this.#attribute) ?? this.#seed(element);
80
+ byAttribute.set(this.#attribute, stack);
81
+ return stack;
82
+ }
83
+ #seed(element) {
84
+ const found = [];
85
+ for (const record of _AttributeLease.#registry.names.get(this.#attribute) ?? []) {
86
+ const read = readLeaseRecord(element, record);
87
+ if (read) found.push({ ...read, record });
88
+ }
89
+ const depth = ({ beneath }) => beneath.filter((name) => found.some((other) => other.record === name)).length;
90
+ return found.sort((one, other) => depth(one) - depth(other)).map(({ record, written, original, linked }, index) => ({
91
+ lease: null,
92
+ record,
93
+ written,
94
+ original,
95
+ linked: linked && index > 0
96
+ }));
97
+ }
98
+ #take(stack) {
99
+ const hold = stack.find((held) => held.lease === this) ?? stack.find((held) => held.lease === null && held.record === this.#record);
100
+ if (hold) hold.lease = this;
101
+ return hold;
102
+ }
103
+ #acquire(element, stack) {
104
+ const current = element.getAttribute(this.#attribute);
105
+ const top = stack.at(-1);
106
+ const bottom = stack[0];
107
+ const over = top?.written === current ? top : void 0;
108
+ const from = this.#base ? bottom : over;
109
+ const hold = {
110
+ lease: this,
111
+ record: this.#record,
112
+ written: current,
113
+ original: from ? from.original : current,
114
+ linked: !this.#base && over !== void 0
115
+ };
116
+ if (this.#base && bottom) {
117
+ bottom.linked = true;
118
+ stack.unshift(hold);
119
+ } else stack.push(hold);
120
+ return hold;
121
+ }
122
+ #covered(element, stack, hold) {
123
+ const current = element.getAttribute(this.#attribute);
124
+ return stack.slice(stack.indexOf(hold) + 1).some((above) => above.written === current);
125
+ }
126
+ #mark(element, stack) {
127
+ const records = /* @__PURE__ */ new Map([[this.#record, null]]);
128
+ stack.forEach((hold, index) => {
129
+ const names = stack.slice(0, index).map((under) => under.record);
130
+ const beneath = [...new Set(names.reverse())].filter((name) => name !== hold.record);
131
+ const marker = hold.linked || beneath.length === 0 ? [] : [false];
132
+ records.set(
133
+ hold.record,
134
+ hold.written === hold.original && beneath.length === 0 && index === stack.length - 1 ? null : JSON.stringify([hold.original, hold.written, ...marker, ...beneath])
135
+ );
136
+ });
137
+ for (const [name, recorded] of records) {
138
+ if (element.getAttribute(name) === recorded) continue;
139
+ if (recorded === null) element.removeAttribute(name);
140
+ else element.setAttribute(name, recorded);
141
+ }
142
+ }
143
+ #reflect(element, value) {
144
+ if (element.getAttribute(this.#attribute) === value) return;
145
+ if (value === null) element.removeAttribute(this.#attribute);
146
+ else element.setAttribute(this.#attribute, value);
147
+ }
148
+ };
4
149
 
5
- // src/utils/focus_candidate.ts
6
150
  function inheritsFieldsetDisabled(control) {
7
151
  let fieldset = control.closest("fieldset[disabled]");
8
152
  while (fieldset) {
@@ -12,8 +156,19 @@ function inheritsFieldsetDisabled(control) {
12
156
  }
13
157
  return false;
14
158
  }
159
+ function unslotted(element) {
160
+ return element.assignedSlot === null && element.parentElement?.shadowRoot != null;
161
+ }
162
+ function flatTreeParent(element) {
163
+ if (unslotted(element)) return null;
164
+ const parent = element.assignedSlot ?? element.parentNode;
165
+ if (parent instanceof ShadowRoot) return parent.host;
166
+ return parent instanceof Element ? parent : null;
167
+ }
15
168
  function canTakeFocus(element) {
16
- if (element.closest("[hidden], [inert]")) return false;
169
+ for (let node = element; node; node = flatTreeParent(node)) {
170
+ if (unslotted(node) || node.matches("[hidden], [inert]")) return false;
171
+ }
17
172
  if (element instanceof HTMLInputElement && element.type === "hidden") return false;
18
173
  if (!("disabled" in element)) return true;
19
174
  if (element.disabled) return false;
@@ -76,7 +231,6 @@ function tabStopsWithin(root) {
76
231
  );
77
232
  }
78
233
 
79
- // src/controllers/dismissible_controller.ts
80
234
  var DismissibleController = class extends Controller {
81
235
  static targets = ["root", "fallback"];
82
236
  static values = {
@@ -85,25 +239,32 @@ var DismissibleController = class extends Controller {
85
239
  };
86
240
  static actions = ["dismiss"];
87
241
  static events = ["dismiss"];
242
+ #openDefault = new AttributeLease("data-state", this.identifier);
243
+ #connected = false;
88
244
  connect() {
89
- const root = this.#root;
90
- if (!root.hasAttribute("data-state")) {
91
- root.setAttribute("data-state", "open");
92
- }
245
+ this.#establishOpen(this.#root);
93
246
  this.#syncEscapeListener();
247
+ this.#connected = true;
94
248
  }
95
249
  disconnect() {
250
+ this.#connected = false;
96
251
  this.element.removeEventListener("keydown", this.#onKeydown);
97
252
  }
98
- /** Keeps the Escape listener aligned with a live `closeOnEscape` Value. */
253
+ rootTargetConnected(root) {
254
+ if (!this.#connected) return;
255
+ if (!this.rootTargets.includes(this.element)) this.#openDefault.return(this.element);
256
+ this.#establishOpen(root);
257
+ }
258
+ rootTargetDisconnected(root) {
259
+ if (!this.rootTargets.includes(root)) this.#openDefault.return(root);
260
+ if (this.#connected && this.hasRootTarget) this.#establishOpen(this.rootTarget);
261
+ }
99
262
  closeOnEscapeValueChanged() {
100
263
  this.#syncEscapeListener();
101
264
  }
102
- /** Dismisses the element. Bound via `data-action` (click on the close button). */
103
265
  dismiss() {
104
266
  this.#performDismiss();
105
267
  }
106
- /** Dismisses on Escape when `closeOnEscape` is set and focus is inside. */
107
268
  #onKeydown = (event) => {
108
269
  if (event.key !== "Escape" || event.defaultPrevented || event.isComposing) return;
109
270
  if (!this.closeOnEscapeValue) return;
@@ -112,14 +273,15 @@ var DismissibleController = class extends Controller {
112
273
  event.preventDefault();
113
274
  this.#performDismiss();
114
275
  };
115
- /** Adds or removes the one stable listener reference without duplicating it. */
116
276
  #syncEscapeListener() {
117
277
  this.element.removeEventListener("keydown", this.#onKeydown);
118
278
  if (this.closeOnEscapeValue) {
119
279
  this.element.addEventListener("keydown", this.#onKeydown);
120
280
  }
121
281
  }
122
- /** The element to dismiss: the explicit `root` target, or the host element. */
282
+ #establishOpen(root) {
283
+ if (!root.hasAttribute("data-state")) this.#openDefault.write(root, "open");
284
+ }
123
285
  get #root() {
124
286
  return this.hasRootTarget ? this.rootTarget : this.element;
125
287
  }
@@ -135,10 +297,6 @@ var DismissibleController = class extends Controller {
135
297
  root.remove();
136
298
  }
137
299
  }
138
- /**
139
- * Moves focus out of `root` *before* it is removed, but only when focus is
140
- * actually inside it — otherwise the user's place elsewhere is left undisturbed.
141
- */
142
300
  #retreatFocus(root) {
143
301
  const active = document.activeElement;
144
302
  if (!(active instanceof HTMLElement) || !root.contains(active)) return;
@@ -148,7 +306,6 @@ var DismissibleController = class extends Controller {
148
306
  }
149
307
  document.body.focus();
150
308
  }
151
- /** Yields available focus fallbacks in precedence order, stopping after focus succeeds. */
152
309
  *#focusFallbacks(root) {
153
310
  const explicitFallback = this.hasFallbackTarget ? this.fallbackTarget : null;
154
311
  if (explicitFallback && !root.contains(explicitFallback) && this.#isExplicitFocusAvailable(explicitFallback)) {
@@ -170,7 +327,6 @@ var DismissibleController = class extends Controller {
170
327
  yield element;
171
328
  }
172
329
  }
173
- /** Whether an explicit fallback can receive programmatic focus right now. */
174
330
  #isExplicitFocusAvailable(element) {
175
331
  const programmaticallyFocusable = element.hasAttribute("tabindex") || isTabStop(element);
176
332
  return programmaticallyFocusable && canTakeFocus(element) && isRenderedForFocus(element);
@@ -178,5 +334,4 @@ var DismissibleController = class extends Controller {
178
334
  };
179
335
 
180
336
  export { DismissibleController };
181
- //# sourceMappingURL=dismissible_controller.js.map
182
337
  //# sourceMappingURL=dismissible_controller.js.map