stimeo-ui 0.16.0 → 0.18.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 (337) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/README.md +27 -11
  3. package/dist/cable/index.d.ts +88 -31
  4. package/dist/cable/index.js +384 -449
  5. package/dist/cable/index.js.map +1 -1
  6. package/dist/coerce-JtZVNG5h.d.ts +20 -0
  7. package/dist/controllers/accordion_controller.d.ts +4 -2
  8. package/dist/controllers/accordion_controller.js +174 -55
  9. package/dist/controllers/accordion_controller.js.map +1 -1
  10. package/dist/controllers/alert_dialog_controller.d.ts +12 -2
  11. package/dist/controllers/alert_dialog_controller.js +903 -235
  12. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  13. package/dist/controllers/announcer_controller.d.ts +37 -5
  14. package/dist/controllers/announcer_controller.js +238 -205
  15. package/dist/controllers/announcer_controller.js.map +1 -1
  16. package/dist/controllers/aspect_ratio_controller.js +0 -10
  17. package/dist/controllers/aspect_ratio_controller.js.map +1 -1
  18. package/dist/controllers/auto_submit_controller.d.ts +30 -6
  19. package/dist/controllers/auto_submit_controller.js +414 -145
  20. package/dist/controllers/auto_submit_controller.js.map +1 -1
  21. package/dist/controllers/avatar_controller.js +185 -107
  22. package/dist/controllers/avatar_controller.js.map +1 -1
  23. package/dist/controllers/breadcrumb_controller.js +0 -130
  24. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  25. package/dist/controllers/bulk_select_controller.d.ts +21 -3
  26. package/dist/controllers/bulk_select_controller.js +282 -58
  27. package/dist/controllers/bulk_select_controller.js.map +1 -1
  28. package/dist/controllers/calendar_controller.d.ts +38 -16
  29. package/dist/controllers/calendar_controller.js +228 -260
  30. package/dist/controllers/calendar_controller.js.map +1 -1
  31. package/dist/controllers/carousel_controller.d.ts +43 -12
  32. package/dist/controllers/carousel_controller.js +536 -436
  33. package/dist/controllers/carousel_controller.js.map +1 -1
  34. package/dist/controllers/character_counter_controller.d.ts +44 -7
  35. package/dist/controllers/character_counter_controller.js +493 -207
  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 +38 -74
  39. package/dist/controllers/checkbox_controller.js.map +1 -1
  40. package/dist/controllers/clipboard_controller.d.ts +16 -5
  41. package/dist/controllers/clipboard_controller.js +261 -149
  42. package/dist/controllers/clipboard_controller.js.map +1 -1
  43. package/dist/controllers/collapsible_controller.d.ts +17 -5
  44. package/dist/controllers/collapsible_controller.js +304 -120
  45. package/dist/controllers/collapsible_controller.js.map +1 -1
  46. package/dist/controllers/color_picker_controller.d.ts +18 -6
  47. package/dist/controllers/color_picker_controller.js +178 -146
  48. package/dist/controllers/color_picker_controller.js.map +1 -1
  49. package/dist/controllers/combobox_controller.d.ts +24 -6
  50. package/dist/controllers/combobox_controller.js +522 -156
  51. package/dist/controllers/combobox_controller.js.map +1 -1
  52. package/dist/controllers/command_palette_controller.d.ts +51 -20
  53. package/dist/controllers/command_palette_controller.js +1156 -395
  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 +300 -171
  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 +980 -262
  60. package/dist/controllers/confirm_controller.js.map +1 -1
  61. package/dist/controllers/context_menu_controller.d.ts +80 -18
  62. package/dist/controllers/context_menu_controller.js +346 -153
  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 +47 -3
  68. package/dist/controllers/countdown_controller.js +281 -158
  69. package/dist/controllers/countdown_controller.js.map +1 -1
  70. package/dist/controllers/currency_input_controller.d.ts +93 -10
  71. package/dist/controllers/currency_input_controller.js +575 -225
  72. package/dist/controllers/currency_input_controller.js.map +1 -1
  73. package/dist/controllers/data_grid_controller.d.ts +25 -12
  74. package/dist/controllers/data_grid_controller.js +248 -214
  75. package/dist/controllers/data_grid_controller.js.map +1 -1
  76. package/dist/controllers/date_range_picker_controller.d.ts +42 -14
  77. package/dist/controllers/date_range_picker_controller.js +338 -235
  78. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  79. package/dist/controllers/dialog_controller.d.ts +17 -6
  80. package/dist/controllers/dialog_controller.js +899 -237
  81. package/dist/controllers/dialog_controller.js.map +1 -1
  82. package/dist/controllers/direct_upload_controller.d.ts +25 -8
  83. package/dist/controllers/direct_upload_controller.js +97 -167
  84. package/dist/controllers/direct_upload_controller.js.map +1 -1
  85. package/dist/controllers/dirty_form_controller.d.ts +6 -1
  86. package/dist/controllers/dirty_form_controller.js +72 -69
  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 +196 -24
  90. package/dist/controllers/dismissible_controller.js.map +1 -1
  91. package/dist/controllers/drawer_controller.d.ts +39 -11
  92. package/dist/controllers/drawer_controller.js +974 -355
  93. package/dist/controllers/drawer_controller.js.map +1 -1
  94. package/dist/controllers/dropdown_controller.d.ts +31 -1
  95. package/dist/controllers/dropdown_controller.js +339 -96
  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 +315 -88
  99. package/dist/controllers/editable_controller.js.map +1 -1
  100. package/dist/controllers/empty_state_controller.js +79 -58
  101. package/dist/controllers/empty_state_controller.js.map +1 -1
  102. package/dist/controllers/file_dropzone_controller.d.ts +63 -18
  103. package/dist/controllers/file_dropzone_controller.js +322 -211
  104. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  105. package/dist/controllers/filter_controller.js +77 -42
  106. package/dist/controllers/filter_controller.js.map +1 -1
  107. package/dist/controllers/flash_controller.d.ts +35 -12
  108. package/dist/controllers/flash_controller.js +238 -347
  109. package/dist/controllers/flash_controller.js.map +1 -1
  110. package/dist/controllers/focus_controller.d.ts +15 -6
  111. package/dist/controllers/focus_controller.js +679 -240
  112. package/dist/controllers/focus_controller.js.map +1 -1
  113. package/dist/controllers/form_field_controller.js +156 -132
  114. package/dist/controllers/form_field_controller.js.map +1 -1
  115. package/dist/controllers/form_validation_controller.d.ts +3 -3
  116. package/dist/controllers/form_validation_controller.js +31 -97
  117. package/dist/controllers/form_validation_controller.js.map +1 -1
  118. package/dist/controllers/frame_loading_controller.d.ts +47 -14
  119. package/dist/controllers/frame_loading_controller.js +275 -217
  120. package/dist/controllers/frame_loading_controller.js.map +1 -1
  121. package/dist/controllers/highlight_controller.d.ts +27 -6
  122. package/dist/controllers/highlight_controller.js +107 -75
  123. package/dist/controllers/highlight_controller.js.map +1 -1
  124. package/dist/controllers/hover_card_controller.d.ts +60 -4
  125. package/dist/controllers/hover_card_controller.js +327 -130
  126. package/dist/controllers/hover_card_controller.js.map +1 -1
  127. package/dist/controllers/idle_controller.d.ts +48 -6
  128. package/dist/controllers/idle_controller.js +271 -101
  129. package/dist/controllers/idle_controller.js.map +1 -1
  130. package/dist/controllers/input_mask_controller.d.ts +30 -13
  131. package/dist/controllers/input_mask_controller.js +483 -97
  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 +150 -106
  135. package/dist/controllers/intersection_controller.js.map +1 -1
  136. package/dist/controllers/lazy_frame_controller.d.ts +19 -2
  137. package/dist/controllers/lazy_frame_controller.js +24 -68
  138. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  139. package/dist/controllers/listbox_controller.d.ts +44 -9
  140. package/dist/controllers/listbox_controller.js +523 -240
  141. package/dist/controllers/listbox_controller.js.map +1 -1
  142. package/dist/controllers/local_time_controller.d.ts +9 -7
  143. package/dist/controllers/local_time_controller.js +102 -77
  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 +80 -96
  147. package/dist/controllers/masonry_controller.js.map +1 -1
  148. package/dist/controllers/menu_controller.d.ts +55 -11
  149. package/dist/controllers/menu_controller.js +302 -188
  150. package/dist/controllers/menu_controller.js.map +1 -1
  151. package/dist/controllers/menubar_controller.d.ts +51 -9
  152. package/dist/controllers/menubar_controller.js +232 -345
  153. package/dist/controllers/menubar_controller.js.map +1 -1
  154. package/dist/controllers/meter_controller.d.ts +33 -20
  155. package/dist/controllers/meter_controller.js +417 -87
  156. package/dist/controllers/meter_controller.js.map +1 -1
  157. package/dist/controllers/multi_select_controller.d.ts +48 -18
  158. package/dist/controllers/multi_select_controller.js +785 -393
  159. package/dist/controllers/multi_select_controller.js.map +1 -1
  160. package/dist/controllers/navigation_menu_controller.d.ts +27 -6
  161. package/dist/controllers/navigation_menu_controller.js +190 -250
  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 +396 -192
  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 +268 -62
  168. package/dist/controllers/network_status_controller.js.map +1 -1
  169. package/dist/controllers/number_input_controller.d.ts +50 -8
  170. package/dist/controllers/number_input_controller.js +610 -303
  171. package/dist/controllers/number_input_controller.js.map +1 -1
  172. package/dist/controllers/optimistic_controller.d.ts +8 -5
  173. package/dist/controllers/optimistic_controller.js +45 -87
  174. package/dist/controllers/optimistic_controller.js.map +1 -1
  175. package/dist/controllers/otp_controller.d.ts +55 -8
  176. package/dist/controllers/otp_controller.js +658 -255
  177. package/dist/controllers/otp_controller.js.map +1 -1
  178. package/dist/controllers/overflow_indicator_controller.d.ts +5 -0
  179. package/dist/controllers/overflow_indicator_controller.js +122 -113
  180. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  181. package/dist/controllers/overflow_menu_controller.d.ts +58 -20
  182. package/dist/controllers/overflow_menu_controller.js +470 -295
  183. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  184. package/dist/controllers/pagination_controller.d.ts +37 -7
  185. package/dist/controllers/pagination_controller.js +332 -123
  186. package/dist/controllers/pagination_controller.js.map +1 -1
  187. package/dist/controllers/password_reveal_controller.d.ts +46 -8
  188. package/dist/controllers/password_reveal_controller.js +392 -121
  189. package/dist/controllers/password_reveal_controller.js.map +1 -1
  190. package/dist/controllers/password_strength_controller.d.ts +15 -6
  191. package/dist/controllers/password_strength_controller.js +111 -158
  192. package/dist/controllers/password_strength_controller.js.map +1 -1
  193. package/dist/controllers/persist_controller.d.ts +11 -4
  194. package/dist/controllers/persist_controller.js +165 -111
  195. package/dist/controllers/persist_controller.js.map +1 -1
  196. package/dist/controllers/pointer_drag_controller.d.ts +35 -12
  197. package/dist/controllers/pointer_drag_controller.js +434 -188
  198. package/dist/controllers/pointer_drag_controller.js.map +1 -1
  199. package/dist/controllers/popover_controller.d.ts +41 -5
  200. package/dist/controllers/popover_controller.js +269 -113
  201. package/dist/controllers/popover_controller.js.map +1 -1
  202. package/dist/controllers/portal_controller.d.ts +44 -4
  203. package/dist/controllers/portal_controller.js +215 -76
  204. package/dist/controllers/portal_controller.js.map +1 -1
  205. package/dist/controllers/preview_guard_controller.d.ts +10 -4
  206. package/dist/controllers/preview_guard_controller.js +243 -116
  207. package/dist/controllers/preview_guard_controller.js.map +1 -1
  208. package/dist/controllers/progress_controller.d.ts +20 -19
  209. package/dist/controllers/progress_controller.js +391 -78
  210. package/dist/controllers/progress_controller.js.map +1 -1
  211. package/dist/controllers/radio_group_controller.d.ts +21 -9
  212. package/dist/controllers/radio_group_controller.js +278 -211
  213. package/dist/controllers/radio_group_controller.js.map +1 -1
  214. package/dist/controllers/range_slider_controller.d.ts +47 -6
  215. package/dist/controllers/range_slider_controller.js +220 -113
  216. package/dist/controllers/range_slider_controller.js.map +1 -1
  217. package/dist/controllers/rating_controller.d.ts +20 -4
  218. package/dist/controllers/rating_controller.js +458 -211
  219. package/dist/controllers/rating_controller.js.map +1 -1
  220. package/dist/controllers/read_more_controller.d.ts +2 -0
  221. package/dist/controllers/read_more_controller.js +138 -63
  222. package/dist/controllers/read_more_controller.js.map +1 -1
  223. package/dist/controllers/reading_progress_controller.d.ts +1 -1
  224. package/dist/controllers/reading_progress_controller.js +166 -137
  225. package/dist/controllers/reading_progress_controller.js.map +1 -1
  226. package/dist/controllers/relative_time_controller.d.ts +20 -6
  227. package/dist/controllers/relative_time_controller.js +211 -112
  228. package/dist/controllers/relative_time_controller.js.map +1 -1
  229. package/dist/controllers/reset_on_restore_controller.d.ts +85 -0
  230. package/dist/controllers/{reset_before_cache_controller.js → reset_on_restore_controller.js} +103 -38
  231. package/dist/controllers/reset_on_restore_controller.js.map +1 -0
  232. package/dist/controllers/resizable_controller.d.ts +31 -2
  233. package/dist/controllers/resizable_controller.js +299 -150
  234. package/dist/controllers/resizable_controller.js.map +1 -1
  235. package/dist/controllers/roving_controller.d.ts +3 -3
  236. package/dist/controllers/roving_controller.js +49 -88
  237. package/dist/controllers/roving_controller.js.map +1 -1
  238. package/dist/controllers/scroll_area_controller.d.ts +7 -3
  239. package/dist/controllers/scroll_area_controller.js +490 -246
  240. package/dist/controllers/scroll_area_controller.js.map +1 -1
  241. package/dist/controllers/scroll_restore_controller.d.ts +3 -3
  242. package/dist/controllers/scroll_restore_controller.js +14 -55
  243. package/dist/controllers/scroll_restore_controller.js.map +1 -1
  244. package/dist/controllers/scroll_visibility_controller.d.ts +28 -12
  245. package/dist/controllers/scroll_visibility_controller.js +432 -149
  246. package/dist/controllers/scroll_visibility_controller.js.map +1 -1
  247. package/dist/controllers/scrollspy_controller.d.ts +6 -1
  248. package/dist/controllers/scrollspy_controller.js +193 -248
  249. package/dist/controllers/scrollspy_controller.js.map +1 -1
  250. package/dist/controllers/separator_controller.d.ts +20 -5
  251. package/dist/controllers/separator_controller.js +285 -142
  252. package/dist/controllers/separator_controller.js.map +1 -1
  253. package/dist/controllers/sidebar_controller.d.ts +65 -8
  254. package/dist/controllers/sidebar_controller.js +1069 -404
  255. package/dist/controllers/sidebar_controller.js.map +1 -1
  256. package/dist/controllers/skeleton_controller.d.ts +23 -0
  257. package/dist/controllers/skeleton_controller.js +259 -101
  258. package/dist/controllers/skeleton_controller.js.map +1 -1
  259. package/dist/controllers/slider_controller.d.ts +29 -2
  260. package/dist/controllers/slider_controller.js +187 -86
  261. package/dist/controllers/slider_controller.js.map +1 -1
  262. package/dist/controllers/smart_sticky_header_controller.d.ts +22 -4
  263. package/dist/controllers/smart_sticky_header_controller.js +173 -66
  264. package/dist/controllers/smart_sticky_header_controller.js.map +1 -1
  265. package/dist/controllers/sortable_controller.d.ts +4 -4
  266. package/dist/controllers/sortable_controller.js +26 -125
  267. package/dist/controllers/sortable_controller.js.map +1 -1
  268. package/dist/controllers/spinner_controller.d.ts +39 -4
  269. package/dist/controllers/spinner_controller.js +288 -173
  270. package/dist/controllers/spinner_controller.js.map +1 -1
  271. package/dist/controllers/step_indicator_controller.d.ts +9 -4
  272. package/dist/controllers/step_indicator_controller.js +105 -46
  273. package/dist/controllers/step_indicator_controller.js.map +1 -1
  274. package/dist/controllers/stepper_controller.d.ts +14 -6
  275. package/dist/controllers/stepper_controller.js +120 -73
  276. package/dist/controllers/stepper_controller.js.map +1 -1
  277. package/dist/controllers/stick_to_bottom_controller.d.ts +9 -1
  278. package/dist/controllers/stick_to_bottom_controller.js +275 -140
  279. package/dist/controllers/stick_to_bottom_controller.js.map +1 -1
  280. package/dist/controllers/sticky_observer_controller.d.ts +11 -0
  281. package/dist/controllers/sticky_observer_controller.js +220 -49
  282. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  283. package/dist/controllers/submit_once_controller.d.ts +24 -8
  284. package/dist/controllers/submit_once_controller.js +376 -227
  285. package/dist/controllers/submit_once_controller.js.map +1 -1
  286. package/dist/controllers/switch_controller.d.ts +18 -4
  287. package/dist/controllers/switch_controller.js +201 -84
  288. package/dist/controllers/switch_controller.js.map +1 -1
  289. package/dist/controllers/tabs_controller.d.ts +9 -6
  290. package/dist/controllers/tabs_controller.js +41 -30
  291. package/dist/controllers/tabs_controller.js.map +1 -1
  292. package/dist/controllers/tags_input_controller.d.ts +16 -3
  293. package/dist/controllers/tags_input_controller.js +293 -173
  294. package/dist/controllers/tags_input_controller.js.map +1 -1
  295. package/dist/controllers/textarea_autosize_controller.d.ts +11 -1
  296. package/dist/controllers/textarea_autosize_controller.js +108 -74
  297. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  298. package/dist/controllers/theme_controller.d.ts +24 -11
  299. package/dist/controllers/theme_controller.js +140 -116
  300. package/dist/controllers/theme_controller.js.map +1 -1
  301. package/dist/controllers/time_picker_controller.d.ts +55 -5
  302. package/dist/controllers/time_picker_controller.js +331 -127
  303. package/dist/controllers/time_picker_controller.js.map +1 -1
  304. package/dist/controllers/toast_controller.d.ts +40 -16
  305. package/dist/controllers/toast_controller.js +244 -324
  306. package/dist/controllers/toast_controller.js.map +1 -1
  307. package/dist/controllers/toggle_group_controller.d.ts +14 -6
  308. package/dist/controllers/toggle_group_controller.js +252 -191
  309. package/dist/controllers/toggle_group_controller.js.map +1 -1
  310. package/dist/controllers/toolbar_controller.d.ts +8 -6
  311. package/dist/controllers/toolbar_controller.js +219 -120
  312. package/dist/controllers/toolbar_controller.js.map +1 -1
  313. package/dist/controllers/tooltip_controller.d.ts +49 -4
  314. package/dist/controllers/tooltip_controller.js +158 -124
  315. package/dist/controllers/tooltip_controller.js.map +1 -1
  316. package/dist/controllers/transition_controller.d.ts +22 -4
  317. package/dist/controllers/transition_controller.js +123 -119
  318. package/dist/controllers/transition_controller.js.map +1 -1
  319. package/dist/controllers/tree_view_controller.d.ts +22 -12
  320. package/dist/controllers/tree_view_controller.js +283 -369
  321. package/dist/controllers/tree_view_controller.js.map +1 -1
  322. package/dist/index.d.ts +10 -5
  323. package/dist/index.js +8573 -10410
  324. package/dist/index.js.map +1 -1
  325. package/dist/inspector/cli.d.ts +38 -14
  326. package/dist/inspector/cli.js +108 -40
  327. package/dist/inspector/cli.js.map +1 -1
  328. package/dist/inspector/cli_bin.js +145 -64
  329. package/dist/inspector/cli_bin.js.map +1 -1
  330. package/dist/inspector/examples.json +20 -20
  331. package/dist/inspector/manifest.json +802 -101
  332. package/dist/positioning/index.d.ts +15 -7
  333. package/dist/positioning/index.js +154 -47
  334. package/dist/positioning/index.js.map +1 -1
  335. package/package.json +11 -10
  336. package/dist/controllers/reset_before_cache_controller.d.ts +0 -76
  337. package/dist/controllers/reset_before_cache_controller.js.map +0 -1
@@ -1,15 +1,7 @@
1
1
  import { Controller } from '@hotwired/stimulus';
2
2
 
3
- // src/controllers/scroll_restore_controller.ts
4
-
5
- // src/utils/frame_coalescer.ts
6
3
  var FrameCoalescer = class {
7
4
  #frame = null;
8
- /**
9
- * Runs `run` on the next frame, unless a frame is already pending — the first
10
- * request of a burst wins and the rest are dropped. The pending frame is
11
- * released before `run`, so `run` may request the next one.
12
- */
13
5
  schedule(run) {
14
6
  if (this.#frame !== null) return;
15
7
  this.#frame = requestAnimationFrame(() => {
@@ -17,14 +9,6 @@ var FrameCoalescer = class {
17
9
  run();
18
10
  });
19
11
  }
20
- /**
21
- * Drops the pending frame, and reaches the platform only when there is one.
22
- *
23
- * There is no handle value that stands for "nothing pending":
24
- * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder
25
- * arrives as a large positive number that the same allocator can hand out, and
26
- * an idle cancel would then drop a frame belonging to someone else.
27
- */
28
12
  cancel() {
29
13
  if (this.#frame === null) return;
30
14
  cancelAnimationFrame(this.#frame);
@@ -32,7 +16,17 @@ var FrameCoalescer = class {
32
16
  }
33
17
  };
34
18
 
35
- // src/controllers/scroll_restore_controller.ts
19
+ var ListenerSet = class {
20
+ #abort = new AbortController();
21
+ add(target, type, handler, options) {
22
+ target.addEventListener(type, handler, { ...options, signal: this.#abort.signal });
23
+ }
24
+ dispose() {
25
+ this.#abort.abort();
26
+ this.#abort = new AbortController();
27
+ }
28
+ };
29
+
36
30
  function parseStored(raw) {
37
31
  if (raw === null) return null;
38
32
  try {
@@ -49,58 +43,39 @@ var ScrollRestoreController = class extends Controller {
49
43
  key: { type: String, default: "" },
50
44
  axis: { type: String, default: "vertical" }
51
45
  };
52
- /** Coalesces scroll bursts into one save per frame. */
53
46
  #frames = new FrameCoalescer();
54
- /** Resolved storage key; empty disables persistence (no key and no id). */
55
47
  #storageKey = "";
56
- /** Last offset captured while the element was live; persisted as-is on teardown. */
57
48
  #lastTop = 0;
58
49
  #lastLeft = 0;
59
- /**
60
- * The offsets this controller's own restore produced, awaiting the `scroll`
61
- * the engine fires for them. A restore the layout cannot reach yet lands short,
62
- * so treating that echo as the reader's position would cut the saved offset
63
- * down to whatever the unfinished layout allowed.
64
- */
65
50
  #echoTop = null;
66
51
  #echoLeft = null;
67
- /** Offsets stored for an axis that is not tracked, carried through saves. */
68
52
  #carried = {};
69
- /**
70
- * Whether each axis holds an offset worth saving — one this namespace restored
71
- * or the reader moved. A save writes only the axes that are marked, so an
72
- * offset taken under a different key or axis is never re-published as if the
73
- * reader had left it there.
74
- */
75
53
  #capturedTop = false;
76
54
  #capturedLeft = false;
77
- /** Whether the controller is connected; Value callbacks outside that window do not resync. */
78
55
  #connected = false;
79
56
  #onScroll = () => {
80
57
  this.#capture();
81
58
  this.#frames.schedule(() => this.#persist());
82
59
  };
60
+ #listeners = new ListenerSet();
83
61
  connect() {
84
62
  this.#connected = true;
85
63
  this.#storageKey = this.#resolveKey();
86
- this.element.addEventListener("scroll", this.#onScroll, { passive: true });
64
+ this.#listeners.add(this.element, "scroll", this.#onScroll, { passive: true });
87
65
  if (this.#storageKey) this.#restore();
88
66
  }
89
67
  disconnect() {
90
68
  this.#connected = false;
91
- this.element.removeEventListener("scroll", this.#onScroll);
69
+ this.#listeners.dispose();
92
70
  this.#frames.cancel();
93
71
  this.#persist();
94
72
  }
95
- /** Re-derives the namespace when application code or a Turbo morph moves `key`. */
96
73
  keyValueChanged() {
97
74
  this.#resync();
98
75
  }
99
- /** Re-derives the tracked axes when application code or a Turbo morph moves `axis`. */
100
76
  axisValueChanged() {
101
77
  this.#resync();
102
78
  }
103
- /** Rebuilds the persistence state around the Values as they now read. */
104
79
  #resync() {
105
80
  if (!this.#connected) return;
106
81
  this.#frames.cancel();
@@ -112,15 +87,6 @@ var ScrollRestoreController = class extends Controller {
112
87
  this.#capturedLeft = false;
113
88
  if (this.#storageKey) this.#restore();
114
89
  }
115
- /**
116
- * Records the live scroll offset for the configured axis.
117
- *
118
- * An axis holds its echo until that axis actually moves. A scroll event names
119
- * no axis, so consuming both on the first one to arrive would leave the other
120
- * unguarded: on `both`, moving one axis would take the clamped reading of the
121
- * untouched axis as the reader's own position and save it, cutting the stored
122
- * offset down to whatever the layout could reach at restore time.
123
- */
124
90
  #capture() {
125
91
  if (this.#tracksVertical) {
126
92
  const top = this.element.scrollTop;
@@ -139,10 +105,6 @@ var ScrollRestoreController = class extends Controller {
139
105
  }
140
106
  }
141
107
  }
142
- /**
143
- * Writes the marked axes, carrying through any offset held for an untracked one.
144
- * Without a key, or with neither axis marked, it writes nothing.
145
- */
146
108
  #persist() {
147
109
  if (!this.#storageKey || !(this.#capturedTop || this.#capturedLeft)) return;
148
110
  const data = { ...this.#carried };
@@ -153,7 +115,6 @@ var ScrollRestoreController = class extends Controller {
153
115
  } catch {
154
116
  }
155
117
  }
156
- /** Applies the persisted scroll offset, if any, without moving focus. */
157
118
  #restore() {
158
119
  let raw;
159
120
  try {
@@ -196,7 +157,6 @@ var ScrollRestoreController = class extends Controller {
196
157
  if (this.element.scrollTop !== beforeTop) this.#echoTop = this.element.scrollTop;
197
158
  if (this.element.scrollLeft !== beforeLeft) this.#echoLeft = this.element.scrollLeft;
198
159
  }
199
- /** The `sessionStorage` key: explicit `key`, else the element `id`, else none. */
200
160
  #resolveKey() {
201
161
  const base = this.keyValue || this.element.id;
202
162
  return base ? `stimeo--scroll-restore:${base}` : "";
@@ -210,5 +170,4 @@ var ScrollRestoreController = class extends Controller {
210
170
  };
211
171
 
212
172
  export { ScrollRestoreController };
213
- //# sourceMappingURL=scroll_restore_controller.js.map
214
173
  //# sourceMappingURL=scroll_restore_controller.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/frame_coalescer.ts","../../src/controllers/scroll_restore_controller.ts"],"names":[],"mappings":"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","file":"scroll_restore_controller.js","sourcesContent":["/**\n * Runs one piece of scroll-driven work per animation frame.\n *\n * A `scroll` listener fires many times between two paints, and each firing would\n * measure the same layout. Requesting a frame on the first firing and ignoring\n * the rest until it runs keeps the reads to one per frame; running on the frame\n * — rather than on a timer — keeps the measurement in step with what the reader\n * sees.\n *\n * **The first request of a burst wins.** Work handed in while a frame is already\n * pending is dropped, so a caller that queues a special first pass and then\n * receives scrolls gets that first pass, not the last scroll's.\n *\n * Cancelling drops the pending frame outright, and does nothing at all when none\n * is pending. A frame really is cancelled, so nothing needs a generation to tell\n * a stale callback from a fresh one, and nothing has to be refused before the\n * caller starts listening.\n *\n * Scope is the scheduling only. What to measure stays with the caller.\n *\n * @example\n * ```ts\n * readonly #frames = new FrameCoalescer();\n *\n * readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n *\n * disconnect(): void {\n * this.#frames.cancel();\n * }\n * ```\n */\nexport class FrameCoalescer {\n #frame: number | null = null;\n\n /**\n * Runs `run` on the next frame, unless a frame is already pending — the first\n * request of a burst wins and the rest are dropped. The pending frame is\n * released before `run`, so `run` may request the next one.\n */\n schedule(run: () => void): void {\n if (this.#frame !== null) return;\n this.#frame = requestAnimationFrame(() => {\n this.#frame = null;\n run();\n });\n }\n\n /**\n * Drops the pending frame, and reaches the platform only when there is one.\n *\n * There is no handle value that stands for \"nothing pending\":\n * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder\n * arrives as a large positive number that the same allocator can hand out, and\n * an idle cancel would then drop a frame belonging to someone else.\n */\n cancel(): void {\n if (this.#frame === null) return;\n cancelAnimationFrame(this.#frame);\n this.#frame = null;\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FrameCoalescer } from \"../utils/frame_coalescer\";\n\n/** Persisted scroll position; fields are present only for the saved axis. */\ninterface StoredScroll {\n top?: number;\n left?: number;\n}\n\n/**\n * Reads one persisted entry, or `null` when there is nothing usable to read.\n *\n * `JSON.parse` returns `null` for the literal `null` without throwing, so the\n * parse guard alone would hand back a value with no fields to read. Every field the\n * caller applies is validated there, which is what makes a bare literal harmless here.\n */\nfunction parseStored(raw: string | null): StoredScroll | null {\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as StoredScroll | null;\n } catch {\n return null;\n }\n}\n\n/** One axis offset from a persisted entry, or `null` when it cannot be applied. */\nfunction storedOffset(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\n/**\n * Headless **Scroll Restore** behavior: persists and restores the scroll\n * position of an inner scrolling region across Turbo Drive navigations (and full\n * reloads within the same tab session). No APG widget — a pure state-preservation\n * utility, like {@link import(\"./scroll_visibility_controller\").ScrollVisibilityController}\n * and the Sticky State Observer.\n *\n * Markup contract (identifier: `stimeo--scroll-restore`):\n * <div data-controller=\"stimeo--scroll-restore\"\n * data-stimeo--scroll-restore-key-value=\"sidebar\"\n * style=\"overflow: auto\">\n * …long content…\n * </div>\n *\n * Turbo swaps the whole `<body>` on navigation, so an inner scroll container is\n * rebuilt with `scrollTop` reset to 0. Rather than have every app hand-write a\n * controller for this (which contradicts a \"ship the behavior\" library), this\n * persists the offset under a stable key in `sessionStorage` and restores it on\n * `connect()`.\n *\n * Values:\n * - `key` — the `sessionStorage` namespace. Empty falls back to the element `id`;\n * with neither, nothing is saved or restored, so positions from unrelated pages\n * cannot mix.\n * - `axis` — which offset is tracked: `vertical` (default, `scrollTop`),\n * `horizontal` (`scrollLeft`) or `both`. An offset stored for an axis that is\n * not tracked is neither applied nor discarded.\n *\n * Both Values are re-derived when they change at runtime, so a morph that\n * rewrites the attribute in place switches namespace or axis instead of going on\n * writing under the one that held at connect. A `key` left empty resolves through\n * the element `id`, which no Value callback observes; runtime `id` changes are\n * out of scope.\n *\n * @remarks\n * Behavior only — it sets no ARIA/`data-*`/CSS and never moves focus. Restoring\n * goes through `scrollTo({ behavior: \"instant\" })` so that a consumer's\n * `scroll-behavior: smooth` cannot turn it into an animation whose intermediate\n * offsets would be recorded as the reader's own scrolling. The `scroll` listener\n * is internal and `passive` (no consumer `data-action` needed): each event\n * records the live offset synchronously, and a `requestAnimationFrame` coalesces\n * the writes to `sessionStorage`. On `disconnect()` (Turbo navigation included)\n * it flushes the **last captured** offset rather than re-reading the element — by\n * the time Turbo fires `disconnect` it has already detached the node, whose\n * `scrollTop` then reads `0`, so a fresh read would clobber the saved position\n * with `0`. Keying by `key` (falling back to the element `id`) in\n * `sessionStorage` makes it multi-instance safe and survives full reloads —\n * unlike a module-scope variable, which only survives Turbo Drive and assumes a\n * single instance. Storage access is best-effort: a browser that refuses it\n * (private mode, quota) leaves the element working, just without persistence.\n */\nexport class ScrollRestoreController extends Controller<HTMLElement> {\n static override values = {\n key: { type: String, default: \"\" },\n axis: { type: String, default: \"vertical\" },\n };\n\n declare keyValue: string;\n declare axisValue: string;\n\n /** Coalesces scroll bursts into one save per frame. */\n readonly #frames = new FrameCoalescer();\n /** Resolved storage key; empty disables persistence (no key and no id). */\n #storageKey = \"\";\n /** Last offset captured while the element was live; persisted as-is on teardown. */\n #lastTop = 0;\n #lastLeft = 0;\n /**\n * The offsets this controller's own restore produced, awaiting the `scroll`\n * the engine fires for them. A restore the layout cannot reach yet lands short,\n * so treating that echo as the reader's position would cut the saved offset\n * down to whatever the unfinished layout allowed.\n */\n #echoTop: number | null = null;\n #echoLeft: number | null = null;\n /** Offsets stored for an axis that is not tracked, carried through saves. */\n #carried: StoredScroll = {};\n /**\n * Whether each axis holds an offset worth saving — one this namespace restored\n * or the reader moved. A save writes only the axes that are marked, so an\n * offset taken under a different key or axis is never re-published as if the\n * reader had left it there.\n */\n #capturedTop = false;\n #capturedLeft = false;\n /** Whether the controller is connected; Value callbacks outside that window do not resync. */\n #connected = false;\n\n readonly #onScroll = (): void => {\n // Capture synchronously while the element is still connected and measurable;\n // the rAF only debounces the sessionStorage write, never the read.\n this.#capture();\n this.#frames.schedule(() => this.#persist());\n };\n\n override connect(): void {\n this.#connected = true;\n this.#storageKey = this.#resolveKey();\n this.element.addEventListener(\"scroll\", this.#onScroll, { passive: true });\n if (this.#storageKey) this.#restore();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.element.removeEventListener(\"scroll\", this.#onScroll);\n this.#frames.cancel();\n // Flush the last captured offset (a throttled burst may have left the final\n // frame unsaved). We do NOT re-read the element here: Turbo detaches the node\n // before disconnect, so its scrollTop is 0 and would overwrite the real value.\n this.#persist();\n }\n\n /** Re-derives the namespace when application code or a Turbo morph moves `key`. */\n keyValueChanged(): void {\n this.#resync();\n }\n\n /** Re-derives the tracked axes when application code or a Turbo morph moves `axis`. */\n axisValueChanged(): void {\n this.#resync();\n }\n\n /** Rebuilds the persistence state around the Values as they now read. */\n #resync(): void {\n if (!this.#connected) return;\n // The pending frame holds an offset belonging to the previous namespace and\n // axis, so it is dropped rather than written under the new ones.\n this.#frames.cancel();\n this.#storageKey = this.#resolveKey();\n this.#echoTop = null;\n this.#echoLeft = null;\n this.#carried = {};\n // An offset is only ever written for an axis this namespace seeded or the\n // reader moved. Clearing both marks drops the ones taken under the previous\n // key and axis, which the restore below re-establishes only where the new\n // key actually holds a value.\n this.#capturedTop = false;\n this.#capturedLeft = false;\n if (this.#storageKey) this.#restore();\n }\n\n /**\n * Records the live scroll offset for the configured axis.\n *\n * An axis holds its echo until that axis actually moves. A scroll event names\n * no axis, so consuming both on the first one to arrive would leave the other\n * unguarded: on `both`, moving one axis would take the clamped reading of the\n * untouched axis as the reader's own position and save it, cutting the stored\n * offset down to whatever the layout could reach at restore time.\n */\n #capture(): void {\n if (this.#tracksVertical) {\n const top = this.element.scrollTop;\n if (top !== this.#echoTop) {\n this.#echoTop = null;\n this.#lastTop = top;\n this.#capturedTop = true;\n }\n }\n if (this.#tracksHorizontal) {\n const left = this.element.scrollLeft;\n if (left !== this.#echoLeft) {\n this.#echoLeft = null;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n }\n }\n }\n\n /**\n * Writes the marked axes, carrying through any offset held for an untracked one.\n * Without a key, or with neither axis marked, it writes nothing.\n */\n #persist(): void {\n if (!this.#storageKey || !(this.#capturedTop || this.#capturedLeft)) return;\n const data: StoredScroll = { ...this.#carried };\n if (this.#tracksVertical && this.#capturedTop) data.top = this.#lastTop;\n if (this.#tracksHorizontal && this.#capturedLeft) data.left = this.#lastLeft;\n try {\n window.sessionStorage.setItem(this.#storageKey, JSON.stringify(data));\n } catch {\n // sessionStorage can throw (private mode / quota); persistence is best-effort.\n }\n }\n\n /** Applies the persisted scroll offset, if any, without moving focus. */\n #restore(): void {\n let raw: string | null;\n try {\n raw = window.sessionStorage.getItem(this.#storageKey);\n } catch {\n return;\n }\n const data = parseStored(raw);\n if (data === null) return;\n\n // A restore writes its own echoes below; one left over from an earlier pass\n // would otherwise swallow the first matching move the reader makes.\n this.#echoTop = null;\n this.#echoLeft = null;\n\n const top = storedOffset(data.top);\n const left = storedOffset(data.left);\n const options: ScrollToOptions = { behavior: \"instant\" };\n let requested = false;\n // Seed the capture cache from the restored value so an immediate teardown\n // (before any scroll) re-persists the restored position, not 0. Guard by the\n // configured axis so a stale field from a different axis is never applied —\n // and keep it, since the next save would otherwise drop it.\n if (this.#tracksVertical) {\n if (top !== null) {\n options.top = top;\n this.#lastTop = top;\n this.#capturedTop = true;\n requested = true;\n }\n } else if (data.top !== undefined) {\n this.#carried.top = data.top;\n }\n if (this.#tracksHorizontal) {\n if (left !== null) {\n options.left = left;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n requested = true;\n }\n } else if (data.left !== undefined) {\n this.#carried.left = data.left;\n }\n if (!requested) return;\n\n const beforeTop = this.element.scrollTop;\n const beforeLeft = this.element.scrollLeft;\n this.element.scrollTo(options);\n if (this.element.scrollTop !== beforeTop) this.#echoTop = this.element.scrollTop;\n if (this.element.scrollLeft !== beforeLeft) this.#echoLeft = this.element.scrollLeft;\n }\n\n /** The `sessionStorage` key: explicit `key`, else the element `id`, else none. */\n #resolveKey(): string {\n const base = this.keyValue || this.element.id;\n return base ? `stimeo--scroll-restore:${base}` : \"\";\n }\n\n get #tracksVertical(): boolean {\n return this.axisValue !== \"horizontal\";\n }\n\n get #tracksHorizontal(): boolean {\n return this.axisValue === \"horizontal\" || this.axisValue === \"both\";\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/frame_coalescer.ts","../../src/utils/listener_set.ts","../../src/controllers/scroll_restore_controller.ts"],"names":[],"mappings":"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","file":"scroll_restore_controller.js","sourcesContent":["/**\n * Runs one piece of scroll-driven work per animation frame.\n *\n * A `scroll` listener fires many times between two paints, and each firing would\n * measure the same layout. Requesting a frame on the first firing and ignoring\n * the rest until it runs keeps the reads to one per frame; running on the frame\n * — rather than on a timer — keeps the measurement in step with what the reader\n * sees.\n *\n * **The first request of a burst wins.** Work handed in while a frame is already\n * pending is dropped, so a caller that queues a special first pass and then\n * receives scrolls gets that first pass, not the last scroll's.\n *\n * Cancelling drops the pending frame outright, and does nothing at all when none\n * is pending. A frame really is cancelled, so nothing needs a generation to tell\n * a stale callback from a fresh one, and nothing has to be refused before the\n * caller starts listening.\n *\n * Scope is the scheduling only. What to measure stays with the caller.\n *\n * @example\n * ```ts\n * readonly #frames = new FrameCoalescer();\n *\n * readonly #onScroll = (): void => this.#frames.schedule(() => this.#measure());\n *\n * disconnect(): void {\n * this.#frames.cancel();\n * }\n * ```\n */\nexport class FrameCoalescer {\n #frame: number | null = null;\n\n /**\n * Runs `run` on the next frame, unless a frame is already pending — the first\n * request of a burst wins and the rest are dropped. The pending frame is\n * released before `run`, so `run` may request the next one.\n */\n schedule(run: () => void): void {\n if (this.#frame !== null) return;\n this.#frame = requestAnimationFrame(() => {\n this.#frame = null;\n run();\n });\n }\n\n /**\n * Drops the pending frame, and reaches the platform only when there is one.\n *\n * There is no handle value that stands for \"nothing pending\":\n * `cancelAnimationFrame` takes an `unsigned long`, so a negative placeholder\n * arrives as a large positive number that the same allocator can hand out, and\n * an idle cancel would then drop a frame belonging to someone else.\n */\n cancel(): void {\n if (this.#frame === null) return;\n cancelAnimationFrame(this.#frame);\n this.#frame = null;\n }\n}\n","/** What {@link ListenerSet.add} passes on besides the signal it supplies itself. */\nexport type ListenerOptions = Omit<AddEventListenerOptions, \"signal\">;\n\n/**\n * The event map a target's own `addEventListener` types its handlers with: the\n * window's, a document's, an HTML or SVG element's, any other element's, or a\n * media query list's. Other targets name none.\n */\ntype EventMapOf<T extends EventTarget> = T extends Window\n ? WindowEventMap\n : T extends Document\n ? DocumentEventMap\n : T extends HTMLElement\n ? HTMLElementEventMap\n : T extends SVGElement\n ? SVGElementEventMap\n : T extends Element\n ? ElementEventMap\n : T extends MediaQueryList\n ? MediaQueryListEventMap\n : Record<never, never>;\n\n/**\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.\n * {@link 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 * A lifetime shorter than the connection — while a Value holds, until a first\n * load, while a subscription is open — gets a set of its own, which the end of\n * that lifetime disposes. A controller therefore holds one set per lifetime: the\n * connection's, and one for each swapped receiver or shorter span.\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, listeners of\n * several elements that come and go one at a time, and those one gesture holds\n * keep their own pair of calls or a signal of their own.\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. A removal by hand would have to repeat that\n * flag; aborting the signal releases the listener without it.\n *\n * A `type` the target's own event map names types `handler` with that event,\n * as the target's `addEventListener` does, so a handler written for a\n * `KeyboardEvent` is passed as it is; any other `type` takes an\n * `EventListener`.\n */\n add<T extends EventTarget, K extends keyof EventMapOf<T> & string>(\n target: T,\n type: K,\n handler: (event: EventMapOf<T>[K]) => void,\n options?: ListenerOptions,\n ): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): void;\n add(target: EventTarget, type: string, handler: EventListener, options?: ListenerOptions): 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","import { Controller } from \"@hotwired/stimulus\";\nimport { FrameCoalescer } from \"../utils/frame_coalescer\";\nimport { ListenerSet } from \"../utils/listener_set\";\n\n/** Persisted scroll position; fields are present only for the saved axis. */\ninterface StoredScroll {\n top?: number;\n left?: number;\n}\n\n/**\n * Reads one persisted entry, or `null` when there is nothing usable to read.\n *\n * `JSON.parse` returns `null` for the literal `null` without throwing, so the\n * parse guard alone would hand back a value with no fields to read. Every field the\n * caller applies is validated there, which is what makes a bare literal harmless here.\n */\nfunction parseStored(raw: string | null): StoredScroll | null {\n if (raw === null) return null;\n try {\n return JSON.parse(raw) as StoredScroll | null;\n } catch {\n return null;\n }\n}\n\n/** One axis offset from a persisted entry, or `null` when it cannot be applied. */\nfunction storedOffset(value: unknown): number | null {\n return typeof value === \"number\" && Number.isFinite(value) ? value : null;\n}\n\n/**\n * Headless **Scroll Restore** behavior: persists and restores the scroll\n * position of an inner scrolling region across Turbo Drive navigations (and full\n * reloads within the same tab session). Like `stimeo--scroll-visibility` and\n * `stimeo--sticky-observer`, it implements no APG widget; it is a pure\n * state-preservation utility.\n *\n * Markup contract (identifier: `stimeo--scroll-restore`):\n * <div data-controller=\"stimeo--scroll-restore\"\n * data-stimeo--scroll-restore-key-value=\"sidebar\"\n * style=\"overflow: auto\">\n * …long content…\n * </div>\n *\n * Turbo swaps the whole `<body>` on navigation, so an inner scroll container is\n * rebuilt with `scrollTop` reset to 0. Rather than have every app hand-write a\n * controller for this (which contradicts a \"ship the behavior\" library), this\n * persists the offset under a stable key in `sessionStorage` and restores it on\n * `connect()`.\n *\n * Values:\n * - `key` — the `sessionStorage` namespace. Empty falls back to the element `id`;\n * with neither, nothing is saved or restored, so positions from unrelated pages\n * cannot mix.\n * - `axis` — which offset is tracked: `vertical` (default, `scrollTop`),\n * `horizontal` (`scrollLeft`) or `both`. An offset stored for an axis that is\n * not tracked is neither applied nor discarded.\n *\n * Both Values are re-derived when they change at runtime, so a morph that\n * rewrites the attribute in place switches namespace or axis instead of going on\n * writing under the one that held at connect. A `key` left empty resolves through\n * the element `id`, which no Value callback observes; runtime `id` changes are\n * out of scope.\n *\n * @remarks\n * Behavior only — it sets no ARIA/`data-*`/CSS and never moves focus. Restoring\n * goes through `scrollTo({ behavior: \"instant\" })` so that a consumer's\n * `scroll-behavior: smooth` cannot turn it into an animation whose intermediate\n * offsets would be recorded as the reader's own scrolling. The `scroll` listener\n * is internal and `passive` (no consumer `data-action` needed): each event\n * records the live offset synchronously, and a `requestAnimationFrame` coalesces\n * the writes to `sessionStorage`. On `disconnect()` (Turbo navigation included)\n * it flushes the **last captured** offset rather than re-reading the element — by\n * the time Turbo fires `disconnect` it has already detached the node, whose\n * `scrollTop` then reads `0`, so a fresh read would clobber the saved position\n * with `0`. Keying by `key` (falling back to the element `id`) in\n * `sessionStorage` makes it multi-instance safe and survives full reloads —\n * unlike a module-scope variable, which only survives Turbo Drive and assumes a\n * single instance. Storage access is best-effort: a browser that refuses it\n * (private mode, quota) leaves the element working, just without persistence.\n */\nexport class ScrollRestoreController extends Controller<HTMLElement> {\n static override values = {\n key: { type: String, default: \"\" },\n axis: { type: String, default: \"vertical\" },\n };\n\n declare keyValue: string;\n declare axisValue: string;\n\n /** Coalesces scroll bursts into one save per frame. */\n readonly #frames = new FrameCoalescer();\n /** Resolved storage key; empty disables persistence (no key and no id). */\n #storageKey = \"\";\n /** Last offset captured while the element was live; persisted as-is on teardown. */\n #lastTop = 0;\n #lastLeft = 0;\n /**\n * The offsets this controller's own restore produced, awaiting the `scroll`\n * the engine fires for them. A restore the layout cannot reach yet lands short,\n * so treating that echo as the reader's position would cut the saved offset\n * down to whatever the unfinished layout allowed.\n */\n #echoTop: number | null = null;\n #echoLeft: number | null = null;\n /** Offsets stored for an axis that is not tracked, carried through saves. */\n #carried: StoredScroll = {};\n /**\n * Whether each axis holds an offset worth saving — one this namespace restored\n * or the reader moved. A save writes only the axes that are marked, so an\n * offset taken under a different key or axis is never re-published as if the\n * reader had left it there.\n */\n #capturedTop = false;\n #capturedLeft = false;\n /** Whether the controller is connected; Value callbacks outside that window do not resync. */\n #connected = false;\n\n readonly #onScroll = (): void => {\n // Capture synchronously while the element is still connected and measurable;\n // the rAF only debounces the sessionStorage write, never the read.\n this.#capture();\n this.#frames.schedule(() => this.#persist());\n };\n\n /** The listeners of one connection, released together on `disconnect()`. */\n readonly #listeners = new ListenerSet();\n\n override connect(): void {\n this.#connected = true;\n this.#storageKey = this.#resolveKey();\n this.#listeners.add(this.element, \"scroll\", this.#onScroll, { passive: true });\n if (this.#storageKey) this.#restore();\n }\n\n override disconnect(): void {\n this.#connected = false;\n this.#listeners.dispose();\n this.#frames.cancel();\n // Flush the last captured offset (a throttled burst may have left the final\n // frame unsaved). We do NOT re-read the element here: Turbo detaches the node\n // before disconnect, so its scrollTop is 0 and would overwrite the real value.\n this.#persist();\n }\n\n /** Re-derives the namespace when application code or a Turbo morph moves `key`. */\n keyValueChanged(): void {\n this.#resync();\n }\n\n /** Re-derives the tracked axes when application code or a Turbo morph moves `axis`. */\n axisValueChanged(): void {\n this.#resync();\n }\n\n /** Rebuilds the persistence state around the Values as they now read. */\n #resync(): void {\n if (!this.#connected) return;\n // A frame a scroll queued before the swap belongs to the previous namespace and\n // axis, so it is dropped rather than run against the new ones.\n this.#frames.cancel();\n this.#storageKey = this.#resolveKey();\n this.#echoTop = null;\n this.#echoLeft = null;\n this.#carried = {};\n // An offset is only ever written for an axis this namespace seeded or the\n // reader moved. Clearing both marks drops the ones taken under the previous\n // key and axis, which the restore below re-establishes only where the new\n // key actually holds a value.\n this.#capturedTop = false;\n this.#capturedLeft = false;\n if (this.#storageKey) this.#restore();\n }\n\n /**\n * Records the live scroll offset for the configured axis.\n *\n * An axis holds its echo until that axis actually moves. A scroll event names\n * no axis, so consuming both on the first one to arrive would leave the other\n * unguarded: on `both`, moving one axis would take the clamped reading of the\n * untouched axis as the reader's own position and save it, cutting the stored\n * offset down to whatever the layout could reach at restore time.\n */\n #capture(): void {\n if (this.#tracksVertical) {\n const top = this.element.scrollTop;\n if (top !== this.#echoTop) {\n this.#echoTop = null;\n this.#lastTop = top;\n this.#capturedTop = true;\n }\n }\n if (this.#tracksHorizontal) {\n const left = this.element.scrollLeft;\n if (left !== this.#echoLeft) {\n this.#echoLeft = null;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n }\n }\n }\n\n /**\n * Writes the marked axes, carrying through any offset held for an untracked one.\n * Without a key, or with neither axis marked, it writes nothing.\n */\n #persist(): void {\n if (!this.#storageKey || !(this.#capturedTop || this.#capturedLeft)) return;\n const data: StoredScroll = { ...this.#carried };\n if (this.#tracksVertical && this.#capturedTop) data.top = this.#lastTop;\n if (this.#tracksHorizontal && this.#capturedLeft) data.left = this.#lastLeft;\n try {\n window.sessionStorage.setItem(this.#storageKey, JSON.stringify(data));\n } catch {\n // sessionStorage can throw (private mode / quota); persistence is best-effort.\n }\n }\n\n /** Applies the persisted scroll offset, if any, without moving focus. */\n #restore(): void {\n let raw: string | null;\n try {\n raw = window.sessionStorage.getItem(this.#storageKey);\n } catch {\n return;\n }\n const data = parseStored(raw);\n if (data === null) return;\n\n // A restore writes its own echoes below; one left over from an earlier pass\n // would otherwise swallow the first matching move the reader makes.\n this.#echoTop = null;\n this.#echoLeft = null;\n\n const top = storedOffset(data.top);\n const left = storedOffset(data.left);\n const options: ScrollToOptions = { behavior: \"instant\" };\n let requested = false;\n // Seed the capture cache from the restored value so an immediate teardown\n // (before any scroll) re-persists the restored position, not 0. Guard by the\n // configured axis so a stale field from a different axis is never applied —\n // and keep it, since the next save would otherwise drop it.\n if (this.#tracksVertical) {\n if (top !== null) {\n options.top = top;\n this.#lastTop = top;\n this.#capturedTop = true;\n requested = true;\n }\n } else if (data.top !== undefined) {\n this.#carried.top = data.top;\n }\n if (this.#tracksHorizontal) {\n if (left !== null) {\n options.left = left;\n this.#lastLeft = left;\n this.#capturedLeft = true;\n requested = true;\n }\n } else if (data.left !== undefined) {\n this.#carried.left = data.left;\n }\n if (!requested) return;\n\n const beforeTop = this.element.scrollTop;\n const beforeLeft = this.element.scrollLeft;\n this.element.scrollTo(options);\n if (this.element.scrollTop !== beforeTop) this.#echoTop = this.element.scrollTop;\n if (this.element.scrollLeft !== beforeLeft) this.#echoLeft = this.element.scrollLeft;\n }\n\n /** The `sessionStorage` key: explicit `key`, else the element `id`, else none. */\n #resolveKey(): string {\n const base = this.keyValue || this.element.id;\n return base ? `stimeo--scroll-restore:${base}` : \"\";\n }\n\n get #tracksVertical(): boolean {\n return this.axisValue !== \"horizontal\";\n }\n\n get #tracksHorizontal(): boolean {\n return this.axisValue === \"horizontal\" || this.axisValue === \"both\";\n }\n}\n"]}
@@ -36,10 +36,14 @@ import { Controller } from '@hotwired/stimulus';
36
36
  * @remarks
37
37
  * Behavior only — the look and any transition are the consumer's CSS. The scroll
38
38
  * listener is `passive`, coalesced through `requestAnimationFrame`, and removed on
39
- * `disconnect()` (Turbo navigation included). `offset` / `mode` are re-read at
40
- * runtime, so a Turbo morph that swaps either attribute is followed without
41
- * waiting for the next scroll; a selector (`root`, `focusSelector`) or a
42
- * threshold that cannot be parsed reads as its default, keeping the rest of the
39
+ * `disconnect()` (Turbo navigation included). `offset`, `mode` and `root` follow a
40
+ * runtime change — a Turbo morph, a Stream, an author script — without waiting for
41
+ * the next scroll: the Values a batch changes are applied in one pass after it. A
42
+ * `root` that names another container, or a morph that reaches this element after
43
+ * replacing the container its selector names, moves the listener to the container
44
+ * resolved now; the position found there is a baseline rather than a movement, and
45
+ * the visibility is decided again from it. A selector (`root`, `focusSelector`) or
46
+ * a threshold that cannot be parsed reads as its default, keeping the rest of the
43
47
  * element alive. **A scroll never hides the control while it owns focus**: that
44
48
  * hide waits for it to blur and is decided again from the scroll position at that
45
49
  * moment. `toTop` honors `prefers-reduced-motion` by forcing an
@@ -72,9 +76,15 @@ declare class ScrollVisibilityController extends Controller<HTMLElement> {
72
76
  default: string;
73
77
  };
74
78
  };
79
+ static valueConstraints: {
80
+ offset: {
81
+ readonly finite: true;
82
+ };
83
+ };
75
84
  static actions: readonly ["toTop"];
76
85
  static events: readonly ["change"];
77
86
  readonly elementTarget: HTMLElement;
87
+ readonly elementTargets: HTMLElement[];
78
88
  readonly hasElementTarget: boolean;
79
89
  offsetValue: number;
80
90
  modeValue: string;
@@ -84,21 +94,27 @@ declare class ScrollVisibilityController extends Controller<HTMLElement> {
84
94
  disconnect(): void;
85
95
  /** Writes the current visibility onto a control that arrives after connect. */
86
96
  elementTargetConnected(element: HTMLElement): void;
87
- /** Drops a held-back hide together with the control it was waiting on. */
88
- elementTargetDisconnected(): void;
89
97
  /**
90
- * Validates `offset` once, then re-renders.
98
+ * Drops a held-back hide with its control, gives a control that no longer resolves its
99
+ * own `hidden` back, even after `disconnect()`, and writes the visibility onto the one
100
+ * that stays.
101
+ */
102
+ elementTargetDisconnected(element: HTMLElement): void;
103
+ /**
104
+ * Validates `offset` once, then asks for a pass.
91
105
  *
92
- * Re-renders when application code (or a Turbo morph) changes `offset` at
93
- * runtime. A declaration that is not a finite number reads as the default, so
94
- * the comparison path never sees `NaN` — which would answer `false` to every
106
+ * A declaration that is not a finite number reads as the default, so the
107
+ * comparison path never sees `NaN` — which would answer `false` to every
95
108
  * comparison and strand the element (in `direction` mode, even the guarantee
96
109
  * that the very top always reveals).
97
110
  */
98
111
  offsetValueChanged(): void;
99
- /** Re-renders when application code (or a Turbo morph) changes `mode` at runtime. */
112
+ /** Asks for a pass when application code (or a Turbo morph) changes `mode`. */
100
113
  modeValueChanged(): void;
101
- /** Validates `root` once so connect never parses a selector that throws. */
114
+ /**
115
+ * Validates `root` once, so no resolve parses a selector that throws, then asks
116
+ * for a pass that follows the source it names.
117
+ */
102
118
  rootValueChanged(): void;
103
119
  /** Validates `focusSelector` once so `toTop` never parses a selector that throws. */
104
120
  focusSelectorValueChanged(): void;