stimeo-ui 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/dist/cable/index.d.ts +67 -13
  3. package/dist/cable/index.js +438 -152
  4. package/dist/cable/index.js.map +1 -1
  5. package/dist/controllers/alert_dialog_controller.d.ts +1 -1
  6. package/dist/controllers/alert_dialog_controller.js.map +1 -1
  7. package/dist/controllers/announcer_controller.d.ts +1 -1
  8. package/dist/controllers/announcer_controller.js.map +1 -1
  9. package/dist/controllers/auto_submit_controller.js.map +1 -1
  10. package/dist/controllers/breadcrumb_controller.d.ts +1 -1
  11. package/dist/controllers/breadcrumb_controller.js.map +1 -1
  12. package/dist/controllers/calendar_controller.d.ts +2 -1
  13. package/dist/controllers/calendar_controller.js +10 -2
  14. package/dist/controllers/calendar_controller.js.map +1 -1
  15. package/dist/controllers/carousel_controller.js +1 -1
  16. package/dist/controllers/carousel_controller.js.map +1 -1
  17. package/dist/controllers/character_counter_controller.js.map +1 -1
  18. package/dist/controllers/clipboard_controller.d.ts +1 -1
  19. package/dist/controllers/clipboard_controller.js.map +1 -1
  20. package/dist/controllers/collapsible_controller.js.map +1 -1
  21. package/dist/controllers/color_picker_controller.d.ts +1 -1
  22. package/dist/controllers/color_picker_controller.js.map +1 -1
  23. package/dist/controllers/command_palette_controller.d.ts +3 -3
  24. package/dist/controllers/command_palette_controller.js +2 -2
  25. package/dist/controllers/command_palette_controller.js.map +1 -1
  26. package/dist/controllers/conditional_fields_controller.js.map +1 -1
  27. package/dist/controllers/confirm_controller.d.ts +2 -2
  28. package/dist/controllers/confirm_controller.js.map +1 -1
  29. package/dist/controllers/context_menu_controller.d.ts +1 -1
  30. package/dist/controllers/context_menu_controller.js.map +1 -1
  31. package/dist/controllers/countdown_controller.d.ts +1 -1
  32. package/dist/controllers/countdown_controller.js.map +1 -1
  33. package/dist/controllers/data_grid_controller.d.ts +1 -1
  34. package/dist/controllers/data_grid_controller.js.map +1 -1
  35. package/dist/controllers/date_range_picker_controller.js +10 -10
  36. package/dist/controllers/date_range_picker_controller.js.map +1 -1
  37. package/dist/controllers/dialog_controller.d.ts +1 -1
  38. package/dist/controllers/dialog_controller.js.map +1 -1
  39. package/dist/controllers/direct_upload_controller.js.map +1 -1
  40. package/dist/controllers/dirty_form_controller.js.map +1 -1
  41. package/dist/controllers/dismissible_controller.js.map +1 -1
  42. package/dist/controllers/drawer_controller.d.ts +2 -2
  43. package/dist/controllers/drawer_controller.js +2 -2
  44. package/dist/controllers/drawer_controller.js.map +1 -1
  45. package/dist/controllers/dropdown_controller.d.ts +1 -1
  46. package/dist/controllers/dropdown_controller.js.map +1 -1
  47. package/dist/controllers/file_dropzone_controller.js.map +1 -1
  48. package/dist/controllers/flash_controller.js.map +1 -1
  49. package/dist/controllers/focus_controller.d.ts +2 -2
  50. package/dist/controllers/focus_controller.js.map +1 -1
  51. package/dist/controllers/form_validation_controller.js.map +1 -1
  52. package/dist/controllers/frame_loading_controller.d.ts +3 -3
  53. package/dist/controllers/frame_loading_controller.js.map +1 -1
  54. package/dist/controllers/highlight_controller.js.map +1 -1
  55. package/dist/controllers/hover_card_controller.d.ts +1 -1
  56. package/dist/controllers/hover_card_controller.js.map +1 -1
  57. package/dist/controllers/idle_controller.d.ts +1 -1
  58. package/dist/controllers/idle_controller.js.map +1 -1
  59. package/dist/controllers/input_mask_controller.js.map +1 -1
  60. package/dist/controllers/intersection_controller.js.map +1 -1
  61. package/dist/controllers/lazy_frame_controller.js.map +1 -1
  62. package/dist/controllers/listbox_controller.js.map +1 -1
  63. package/dist/controllers/local_time_controller.d.ts +1 -1
  64. package/dist/controllers/local_time_controller.js.map +1 -1
  65. package/dist/controllers/masonry_controller.d.ts +1 -1
  66. package/dist/controllers/masonry_controller.js.map +1 -1
  67. package/dist/controllers/menu_controller.d.ts +1 -1
  68. package/dist/controllers/menu_controller.js.map +1 -1
  69. package/dist/controllers/menubar_controller.d.ts +2 -2
  70. package/dist/controllers/menubar_controller.js.map +1 -1
  71. package/dist/controllers/navigation_menu_controller.d.ts +1 -1
  72. package/dist/controllers/navigation_menu_controller.js.map +1 -1
  73. package/dist/controllers/nested_form_controller.js.map +1 -1
  74. package/dist/controllers/network_status_controller.js.map +1 -1
  75. package/dist/controllers/number_input_controller.js.map +1 -1
  76. package/dist/controllers/optimistic_controller.d.ts +23 -10
  77. package/dist/controllers/optimistic_controller.js +141 -20
  78. package/dist/controllers/optimistic_controller.js.map +1 -1
  79. package/dist/controllers/overflow_indicator_controller.d.ts +1 -1
  80. package/dist/controllers/overflow_indicator_controller.js.map +1 -1
  81. package/dist/controllers/overflow_menu_controller.js.map +1 -1
  82. package/dist/controllers/pagination_controller.js.map +1 -1
  83. package/dist/controllers/password_reveal_controller.js.map +1 -1
  84. package/dist/controllers/password_strength_controller.d.ts +2 -1
  85. package/dist/controllers/password_strength_controller.js.map +1 -1
  86. package/dist/controllers/persist_controller.js.map +1 -1
  87. package/dist/controllers/popover_controller.d.ts +1 -1
  88. package/dist/controllers/popover_controller.js.map +1 -1
  89. package/dist/controllers/portal_controller.d.ts +2 -2
  90. package/dist/controllers/portal_controller.js.map +1 -1
  91. package/dist/controllers/radio_group_controller.d.ts +1 -1
  92. package/dist/controllers/radio_group_controller.js.map +1 -1
  93. package/dist/controllers/relative_time_controller.d.ts +3 -3
  94. package/dist/controllers/relative_time_controller.js.map +1 -1
  95. package/dist/controllers/roving_controller.d.ts +1 -1
  96. package/dist/controllers/roving_controller.js.map +1 -1
  97. package/dist/controllers/scroll_area_controller.js.map +1 -1
  98. package/dist/controllers/scrollspy_controller.js.map +1 -1
  99. package/dist/controllers/sidebar_controller.d.ts +1 -1
  100. package/dist/controllers/sidebar_controller.js.map +1 -1
  101. package/dist/controllers/skeleton_controller.d.ts +2 -2
  102. package/dist/controllers/skeleton_controller.js.map +1 -1
  103. package/dist/controllers/spinner_controller.d.ts +3 -3
  104. package/dist/controllers/spinner_controller.js.map +1 -1
  105. package/dist/controllers/sticky_observer_controller.js.map +1 -1
  106. package/dist/controllers/submit_once_controller.d.ts +3 -3
  107. package/dist/controllers/submit_once_controller.js.map +1 -1
  108. package/dist/controllers/switch_controller.js.map +1 -1
  109. package/dist/controllers/textarea_autosize_controller.d.ts +1 -1
  110. package/dist/controllers/textarea_autosize_controller.js.map +1 -1
  111. package/dist/controllers/theme_controller.d.ts +3 -2
  112. package/dist/controllers/theme_controller.js.map +1 -1
  113. package/dist/controllers/toast_controller.js +1 -1
  114. package/dist/controllers/toast_controller.js.map +1 -1
  115. package/dist/controllers/toggle_group_controller.js.map +1 -1
  116. package/dist/controllers/toolbar_controller.d.ts +1 -1
  117. package/dist/controllers/toolbar_controller.js.map +1 -1
  118. package/dist/controllers/tooltip_controller.d.ts +1 -1
  119. package/dist/controllers/tooltip_controller.js.map +1 -1
  120. package/dist/controllers/transition_controller.d.ts +1 -1
  121. package/dist/controllers/transition_controller.js.map +1 -1
  122. package/dist/controllers/tree_view_controller.d.ts +4 -3
  123. package/dist/controllers/tree_view_controller.js.map +1 -1
  124. package/dist/index.js +101 -38
  125. package/dist/index.js.map +1 -1
  126. package/dist/inspector/cli.d.ts +1 -1
  127. package/dist/inspector/cli.js +1 -1
  128. package/dist/inspector/cli.js.map +1 -1
  129. package/dist/inspector/cli_bin.js +1 -1
  130. package/dist/inspector/cli_bin.js.map +1 -1
  131. package/dist/inspector/examples.json +1 -1
  132. package/dist/inspector/manifest.json +6 -3
  133. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/escape_layer.ts","../../src/utils/focus_candidate.ts","../../src/utils/focus_trap.ts","../../src/controllers/dialog_controller.ts"],"names":[],"mappings":";;;;;AA2CO,IAAM,gBAAA,GAAN,MAAM,iBAAA,CAAiB;AAAA;AAAA,EAE5B,OAAgB,YAAA,mBAAe,IAAI,GAAA,EAAsB;AAAA;AAAA,EAGzD,OAAgB,iBAAiB,MAAY;AAC3C,IAAA,KAAA,MAAW,UAAA,IAAc,iBAAA,CAAiB,YAAA,EAAc,UAAA,CAAW,OAAA,EAAQ;AAAA,EAC7E,CAAA;AAAA,EAES,OAAA;AAAA;AAAA,EAGT,YAAY,MAAA,EAAoB;AAC9B,IAAA,IAAA,CAAK,OAAA,GAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGA,QAAA,GAAiB;AACf,IAAA,MAAM,KAAA,GAAQ,iBAAA,CAAiB,YAAA,CAAa,IAAA,KAAS,CAAA;AACrD,IAAA,iBAAA,CAAiB,YAAA,CAAa,IAAI,IAAI,CAAA;AACtC,IAAA,IAAI,KAAA,EAAO;AACT,MAAA,QAAA,CAAS,gBAAA,CAAiB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,IACjF;AAAA,EACF;AAAA;AAAA,EAGA,UAAA,GAAmB;AACjB,IAAA,iBAAA,CAAiB,YAAA,CAAa,OAAO,IAAI,CAAA;AACzC,IAAA,IAAI,iBAAA,CAAiB,YAAA,CAAa,IAAA,GAAO,CAAA,EAAG;AAC5C,IAAA,QAAA,CAAS,mBAAA,CAAoB,oBAAA,EAAsB,iBAAA,CAAiB,cAAc,CAAA;AAAA,EACpF;AACF,CAAA;;;ACHO,IAAM,WAAA,GAAN,MAAM,YAAA,CAAY;AAAA,EACvB,OAAgB,WAAA,mBAAc,IAAI,OAAA,EAAuC;AAAA,EAEzE,cAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,UAAA,GAAkC,IAAA;AAAA;AAAA,EAElC,OAAA,GAAkC,IAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlC,QAAA,CAAS,aAAA,GAA0B,QAAA,EAAU,OAAA,EAAmC;AAC9E,IAAA,IAAA,CAAK,UAAA,EAAW;AAChB,IAAA,IAAI,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AACxD,IAAA,IAAI,CAAC,QAAA,EAAU;AACb,MAAA,QAAA,GAAW,aAAY,eAAA,EAAgB;AACvC,MAAA,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAA,EAAe,QAAQ,CAAA;AACnD,MAAA,aAAA,CAAc,gBAAA,CAAiB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAAA,IAC9D;AACA,IAAA,QAAA,CAAS,KAAA,CAAM,KAAK,IAAI,CAAA;AACxB,IAAA,IAAA,CAAK,cAAA,GAAiB,aAAA;AACtB,IAAA,IAAA,CAAK,aAAa,OAAA,CAAQ,SAAA;AAC1B,IAAA,IAAA,CAAK,OAAA,GAAU,QAAQ,MAAA,IAAU,IAAA;AAAA,EACnC;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,UAAA,GAAmB;AACjB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,aAAA,EAAe;AAEpB,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,QAAA,EAAU;AACZ,MAAA,MAAM,KAAA,GAAQ,QAAA,CAAS,KAAA,CAAM,WAAA,CAAY,IAAI,CAAA;AAC7C,MAAA,IAAI,SAAS,CAAA,EAAG,QAAA,CAAS,KAAA,CAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AAC9C,MAAA,IAAI,QAAA,CAAS,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAC/B,QAAA,aAAA,CAAc,mBAAA,CAAoB,SAAA,EAAW,QAAA,CAAS,SAAS,CAAA;AAC/D,QAAA,YAAA,CAAY,WAAA,CAAY,OAAO,aAAa,CAAA;AAAA,MAC9C;AAAA,IACF;AACA,IAAA,IAAA,CAAK,cAAA,GAAiB,IAAA;AACtB,IAAA,IAAA,CAAK,UAAA,GAAa,IAAA;AAClB,IAAA,IAAA,CAAK,OAAA,GAAU,IAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,IAAI,UAAA,GAAsB;AACxB,IAAA,MAAM,gBAAgB,IAAA,CAAK,cAAA;AAC3B,IAAA,IAAI,CAAC,eAAe,OAAO,KAAA;AAC3B,IAAA,MAAM,QAAA,GAAW,YAAA,CAAY,WAAA,CAAY,GAAA,CAAI,aAAa,CAAA;AAC1D,IAAA,IAAI,CAAC,UAAU,OAAO,KAAA;AACtB,IAAA,OAAO,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA,KAAM,IAAA;AAAA,EACvD;AAAA;AAAA,EAGA,OAAO,eAAA,GAAuC;AAC5C,IAAA,MAAM,QAAA,GAAgC;AAAA,MACpC,OAAO,EAAC;AAAA,MACR,SAAA,EAAW,CAAC,KAAA,KAA+B;AACzC,QAAA,IAAI,MAAM,GAAA,KAAQ,QAAA,IAAY,KAAA,CAAM,gBAAA,IAAoB,MAAM,WAAA,EAAa;AAC3E,QAAA,MAAM,KAAA,GAAQ,YAAA,CAAY,aAAA,CAAc,QAAA,CAAS,KAAK,CAAA;AACtD,QAAA,IAAI,CAAC,KAAA,EAAO;AACZ,QAAA,KAAA,CAAM,cAAA,EAAe;AACrB,QAAA,KAAA,CAAM,UAAA,IAAa;AAAA,MACrB;AAAA,KACF;AACA,IAAA,OAAO,QAAA;AAAA,EACT;AAAA;AAAA,EAGA,OAAO,cAAc,KAAA,EAA0C;AAC7D,IAAA,KAAA,IAAS,QAAQ,KAAA,CAAM,MAAA,GAAS,CAAA,EAAG,KAAA,IAAS,GAAG,KAAA,EAAA,EAAS;AACtD,MAAA,MAAM,KAAA,GAAQ,MAAM,KAAK,CAAA;AACzB,MAAA,IAAI,CAAC,KAAA,EAAO;AACZ,MAAA,IAAI,KAAA,CAAM,OAAA,IAAW,CAAC,KAAA,CAAM,SAAQ,EAAG;AACvC,MAAA,OAAO,KAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACF,CAAA;;;ACpJO,SAAS,yBAAyB,OAAA,EAA+B;AACtE,EAAA,IAAI,QAAA,GAA2B,OAAA,CAAQ,OAAA,CAAQ,oBAAoB,CAAA;AACnE,EAAA,OAAO,QAAA,EAAU;AACf,IAAA,MAAM,MAAA,GAAS,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,KAAY,QAAQ,CAAA;AACvF,IAAA,IAAI,CAAC,MAAA,EAAQ,QAAA,CAAS,OAAO,GAAG,OAAO,IAAA;AACvC,IAAA,QAAA,GAAW,QAAA,CAAS,aAAA,EAAe,OAAA,CAAQ,oBAAoB,CAAA,IAAK,IAAA;AAAA,EACtE;AACA,EAAA,OAAO,KAAA;AACT;AAgDO,SAAS,aAAa,OAAA,EAA+B;AAC1D,EAAA,IAAI,OAAA,CAAQ,OAAA,CAAQ,mBAAmB,CAAA,EAAG,OAAO,KAAA;AACjD,EAAA,IAAI,OAAA,YAAmB,gBAAA,IAAoB,OAAA,CAAQ,IAAA,KAAS,UAAU,OAAO,KAAA;AAC7E,EAAA,IAAI,EAAE,UAAA,IAAc,OAAA,CAAA,EAAU,OAAO,IAAA;AACrC,EAAA,IAAK,OAAA,CAAgD,UAAU,OAAO,KAAA;AACtE,EAAA,OAAO,CAAC,yBAAyB,OAAO,CAAA;AAC1C;AAGO,IAAM,2BAAA,GAA8B;AAAA,EACzC,SAAA;AAAA,EACA,YAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAA;AAAA,EACA,QAAA;AAAA,EACA,UAAA;AAAA,EACA,SAAA;AAAA,EACA,QAAA;AAAA,EACA,iBAAA;AAAA,EACA,iBAAA;AAAA,EACA,YAAA;AAAA,EACA;AACF,CAAA,CAAE,KAAK,GAAG,CAAA;AAQH,SAAS,mBAAmB,OAAA,EAA+B;AAChE,EAAA,MAAM,QAAS,OAAA,CAA8C,eAAA;AAC7D,EAAA,OAAO,OAAO,KAAA,KAAU,UAAA,GAAa,KAAA,CAAM,IAAA,CAAK,SAAS,EAAE,kBAAA,EAAoB,IAAA,EAAM,CAAA,GAAI,IAAA;AAC3F;AAGA,SAAS,iBAAiB,OAAA,EAAqC;AAC7D,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,YAAA,CAAa,UAAU,CAAA;AAC7C,EAAA,IAAI,KAAA,KAAU,QAAQ,CAAC,YAAA,CAAa,KAAK,KAAA,CAAM,IAAA,EAAM,CAAA,EAAG,OAAO,IAAA;AAC/D,EAAA,OAAO,OAAO,KAAK,CAAA;AACrB;AAGA,SAAS,iBAAiB,OAAA,EAA+B;AACvD,EAAA,IAAI,OAAA,YAAmB,iBAAA,IAAqB,OAAA,YAAmB,eAAA,EAAiB;AAC9E,IAAA,OAAO,OAAA,CAAQ,aAAa,MAAM,CAAA;AAAA,EACpC;AACA,EAAA,IACE,OAAA,YAAmB,iBAAA,IACnB,OAAA,YAAmB,iBAAA,IACnB,mBAAmB,mBAAA,EACnB;AACA,IAAA,OAAO,IAAA;AAAA,EACT;AACA,EAAA,IAAI,OAAA,YAAmB,gBAAA,EAAkB,OAAO,OAAA,CAAQ,IAAA,KAAS,QAAA;AACjE,EAAA,IAAI,OAAA,YAAmB,mBAAmB,OAAO,IAAA;AACjD,EAAA,IAAI,OAAA,CAAQ,OAAA,KAAY,OAAA,IAAW,OAAA,CAAQ,YAAY,OAAA,EAAS;AAC9D,IAAA,OAAO,OAAA,CAAQ,aAAa,UAAU,CAAA;AAAA,EACxC;AACA,EAAA,IAAI,OAAA,YAAmB,WAAA,IAAe,OAAA,CAAQ,OAAA,KAAY,SAAA,EAAW;AACnE,IAAA,MAAM,UAAU,OAAA,CAAQ,aAAA;AACxB,IAAA,OACE,OAAA,YAAmB,kBAAA,IACnB,KAAA,CAAM,IAAA,CAAK,OAAA,CAAQ,QAAQ,CAAA,CAAE,IAAA,CAAK,CAAC,KAAA,KAAU,KAAA,CAAM,OAAA,KAAY,SAAS,CAAA,KAAM,OAAA;AAAA,EAElF;AACA,EAAA,OAAO,KAAA;AACT;AAGA,SAAS,mBAAmB,OAAA,EAA+B;AACzD,EAAA,MAAM,KAAA,GAAQ,OAAA,CAAQ,YAAA,CAAa,iBAAiB,GAAG,WAAA,EAAY;AACnE,EAAA,OAAO,KAAA,KAAU,EAAA,IAAM,KAAA,KAAU,MAAA,IAAU,KAAA,KAAU,gBAAA;AACvD;AAUO,SAAS,UAAU,OAAA,EAA+B;AACvD,EAAA,IAAI,CAAC,aAAa,OAAO,CAAA,IAAK,CAAC,kBAAA,CAAmB,OAAO,GAAG,OAAO,KAAA;AAEnE,EAAA,MAAM,QAAA,GAAW,iBAAiB,OAAO,CAAA;AACzC,EAAA,IAAI,QAAA,KAAa,IAAA,EAAM,OAAO,QAAA,IAAY,CAAA;AAC1C,EAAA,OAAO,gBAAA,CAAiB,OAAO,CAAA,IAAK,kBAAA,CAAmB,OAAO,CAAA;AAChE;AAGO,SAAS,eAAe,IAAA,EAAiC;AAC9D,EAAA,OAAO,MAAM,IAAA,CAAK,IAAA,CAAK,gBAAA,CAA8B,2BAA2B,CAAC,CAAA,CAAE,MAAA;AAAA,IACjF;AAAA,GACF;AACF;;;ACtFO,IAAM,YAAN,MAAgB;AAAA;AAAA,EAErB,kBAAA,GAAyC,IAAA;AAAA;AAAA,EAEzC,qBAAA,GAAwB,EAAA;AAAA;AAAA,EAExB,aAAA,GAAgB,KAAA;AAAA;AAAA,EAEhB,mBAAkC,EAAC;AAAA;AAAA,EAEnC,YAAA,GAAe,KAAA;AAAA;AAAA,EAEN,YAAA,GAAe,IAAI,WAAA,EAAY;AAAA;AAAA,EAG/B,aAAA;AAAA;AAAA,EAEA,QAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOT,WAAA,CAAY,YAAA,EAAiC,OAAA,GAA4B,EAAC,EAAG;AAC3E,IAAA,IAAA,CAAK,aAAA,GAAgB,YAAA;AACrB,IAAA,IAAA,CAAK,QAAA,GAAW,OAAA;AAAA,EAClB;AAAA;AAAA,EAGA,IAAI,MAAA,GAAkB;AACpB,IAAA,OAAO,IAAA,CAAK,YAAA;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,QAAA,GAAiB;AACf,IAAA,IAAI,KAAK,YAAA,EAAc;AACvB,IAAA,IAAA,CAAK,YAAA,GAAe,IAAA;AAIpB,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AACxB,IAAA,IAAA,CAAK,qBACH,MAAA,YAAkB,WAAA,IAAe,MAAA,KAAW,QAAA,CAAS,OAAO,MAAA,GAAS,IAAA;AACvE,IAAA,IAAI,KAAK,KAAA,CAAM,IAAA,CAAK,QAAA,CAAS,UAAA,EAAY,IAAI,CAAA,EAAG;AAC9C,MAAA,IAAA,CAAK,qBAAA,GAAwB,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA;AACjD,MAAA,QAAA,CAAS,IAAA,CAAK,MAAM,QAAA,GAAW,QAAA;AAC/B,MAAA,IAAA,CAAK,aAAA,GAAgB,IAAA;AAAA,IACvB;AACA,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,CAAK,QAAA,CAAS,SAAS,IAAI,CAAA,OAAQ,kBAAA,EAAmB;AACrE,IAAA,QAAA,CAAS,gBAAA,CAAiB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACpD,IAAA,IAAA,CAAK,aAAa,QAAA,EAAS;AAC3B,IAAA,MAAM,QAAA,GAAW,KAAK,QAAA,CAAS,QAAA;AAC/B,IAAA,IAAI,QAAA,EAAU,IAAA,CAAK,YAAA,CAAa,QAAA,CAAS,QAAA,EAAU,EAAE,SAAA,EAAW,MAAM,QAAA,EAAS,EAAG,CAAA;AAClF,IAAA,IAAI,IAAA,CAAK,MAAM,IAAA,CAAK,QAAA,CAAS,WAAW,IAAI,CAAA,OAAQ,aAAA,EAAc;AAAA,EACpE;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,WAAW,EAAE,YAAA,GAAe,IAAA,EAAK,GAAgC,EAAC,EAAS;AACzE,IAAA,IAAI,CAAC,KAAK,YAAA,EAAc;AACxB,IAAA,IAAA,CAAK,YAAA,GAAe,KAAA;AACpB,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,QAAA,CAAS,mBAAA,CAAoB,SAAA,EAAW,IAAA,CAAK,UAAU,CAAA;AACvD,IAAA,IAAA,CAAK,aAAa,UAAA,EAAW;AAC7B,IAAA,IAAI,KAAK,aAAA,EAAe;AACtB,MAAA,QAAA,CAAS,IAAA,CAAK,KAAA,CAAM,QAAA,GAAW,IAAA,CAAK,qBAAA;AACpC,MAAA,IAAA,CAAK,aAAA,GAAgB,KAAA;AAAA,IACvB;AACA,IAAA,IAAA,CAAK,kBAAA,EAAmB;AACxB,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,MAAM,SAAS,IAAA,CAAK,kBAAA,IAAsB,IAAA,CAAK,QAAA,CAAS,iBAAgB,IAAK,IAAA;AAC7E,MAAA,MAAA,EAAQ,KAAA,EAAM;AAAA,IAChB;AAAA,EACF;AAAA;AAAA,EAGA,KAAA,CAAM,QAA+C,QAAA,EAA4B;AAC/E,IAAA,IAAI,MAAA,KAAW,QAAW,OAAO,QAAA;AACjC,IAAA,OAAO,OAAO,MAAA,KAAW,UAAA,GAAa,MAAA,EAAO,GAAI,MAAA;AAAA,EACnD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAUS,YAAA,GAAe,IAAI,gBAAA,CAAiB,MAAM,IAAA,CAAK,WAAW,EAAE,YAAA,EAAc,KAAA,EAAO,CAAC,CAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOlF,UAAA,GAAa,CAAC,KAAA,KAA+B;AACpD,IAAA,IAAI,KAAA,CAAM,GAAA,KAAQ,KAAA,EAAO,IAAA,CAAK,SAAS,KAAK,CAAA;AAAA,EAC9C,CAAA;AAAA;AAAA,EAGA,SAAS,KAAA,EAA4B;AACnC,IAAA,MAAM,SAAA,GAAY,KAAK,kBAAA,EAAmB;AAC1C,IAAA,IAAI,SAAA,CAAU,WAAW,CAAA,EAAG;AAC1B,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,KAAA,GAAQ,UAAU,CAAC,CAAA;AACzB,IAAA,MAAM,IAAA,GAAO,SAAA,CAAU,SAAA,CAAU,MAAA,GAAS,CAAC,CAAA;AAC3C,IAAA,MAAM,SAAS,QAAA,CAAS,aAAA;AAGxB,IAAA,IAAI,EAAE,kBAAkB,IAAA,CAAA,IAAS,CAAC,KAAK,aAAA,EAAc,CAAE,QAAA,CAAS,MAAM,CAAA,EAAG;AACvE,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,KAAA,EAAO,KAAA,EAAM;AACb,MAAA;AAAA,IACF;AAEA,IAAA,IAAI,KAAA,CAAM,QAAA,IAAY,MAAA,KAAW,KAAA,EAAO;AACtC,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,IAAA,EAAM,KAAA,EAAM;AAAA,IACd,CAAA,MAAA,IAAW,CAAC,KAAA,CAAM,QAAA,IAAY,WAAW,IAAA,EAAM;AAC7C,MAAA,KAAA,CAAM,cAAA,EAAe;AACrB,MAAA,KAAA,EAAO,KAAA,EAAM;AAAA,IACf;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAaA,kBAAA,GAA2B;AACzB,IAAA,MAAM,SAAA,GAAY,KAAK,aAAA,EAAc;AACrC,IAAA,IAAA,CAAK,mBAAmB,EAAC;AACzB,IAAA,KAAA,IAAS,IAAA,GAAoB,SAAA,EAAW,IAAA,KAAS,QAAA,CAAS,IAAA,IAAQ;AAChE,MAAA,MAAM,SAAS,IAAA,CAAK,aAAA;AACpB,MAAA,IAAI,CAAC,MAAA,EAAQ;AACb,MAAA,KAAA,MAAW,OAAA,IAAW,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,QAAQ,CAAA,EAAG;AACjD,QAAA,IAAI,EAAE,mBAAmB,WAAA,CAAA,EAAc;AACvC,QAAA,IAAI,OAAA,KAAY,IAAA,IAAQ,OAAA,CAAQ,KAAA,EAAO;AACvC,QAAA,OAAA,CAAQ,KAAA,GAAQ,IAAA;AAChB,QAAA,IAAA,CAAK,gBAAA,CAAiB,KAAK,OAAO,CAAA;AAAA,MACpC;AACA,MAAA,IAAA,GAAO,MAAA;AAAA,IACT;AAAA,EACF;AAAA;AAAA,EAGA,kBAAA,GAA2B;AACzB,IAAA,KAAA,MAAW,OAAA,IAAW,KAAK,gBAAA,EAAkB;AAC3C,MAAA,OAAA,CAAQ,KAAA,GAAQ,KAAA;AAAA,IAClB;AACA,IAAA,IAAA,CAAK,mBAAmB,EAAC;AAAA,EAC3B;AAAA;AAAA,EAGA,aAAA,GAAsB;AACpB,IAAA,MAAM,SAAA,GAAY,IAAA,CAAK,QAAA,CAAS,YAAA,IAAe;AAC/C,IAAA,IAAI,SAAA,EAAW;AACb,MAAA,SAAA,CAAU,KAAA,EAAM;AAChB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,kBAAA,EAAmB;AAC1C,IAAA,IAAI,SAAA,CAAU,CAAC,CAAA,EAAG;AAChB,MAAA,SAAA,CAAU,CAAC,EAAE,KAAA,EAAM;AACnB,MAAA;AAAA,IACF;AACA,IAAA,MAAM,SAAA,GAAY,KAAK,aAAA,EAAc;AACrC,IAAA,SAAA,CAAU,QAAA,GAAW,EAAA;AACrB,IAAA,SAAA,CAAU,KAAA,EAAM;AAAA,EAClB;AAAA;AAAA,EAGA,kBAAA,GAAoC;AAClC,IAAA,OAAO,cAAA,CAAe,IAAA,CAAK,aAAA,EAAe,CAAA;AAAA,EAC5C;AACF,CAAA;;;AC9OO,IAAM,gBAAA,GAAN,cAA+B,UAAA,CAAwB;AAAA,EAC5D,OAAgB,OAAA,GAAU,CAAC,SAAA,EAAW,QAAQ,CAAA;AAAA,EAC9C,OAAO,OAAA,GAAU,CAAC,OAAA,EAAS,mBAAmB,MAAM,CAAA;AAAA;AAAA,EAQ3C,KAAA,GAAQ,IAAI,SAAA,CAAU,MAAM,KAAK,YAAA,EAAc;AAAA,IACtD,QAAA,EAAU,MAAM,IAAA,CAAK,KAAA,EAAM;AAAA,IAC3B,aAAA,EAAe,MAAO,IAAA,CAAK,gBAAA,GAAmB,KAAK,aAAA,GAAgB;AAAA,GACpE,CAAA;AAAA;AAAA,EAGQ,OAAA,GAAgB;AACvB,IAAA,IAAI,IAAA,CAAK,eAAA,EAAiB,IAAA,CAAK,YAAA,CAAa,MAAA,GAAS,IAAA;AAAA,EACvD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQS,UAAA,GAAmB;AAC1B,IAAA,IAAA,CAAK,KAAA,CAAM,UAAA,CAAW,EAAE,YAAA,EAAc,OAAO,CAAA;AAAA,EAC/C;AAAA;AAAA,EAGA,IAAA,GAAa;AACX,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,IAAA,CAAK,OAAA,EAAS;AAC3C,IAAA,IAAA,CAAK,aAAa,MAAA,GAAS,KAAA;AAC3B,IAAA,IAAA,CAAK,MAAM,QAAA,EAAS;AAAA,EACtB;AAAA;AAAA,EAGA,KAAA,GAAc;AACZ,IAAA,IAAI,CAAC,IAAA,CAAK,eAAA,IAAmB,CAAC,KAAK,OAAA,EAAS;AAC5C,IAAA,IAAA,CAAK,aAAa,MAAA,GAAS,IAAA;AAC3B,IAAA,IAAA,CAAK,MAAM,UAAA,EAAW;AAAA,EACxB;AAAA;AAAA,EAGA,gBAAgB,KAAA,EAAyB;AACvC,IAAA,IAAI,KAAA,CAAM,MAAA,KAAW,IAAA,CAAK,YAAA,OAAmB,KAAA,EAAM;AAAA,EACrD;AAAA;AAAA,EAGA,IAAI,OAAA,GAAmB;AACrB,IAAA,OAAO,IAAA,CAAK,eAAA,IAAmB,CAAC,IAAA,CAAK,YAAA,CAAa,MAAA;AAAA,EACpD;AACF","file":"dialog_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the three conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\nimport { EscapeLayer } from \"./escape_layer\";\nimport { tabStopsWithin } from \"./focus_candidate\";\n\n/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / confirm / drawer / command-palette / sidebar); the\n * non-modal focus scope (focus) reuses it with the modal side effects opted out.\n *\n * The WAI-ARIA APG modal pattern is more than \"cycle Tab inside a box\": a modal\n * also locks background scroll, makes the rest of the page `inert` (so assistive\n * technology and pointer/Tab cannot reach it, honoring `aria-modal=\"true\"`),\n * sends focus inside on open, and restores it to the opener on close — and every\n * one of those side effects must be reverted if the element is torn down while\n * open (a Turbo navigation mid-dialog) and kept out of the snapshot Turbo\n * caches (`turbo:before-cache`). {@link FocusTrap} owns that whole modal\n * lifecycle so each controller only decides *when* to open/close and *what*\n * \"close\" means.\n *\n * It is intentionally **policy-free about closing**. Escape semantics differ per\n * widget (a plain dialog just closes; an alert-dialog closes *as a cancel* with a\n * reason; a drawer runs an exit transition), so the trap merely forwards Escape\n * to an {@link FocusTrapOptions.onEscape | onEscape} callback and never decides on\n * its own what closing entails.\n *\n * @remarks\n * The container is read through a getter so a controller can hand over a Stimulus\n * target without worrying about when the trap instance is constructed relative to\n * `connect()`.\n */\n\n/** Behavior hooks a controller supplies when constructing a {@link FocusTrap}. */\nexport interface FocusTrapOptions {\n /**\n * Called when `Escape` is pressed while the trap is active. When omitted,\n * `Escape` is left alone (the trap never joins the Escape stack). Dismissal\n * is resolved by the shared {@link EscapeLayer}: an Escape already consumed\n * by an inner handler is ignored, and among active layers the most recently\n * activated claiming one owns the press. The resolver consumes the event\n * before invoking the callback.\n */\n onEscape?: () => void;\n /**\n * Returns the element to focus when the trap activates. When it returns `null`\n * (or is omitted), the first focusable descendant is used, falling back to the\n * container itself (made programmatically focusable with `tabindex=-1`).\n */\n initialFocus?: () => HTMLElement | null;\n /**\n * Returns the element to focus on deactivation when nothing was focused before\n * the trap opened (e.g. the trigger). The element focused *before* opening\n * always takes precedence.\n */\n fallbackFocus?: () => HTMLElement | null;\n /**\n * Lock background scroll (`body` overflow) while active. Defaults to `true` for\n * the modal overlays; a lighter focus scope passes `false`. Read on `activate`.\n */\n lockScroll?: boolean | (() => boolean);\n /**\n * Make background siblings `inert` while active (the `aria-modal` isolation).\n * Defaults to `true` for the modal overlays; a soft focus scope can opt out so\n * the background stays reachable while `Tab` still cycles inside. Read on `activate`.\n */\n isolate?: boolean | (() => boolean);\n /**\n * Move focus inside on `activate`. Defaults to `true`; a focus scope that only\n * wants the `Tab` boundary (no focus move) passes `false`. Read on `activate`.\n */\n autoFocus?: boolean | (() => boolean);\n}\n\n/**\n * Owns the modal side effects (scroll lock, background `inert`, focus trap, focus\n * restore) for a single container, applied on {@link activate} and reverted on\n * {@link deactivate}.\n */\nexport class FocusTrap {\n /** The element focused before activation, restored on deactivation. */\n #previouslyFocused: HTMLElement | null = null;\n /** The body's inline `overflow` before locking, restored on deactivation. */\n #previousBodyOverflow = \"\";\n /** Whether scroll was locked this activation (so it is only restored if applied). */\n #scrollLocked = false;\n /** Background siblings made `inert` while active, restored on deactivation. */\n #inertedSiblings: HTMLElement[] = [];\n /** Whether the modal side effects are currently applied. */\n #activeState = false;\n /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Returns the trapped element; called on every operation for the live target. */\n readonly #getContainer: () => HTMLElement;\n /** Closing/focus hooks; see {@link FocusTrapOptions}. */\n readonly #options: FocusTrapOptions;\n\n /**\n * @param getContainer - Returns the trapped element. Called on every operation\n * so the live target is always used.\n * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.\n */\n constructor(getContainer: () => HTMLElement, options: FocusTrapOptions = {}) {\n this.#getContainer = getContainer;\n this.#options = options;\n }\n\n /** Whether the trap is currently active. */\n get active(): boolean {\n return this.#activeState;\n }\n\n /**\n * Applies the trap: records the current focus, optionally locks background scroll\n * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless\n * `autoFocus` is off) moves focus inside. No-ops if already active.\n */\n activate(): void {\n if (this.#activeState) return;\n this.#activeState = true;\n // Record the opener so it can be refocused on close. `<body>` (the default\n // active element when nothing is focused) is treated as \"nothing\", so the\n // fallback target — typically the trigger — wins in that case.\n const active = document.activeElement;\n this.#previouslyFocused =\n active instanceof HTMLElement && active !== document.body ? active : null;\n if (this.#flag(this.#options.lockScroll, true)) {\n this.#previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n this.#scrollLocked = true;\n }\n if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();\n document.addEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.activate();\n const onEscape = this.#options.onEscape;\n if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });\n if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();\n }\n\n /**\n * Reverts every side effect applied by {@link activate}. No-ops if inactive, so\n * a controller can call it defensively from both `close()` and `disconnect()`.\n *\n * @param restoreFocus - Move focus back to the opener (default `true`). Pass\n * `false` on teardown (`disconnect`), where yanking focus is undesirable.\n */\n deactivate({ restoreFocus = true }: { restoreFocus?: boolean } = {}): void {\n if (!this.#activeState) return;\n this.#activeState = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.deactivate();\n if (this.#scrollLocked) {\n document.body.style.overflow = this.#previousBodyOverflow;\n this.#scrollLocked = false;\n }\n this.#releaseBackground();\n if (restoreFocus) {\n const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;\n target?.focus();\n }\n }\n\n /** Resolves a boolean-or-getter option, defaulting when it was not provided. */\n #flag(option: boolean | (() => boolean) | undefined, fallback: boolean): boolean {\n if (option === undefined) return fallback;\n return typeof option === \"function\" ? option() : option;\n }\n\n /**\n * Reverts the side effects just before Turbo caches the page snapshot, so an\n * overlay left open does not bake the scroll lock into `body[style]` — a\n * restored page would feed that locked value back into {@link activate} as the\n * baseline, and closing would then never unlock the page. Markup state stays\n * untouched (restore-open designs reopen against a clean baseline), and focus\n * is left alone mid-navigation. The listener lives only while active.\n */\n readonly #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));\n\n /**\n * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the\n * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of\n * which layer currently owns Escape.\n */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Tab\") this.#trapTab(event);\n };\n\n /** Keeps `Tab` focus cycling within the container's focusable elements. */\n #trapTab(event: KeyboardEvent): void {\n const focusable = this.#focusableElements();\n if (focusable.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n\n // If focus has somehow escaped the container, pull it back to the first item.\n if (!(active instanceof Node) || !this.#getContainer().contains(active)) {\n event.preventDefault();\n first?.focus();\n return;\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first?.focus();\n }\n }\n\n /**\n * Marks every element outside the container's subtree as `inert` so background\n * content cannot be focused or reached by assistive technology, honoring the\n * `aria-modal=\"true\"` contract. An element that was *already* `inert` is left\n * untracked so `#releaseBackground` does not wrongly clear it.\n *\n * The walk climbs from the container to `body` and inerts each ancestor's other\n * children. Scanning only `body`'s children would skip the branch the container\n * sits in — everything beside it inside that branch is background too, and a\n * nested container is the ordinary case.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (let node: HTMLElement = container; node !== document.body; ) {\n const parent = node.parentElement;\n if (!parent) break;\n for (const sibling of Array.from(parent.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling === node || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n node = parent;\n }\n }\n\n /** Reverts the `inert` flags applied by `#isolateBackground`. */\n #releaseBackground(): void {\n for (const sibling of this.#inertedSiblings) {\n sibling.inert = false;\n }\n this.#inertedSiblings = [];\n }\n\n /** Moves focus to the initial target, the first focusable, or the container. */\n #focusInitial(): void {\n const preferred = this.#options.initialFocus?.();\n if (preferred) {\n preferred.focus();\n return;\n }\n const focusable = this.#focusableElements();\n if (focusable[0]) {\n focusable[0].focus();\n return;\n }\n const container = this.#getContainer();\n container.tabIndex = -1;\n container.focus();\n }\n\n /** Collects the container's currently focusable descendants in DOM order. */\n #focusableElements(): HTMLElement[] {\n return tabStopsWithin(this.#getContainer());\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/**\n * Headless, accessible modal dialog behavior.\n *\n * Markup contract (identifier: `stimeo--dialog`):\n * <div data-controller=\"stimeo--dialog\">\n * <button data-stimeo--dialog-target=\"trigger\"\n * data-action=\"stimeo--dialog#open\">Open</button>\n * <div data-stimeo--dialog-target=\"dialog\" role=\"dialog\" aria-modal=\"true\"\n * aria-labelledby=\"title\" hidden>\n * <h2 id=\"title\">…</h2>\n * <button data-action=\"stimeo--dialog#close\">Close</button>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog (Modal)** pattern: focus moves into the\n * dialog on open and is trapped within it, `Escape` closes it, background scroll\n * is locked, and focus returns to the trigger on close.\n *\n * @remarks\n * Behavior only. The sole visual side effect is locking `document.body`'s scroll\n * while open (the minimum required by the pattern); all other styling is the\n * consumer's. Clicking the dialog backdrop (the dialog target itself, outside\n * its content) also closes it.\n *\n * The modal lifecycle — focus trap, scroll lock, background `inert`, focus\n * restore, and teardown reversal — is delegated to the shared {@link FocusTrap}\n * primitive (also used by alert-dialog and drawer). This controller only owns\n * *when* to open/close and the dialog-specific backdrop click.\n */\nexport class DialogController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"dialog\"];\n static actions = [\"close\", \"closeOnBackdrop\", \"open\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly dialogTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasDialogTarget: boolean;\n\n /** Owns the modal side effects; Escape closes, focus falls back to the trigger. */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.close(),\n fallbackFocus: () => (this.hasTriggerTarget ? this.triggerTarget : null),\n });\n\n /** Starts closed (idempotently reflects the closed state on the markup). */\n override connect(): void {\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n }\n\n /**\n * Reverts the modal side effects (scroll lock, background `inert`, keydown\n * listener) if the controller is torn down while open (e.g. a Turbo navigation\n * replaces the page while the dialog is showing). Focus is not restored on\n * teardown.\n */\n override disconnect(): void {\n this.#trap.deactivate({ restoreFocus: false });\n }\n\n /** Opens the dialog, traps focus, and locks background scroll. */\n open(): void {\n if (!this.hasDialogTarget || this.#isOpen) return;\n this.dialogTarget.hidden = false;\n this.#trap.activate();\n }\n\n /** Closes the dialog, restores scroll, and returns focus to the opener. */\n close(): void {\n if (!this.hasDialogTarget || !this.#isOpen) return;\n this.dialogTarget.hidden = true;\n this.#trap.deactivate();\n }\n\n /** Closes when the backdrop (the dialog target itself) is clicked. */\n closeOnBackdrop(event: MouseEvent): void {\n if (event.target === this.dialogTarget) this.close();\n }\n\n /** Whether the dialog is currently visible. */\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/before_cache_reset.ts","../../src/utils/escape_layer.ts","../../src/utils/focus_candidate.ts","../../src/utils/focus_trap.ts","../../src/controllers/dialog_controller.ts"],"names":[],"mappings":"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","file":"dialog_controller.js","sourcesContent":["/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Single resolver for layered Escape dismissal.\n *\n * Every Escape-dismissable overlay layer (modal focus traps, disclosure\n * overlays like dropdown/popover/menu, and hover-triggered transient layers)\n * registers here while it is open. One document-level listener per document\n * resolves each press to exactly one owner and invokes that layer's\n * {@link EscapeLayerOptions.onDismiss} — controllers never listen for a\n * dismissing Escape themselves.\n *\n * @remarks\n * Each document owns an activation-ordered stack. The owner of a press is the\n * **topmost layer whose {@link EscapeLayerOptions.claims} passes**; a layer\n * that declines is transparent, so a background overlay opened behind a modal\n * never blocks it. Because a layer opened from within another is necessarily\n * activated later, LIFO order is also inner-first for nested layers — no DOM\n * inspection is needed.\n *\n * The shared listener runs in the document bubble phase and honors\n * `event.defaultPrevented`, so an element-level widget handler that consumes\n * Escape first (an editor cancelling its edit, a combobox closing its list)\n * always wins over every registered layer — the deepest handler resolves the\n * press. A keydown that is part of an IME composition (`event.isComposing`)\n * cancels the composition, never a layer, and is ignored here for every layer\n * at once.\n *\n * A WeakMap keeps documents collectible; the listener is installed only while\n * a document's stack is non-empty, and controller lifecycle hooks guarantee\n * that disconnected layers never remain registered.\n */\n\n/** Behavior a layer registers when it activates. */\nexport interface EscapeLayerOptions {\n /**\n * Dismisses the layer. Called by the shared resolver when this layer owns a\n * press; the resolver has already consumed the event (`preventDefault()`),\n * so the callback only needs to close and place focus per the widget's\n * contract.\n */\n onDismiss: () => void;\n /**\n * Whether the layer claims the current press. Evaluated per press, so it can\n * depend on live state (e.g. \"focus is inside me or fell to the body\"). A\n * declining layer is skipped and the next layer down is consulted; omitting\n * it means the layer always claims while active.\n */\n claims?: () => boolean;\n}\n\n/** A document's stack plus the one shared listener bound to it. */\ninterface EscapeLayerRegistry {\n stack: EscapeLayer[];\n onKeydown: (event: KeyboardEvent) => void;\n}\n\n/**\n * Claims predicate shared by the click-opened disclosure overlays (dropdown /\n * popover / navigation-menu / menu / context-menu / menubar): the layer claims\n * a press while focus is inside `element`, or after focus fell to the body —\n * a click on non-focusable overlay content blurs to `<body>`, and Escape must\n * still close the overlay (the \"body-focus rescue\"). A press made after focus\n * moved to another interactive element is declined, so closing never yanks\n * focus away from where the user deliberately went.\n */\nexport function claimsWhileFocusWithin(element: Element): () => boolean {\n return () => {\n const active = element.ownerDocument.activeElement;\n return active === null || active === element.ownerDocument.body || element.contains(active);\n };\n}\n\nexport class EscapeLayer {\n static readonly #registries = new WeakMap<Document, EscapeLayerRegistry>();\n\n #ownerDocument: Document | null = null;\n /** Dismissal callback while active; `null` when inactive. */\n #onDismiss: (() => void) | null = null;\n /** Live predicate deciding whether the layer claims a press; `null` = always. */\n #claims: (() => boolean) | null = null;\n\n /**\n * Activates this layer at the top of its document's Escape stack, installing\n * the document's shared resolver listener if this is its first layer.\n * Re-activating an already-active layer moves it to the top.\n */\n activate(ownerDocument: Document = document, options: EscapeLayerOptions): void {\n this.deactivate();\n let registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) {\n registry = EscapeLayer.#createRegistry();\n EscapeLayer.#registries.set(ownerDocument, registry);\n ownerDocument.addEventListener(\"keydown\", registry.onKeydown);\n }\n registry.stack.push(this);\n this.#ownerDocument = ownerDocument;\n this.#onDismiss = options.onDismiss;\n this.#claims = options.claims ?? null;\n }\n\n /**\n * Removes this layer from its document's Escape stack, uninstalling the\n * shared listener when the stack empties. Safe to call when inactive.\n */\n deactivate(): void {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return;\n\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (registry) {\n const index = registry.stack.lastIndexOf(this);\n if (index >= 0) registry.stack.splice(index, 1);\n if (registry.stack.length === 0) {\n ownerDocument.removeEventListener(\"keydown\", registry.onKeydown);\n EscapeLayer.#registries.delete(ownerDocument);\n }\n }\n this.#ownerDocument = null;\n this.#onDismiss = null;\n this.#claims = null;\n }\n\n /**\n * Whether this active layer would own a press right now: it is the topmost\n * layer whose {@link EscapeLayerOptions.claims} passes. Exposed for tests\n * and diagnostics — production dismissal goes through the shared listener.\n */\n get ownsEscape(): boolean {\n const ownerDocument = this.#ownerDocument;\n if (!ownerDocument) return false;\n const registry = EscapeLayer.#registries.get(ownerDocument);\n if (!registry) return false;\n return EscapeLayer.#resolveOwner(registry.stack) === this;\n }\n\n /** Builds a document's registry with its shared resolver listener. */\n static #createRegistry(): EscapeLayerRegistry {\n const registry: EscapeLayerRegistry = {\n stack: [],\n onKeydown: (event: KeyboardEvent): void => {\n if (event.key !== \"Escape\" || event.defaultPrevented || event.isComposing) return;\n const owner = EscapeLayer.#resolveOwner(registry.stack);\n if (!owner) return;\n event.preventDefault();\n owner.#onDismiss?.();\n },\n };\n return registry;\n }\n\n /** The topmost stack layer whose claims predicate passes, or `null`. */\n static #resolveOwner(stack: EscapeLayer[]): EscapeLayer | null {\n for (let index = stack.length - 1; index >= 0; index--) {\n const layer = stack[index];\n if (!layer) continue;\n if (layer.#claims && !layer.#claims()) continue;\n return layer;\n }\n return null;\n }\n}\n","/**\n * Whether a disabled `<fieldset>` ancestor actually reaches `control`.\n *\n * HTML exempts the contents of a fieldset's **first direct-child `<legend>`**, and\n * the exemption is per fieldset — a control legal in one legend can still be\n * disabled by a second, outer fieldset, so the walk continues upward.\n *\n * Exported on its own for callers that need the disabled-inheritance rule without\n * the rest of {@link canTakeFocus} — a control's availability check, or a `hidden`\n * walk bounded at a widget root, which is a different rule to compose with.\n */\nexport function inheritsFieldsetDisabled(control: HTMLElement): boolean {\n let fieldset: Element | null = control.closest(\"fieldset[disabled]\");\n while (fieldset) {\n const legend = Array.from(fieldset.children).find((child) => child.tagName === \"LEGEND\");\n if (!legend?.contains(control)) return true;\n fieldset = fieldset.parentElement?.closest(\"fieldset[disabled]\") ?? null;\n }\n return false;\n}\n\n/**\n * Whether an element can actually take focus, checked **before** `focus()` runs.\n *\n * A controller that must move focus off a control it is about to disable or hide\n * picks a destination and calls `focus()` on it. If that destination cannot take\n * focus, the call **fails silently**: `hidden` and natively `disabled` elements\n * swallow it, so the caret stays in the subtree that is disappearing and lands on\n * `<body>` a frame later — the exact outcome the rescue exists to prevent, minus\n * any signal that it happened.\n *\n * Testing after the fact is the obvious alternative and is deliberately not the\n * rule. Reading `document.activeElement` back only works in a real browser, and\n * looping over candidates that way performs a real focus move per failure —\n * observable to assistive technology. Checking first costs nothing and catches\n * the cases that actually occur.\n *\n * **`aria-disabled` is not disqualifying.** It is the attribute an author uses\n * for a control that must stay *discoverable*, and the roving contract keeps\n * such items reachable. Only the four conditions that make the platform refuse\n * focus are checked: `hidden` / `inert` (an ancestor's counts),\n * `input[type=\"hidden\"]`, the native `disabled` property, and `disabled`\n * inherited from an ancestor `fieldset`. CSS-only invisibility is handled by\n * {@link isRenderedForFocus} when a consumer needs sequential-focus semantics.\n *\n * Reading `:disabled` instead of walking the fieldset chain would be shorter, but\n * that pseudo-class is not evaluated consistently outside real browsers and this\n * has to be right headlessly too. happy-dom in particular focuses a `<button>`\n * inside a disabled fieldset where a real engine refuses, so the inheritance is\n * spelled out rather than delegated.\n *\n * What a consumer does when nothing survives is its own call: some fall back to\n * their landmark, while a widget whose caret already sits somewhere legitimate\n * refuses the move outright rather than relocating it.\n *\n * @example\n * ```ts\n * const target = candidates.find(canTakeFocus);\n * if (target) target.focus();\n * else {\n * this.#tabindex.lend(this.element); // nothing left: fall back to the landmark\n * this.element.focus();\n * }\n * ```\n *\n * @param element - the candidate destination\n */\nexport function canTakeFocus(element: HTMLElement): boolean {\n if (element.closest(\"[hidden], [inert]\")) return false;\n if (element instanceof HTMLInputElement && element.type === \"hidden\") return false;\n if (!(\"disabled\" in element)) return true;\n if ((element as HTMLElement & { disabled: boolean }).disabled) return false;\n return !inheritsFieldsetDisabled(element);\n}\n\n/** Elements whose semantics or authored attributes can place them in sequential focus order. */\nexport const TAB_STOP_CANDIDATE_SELECTOR = [\n \"a[href]\",\n \"area[href]\",\n \"button\",\n \"input\",\n \"select\",\n \"textarea\",\n \"summary\",\n \"iframe\",\n \"audio[controls]\",\n \"video[controls]\",\n \"[tabindex]\",\n \"[contenteditable]\",\n].join(\",\");\n\n/** Optional browser visibility API used to exclude CSS-hidden candidates. */\ninterface VisibilityCheckable {\n checkVisibility?: (options?: { visibilityProperty?: boolean }) => boolean;\n}\n\n/** Whether CSS visibility allows an otherwise eligible element to participate in focus order. */\nexport function isRenderedForFocus(element: HTMLElement): boolean {\n const check = (element as HTMLElement & VisibilityCheckable).checkVisibility;\n return typeof check === \"function\" ? check.call(element, { visibilityProperty: true }) : true;\n}\n\n/** Parses an authored `tabindex`; invalid syntax has no explicit focus-order meaning. */\nfunction authoredTabindex(element: HTMLElement): number | null {\n const value = element.getAttribute(\"tabindex\");\n if (value === null || !/^[+-]?\\d+$/.test(value.trim())) return null;\n return Number(value);\n}\n\n/** Whether the element's native semantics place it in sequential focus order. */\nfunction hasNativeTabStop(element: HTMLElement): boolean {\n if (element instanceof HTMLAnchorElement || element instanceof HTMLAreaElement) {\n return element.hasAttribute(\"href\");\n }\n if (\n element instanceof HTMLButtonElement ||\n element instanceof HTMLSelectElement ||\n element instanceof HTMLTextAreaElement\n ) {\n return true;\n }\n if (element instanceof HTMLInputElement) return element.type !== \"hidden\";\n if (element instanceof HTMLIFrameElement) return true;\n if (element.tagName === \"AUDIO\" || element.tagName === \"VIDEO\") {\n return element.hasAttribute(\"controls\");\n }\n if (element instanceof HTMLElement && element.tagName === \"SUMMARY\") {\n const details = element.parentElement;\n return (\n details instanceof HTMLDetailsElement &&\n Array.from(details.children).find((child) => child.tagName === \"SUMMARY\") === element\n );\n }\n return false;\n}\n\n/** Whether an explicit `contenteditable` value creates an editable tab stop. */\nfunction hasEditableTabStop(element: HTMLElement): boolean {\n const value = element.getAttribute(\"contenteditable\")?.toLowerCase();\n return value === \"\" || value === \"true\" || value === \"plaintext-only\";\n}\n\n/**\n * Whether an element is a usable sequential Tab stop right now.\n *\n * Native semantics, authored `tabindex`, editable hosts, inherited disabled state,\n * HTML `hidden`/`inert`, and CSS visibility are evaluated together. `aria-disabled`\n * remains focusable because it communicates unavailability without removing the\n * control from discovery order.\n */\nexport function isTabStop(element: HTMLElement): boolean {\n if (!canTakeFocus(element) || !isRenderedForFocus(element)) return false;\n\n const tabindex = authoredTabindex(element);\n if (tabindex !== null) return tabindex >= 0;\n return hasNativeTabStop(element) || hasEditableTabStop(element);\n}\n\n/** Returns every usable sequential Tab stop below `root` in document order. */\nexport function tabStopsWithin(root: ParentNode): HTMLElement[] {\n return Array.from(root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)).filter(\n isTabStop,\n );\n}\n\n/** Returns the first usable sequential Tab stop below `root`, if one exists. */\nexport function firstTabStop(root: ParentNode): HTMLElement | null {\n for (const candidate of root.querySelectorAll<HTMLElement>(TAB_STOP_CANDIDATE_SELECTOR)) {\n if (isTabStop(candidate)) return candidate;\n }\n return null;\n}\n\n/** Whether `root` contains at least one usable sequential Tab stop. */\nexport function hasTabStop(root: ParentNode): boolean {\n return firstTabStop(root) !== null;\n}\n","import { BeforeCacheReset } from \"./before_cache_reset\";\nimport { EscapeLayer } from \"./escape_layer\";\nimport { tabStopsWithin } from \"./focus_candidate\";\n\n/**\n * Modal focus-trap primitive shared by the modal-overlay controllers\n * (dialog / alert-dialog / confirm / drawer / command-palette / sidebar); the\n * non-modal focus scope (focus) reuses it with the modal side effects opted out.\n *\n * The WAI-ARIA APG modal pattern is more than \"cycle Tab inside a box\": a modal\n * also locks background scroll, makes the rest of the page `inert` (so assistive\n * technology and pointer/Tab cannot reach it, honoring `aria-modal=\"true\"`),\n * sends focus inside on open, and restores it to the opener on close — and every\n * one of those side effects must be reverted if the element is torn down while\n * open (a Turbo navigation mid-dialog) and kept out of the snapshot Turbo\n * caches (`turbo:before-cache`). {@link FocusTrap} owns that whole modal\n * lifecycle so each controller only decides *when* to open/close and *what*\n * \"close\" means.\n *\n * It is intentionally **policy-free about closing**. Escape semantics differ per\n * widget (a plain dialog just closes; an alert-dialog closes *as a cancel* with a\n * reason; a drawer runs an exit transition), so the trap merely forwards Escape\n * to an {@link FocusTrapOptions.onEscape | onEscape} callback and never decides on\n * its own what closing entails.\n *\n * @remarks\n * The container is read through a getter so a controller can hand over a Stimulus\n * target without worrying about when the trap instance is constructed relative to\n * `connect()`.\n */\n\n/** Behavior hooks a controller supplies when constructing a {@link FocusTrap}. */\nexport interface FocusTrapOptions {\n /**\n * Called when `Escape` is pressed while the trap is active. When omitted,\n * `Escape` is left alone (the trap never joins the Escape stack). Dismissal\n * is resolved by the shared {@link EscapeLayer}: an Escape already consumed\n * by an inner handler is ignored, and among active layers the most recently\n * activated claiming one owns the press. The resolver consumes the event\n * before invoking the callback.\n */\n onEscape?: () => void;\n /**\n * Returns the element to focus when the trap activates. When it returns `null`\n * (or is omitted), the first focusable descendant is used, falling back to the\n * container itself (made programmatically focusable with `tabindex=-1`).\n */\n initialFocus?: () => HTMLElement | null;\n /**\n * Returns the element to focus on deactivation when nothing was focused before\n * the trap opened (e.g. the trigger). The element focused *before* opening\n * always takes precedence.\n */\n fallbackFocus?: () => HTMLElement | null;\n /**\n * Lock background scroll (`body` overflow) while active. Defaults to `true` for\n * the modal overlays; a lighter focus scope passes `false`. Read on `activate`.\n */\n lockScroll?: boolean | (() => boolean);\n /**\n * Make background siblings `inert` while active (the `aria-modal` isolation).\n * Defaults to `true` for the modal overlays; a soft focus scope can opt out so\n * the background stays reachable while `Tab` still cycles inside. Read on `activate`.\n */\n isolate?: boolean | (() => boolean);\n /**\n * Move focus inside on `activate`. Defaults to `true`; a focus scope that only\n * wants the `Tab` boundary (no focus move) passes `false`. Read on `activate`.\n */\n autoFocus?: boolean | (() => boolean);\n}\n\n/**\n * Owns the modal side effects (scroll lock, background `inert`, focus trap, focus\n * restore) for a single container, applied on {@link activate} and reverted on\n * {@link deactivate}.\n */\nexport class FocusTrap {\n /** The element focused before activation, restored on deactivation. */\n #previouslyFocused: HTMLElement | null = null;\n /** The body's inline `overflow` before locking, restored on deactivation. */\n #previousBodyOverflow = \"\";\n /** Whether scroll was locked this activation (so it is only restored if applied). */\n #scrollLocked = false;\n /** Background siblings made `inert` while active, restored on deactivation. */\n #inertedSiblings: HTMLElement[] = [];\n /** Whether the modal side effects are currently applied. */\n #activeState = false;\n /** Registers the trap on the shared Escape stack while active (see {@link EscapeLayer}). */\n readonly #escapeLayer = new EscapeLayer();\n\n /** Returns the trapped element; called on every operation for the live target. */\n readonly #getContainer: () => HTMLElement;\n /** Closing/focus hooks; see {@link FocusTrapOptions}. */\n readonly #options: FocusTrapOptions;\n\n /**\n * @param getContainer - Returns the trapped element. Called on every operation\n * so the live target is always used.\n * @param options - Closing/focus hooks; see {@link FocusTrapOptions}.\n */\n constructor(getContainer: () => HTMLElement, options: FocusTrapOptions = {}) {\n this.#getContainer = getContainer;\n this.#options = options;\n }\n\n /** Whether the trap is currently active. */\n get active(): boolean {\n return this.#activeState;\n }\n\n /**\n * Applies the trap: records the current focus, optionally locks background scroll\n * and makes background siblings `inert`, listens for `Tab`/`Escape`, and (unless\n * `autoFocus` is off) moves focus inside. No-ops if already active.\n */\n activate(): void {\n if (this.#activeState) return;\n this.#activeState = true;\n // Record the opener so it can be refocused on close. `<body>` (the default\n // active element when nothing is focused) is treated as \"nothing\", so the\n // fallback target — typically the trigger — wins in that case.\n const active = document.activeElement;\n this.#previouslyFocused =\n active instanceof HTMLElement && active !== document.body ? active : null;\n if (this.#flag(this.#options.lockScroll, true)) {\n this.#previousBodyOverflow = document.body.style.overflow;\n document.body.style.overflow = \"hidden\";\n this.#scrollLocked = true;\n }\n if (this.#flag(this.#options.isolate, true)) this.#isolateBackground();\n document.addEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.activate();\n const onEscape = this.#options.onEscape;\n if (onEscape) this.#escapeLayer.activate(document, { onDismiss: () => onEscape() });\n if (this.#flag(this.#options.autoFocus, true)) this.#focusInitial();\n }\n\n /**\n * Reverts every side effect applied by {@link activate}. No-ops if inactive, so\n * a controller can call it defensively from both `close()` and `disconnect()`.\n *\n * @param restoreFocus - Move focus back to the opener (default `true`). Pass\n * `false` on teardown (`disconnect`), where yanking focus is undesirable.\n */\n deactivate({ restoreFocus = true }: { restoreFocus?: boolean } = {}): void {\n if (!this.#activeState) return;\n this.#activeState = false;\n this.#escapeLayer.deactivate();\n document.removeEventListener(\"keydown\", this.#onKeydown);\n this.#beforeCache.deactivate();\n if (this.#scrollLocked) {\n document.body.style.overflow = this.#previousBodyOverflow;\n this.#scrollLocked = false;\n }\n this.#releaseBackground();\n if (restoreFocus) {\n const target = this.#previouslyFocused ?? this.#options.fallbackFocus?.() ?? null;\n target?.focus();\n }\n }\n\n /** Resolves a boolean-or-getter option, defaulting when it was not provided. */\n #flag(option: boolean | (() => boolean) | undefined, fallback: boolean): boolean {\n if (option === undefined) return fallback;\n return typeof option === \"function\" ? option() : option;\n }\n\n /**\n * Reverts the side effects just before Turbo caches the page snapshot, so an\n * overlay left open does not bake the scroll lock into `body[style]` — a\n * restored page would feed that locked value back into {@link activate} as the\n * baseline, and closing would then never unlock the page. Markup state stays\n * untouched (restore-open designs reopen against a clean baseline), and focus\n * is left alone mid-navigation. The listener lives only while active.\n */\n readonly #beforeCache = new BeforeCacheReset(() => this.deactivate({ restoreFocus: false }));\n\n /**\n * Handles `Tab` (focus trap) while active. `Escape` dismissal is owned by the\n * shared {@link EscapeLayer} resolver, so Tab trapping stays independent of\n * which layer currently owns Escape.\n */\n readonly #onKeydown = (event: KeyboardEvent): void => {\n if (event.key === \"Tab\") this.#trapTab(event);\n };\n\n /** Keeps `Tab` focus cycling within the container's focusable elements. */\n #trapTab(event: KeyboardEvent): void {\n const focusable = this.#focusableElements();\n if (focusable.length === 0) {\n event.preventDefault();\n return;\n }\n const first = focusable[0];\n const last = focusable[focusable.length - 1];\n const active = document.activeElement;\n\n // If focus has somehow escaped the container, pull it back to the first item.\n if (!(active instanceof Node) || !this.#getContainer().contains(active)) {\n event.preventDefault();\n first?.focus();\n return;\n }\n\n if (event.shiftKey && active === first) {\n event.preventDefault();\n last?.focus();\n } else if (!event.shiftKey && active === last) {\n event.preventDefault();\n first?.focus();\n }\n }\n\n /**\n * Marks every element outside the container's subtree as `inert` so background\n * content cannot be focused or reached by assistive technology, honoring the\n * `aria-modal=\"true\"` contract. An element that was *already* `inert` is left\n * untracked so `#releaseBackground` does not wrongly clear it.\n *\n * The walk climbs from the container to `body` and inerts each ancestor's other\n * children. Scanning only `body`'s children would skip the branch the container\n * sits in — everything beside it inside that branch is background too, and a\n * nested container is the ordinary case.\n */\n #isolateBackground(): void {\n const container = this.#getContainer();\n this.#inertedSiblings = [];\n for (let node: HTMLElement = container; node !== document.body; ) {\n const parent = node.parentElement;\n if (!parent) break;\n for (const sibling of Array.from(parent.children)) {\n if (!(sibling instanceof HTMLElement)) continue;\n if (sibling === node || sibling.inert) continue;\n sibling.inert = true;\n this.#inertedSiblings.push(sibling);\n }\n node = parent;\n }\n }\n\n /** Reverts the `inert` flags applied by `#isolateBackground`. */\n #releaseBackground(): void {\n for (const sibling of this.#inertedSiblings) {\n sibling.inert = false;\n }\n this.#inertedSiblings = [];\n }\n\n /** Moves focus to the initial target, the first focusable, or the container. */\n #focusInitial(): void {\n const preferred = this.#options.initialFocus?.();\n if (preferred) {\n preferred.focus();\n return;\n }\n const focusable = this.#focusableElements();\n if (focusable[0]) {\n focusable[0].focus();\n return;\n }\n const container = this.#getContainer();\n container.tabIndex = -1;\n container.focus();\n }\n\n /** Collects the container's currently focusable descendants in DOM order. */\n #focusableElements(): HTMLElement[] {\n return tabStopsWithin(this.#getContainer());\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { FocusTrap } from \"../utils/focus_trap\";\n\n/**\n * Headless, accessible modal dialog behavior.\n *\n * Markup contract (identifier: `stimeo--dialog`):\n * <div data-controller=\"stimeo--dialog\">\n * <button data-stimeo--dialog-target=\"trigger\"\n * data-action=\"stimeo--dialog#open\">Open</button>\n * <div data-stimeo--dialog-target=\"dialog\" role=\"dialog\" aria-modal=\"true\"\n * aria-labelledby=\"title\" hidden>\n * <h2 id=\"title\">…</h2>\n * <button data-action=\"stimeo--dialog#close\">Close</button>\n * </div>\n * </div>\n *\n * Implements the WAI-ARIA APG **Dialog (Modal)** pattern: focus moves into the\n * dialog on open and is trapped within it, `Escape` closes it, background scroll\n * is locked, and focus returns to the trigger on close.\n *\n * @remarks\n * Behavior only. The sole visual side effect is locking `document.body`'s scroll\n * while open (the minimum required by the pattern); all other styling is the\n * consumer's. Clicking the dialog backdrop (the dialog target itself, outside\n * its content) also closes it.\n *\n * The modal lifecycle — focus trap, scroll lock, background `inert`, focus\n * restore, and teardown reversal — is delegated to the shared `FocusTrap`\n * primitive (also used by alert-dialog and drawer). This controller only owns\n * *when* to open/close and the dialog-specific backdrop click.\n */\nexport class DialogController extends Controller<HTMLElement> {\n static override targets = [\"trigger\", \"dialog\"];\n static actions = [\"close\", \"closeOnBackdrop\", \"open\"] as const;\n\n declare readonly triggerTarget: HTMLElement;\n declare readonly dialogTarget: HTMLElement;\n declare readonly hasTriggerTarget: boolean;\n declare readonly hasDialogTarget: boolean;\n\n /** Owns the modal side effects; Escape closes, focus falls back to the trigger. */\n readonly #trap = new FocusTrap(() => this.dialogTarget, {\n onEscape: () => this.close(),\n fallbackFocus: () => (this.hasTriggerTarget ? this.triggerTarget : null),\n });\n\n /** Starts closed (idempotently reflects the closed state on the markup). */\n override connect(): void {\n if (this.hasDialogTarget) this.dialogTarget.hidden = true;\n }\n\n /**\n * Reverts the modal side effects (scroll lock, background `inert`, keydown\n * listener) if the controller is torn down while open (e.g. a Turbo navigation\n * replaces the page while the dialog is showing). Focus is not restored on\n * teardown.\n */\n override disconnect(): void {\n this.#trap.deactivate({ restoreFocus: false });\n }\n\n /** Opens the dialog, traps focus, and locks background scroll. */\n open(): void {\n if (!this.hasDialogTarget || this.#isOpen) return;\n this.dialogTarget.hidden = false;\n this.#trap.activate();\n }\n\n /** Closes the dialog, restores scroll, and returns focus to the opener. */\n close(): void {\n if (!this.hasDialogTarget || !this.#isOpen) return;\n this.dialogTarget.hidden = true;\n this.#trap.deactivate();\n }\n\n /** Closes when the backdrop (the dialog target itself) is clicked. */\n closeOnBackdrop(event: MouseEvent): void {\n if (event.target === this.dialogTarget) this.close();\n }\n\n /** Whether the dialog is currently visible. */\n get #isOpen(): boolean {\n return this.hasDialogTarget && !this.dialogTarget.hidden;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/controllers/direct_upload_controller.ts"],"names":[],"mappings":"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","file":"direct_upload_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { announce, fillTemplate } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Detail shapes for the ActiveStorage `direct-upload:*` events. */\ninterface UploadDetail {\n id?: string | number;\n file?: { name?: string };\n progress?: number;\n error?: string;\n}\n\n/** Delay (ms) before a completed row is removed when `removeOnDone` is set. */\nconst REMOVE_DELAY = 4000;\n\n/**\n * Headless progress UI for ActiveStorage Direct Uploads: subscribes to the\n * `direct-upload:*` events and renders a per-file progress row (no dedicated APG\n * pattern; the rows follow the `role=\"progressbar\"` practice). The companion to\n * {@link FileDropzoneController}, which leaves transport out of scope.\n *\n * Markup contract (identifier: `stimeo--direct-upload`):\n * <div data-controller=\"stimeo--direct-upload\"\n * data-stimeo--direct-upload-announce-done-text-value=\"{name} uploaded\"\n * data-stimeo--direct-upload-announce-error-text-value=\"{name} failed\">\n * <div data-stimeo--direct-upload-target=\"list\"></div>\n * <template data-stimeo--direct-upload-target=\"row\">\n * <div role=\"progressbar\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n * <span data-field=\"name\"></span><span data-field=\"percent\"></span>\n * </div>\n * </template>\n * </div>\n *\n * For each upload it clones the `row` template into `list`, then keeps\n * `aria-valuenow` / `aria-valuetext`, the `[data-field=\"percent\"]` text, and the\n * `--stimeo--upload-progress` custom property (0–100, rounded and clamped) in\n * sync as `direct-upload:progress` arrives. The file name carried by the events\n * fills `[data-field=\"name\"]` and becomes the row's `aria-label` unless the\n * template already authors one. The aggregate across live rows is mirrored on\n * the controller element as `data-upload-progress` plus the same custom\n * property, recomputed whenever a row is added, updated, or removed, and\n * withdrawn entirely once no rows remain.\n *\n * A row leaves `uploading` exactly once: `direct-upload:error` settles it as\n * `error`, and `direct-upload:end` — which ActiveStorage fires after success\n * *and* failure — completes only rows not already settled, pinning them at 100%\n * and flipping `data-upload-state` to `done`. Later events never rewrite a\n * settled row, so a failure survives the `end` that follows it.\n *\n * Completion and failure are handed to the page's shared `stimeo--announcer` as\n * one polite message each, worded by the consumer via `announceDoneText` /\n * `announceErrorText` (`{name}` expands to the file name; empty templates stay\n * silent, keeping announcements opt-in and i18n-neutral). Per-tick progress is\n * not announced — the progressbar's `aria-valuenow` conveys it — to avoid\n * flooding. A failure this controller has rendered also cancels the\n * `direct-upload:error` default, which suppresses ActiveStorage's native\n * `alert()`; when no row could be rendered the alert stays as the fallback.\n *\n * Events dispatched on the controller element (all bubble):\n * - `stimeo--direct-upload:progress` dispatches `{ id: string, percent: number }`\n * on each progress update, after clamping.\n * - `stimeo--direct-upload:done` dispatches `{ id: string }` when an upload\n * completes successfully.\n * - `stimeo--direct-upload:reconcile` dispatches `{ ids: string[] }` — the uploads\n * the Turbo cache rewind discarded, none of which can still finish.\n * - `stimeo--direct-upload:error` dispatches `{ id: string, error: string }`\n * when an upload fails.\n *\n * @remarks\n * Behavior only — no bars are drawn. The `direct-upload:*` listeners live on\n * `document` (the events bubble there) and are removed on `disconnect()` (Turbo\n * navigation included), along with any pending removal timers, so a callback\n * that arrives after teardown never touches a detached row. Rows are transient\n * UI: just before Turbo caches the page they are removed and the aggregate is\n * withdrawn, so a restored snapshot starts pristine instead of replaying stale\n * rows (a dead upload cannot resume after restoration). The live DOM stays the\n * source of truth — a row removed or replaced outside this controller is\n * forgotten and rebuilt on the next event for its id, and a clone stranded\n * outside the current `list` (a re-pointed target) is removed outright, so\n * generated rows only ever live there.\n *\n * With multiple upload widgets on one page, set `scope` to a selector for the\n * owning form/root so each widget only handles its own uploads. A `scope` that\n * does not parse as a selector falls back to the default (handle all) instead\n * of throwing from the event handlers, so one broken declaration cannot silence\n * the widget.\n */\nexport class DirectUploadController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"row\"];\n static override values = {\n removeOnDone: { type: Boolean, default: false },\n announceDoneText: { type: String, default: \"\" },\n announceErrorText: { type: String, default: \"\" },\n scope: { type: String, default: \"\" },\n };\n static events = [\"progress\", \"done\", \"error\", \"reconcile\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly rowTarget: HTMLTemplateElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasRowTarget: boolean;\n\n declare removeOnDoneValue: boolean;\n declare announceDoneTextValue: string;\n declare announceErrorTextValue: string;\n declare scopeValue: string;\n\n readonly #timeouts = new SafeTimeout();\n readonly #rows = new Map<string, HTMLElement>();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** The validated `scope` selector; a broken declaration falls back to `\"\"`. */\n #scopeSelector = \"\";\n\n readonly #onInitialize = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#rowFor(detail.id, this.#name(detail));\n };\n\n readonly #onProgress = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#updateProgress(this.#key(detail.id), detail.progress ?? 0, this.#name(detail));\n };\n\n readonly #onError = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n const rendered = this.#fail(this.#key(detail.id), detail.error ?? \"\", this.#name(detail));\n // ActiveStorage alert()s the raw error unless the event is cancelled. A\n // failure this widget displays is handled, so the blocking duplicate is\n // suppressed; an unrendered failure keeps the alert as its only signal.\n if (rendered) event.preventDefault();\n };\n\n readonly #onEnd = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#complete(this.#key(detail.id), this.#name(detail));\n };\n\n override connect(): void {\n document.addEventListener(\"direct-upload:initialize\", this.#onInitialize);\n document.addEventListener(\"direct-upload:progress\", this.#onProgress);\n document.addEventListener(\"direct-upload:error\", this.#onError);\n document.addEventListener(\"direct-upload:end\", this.#onEnd);\n this.#beforeCache.activate();\n // An in-page move runs disconnect() → connect() on the same instance, and\n // teardown cancelled any pending removals; completed rows re-earn theirs.\n this.#rescheduleRemovals();\n }\n\n override disconnect(): void {\n document.removeEventListener(\"direct-upload:initialize\", this.#onInitialize);\n document.removeEventListener(\"direct-upload:progress\", this.#onProgress);\n document.removeEventListener(\"direct-upload:error\", this.#onError);\n document.removeEventListener(\"direct-upload:end\", this.#onEnd);\n this.#beforeCache.deactivate();\n this.#timeouts.clearAll();\n // `#rows` is kept: `disconnect()` also fires on an in-page move, where the\n // rows travel with the element and the next event should keep updating\n // them. Stale entries self-heal via `#prune`, and the Turbo-cache path\n // clears everything in `#reset()`.\n }\n\n /** Validates `scope` once so the per-event path never parses or throws. */\n scopeValueChanged(): void {\n const selector = this.scopeValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#scopeSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#scopeSelector = \"\";\n }\n\n /** Updates a row's progress and the aggregate, emitting `progress`. */\n #updateProgress(id: string, percent: number, name: string): void {\n const row = this.#rowFor(id, name);\n if (row === null || this.#isSettled(row)) return;\n const clamped = this.#applyProgress(row, percent);\n this.#syncAggregate();\n this.dispatch(\"progress\", { detail: { id, percent: clamped } });\n }\n\n /** Marks a not-yet-settled row done at 100%, announces it, and emits `done`. */\n #complete(id: string, name: string): void {\n // Resolve lazily like `#fail`/`#updateProgress` so an `end` that arrives\n // without a prior `initialize`/`progress` (no row yet) still records the\n // completion instead of silently dropping it.\n const row = this.#rowFor(id, name);\n if (row === null || this.#isSettled(row)) return;\n row.setAttribute(\"data-upload-state\", \"done\");\n this.#applyProgress(row, 100);\n this.#syncAggregate();\n this.#announce(this.announceDoneTextValue, name, row);\n this.dispatch(\"done\", { detail: { id } });\n if (this.removeOnDoneValue) {\n this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);\n }\n }\n\n /**\n * Marks a not-yet-settled row failed, announces it, and emits `error`.\n * Returns whether the failure is rendered by this widget (used to decide the\n * `direct-upload:error` default), which also holds when the row already\n * displays an earlier failure.\n */\n #fail(id: string, error: string, name: string): boolean {\n const row = this.#rowFor(id, name);\n if (row === null) return false;\n if (this.#isSettled(row)) return row.getAttribute(\"data-upload-state\") === \"error\";\n row.setAttribute(\"data-upload-state\", \"error\");\n this.#announce(this.announceErrorTextValue, name, row);\n this.dispatch(\"error\", { detail: { id, error } });\n return true;\n }\n\n /** Whether the row reached a terminal state; settled rows are never rewritten. */\n #isSettled(row: HTMLElement): boolean {\n const state = row.getAttribute(\"data-upload-state\");\n return state === \"done\" || state === \"error\";\n }\n\n /** Re-arms `removeOnDone` for completed rows after a reconnect. */\n #rescheduleRemovals(): void {\n if (!this.removeOnDoneValue) return;\n this.#prune();\n for (const [id, row] of this.#rows) {\n if (row.getAttribute(\"data-upload-state\") === \"done\") {\n this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);\n }\n }\n }\n\n /** Returns the live row for `id`, creating (and labeling) one on first sight. */\n #rowFor(id: string | number | undefined, name: string): HTMLElement | null {\n const key = this.#key(id);\n const existing = this.#rows.get(key);\n if (existing !== undefined) {\n if (this.#tracksRow(existing)) {\n this.#applyName(existing, name);\n return existing;\n }\n // Removed, replaced, or no longer inside the current list: retire the\n // clone (removing it is a no-op when something else already did) and\n // rebuild from the live DOM.\n existing.remove();\n this.#rows.delete(key);\n }\n if (!this.hasRowTarget || !this.hasListTarget) return null;\n const clone = this.rowTarget.content.firstElementChild?.cloneNode(true);\n if (!(clone instanceof HTMLElement)) return null;\n this.#applyName(clone, name);\n clone.setAttribute(\"data-upload-state\", \"uploading\");\n this.#applyProgress(clone, 0);\n this.listTarget.appendChild(clone);\n this.#rows.set(key, clone);\n this.#syncAggregate();\n return clone;\n }\n\n /**\n * Writes the event's file name into `[data-field=\"name\"]` and, unless the row\n * already carries a non-blank `aria-label` — authored on the template or\n * applied by an earlier event — makes it the accessible name too. The visible\n * name never depends on the label: an authored label keeps its wording while\n * the field still shows which file this row tracks.\n */\n #applyName(row: HTMLElement, name: string): void {\n if (name.length === 0) return;\n this.#setField(row, \"name\", name);\n // A whitespace-only label computes to no accessible name, so only a\n // non-blank one counts as authored.\n if ((row.getAttribute(\"aria-label\") ?? \"\").trim().length > 0) return;\n row.setAttribute(\"aria-label\", name);\n }\n\n /** Writes one progress value to every per-row hook; returns the clamped percent. */\n #applyProgress(row: HTMLElement, percent: number): number {\n const clamped = Math.max(0, Math.min(100, Math.round(percent)));\n row.setAttribute(\"aria-valuenow\", String(clamped));\n row.setAttribute(\"aria-valuetext\", `${clamped}%`);\n row.style.setProperty(\"--stimeo--upload-progress\", `${clamped}%`);\n this.#setField(row, \"percent\", `${clamped}%`);\n return clamped;\n }\n\n #removeRow(id: string): void {\n const row = this.#rows.get(id);\n if (row === undefined) return;\n row.remove();\n this.#rows.delete(id);\n this.#syncAggregate();\n }\n\n /**\n * Reflects the average progress across live rows on the controller element,\n * withdrawing both hooks once no rows remain.\n */\n #syncAggregate(): void {\n this.#prune();\n if (this.#rows.size === 0) {\n this.element.removeAttribute(\"data-upload-progress\");\n this.element.style.removeProperty(\"--stimeo--upload-progress\");\n return;\n }\n let total = 0;\n for (const row of this.#rows.values()) {\n total += Number(row.getAttribute(\"aria-valuenow\") ?? \"0\");\n }\n const overall = Math.round(total / this.#rows.size);\n this.element.setAttribute(\"data-upload-progress\", String(overall));\n this.element.style.setProperty(\"--stimeo--upload-progress\", `${overall}%`);\n }\n\n /**\n * Whether a bookkept row is still this widget's live UI: connected, and — when\n * a `list` target is present — inside the *current* one, so re-pointing the\n * target attribute at a new element retires rows kept alive in the old list.\n */\n #tracksRow(row: HTMLElement): boolean {\n if (!row.isConnected) return false;\n return !this.hasListTarget || this.listTarget.contains(row);\n }\n\n /**\n * Retires rows that left the live UI (list swap, external removal). A retired\n * clone is removed outright — generated rows only ever live under the current\n * `list`, so the before-cache rewind never has an untracked leftover to miss.\n */\n #prune(): void {\n for (const [id, row] of this.#rows) {\n if (this.#tracksRow(row)) continue;\n row.remove();\n this.#rows.delete(id);\n }\n }\n\n /**\n * Rewinds for the snapshot and reports what that discarded. An upload in flight\n * cannot survive the navigation, so a consumer mirroring the rows would keep a\n * progress bar that never resolves.\n */\n #rewindForCache(): void {\n const ids = [...this.#rows.keys()];\n this.#reset();\n if (ids.length > 0) this.dispatch(\"reconcile\", { detail: { ids } });\n }\n\n #reset(): void {\n for (const row of this.#rows.values()) row.remove();\n this.#rows.clear();\n this.#timeouts.clearAll();\n this.#syncAggregate();\n }\n\n /**\n * Sends one consumer-worded message to the page's shared announcer. `{name}`\n * resolves to the row's displayed name first — the event that settles an\n * upload may omit the file although an earlier event already named the row —\n * then the event's own name, then the accessible name (which an authored\n * label owns, so it is the last resort, not the primary source).\n */\n #announce(template: string, name: string, row: HTMLElement): void {\n const stored = this.#field(row, \"name\")?.textContent ?? \"\";\n const label =\n stored.length > 0 ? stored : name.length > 0 ? name : (row.getAttribute(\"aria-label\") ?? \"\");\n announce(fillTemplate(template, { name: label }));\n }\n\n #field(row: HTMLElement, name: string): HTMLElement | null {\n return row.querySelector<HTMLElement>(`[data-field=\"${name}\"]`);\n }\n\n #setField(row: HTMLElement, name: string, text: string): void {\n const field = this.#field(row, name);\n if (field !== null) field.textContent = text;\n }\n\n #detail(event: Event): UploadDetail {\n return (event as CustomEvent<UploadDetail>).detail ?? {};\n }\n\n /** The event's file name, or `\"\"` when it carries none. */\n #name(detail: UploadDetail): string {\n return detail.file?.name ?? \"\";\n }\n\n /**\n * Whether an event belongs to this controller. With `scope` set, only events\n * whose target (the file input) sits inside an element matching `scope` are\n * handled, so several upload widgets on one page do not cross-populate.\n * Resolved with `closest()` from the target itself, so the chatty `progress`\n * stream never pays a document-wide query. Empty `scope` handles all.\n */\n #inScope(event: Event): boolean {\n if (this.#scopeSelector.length === 0) return true;\n const target = event.target;\n return target instanceof Element && target.closest(this.#scopeSelector) !== null;\n }\n\n #key(id: string | number | undefined): string {\n return String(id ?? \"\");\n }\n}\n"]}
1
+ {"version":3,"sources":["../../src/utils/announce.ts","../../src/utils/before_cache_reset.ts","../../src/utils/safe_timeout.ts","../../src/controllers/direct_upload_controller.ts"],"names":[],"mappings":"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","file":"direct_upload_controller.js","sourcesContent":["/**\n * Sends one message to the page's shared `stimeo--announcer`.\n *\n * A component that has to reach assistive tech does not carry a live region of its\n * own: a region only announces what changes *after* assistive tech already knows\n * about it, which a region that appears (or is un-hidden) with its message cannot\n * satisfy. The one region that can is the announcer sitting in the page from the\n * start, so state changes are handed to it as an event and it does the reading.\n *\n * The event goes to `window` because the announcer is usually a sibling high in the\n * document rather than an ancestor of the component dispatching it.\n *\n * Wording comes from the consumer — the library ships no English strings — so an\n * empty message is silently dropped and nothing is announced.\n *\n * @example\n * ```ts\n * announce(this.announceTextValue, { assertive: false });\n * ```\n */\nexport function announce(message: string, options: { assertive?: boolean } = {}): void {\n const text = message.trim();\n if (text.length === 0) return;\n window.dispatchEvent(\n new CustomEvent(\"stimeo--announcer:announce\", {\n detail: { message: text, assertive: options.assertive === true },\n }),\n );\n}\n\n/**\n * Fills `{name}` placeholders in an announcement template from `values`.\n *\n * The same substitution the value-text templates use, so a consumer writes\n * `\"{percent}% complete\"` in one attribute and gets the same rules everywhere. A\n * placeholder with no matching entry is left as authored rather than blanked, which\n * keeps a typo visible instead of silently swallowing the word.\n */\nexport function fillTemplate(template: string, values: Record<string, string | number>): string {\n return template.replace(/\\{([a-zA-Z][a-zA-Z0-9]*)\\}/g, (match, name: string) => {\n const replacement = values[name];\n return replacement === undefined ? match : String(replacement);\n });\n}\n","/**\n * Runs a controller's \"return to the initial state\" pass just before Turbo\n * caches the page.\n *\n * **`disconnect()` cannot do this job, for two independent reasons.** Turbo\n * queues the clone from this event rather than taking it here, and the body swap\n * that runs the controller's `disconnect()` is queued separately — so which of\n * the two lands first is not something a controller can rely on, and a rewind\n * written in `disconnect()` may reach only the DOM being thrown away. In the\n * other direction, `disconnect()` also fires on an in-page move (Stimulus tears\n * down and reconnects the same element), where rewinding would wipe a\n * legitimately in-progress interaction — a spinner mid-load would vanish. One\n * timing is unreliable, the other is too eager; `turbo:before-cache` is the only\n * point that is exactly \"the page is about to be frozen\".\n *\n * Scope is the subscription only: registering on `activate()`, unregistering on\n * `deactivate()`, and one shared document listener no matter how many instances\n * are live. *What* to return to its initial state — which `data-state`, which\n * `hidden`, which `aria-busy` — stays in the controller, because no two\n * consumers answer it the same way.\n *\n * **Rewind state, not appearance.** The pass writes attributes the controller\n * itself owns; the visual result of those attributes is the consumer's CSS, and\n * a library that reached for style or class names would be guessing at markup\n * it does not own.\n *\n * Both entry points are idempotent, so the lifecycle hooks can call them\n * unconditionally: a second `activate()` does not double-subscribe and does not\n * make the callback run twice, and `deactivate()` on an instance that never\n * subscribed is a no-op.\n *\n * This file's own doc block is dropped from `dist`, but every member comment is\n * inlined into each consumer entry (`tsup` builds with `splitting: false`), so\n * rationale belongs here and only the contract belongs on the members.\n *\n * @example\n * ```ts\n * readonly #beforeCache = new BeforeCacheReset(() => this.#rewind());\n *\n * connect() { this.#beforeCache.activate(); }\n * disconnect() { this.#beforeCache.deactivate(); }\n * ```\n */\nexport class BeforeCacheReset {\n /** Every subscribed instance, iterated by the one shared document listener. */\n static readonly #subscribers = new Set<BeforeCacheReset>();\n\n /** The shared listener; installed while at least one instance is subscribed. */\n static readonly #onBeforeCache = (): void => {\n for (const subscriber of BeforeCacheReset.#subscribers) subscriber.#rewind();\n };\n\n readonly #rewind: () => void;\n\n /** @param rewind - the pass that returns this controller's state to its initial form. */\n constructor(rewind: () => void) {\n this.#rewind = rewind;\n }\n\n /** Subscribes to `turbo:before-cache`; call from `connect()`. Idempotent. */\n activate(): void {\n const first = BeforeCacheReset.#subscribers.size === 0;\n BeforeCacheReset.#subscribers.add(this);\n if (first) {\n document.addEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n }\n\n /** Unsubscribes; call from `disconnect()`. Safe when never subscribed. */\n deactivate(): void {\n BeforeCacheReset.#subscribers.delete(this);\n if (BeforeCacheReset.#subscribers.size > 0) return;\n document.removeEventListener(\"turbo:before-cache\", BeforeCacheReset.#onBeforeCache);\n }\n}\n","/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * 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 { announce, fillTemplate } from \"../utils/announce\";\nimport { BeforeCacheReset } from \"../utils/before_cache_reset\";\nimport { SafeTimeout } from \"../utils/safe_timeout\";\n\n/** Detail shapes for the ActiveStorage `direct-upload:*` events. */\ninterface UploadDetail {\n id?: string | number;\n file?: { name?: string };\n progress?: number;\n error?: string;\n}\n\n/** Delay (ms) before a completed row is removed when `removeOnDone` is set. */\nconst REMOVE_DELAY = 4000;\n\n/**\n * Headless progress UI for ActiveStorage Direct Uploads: subscribes to the\n * `direct-upload:*` events and renders a per-file progress row (no dedicated APG\n * pattern; the rows follow the `role=\"progressbar\"` practice). The companion to\n * {@link FileDropzoneController}, which leaves transport out of scope.\n *\n * Markup contract (identifier: `stimeo--direct-upload`):\n * <div data-controller=\"stimeo--direct-upload\"\n * data-stimeo--direct-upload-announce-done-text-value=\"{name} uploaded\"\n * data-stimeo--direct-upload-announce-error-text-value=\"{name} failed\">\n * <div data-stimeo--direct-upload-target=\"list\"></div>\n * <template data-stimeo--direct-upload-target=\"row\">\n * <div role=\"progressbar\" aria-valuemin=\"0\" aria-valuemax=\"100\">\n * <span data-field=\"name\"></span><span data-field=\"percent\"></span>\n * </div>\n * </template>\n * </div>\n *\n * For each upload it clones the `row` template into `list`, then keeps\n * `aria-valuenow` / `aria-valuetext`, the `[data-field=\"percent\"]` text, and the\n * `--stimeo--upload-progress` custom property (0–100, rounded and clamped) in\n * sync as `direct-upload:progress` arrives. The file name carried by the events\n * fills `[data-field=\"name\"]` and becomes the row's `aria-label` unless the\n * template already authors one. The aggregate across live rows is mirrored on\n * the controller element as `data-upload-progress` plus the same custom\n * property, recomputed whenever a row is added, updated, or removed, and\n * withdrawn entirely once no rows remain.\n *\n * A row leaves `uploading` exactly once: `direct-upload:error` settles it as\n * `error`, and `direct-upload:end` — which ActiveStorage fires after success\n * *and* failure — completes only rows not already settled, pinning them at 100%\n * and flipping `data-upload-state` to `done`. Later events never rewrite a\n * settled row, so a failure survives the `end` that follows it.\n *\n * Completion and failure are handed to the page's shared `stimeo--announcer` as\n * one polite message each, worded by the consumer via `announceDoneText` /\n * `announceErrorText` (`{name}` expands to the file name; empty templates stay\n * silent, keeping announcements opt-in and i18n-neutral). Per-tick progress is\n * not announced — the progressbar's `aria-valuenow` conveys it — to avoid\n * flooding. A failure this controller has rendered also cancels the\n * `direct-upload:error` default, which suppresses ActiveStorage's native\n * `alert()`; when no row could be rendered the alert stays as the fallback.\n *\n * Events dispatched on the controller element (all bubble):\n * - `stimeo--direct-upload:progress` dispatches `{ id: string, percent: number }`\n * on each progress update, after clamping.\n * - `stimeo--direct-upload:done` dispatches `{ id: string }` when an upload\n * completes successfully.\n * - `stimeo--direct-upload:reconcile` dispatches `{ ids: string[] }` — the uploads\n * the Turbo cache rewind discarded, none of which can still finish.\n * - `stimeo--direct-upload:error` dispatches `{ id: string, error: string }`\n * when an upload fails.\n *\n * @remarks\n * Behavior only — no bars are drawn. The `direct-upload:*` listeners live on\n * `document` (the events bubble there) and are removed on `disconnect()` (Turbo\n * navigation included), along with any pending removal timers, so a callback\n * that arrives after teardown never touches a detached row. Rows are transient\n * UI: just before Turbo caches the page they are removed and the aggregate is\n * withdrawn, so a restored snapshot starts pristine instead of replaying stale\n * rows (a dead upload cannot resume after restoration). The live DOM stays the\n * source of truth — a row removed or replaced outside this controller is\n * forgotten and rebuilt on the next event for its id, and a clone stranded\n * outside the current `list` (a re-pointed target) is removed outright, so\n * generated rows only ever live there.\n *\n * With multiple upload widgets on one page, set `scope` to a selector for the\n * owning form/root so each widget only handles its own uploads. A `scope` that\n * does not parse as a selector falls back to the default (handle all) instead\n * of throwing from the event handlers, so one broken declaration cannot silence\n * the widget.\n */\nexport class DirectUploadController extends Controller<HTMLElement> {\n static override targets = [\"list\", \"row\"];\n static override values = {\n removeOnDone: { type: Boolean, default: false },\n announceDoneText: { type: String, default: \"\" },\n announceErrorText: { type: String, default: \"\" },\n scope: { type: String, default: \"\" },\n };\n static events = [\"progress\", \"done\", \"error\", \"reconcile\"] as const;\n\n declare readonly listTarget: HTMLElement;\n declare readonly rowTarget: HTMLTemplateElement;\n declare readonly hasListTarget: boolean;\n declare readonly hasRowTarget: boolean;\n\n declare removeOnDoneValue: boolean;\n declare announceDoneTextValue: string;\n declare announceErrorTextValue: string;\n declare scopeValue: string;\n\n readonly #timeouts = new SafeTimeout();\n readonly #rows = new Map<string, HTMLElement>();\n readonly #beforeCache = new BeforeCacheReset(() => this.#rewindForCache());\n\n /** The validated `scope` selector; a broken declaration falls back to `\"\"`. */\n #scopeSelector = \"\";\n\n readonly #onInitialize = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#rowFor(detail.id, this.#name(detail));\n };\n\n readonly #onProgress = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#updateProgress(this.#key(detail.id), detail.progress ?? 0, this.#name(detail));\n };\n\n readonly #onError = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n const rendered = this.#fail(this.#key(detail.id), detail.error ?? \"\", this.#name(detail));\n // ActiveStorage alert()s the raw error unless the event is cancelled. A\n // failure this widget displays is handled, so the blocking duplicate is\n // suppressed; an unrendered failure keeps the alert as its only signal.\n if (rendered) event.preventDefault();\n };\n\n readonly #onEnd = (event: Event): void => {\n if (!this.#inScope(event)) return;\n const detail = this.#detail(event);\n this.#complete(this.#key(detail.id), this.#name(detail));\n };\n\n override connect(): void {\n document.addEventListener(\"direct-upload:initialize\", this.#onInitialize);\n document.addEventListener(\"direct-upload:progress\", this.#onProgress);\n document.addEventListener(\"direct-upload:error\", this.#onError);\n document.addEventListener(\"direct-upload:end\", this.#onEnd);\n this.#beforeCache.activate();\n // An in-page move runs disconnect() → connect() on the same instance, and\n // teardown cancelled any pending removals; completed rows re-earn theirs.\n this.#rescheduleRemovals();\n }\n\n override disconnect(): void {\n document.removeEventListener(\"direct-upload:initialize\", this.#onInitialize);\n document.removeEventListener(\"direct-upload:progress\", this.#onProgress);\n document.removeEventListener(\"direct-upload:error\", this.#onError);\n document.removeEventListener(\"direct-upload:end\", this.#onEnd);\n this.#beforeCache.deactivate();\n this.#timeouts.clearAll();\n // `#rows` is kept: `disconnect()` also fires on an in-page move, where the\n // rows travel with the element and the next event should keep updating\n // them. Stale entries self-heal via `#prune`, and the Turbo-cache path\n // clears everything in `#reset()`.\n }\n\n /** Validates `scope` once so the per-event path never parses or throws. */\n scopeValueChanged(): void {\n const selector = this.scopeValue;\n if (selector.length > 0) {\n try {\n this.element.matches(selector);\n this.#scopeSelector = selector;\n return;\n } catch {\n // Unparsable selector: fall through to the default below.\n }\n }\n this.#scopeSelector = \"\";\n }\n\n /** Updates a row's progress and the aggregate, emitting `progress`. */\n #updateProgress(id: string, percent: number, name: string): void {\n const row = this.#rowFor(id, name);\n if (row === null || this.#isSettled(row)) return;\n const clamped = this.#applyProgress(row, percent);\n this.#syncAggregate();\n this.dispatch(\"progress\", { detail: { id, percent: clamped } });\n }\n\n /** Marks a not-yet-settled row done at 100%, announces it, and emits `done`. */\n #complete(id: string, name: string): void {\n // Resolve lazily like `#fail`/`#updateProgress` so an `end` that arrives\n // without a prior `initialize`/`progress` (no row yet) still records the\n // completion instead of silently dropping it.\n const row = this.#rowFor(id, name);\n if (row === null || this.#isSettled(row)) return;\n row.setAttribute(\"data-upload-state\", \"done\");\n this.#applyProgress(row, 100);\n this.#syncAggregate();\n this.#announce(this.announceDoneTextValue, name, row);\n this.dispatch(\"done\", { detail: { id } });\n if (this.removeOnDoneValue) {\n this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);\n }\n }\n\n /**\n * Marks a not-yet-settled row failed, announces it, and emits `error`.\n * Returns whether the failure is rendered by this widget (used to decide the\n * `direct-upload:error` default), which also holds when the row already\n * displays an earlier failure.\n */\n #fail(id: string, error: string, name: string): boolean {\n const row = this.#rowFor(id, name);\n if (row === null) return false;\n if (this.#isSettled(row)) return row.getAttribute(\"data-upload-state\") === \"error\";\n row.setAttribute(\"data-upload-state\", \"error\");\n this.#announce(this.announceErrorTextValue, name, row);\n this.dispatch(\"error\", { detail: { id, error } });\n return true;\n }\n\n /** Whether the row reached a terminal state; settled rows are never rewritten. */\n #isSettled(row: HTMLElement): boolean {\n const state = row.getAttribute(\"data-upload-state\");\n return state === \"done\" || state === \"error\";\n }\n\n /** Re-arms `removeOnDone` for completed rows after a reconnect. */\n #rescheduleRemovals(): void {\n if (!this.removeOnDoneValue) return;\n this.#prune();\n for (const [id, row] of this.#rows) {\n if (row.getAttribute(\"data-upload-state\") === \"done\") {\n this.#timeouts.set(() => this.#removeRow(id), REMOVE_DELAY);\n }\n }\n }\n\n /** Returns the live row for `id`, creating (and labeling) one on first sight. */\n #rowFor(id: string | number | undefined, name: string): HTMLElement | null {\n const key = this.#key(id);\n const existing = this.#rows.get(key);\n if (existing !== undefined) {\n if (this.#tracksRow(existing)) {\n this.#applyName(existing, name);\n return existing;\n }\n // Removed, replaced, or no longer inside the current list: retire the\n // clone (removing it is a no-op when something else already did) and\n // rebuild from the live DOM.\n existing.remove();\n this.#rows.delete(key);\n }\n if (!this.hasRowTarget || !this.hasListTarget) return null;\n const clone = this.rowTarget.content.firstElementChild?.cloneNode(true);\n if (!(clone instanceof HTMLElement)) return null;\n this.#applyName(clone, name);\n clone.setAttribute(\"data-upload-state\", \"uploading\");\n this.#applyProgress(clone, 0);\n this.listTarget.appendChild(clone);\n this.#rows.set(key, clone);\n this.#syncAggregate();\n return clone;\n }\n\n /**\n * Writes the event's file name into `[data-field=\"name\"]` and, unless the row\n * already carries a non-blank `aria-label` — authored on the template or\n * applied by an earlier event — makes it the accessible name too. The visible\n * name never depends on the label: an authored label keeps its wording while\n * the field still shows which file this row tracks.\n */\n #applyName(row: HTMLElement, name: string): void {\n if (name.length === 0) return;\n this.#setField(row, \"name\", name);\n // A whitespace-only label computes to no accessible name, so only a\n // non-blank one counts as authored.\n if ((row.getAttribute(\"aria-label\") ?? \"\").trim().length > 0) return;\n row.setAttribute(\"aria-label\", name);\n }\n\n /** Writes one progress value to every per-row hook; returns the clamped percent. */\n #applyProgress(row: HTMLElement, percent: number): number {\n const clamped = Math.max(0, Math.min(100, Math.round(percent)));\n row.setAttribute(\"aria-valuenow\", String(clamped));\n row.setAttribute(\"aria-valuetext\", `${clamped}%`);\n row.style.setProperty(\"--stimeo--upload-progress\", `${clamped}%`);\n this.#setField(row, \"percent\", `${clamped}%`);\n return clamped;\n }\n\n #removeRow(id: string): void {\n const row = this.#rows.get(id);\n if (row === undefined) return;\n row.remove();\n this.#rows.delete(id);\n this.#syncAggregate();\n }\n\n /**\n * Reflects the average progress across live rows on the controller element,\n * withdrawing both hooks once no rows remain.\n */\n #syncAggregate(): void {\n this.#prune();\n if (this.#rows.size === 0) {\n this.element.removeAttribute(\"data-upload-progress\");\n this.element.style.removeProperty(\"--stimeo--upload-progress\");\n return;\n }\n let total = 0;\n for (const row of this.#rows.values()) {\n total += Number(row.getAttribute(\"aria-valuenow\") ?? \"0\");\n }\n const overall = Math.round(total / this.#rows.size);\n this.element.setAttribute(\"data-upload-progress\", String(overall));\n this.element.style.setProperty(\"--stimeo--upload-progress\", `${overall}%`);\n }\n\n /**\n * Whether a bookkept row is still this widget's live UI: connected, and — when\n * a `list` target is present — inside the *current* one, so re-pointing the\n * target attribute at a new element retires rows kept alive in the old list.\n */\n #tracksRow(row: HTMLElement): boolean {\n if (!row.isConnected) return false;\n return !this.hasListTarget || this.listTarget.contains(row);\n }\n\n /**\n * Retires rows that left the live UI (list swap, external removal). A retired\n * clone is removed outright — generated rows only ever live under the current\n * `list`, so the before-cache rewind never has an untracked leftover to miss.\n */\n #prune(): void {\n for (const [id, row] of this.#rows) {\n if (this.#tracksRow(row)) continue;\n row.remove();\n this.#rows.delete(id);\n }\n }\n\n /**\n * Rewinds for the snapshot and reports what that discarded. An upload in flight\n * cannot survive the navigation, so a consumer mirroring the rows would keep a\n * progress bar that never resolves.\n */\n #rewindForCache(): void {\n const ids = [...this.#rows.keys()];\n this.#reset();\n if (ids.length > 0) this.dispatch(\"reconcile\", { detail: { ids } });\n }\n\n #reset(): void {\n for (const row of this.#rows.values()) row.remove();\n this.#rows.clear();\n this.#timeouts.clearAll();\n this.#syncAggregate();\n }\n\n /**\n * Sends one consumer-worded message to the page's shared announcer. `{name}`\n * resolves to the row's displayed name first — the event that settles an\n * upload may omit the file although an earlier event already named the row —\n * then the event's own name, then the accessible name (which an authored\n * label owns, so it is the last resort, not the primary source).\n */\n #announce(template: string, name: string, row: HTMLElement): void {\n const stored = this.#field(row, \"name\")?.textContent ?? \"\";\n const label =\n stored.length > 0 ? stored : name.length > 0 ? name : (row.getAttribute(\"aria-label\") ?? \"\");\n announce(fillTemplate(template, { name: label }));\n }\n\n #field(row: HTMLElement, name: string): HTMLElement | null {\n return row.querySelector<HTMLElement>(`[data-field=\"${name}\"]`);\n }\n\n #setField(row: HTMLElement, name: string, text: string): void {\n const field = this.#field(row, name);\n if (field !== null) field.textContent = text;\n }\n\n #detail(event: Event): UploadDetail {\n return (event as CustomEvent<UploadDetail>).detail ?? {};\n }\n\n /** The event's file name, or `\"\"` when it carries none. */\n #name(detail: UploadDetail): string {\n return detail.file?.name ?? \"\";\n }\n\n /**\n * Whether an event belongs to this controller. With `scope` set, only events\n * whose target (the file input) sits inside an element matching `scope` are\n * handled, so several upload widgets on one page do not cross-populate.\n * Resolved with `closest()` from the target itself, so the chatty `progress`\n * stream never pays a document-wide query. Empty `scope` handles all.\n */\n #inScope(event: Event): boolean {\n if (this.#scopeSelector.length === 0) return true;\n const target = event.target;\n return target instanceof Element && target.closest(this.#scopeSelector) !== null;\n }\n\n #key(id: string | number | undefined): string {\n return String(id ?? \"\");\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/utils/safe_timeout.ts","../../src/controllers/dirty_form_controller.ts"],"names":[],"mappings":"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","file":"dirty_form_controller.js","sourcesContent":["/**\n * Self-cleaning timer registries shared by Stimeo controllers.\n *\n * Stimulus controllers frequently schedule `setTimeout` / `setInterval` work\n * (auto-dismiss, debouncing, polling). When the element leaves the DOM — a\n * Turbo Drive navigation, a Turbo Stream replacement, or any `disconnect()` —\n * orphaned timers keep firing against a detached controller, leaking memory and\n * mutating stale state. {@link SafeTimeout} and {@link SafeInterval} track every\n * timer they create so a single {@link TimerRegistry.clearAll | clearAll()} call\n * in `disconnect()` tears them all down.\n *\n * These are intentionally low-level primitives: they own *registration and\n * cleanup only*. Higher-level policy (pause/resume, remaining-time accounting)\n * stays in the individual controllers so per-widget semantics are not flattened\n * into a lowest-common-denominator helper.\n */\n\n/**\n * Shared registry bookkeeping for the timeout/interval variants.\n *\n * Subclasses provide the scheduling primitive ({@link schedule}) and its matching\n * canceller ({@link cancel}); this base owns the set of live ids plus the\n * per-id and bulk teardown shared by both.\n */\nabstract class TimerRegistry {\n /** Live timer ids that have not yet been cleared (or, for timeouts, fired). */\n protected readonly ids = new Set<number>();\n\n /** Schedules the underlying platform timer and returns its id. */\n protected abstract schedule(callback: () => void, delay: number): number;\n\n /** Cancels the underlying platform timer for `id`. */\n protected abstract cancel(id: number): void;\n\n /**\n * Cancels a single tracked timer.\n *\n * No-ops if the id is unknown (already cleared, fired, or never owned by this\n * registry), so callers can clear defensively without guarding.\n */\n clear(id: number): void {\n if (this.ids.delete(id)) {\n this.cancel(id);\n }\n }\n\n /**\n * Cancels every tracked timer. Call this from a controller's `disconnect()`\n * to guarantee no timer outlives the element.\n */\n clearAll(): void {\n for (const id of this.ids) {\n this.cancel(id);\n }\n this.ids.clear();\n }\n\n /** Number of timers currently tracked (pending). */\n get size(): number {\n return this.ids.size;\n }\n}\n\n/**\n * `setTimeout` wrapper that auto-forgets each timer once it fires and supports\n * bulk teardown on disconnect.\n *\n * @example\n * ```ts\n * #timers = new SafeTimeout();\n *\n * connect() {\n * this.#timers.set(() => this.dismiss(), 5000);\n * }\n *\n * disconnect() {\n * this.#timers.clearAll();\n * }\n * ```\n */\nexport class SafeTimeout extends TimerRegistry {\n /**\n * Schedules `callback` after `delay` ms and returns the timer id.\n *\n * The id is removed from the registry automatically when the timeout fires,\n * so {@link TimerRegistry.size | size} reflects only still-pending timers.\n */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(() => {\n this.ids.delete(id);\n callback();\n }, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setTimeout(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearTimeout(id);\n }\n}\n\n/**\n * `setInterval` wrapper that tracks every interval for bulk teardown on\n * disconnect. Unlike {@link SafeTimeout}, intervals are retained until they are\n * explicitly cleared because they fire repeatedly.\n *\n * @example\n * ```ts\n * #intervals = new SafeInterval();\n *\n * connect() {\n * this.#intervals.set(() => this.tick(), 1000);\n * }\n *\n * disconnect() {\n * this.#intervals.clearAll();\n * }\n * ```\n */\nexport class SafeInterval extends TimerRegistry {\n /** Schedules a repeating `callback` every `delay` ms and returns the timer id. */\n set(callback: () => void, delay: number): number {\n const id = this.schedule(callback, delay);\n this.ids.add(id);\n return id;\n }\n\n protected schedule(callback: () => void, delay: number): number {\n return window.setInterval(callback, delay);\n }\n\n protected cancel(id: number): void {\n window.clearInterval(id);\n }\n}\n","import { Controller } from \"@hotwired/stimulus\";\nimport { 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 /** 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.element.addEventListener(\"input\", this.#onFieldChange);\n this.element.addEventListener(\"change\", this.#onFieldChange);\n this.element.addEventListener(\"submit\", this.#onSubmit);\n this.element.addEventListener(\"formdata\", this.#onFormData);\n this.element.addEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.addEventListener(\"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.element.removeEventListener(\"input\", this.#onFieldChange);\n this.element.removeEventListener(\"change\", this.#onFieldChange);\n this.element.removeEventListener(\"submit\", this.#onSubmit);\n this.element.removeEventListener(\"formdata\", this.#onFormData);\n this.element.removeEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\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/safe_timeout.ts","../../src/controllers/dirty_form_controller.ts"],"names":[],"mappings":"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","file":"dirty_form_controller.js","sourcesContent":["/**\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 { 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 /** 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.element.addEventListener(\"input\", this.#onFieldChange);\n this.element.addEventListener(\"change\", this.#onFieldChange);\n this.element.addEventListener(\"submit\", this.#onSubmit);\n this.element.addEventListener(\"formdata\", this.#onFormData);\n this.element.addEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.addEventListener(\"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.element.removeEventListener(\"input\", this.#onFieldChange);\n this.element.removeEventListener(\"change\", this.#onFieldChange);\n this.element.removeEventListener(\"submit\", this.#onSubmit);\n this.element.removeEventListener(\"formdata\", this.#onFormData);\n this.element.removeEventListener(\"turbo:submit-start\", this.#onSubmitStart);\n this.element.removeEventListener(\"turbo:submit-end\", this.#onSubmitEnd);\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"]}