gina 0.6.31 → 0.6.32

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 (621) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +62 -25
  3. package/ROADMAP.md +3 -1
  4. package/framework/v0.6.32/VERSION +1 -0
  5. package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/js/gina.js +2389 -202
  6. package/framework/v0.6.32/core/asset/plugin/dist/vendor/gina/js/gina.min.js +716 -0
  7. package/framework/v0.6.32/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
  8. package/framework/v0.6.32/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
  9. package/framework/{v0.6.31 → v0.6.32}/core/config.js +45 -7
  10. package/framework/{v0.6.31 → v0.6.32}/core/connectors/couchbase/index.js +9 -3
  11. package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.js +164 -17
  12. package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.render-json.js +35 -1
  13. package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.render-nunjucks-async.js +20 -0
  14. package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.render-nunjucks.js +69 -6
  15. package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.render-swig-async.js +20 -0
  16. package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.render-swig.js +40 -0
  17. package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.render-xml.js +20 -0
  18. package/framework/{v0.6.31 → v0.6.32}/core/gna.js +50 -2
  19. package/framework/{v0.6.31 → v0.6.32}/core/model/entity.js +79 -28
  20. package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/validator/src/form-validator.js +32 -1
  21. package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/validator/src/main.js +1579 -55
  22. package/framework/{v0.6.31 → v0.6.32}/core/router.js +45 -1
  23. package/framework/{v0.6.31 → v0.6.32}/core/server.isaac.js +41 -5
  24. package/framework/{v0.6.31 → v0.6.32}/core/server.js +277 -464
  25. package/framework/{v0.6.31 → v0.6.32}/helpers/json/src/main.js +57 -4
  26. package/framework/v0.6.32/lib/duration/package.json +17 -0
  27. package/framework/v0.6.32/lib/duration/src/main.js +82 -0
  28. package/framework/{v0.6.31 → v0.6.32}/lib/index.js +18 -4
  29. package/framework/{v0.6.31 → v0.6.32}/lib/maintenance/src/main.js +387 -7
  30. package/framework/{v0.6.31 → v0.6.32}/lib/model.js +86 -1
  31. package/framework/v0.6.32/lib/session-lifetime/package.json +17 -0
  32. package/framework/v0.6.32/lib/session-lifetime/src/main.js +337 -0
  33. package/framework/{v0.6.31 → v0.6.32}/lib/storage/src/meta-store.js +45 -37
  34. package/framework/{v0.6.31 → v0.6.32}/lib/storage/src/util.js +8 -21
  35. package/framework/{v0.6.31 → v0.6.32}/package.json +1 -1
  36. package/gna.js +4 -4
  37. package/llms.txt +14 -14
  38. package/package.json +3 -3
  39. package/schema/security.json +35 -0
  40. package/schema/settings.json +13 -1
  41. package/script/smoke_ws_h2.js +353 -0
  42. package/test-results/validator-upload-popin-con-a5268-n-change-fires-RED-pre-fix--chromium/error-context.md +172 -0
  43. package/test-results/validator-upload-popin-con-cf673--the-page-form-RED-pre-fix--chromium/error-context.md +212 -0
  44. package/types/gna.d.ts +3 -2
  45. package/types/index.d.ts +32 -0
  46. package/framework/v0.6.31/VERSION +0 -1
  47. package/framework/v0.6.31/core/asset/plugin/dist/vendor/gina/js/gina.min.js +0 -673
  48. package/framework/v0.6.31/core/asset/plugin/dist/vendor/gina/js/gina.min.js.br +0 -0
  49. package/framework/v0.6.31/core/asset/plugin/dist/vendor/gina/js/gina.min.js.gz +0 -0
  50. package/test-results/validator-upload-preview-f-c70b4-t-in-the-DOM-or-on-the-wire-chromium/error-context.md +0 -187
  51. /package/framework/{v0.6.31 → v0.6.32}/AUTHORS +0 -0
  52. /package/framework/{v0.6.31 → v0.6.32}/LICENSE +0 -0
  53. /package/framework/{v0.6.31 → v0.6.32}/core/asset/html/nolayout.html +0 -0
  54. /package/framework/{v0.6.31 → v0.6.32}/core/asset/html/static.html +0 -0
  55. /package/framework/{v0.6.31 → v0.6.32}/core/asset/img/android-chrome-192x192.png +0 -0
  56. /package/framework/{v0.6.31 → v0.6.32}/core/asset/img/android-chrome-512x512.png +0 -0
  57. /package/framework/{v0.6.31 → v0.6.32}/core/asset/img/apple-touch-icon.png +0 -0
  58. /package/framework/{v0.6.31 → v0.6.32}/core/asset/img/favicon-16x16.png +0 -0
  59. /package/framework/{v0.6.31 → v0.6.32}/core/asset/img/favicon-32x32.png +0 -0
  60. /package/framework/{v0.6.31 → v0.6.32}/core/asset/img/favicon.ico +0 -0
  61. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/README.md +0 -0
  62. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.css +0 -0
  63. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/beemaster/beemaster.js +0 -0
  64. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/beemaster/index.html +0 -0
  65. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/css/gina.min.css +0 -0
  66. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.br +0 -0
  67. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/css/gina.min.css.gz +0 -0
  68. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/html/statusbar.html +0 -0
  69. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.br +0 -0
  70. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/html/statusbar.html.gz +0 -0
  71. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/inspector/have_heart_one-webfont.woff2 +0 -0
  72. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/inspector/index.html +0 -0
  73. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/inspector/inspector.css +0 -0
  74. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/inspector/inspector.js +0 -0
  75. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/inspector/logo.svg +0 -0
  76. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js +0 -0
  77. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.br +0 -0
  78. /package/framework/{v0.6.31 → v0.6.32}/core/asset/plugin/dist/vendor/gina/js/gina.onload.min.js.gz +0 -0
  79. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/ai/index.js +0 -0
  80. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/ai/lib/connector.js +0 -0
  81. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/couchbase/lib/connector.js +0 -0
  82. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/couchbase/lib/connector.v3.js +0 -0
  83. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/couchbase/lib/connector.v4.js +0 -0
  84. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/couchbase/lib/n1ql.js +0 -0
  85. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/couchbase/lib/session-store.js +0 -0
  86. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/couchbase/lib/session-store.v3.js +0 -0
  87. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/couchbase/lib/session-store.v4.js +0 -0
  88. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/couchbase/lib/storage-store.js +0 -0
  89. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/duckdb/index.js +0 -0
  90. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/duckdb/lib/connector.js +0 -0
  91. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/mongodb/index.js +0 -0
  92. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/mongodb/lib/connector.js +0 -0
  93. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/mongodb/lib/job-store.js +0 -0
  94. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/mongodb/lib/pipeline-loader.js +0 -0
  95. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/mongodb/lib/session-store.js +0 -0
  96. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/mysql/index.js +0 -0
  97. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/mysql/lib/connector.js +0 -0
  98. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/param-redact.js +0 -0
  99. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/postgresql/index.js +0 -0
  100. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/postgresql/lib/connector.js +0 -0
  101. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/redis/index.js +0 -0
  102. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/redis/lib/connector.js +0 -0
  103. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/redis/lib/job-store.js +0 -0
  104. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/redis/lib/kv-store.js +0 -0
  105. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/redis/lib/render-cache-store.js +0 -0
  106. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/redis/lib/session-store.js +0 -0
  107. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/scylladb/index.js +0 -0
  108. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/scylladb/lib/connector.js +0 -0
  109. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/scylladb/lib/session-store.js +0 -0
  110. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/settle-once.js +0 -0
  111. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/sql-parser.js +0 -0
  112. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/sqlite/index.js +0 -0
  113. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/sqlite/lib/connector.js +0 -0
  114. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/sqlite/lib/job-store.js +0 -0
  115. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/sqlite/lib/kv-store.js +0 -0
  116. /package/framework/{v0.6.31 → v0.6.32}/core/connectors/sqlite/lib/session-store.js +0 -0
  117. /package/framework/{v0.6.31 → v0.6.32}/core/content.encoding +0 -0
  118. /package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.framework.js +0 -0
  119. /package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.render-stream.js +0 -0
  120. /package/framework/{v0.6.31 → v0.6.32}/core/controller/controller.render-v1.js +0 -0
  121. /package/framework/{v0.6.31 → v0.6.32}/core/controller/index.js +0 -0
  122. /package/framework/{v0.6.31 → v0.6.32}/core/controller/inline-script.js +0 -0
  123. /package/framework/{v0.6.31 → v0.6.32}/core/controller/inspector-window-emit.js +0 -0
  124. /package/framework/{v0.6.31 → v0.6.32}/core/controller/release-banner.js +0 -0
  125. /package/framework/{v0.6.31 → v0.6.32}/core/dev/index.js +0 -0
  126. /package/framework/{v0.6.31 → v0.6.32}/core/dev/lib/class.js +0 -0
  127. /package/framework/{v0.6.31 → v0.6.32}/core/dev/lib/factory.js +0 -0
  128. /package/framework/{v0.6.31 → v0.6.32}/core/dev/lib/tools.js +0 -0
  129. /package/framework/{v0.6.31 → v0.6.32}/core/locales/README.md +0 -0
  130. /package/framework/{v0.6.31 → v0.6.32}/core/locales/currency.json +0 -0
  131. /package/framework/{v0.6.31 → v0.6.32}/core/locales/dist/language/en.json +0 -0
  132. /package/framework/{v0.6.31 → v0.6.32}/core/locales/dist/language/fr.json +0 -0
  133. /package/framework/{v0.6.31 → v0.6.32}/core/locales/dist/region/en.json +0 -0
  134. /package/framework/{v0.6.31 → v0.6.32}/core/locales/dist/region/fr.json +0 -0
  135. /package/framework/{v0.6.31 → v0.6.32}/core/locales/index.js +0 -0
  136. /package/framework/{v0.6.31 → v0.6.32}/core/mime.types +0 -0
  137. /package/framework/{v0.6.31 → v0.6.32}/core/model/index.js +0 -0
  138. /package/framework/{v0.6.31 → v0.6.32}/core/model/template/entityFactory.js +0 -0
  139. /package/framework/{v0.6.31 → v0.6.32}/core/model/template/index.js +0 -0
  140. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/README.md +0 -0
  141. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/index.js +0 -0
  142. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/csrf/README.md +0 -0
  143. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/csrf/package.json +0 -0
  144. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/csrf/src/main.js +0 -0
  145. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/README.md +0 -0
  146. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/coep/README.md +0 -0
  147. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/coep/package.json +0 -0
  148. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/coep/src/main.js +0 -0
  149. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/coop/README.md +0 -0
  150. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/coop/package.json +0 -0
  151. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/coop/src/main.js +0 -0
  152. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/corp/README.md +0 -0
  153. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/corp/package.json +0 -0
  154. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/corp/src/main.js +0 -0
  155. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/csp/README.md +0 -0
  156. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/csp/package.json +0 -0
  157. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/csp/src/main.js +0 -0
  158. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/hide-powered-by/README.md +0 -0
  159. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/hide-powered-by/package.json +0 -0
  160. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/hide-powered-by/src/main.js +0 -0
  161. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/hsts/README.md +0 -0
  162. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/hsts/package.json +0 -0
  163. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/hsts/src/main.js +0 -0
  164. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/origin-agent-cluster/README.md +0 -0
  165. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/origin-agent-cluster/package.json +0 -0
  166. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/origin-agent-cluster/src/main.js +0 -0
  167. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/package.json +0 -0
  168. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/referrer-policy/README.md +0 -0
  169. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/referrer-policy/package.json +0 -0
  170. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/referrer-policy/src/main.js +0 -0
  171. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/src/main.js +0 -0
  172. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-content-type-options/README.md +0 -0
  173. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-content-type-options/package.json +0 -0
  174. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-content-type-options/src/main.js +0 -0
  175. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-dns-prefetch-control/README.md +0 -0
  176. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-dns-prefetch-control/package.json +0 -0
  177. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-dns-prefetch-control/src/main.js +0 -0
  178. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-download-options/README.md +0 -0
  179. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-download-options/package.json +0 -0
  180. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-download-options/src/main.js +0 -0
  181. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-frame-options/README.md +0 -0
  182. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-frame-options/package.json +0 -0
  183. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-frame-options/src/main.js +0 -0
  184. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/README.md +0 -0
  185. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/package.json +0 -0
  186. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-permitted-cross-domain-policies/src/main.js +0 -0
  187. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-xss-protection/README.md +0 -0
  188. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-xss-protection/package.json +0 -0
  189. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/security-headers/x-xss-protection/src/main.js +0 -0
  190. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/session/README.md +0 -0
  191. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/session/package.json +0 -0
  192. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/session/src/main.js +0 -0
  193. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/storage/README.md +0 -0
  194. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/storage/build.json +0 -0
  195. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/storage/package.json +0 -0
  196. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/storage/src/main.js +0 -0
  197. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/validator/README.md +0 -0
  198. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/validator/build.json +0 -0
  199. /package/framework/{v0.6.31 → v0.6.32}/core/plugins/lib/validator/package.json +0 -0
  200. /package/framework/{v0.6.31 → v0.6.32}/core/server.express.js +0 -0
  201. /package/framework/{v0.6.31 → v0.6.32}/core/status.codes +0 -0
  202. /package/framework/{v0.6.31 → v0.6.32}/core/template/_gitignore +0 -0
  203. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/config/app.json +0 -0
  204. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/config/connectors.json +0 -0
  205. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/config/routing.json +0 -0
  206. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/config/settings.json +0 -0
  207. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/config/settings.server.json +0 -0
  208. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/config/templates.json +0 -0
  209. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/config/watchers.json +0 -0
  210. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/controllers/controller.content.js +0 -0
  211. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/controllers/controller.js +0 -0
  212. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/controllers/setup.js +0 -0
  213. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/index.js +0 -0
  214. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle/locales/en.json +0 -0
  215. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_namespace/controllers/controller.js +0 -0
  216. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_public/css/default.css +0 -0
  217. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_public/css/home.css +0 -0
  218. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_public/css/vendor/readme.md +0 -0
  219. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_public/favicon.ico +0 -0
  220. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_public/js/components/x-checklist.js +0 -0
  221. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_public/js/vendor/readme.md +0 -0
  222. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_public/manifest.webmanifest +0 -0
  223. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_public/readme.md +0 -0
  224. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_public/sw.js +0 -0
  225. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_templates/handlers/main.js +0 -0
  226. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_templates/html/content/homepage.html +0 -0
  227. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_templates/html/includes/error-msg-noscript.html +0 -0
  228. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_templates/html/includes/error-msg-outdated-browser.html +0 -0
  229. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_templates/html/includes/x-checklist.html +0 -0
  230. /package/framework/{v0.6.31 → v0.6.32}/core/template/boilerplate/bundle_templates/html/layouts/main.html +0 -0
  231. /package/framework/{v0.6.31 → v0.6.32}/core/template/command/gina.bat.tpl +0 -0
  232. /package/framework/{v0.6.31 → v0.6.32}/core/template/command/gina.tpl +0 -0
  233. /package/framework/{v0.6.31 → v0.6.32}/core/template/conf/env.json +0 -0
  234. /package/framework/{v0.6.31 → v0.6.32}/core/template/conf/manifest.json +0 -0
  235. /package/framework/{v0.6.31 → v0.6.32}/core/template/conf/package.json +0 -0
  236. /package/framework/{v0.6.31 → v0.6.32}/core/template/conf/settings.json +0 -0
  237. /package/framework/{v0.6.31 → v0.6.32}/core/template/conf/statics.json +0 -0
  238. /package/framework/{v0.6.31 → v0.6.32}/core/template/conf/templates.json +0 -0
  239. /package/framework/{v0.6.31 → v0.6.32}/core/template/error/client/json/401.json +0 -0
  240. /package/framework/{v0.6.31 → v0.6.32}/core/template/error/client/json/403.json +0 -0
  241. /package/framework/{v0.6.31 → v0.6.32}/core/template/error/client/json/404.json +0 -0
  242. /package/framework/{v0.6.31 → v0.6.32}/core/template/error/server/html/50x.html +0 -0
  243. /package/framework/{v0.6.31 → v0.6.32}/core/template/error/server/json/500.json +0 -0
  244. /package/framework/{v0.6.31 → v0.6.32}/core/template/error/server/json/503.json +0 -0
  245. /package/framework/{v0.6.31 → v0.6.32}/core/template/extensions/logger/config.json +0 -0
  246. /package/framework/{v0.6.31 → v0.6.32}/helpers/console.js +0 -0
  247. /package/framework/{v0.6.31 → v0.6.32}/helpers/context.js +0 -0
  248. /package/framework/{v0.6.31 → v0.6.32}/helpers/data/LICENSE +0 -0
  249. /package/framework/{v0.6.31 → v0.6.32}/helpers/data/README.md +0 -0
  250. /package/framework/{v0.6.31 → v0.6.32}/helpers/data/package.json +0 -0
  251. /package/framework/{v0.6.31 → v0.6.32}/helpers/data/src/main.js +0 -0
  252. /package/framework/{v0.6.31 → v0.6.32}/helpers/dateFormat.js +0 -0
  253. /package/framework/{v0.6.31 → v0.6.32}/helpers/index.js +0 -0
  254. /package/framework/{v0.6.31 → v0.6.32}/helpers/json/LICENSE +0 -0
  255. /package/framework/{v0.6.31 → v0.6.32}/helpers/json/README.md +0 -0
  256. /package/framework/{v0.6.31 → v0.6.32}/helpers/json/package.json +0 -0
  257. /package/framework/{v0.6.31 → v0.6.32}/helpers/path.js +0 -0
  258. /package/framework/{v0.6.31 → v0.6.32}/helpers/plugins/README.md +0 -0
  259. /package/framework/{v0.6.31 → v0.6.32}/helpers/plugins/package.json +0 -0
  260. /package/framework/{v0.6.31 → v0.6.32}/helpers/plugins/src/api-error.js +0 -0
  261. /package/framework/{v0.6.31 → v0.6.32}/helpers/plugins/src/main.js +0 -0
  262. /package/framework/{v0.6.31 → v0.6.32}/helpers/prototypes.js +0 -0
  263. /package/framework/{v0.6.31 → v0.6.32}/helpers/task.js +0 -0
  264. /package/framework/{v0.6.31 → v0.6.32}/helpers/text.js +0 -0
  265. /package/framework/{v0.6.31 → v0.6.32}/lib/admin/package.json +0 -0
  266. /package/framework/{v0.6.31 → v0.6.32}/lib/admin/src/main.js +0 -0
  267. /package/framework/{v0.6.31 → v0.6.32}/lib/archiver/README.md +0 -0
  268. /package/framework/{v0.6.31 → v0.6.32}/lib/archiver/build.json +0 -0
  269. /package/framework/{v0.6.31 → v0.6.32}/lib/archiver/package.json +0 -0
  270. /package/framework/{v0.6.31 → v0.6.32}/lib/archiver/src/dep/jszip.min.js +0 -0
  271. /package/framework/{v0.6.31 → v0.6.32}/lib/archiver/src/main.js +0 -0
  272. /package/framework/{v0.6.31 → v0.6.32}/lib/async/package.json +0 -0
  273. /package/framework/{v0.6.31 → v0.6.32}/lib/async/src/main.js +0 -0
  274. /package/framework/{v0.6.31 → v0.6.32}/lib/audit/package.json +0 -0
  275. /package/framework/{v0.6.31 → v0.6.32}/lib/audit/src/main.js +0 -0
  276. /package/framework/{v0.6.31 → v0.6.32}/lib/audit-store.js +0 -0
  277. /package/framework/{v0.6.31 → v0.6.32}/lib/authn/package.json +0 -0
  278. /package/framework/{v0.6.31 → v0.6.32}/lib/authn/src/lockout.js +0 -0
  279. /package/framework/{v0.6.31 → v0.6.32}/lib/authn/src/main.js +0 -0
  280. /package/framework/{v0.6.31 → v0.6.32}/lib/authn/src/totp.js +0 -0
  281. /package/framework/{v0.6.31 → v0.6.32}/lib/authz-gate/package.json +0 -0
  282. /package/framework/{v0.6.31 → v0.6.32}/lib/authz-gate/src/main.js +0 -0
  283. /package/framework/{v0.6.31 → v0.6.32}/lib/cache/README.md +0 -0
  284. /package/framework/{v0.6.31 → v0.6.32}/lib/cache/build.json +0 -0
  285. /package/framework/{v0.6.31 → v0.6.32}/lib/cache/package.json +0 -0
  286. /package/framework/{v0.6.31 → v0.6.32}/lib/cache/src/main.js +0 -0
  287. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/aliases.json +0 -0
  288. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/audit/arguments.json +0 -0
  289. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/audit/help.txt +0 -0
  290. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/audit/verify.js +0 -0
  291. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/add.js +0 -0
  292. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/arguments.json +0 -0
  293. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/build.js +0 -0
  294. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/copy.js +0 -0
  295. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/cp.js +0 -0
  296. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/help.js +0 -0
  297. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/help.txt +0 -0
  298. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/inc/name-rewrite.js +0 -0
  299. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/list.js +0 -0
  300. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/man.js +0 -0
  301. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/mcp-start.js +0 -0
  302. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/mcp.js +0 -0
  303. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/oas.js +0 -0
  304. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/openapi.js +0 -0
  305. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/remove.js +0 -0
  306. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/rename.js +0 -0
  307. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/restart.js +0 -0
  308. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/rm.js +0 -0
  309. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/start.js +0 -0
  310. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/status.js +0 -0
  311. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/stop.js +0 -0
  312. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/bundle/types.js +0 -0
  313. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/cache/arguments.json +0 -0
  314. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/cache/clear.js +0 -0
  315. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/cache/help.txt +0 -0
  316. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/cache/stats.js +0 -0
  317. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/add.js +0 -0
  318. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/arguments.json +0 -0
  319. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/help.js +0 -0
  320. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/help.txt +0 -0
  321. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/infer.js +0 -0
  322. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/list.js +0 -0
  323. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/migrate.js +0 -0
  324. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/models.js +0 -0
  325. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/remove.js +0 -0
  326. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/rm.js +0 -0
  327. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/connector/test.js +0 -0
  328. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/container/arguments.json +0 -0
  329. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/container/help.js +0 -0
  330. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/container/help.txt +0 -0
  331. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/container/man.js +0 -0
  332. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/container/ps.js +0 -0
  333. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/container/stop.js +0 -0
  334. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/add.js +0 -0
  335. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/arguments.json +0 -0
  336. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/help.txt +0 -0
  337. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/inc/args.js +0 -0
  338. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/inc/namespace.js +0 -0
  339. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/inc/reference-rewrite.js +0 -0
  340. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/inc/reference-scan.js +0 -0
  341. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/inc/scaffold.js +0 -0
  342. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/remove.js +0 -0
  343. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/rename.js +0 -0
  344. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/controller/rm.js +0 -0
  345. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/add.js +0 -0
  346. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/get.js +0 -0
  347. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/help.js +0 -0
  348. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/help.txt +0 -0
  349. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/link-dev.js +0 -0
  350. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/list.js +0 -0
  351. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/remove.js +0 -0
  352. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/rm.js +0 -0
  353. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/set.js +0 -0
  354. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/unset.js +0 -0
  355. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/env/use.js +0 -0
  356. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/add.js +0 -0
  357. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/arguments.json +0 -0
  358. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/build.js +0 -0
  359. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/dot.js +0 -0
  360. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/get.js +0 -0
  361. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/help.js +0 -0
  362. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/help.txt +0 -0
  363. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/init.js +0 -0
  364. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/link-node-modules.js +0 -0
  365. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/link.js +0 -0
  366. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/list.js +0 -0
  367. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/man.js +0 -0
  368. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/msg.json +0 -0
  369. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/open.js +0 -0
  370. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/remove.js +0 -0
  371. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/reset.js +0 -0
  372. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/restart.js +0 -0
  373. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/set.js +0 -0
  374. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/start.js +0 -0
  375. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/status.js +0 -0
  376. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/stop.js +0 -0
  377. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/tail.js +0 -0
  378. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/update.js +0 -0
  379. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/framework/version.js +0 -0
  380. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/gina-dev.1.md +0 -0
  381. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/gina-framework.1.md +0 -0
  382. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/gina.1.md +0 -0
  383. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/helper.js +0 -0
  384. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/i18n/add.js +0 -0
  385. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/i18n/arguments.json +0 -0
  386. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/i18n/export.js +0 -0
  387. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/i18n/help.js +0 -0
  388. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/i18n/help.txt +0 -0
  389. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/i18n/import.js +0 -0
  390. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/i18n/scan.js +0 -0
  391. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/image/_host.js +0 -0
  392. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/image/arguments.json +0 -0
  393. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/image/build.js +0 -0
  394. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/image/help.js +0 -0
  395. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/image/help.txt +0 -0
  396. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/image/list.js +0 -0
  397. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/image/man.js +0 -0
  398. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/image/rm.js +0 -0
  399. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/image/run.js +0 -0
  400. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/index.js +0 -0
  401. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/inspector/help.js +0 -0
  402. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/inspector/help.txt +0 -0
  403. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/inspector/open.js +0 -0
  404. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/man-render.js +0 -0
  405. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/minion/arguments.json +0 -0
  406. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/minion/help.js +0 -0
  407. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/minion/help.txt +0 -0
  408. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/minion/kill.js +0 -0
  409. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/minion/list.js +0 -0
  410. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/msg.json +0 -0
  411. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/port/help.js +0 -0
  412. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/port/help.txt +0 -0
  413. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/port/inc/scan.js +0 -0
  414. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/port/list.js +0 -0
  415. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/port/reset.js +0 -0
  416. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/port/set.js +0 -0
  417. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/add.js +0 -0
  418. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/arguments.json +0 -0
  419. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/backup.js +0 -0
  420. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/build.js +0 -0
  421. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/help.js +0 -0
  422. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/help.txt +0 -0
  423. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/import.js +0 -0
  424. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/list.js +0 -0
  425. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/man.js +0 -0
  426. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/move.js +0 -0
  427. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/remove.js +0 -0
  428. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/rename.js +0 -0
  429. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/restart.js +0 -0
  430. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/restore.js +0 -0
  431. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/rm.js +0 -0
  432. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/start.js +0 -0
  433. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/status.js +0 -0
  434. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/project/stop.js +0 -0
  435. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/protocol/arguments.json +0 -0
  436. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/protocol/help.js +0 -0
  437. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/protocol/help.txt +0 -0
  438. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/protocol/list.js +0 -0
  439. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/protocol/remove.js +0 -0
  440. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/protocol/set.js +0 -0
  441. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/scope/add.js +0 -0
  442. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/scope/help.js +0 -0
  443. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/scope/help.txt +0 -0
  444. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/scope/link-local.js +0 -0
  445. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/scope/link-production.js +0 -0
  446. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/scope/list.js +0 -0
  447. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/scope/remove.js +0 -0
  448. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/scope/rm.js +0 -0
  449. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/scope/use.js +0 -0
  450. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/secrets/arguments.json +0 -0
  451. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/secrets/check.js +0 -0
  452. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/secrets/help.js +0 -0
  453. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/secrets/help.txt +0 -0
  454. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/secrets/scan.js +0 -0
  455. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/service/help.js +0 -0
  456. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/service/help.txt +0 -0
  457. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/service/list.js +0 -0
  458. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/service/man.js +0 -0
  459. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/service/start.js +0 -0
  460. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/storage/arguments.json +0 -0
  461. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/storage/gc.js +0 -0
  462. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/storage/help.txt +0 -0
  463. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/storage/stats.js +0 -0
  464. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/storage/verify.js +0 -0
  465. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd/view/add.js +0 -0
  466. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd-status-format/package.json +0 -0
  467. /package/framework/{v0.6.31 → v0.6.32}/lib/cmd-status-format/src/main.js +0 -0
  468. /package/framework/{v0.6.31 → v0.6.32}/lib/collection/README.md +0 -0
  469. /package/framework/{v0.6.31 → v0.6.32}/lib/collection/build.json +0 -0
  470. /package/framework/{v0.6.31 → v0.6.32}/lib/collection/package.json +0 -0
  471. /package/framework/{v0.6.31 → v0.6.32}/lib/collection/src/main.js +0 -0
  472. /package/framework/{v0.6.31 → v0.6.32}/lib/conf-view/package.json +0 -0
  473. /package/framework/{v0.6.31 → v0.6.32}/lib/conf-view/src/main.js +0 -0
  474. /package/framework/{v0.6.31 → v0.6.32}/lib/config.js +0 -0
  475. /package/framework/{v0.6.31 → v0.6.32}/lib/connector-config/package.json +0 -0
  476. /package/framework/{v0.6.31 → v0.6.32}/lib/connector-config/src/main.js +0 -0
  477. /package/framework/{v0.6.31 → v0.6.32}/lib/connector-error/package.json +0 -0
  478. /package/framework/{v0.6.31 → v0.6.32}/lib/connector-error/src/main.js +0 -0
  479. /package/framework/{v0.6.31 → v0.6.32}/lib/connector-registry/package.json +0 -0
  480. /package/framework/{v0.6.31 → v0.6.32}/lib/connector-registry/src/main.js +0 -0
  481. /package/framework/{v0.6.31 → v0.6.32}/lib/cron/README.md +0 -0
  482. /package/framework/{v0.6.31 → v0.6.32}/lib/cron/package.json +0 -0
  483. /package/framework/{v0.6.31 → v0.6.32}/lib/cron/src/main.js +0 -0
  484. /package/framework/{v0.6.31 → v0.6.32}/lib/domain/LICENSE +0 -0
  485. /package/framework/{v0.6.31 → v0.6.32}/lib/domain/README.md +0 -0
  486. /package/framework/{v0.6.31 → v0.6.32}/lib/domain/package.json +0 -0
  487. /package/framework/{v0.6.31 → v0.6.32}/lib/domain/src/main.js +0 -0
  488. /package/framework/{v0.6.31 → v0.6.32}/lib/dto/package.json +0 -0
  489. /package/framework/{v0.6.31 → v0.6.32}/lib/dto/src/main.js +0 -0
  490. /package/framework/{v0.6.31 → v0.6.32}/lib/dto-pipe/package.json +0 -0
  491. /package/framework/{v0.6.31 → v0.6.32}/lib/dto-pipe/src/main.js +0 -0
  492. /package/framework/{v0.6.31 → v0.6.32}/lib/dto-types/package.json +0 -0
  493. /package/framework/{v0.6.31 → v0.6.32}/lib/dto-types/src/main.js +0 -0
  494. /package/framework/{v0.6.31 → v0.6.32}/lib/generator/index.js +0 -0
  495. /package/framework/{v0.6.31 → v0.6.32}/lib/i18n/package.json +0 -0
  496. /package/framework/{v0.6.31 → v0.6.32}/lib/i18n/src/main.js +0 -0
  497. /package/framework/{v0.6.31 → v0.6.32}/lib/idempotency/package.json +0 -0
  498. /package/framework/{v0.6.31 → v0.6.32}/lib/idempotency/src/main.js +0 -0
  499. /package/framework/{v0.6.31 → v0.6.32}/lib/image-build/package.json +0 -0
  500. /package/framework/{v0.6.31 → v0.6.32}/lib/image-build/src/main.js +0 -0
  501. /package/framework/{v0.6.31 → v0.6.32}/lib/inherits/LICENSE +0 -0
  502. /package/framework/{v0.6.31 → v0.6.32}/lib/inherits/README.md +0 -0
  503. /package/framework/{v0.6.31 → v0.6.32}/lib/inherits/package.json +0 -0
  504. /package/framework/{v0.6.31 → v0.6.32}/lib/inherits/src/main.js +0 -0
  505. /package/framework/{v0.6.31 → v0.6.32}/lib/inspector-events/package.json +0 -0
  506. /package/framework/{v0.6.31 → v0.6.32}/lib/inspector-events/src/main.js +0 -0
  507. /package/framework/{v0.6.31 → v0.6.32}/lib/inspector-redact/package.json +0 -0
  508. /package/framework/{v0.6.31 → v0.6.32}/lib/inspector-redact/src/main.js +0 -0
  509. /package/framework/{v0.6.31 → v0.6.32}/lib/instrument/package.json +0 -0
  510. /package/framework/{v0.6.31 → v0.6.32}/lib/instrument/src/main.js +0 -0
  511. /package/framework/{v0.6.31 → v0.6.32}/lib/job/package.json +0 -0
  512. /package/framework/{v0.6.31 → v0.6.32}/lib/job/src/main.js +0 -0
  513. /package/framework/{v0.6.31 → v0.6.32}/lib/job-store.js +0 -0
  514. /package/framework/{v0.6.31 → v0.6.32}/lib/json-config-header/package.json +0 -0
  515. /package/framework/{v0.6.31 → v0.6.32}/lib/json-config-header/src/main.js +0 -0
  516. /package/framework/{v0.6.31 → v0.6.32}/lib/kv/package.json +0 -0
  517. /package/framework/{v0.6.31 → v0.6.32}/lib/kv/src/main.js +0 -0
  518. /package/framework/{v0.6.31 → v0.6.32}/lib/kv-store.js +0 -0
  519. /package/framework/{v0.6.31 → v0.6.32}/lib/loading-state/package.json +0 -0
  520. /package/framework/{v0.6.31 → v0.6.32}/lib/loading-state/src/main.js +0 -0
  521. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/README.md +0 -0
  522. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/package.json +0 -0
  523. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/src/containers/default/index.js +0 -0
  524. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/src/containers/file/index.js +0 -0
  525. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/src/containers/mq/index.js +0 -0
  526. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/src/containers/mq/listener.js +0 -0
  527. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/src/containers/mq/speaker.js +0 -0
  528. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/src/helper.js +0 -0
  529. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/src/main.js +0 -0
  530. /package/framework/{v0.6.31 → v0.6.32}/lib/logger/src/redact.js +0 -0
  531. /package/framework/{v0.6.31 → v0.6.32}/lib/maintenance/package.json +0 -0
  532. /package/framework/{v0.6.31 → v0.6.32}/lib/math/index.js +0 -0
  533. /package/framework/{v0.6.31 → v0.6.32}/lib/mcp-dispatch/package.json +0 -0
  534. /package/framework/{v0.6.31 → v0.6.32}/lib/mcp-dispatch/src/main.js +0 -0
  535. /package/framework/{v0.6.31 → v0.6.32}/lib/mcp-http/package.json +0 -0
  536. /package/framework/{v0.6.31 → v0.6.32}/lib/mcp-http/src/main.js +0 -0
  537. /package/framework/{v0.6.31 → v0.6.32}/lib/mcp-server/package.json +0 -0
  538. /package/framework/{v0.6.31 → v0.6.32}/lib/mcp-server/src/main.js +0 -0
  539. /package/framework/{v0.6.31 → v0.6.32}/lib/merge/README.md +0 -0
  540. /package/framework/{v0.6.31 → v0.6.32}/lib/merge/package.json +0 -0
  541. /package/framework/{v0.6.31 → v0.6.32}/lib/merge/src/main.js +0 -0
  542. /package/framework/{v0.6.31 → v0.6.32}/lib/message-validator/package.json +0 -0
  543. /package/framework/{v0.6.31 → v0.6.32}/lib/message-validator/src/main.js +0 -0
  544. /package/framework/{v0.6.31 → v0.6.32}/lib/metrics/package.json +0 -0
  545. /package/framework/{v0.6.31 → v0.6.32}/lib/metrics/src/main.js +0 -0
  546. /package/framework/{v0.6.31 → v0.6.32}/lib/money/package.json +0 -0
  547. /package/framework/{v0.6.31 → v0.6.32}/lib/money/src/main.js +0 -0
  548. /package/framework/{v0.6.31 → v0.6.32}/lib/multipart/package.json +0 -0
  549. /package/framework/{v0.6.31 → v0.6.32}/lib/multipart/src/main.js +0 -0
  550. /package/framework/{v0.6.31 → v0.6.32}/lib/net-locality/package.json +0 -0
  551. /package/framework/{v0.6.31 → v0.6.32}/lib/net-locality/src/main.js +0 -0
  552. /package/framework/{v0.6.31 → v0.6.32}/lib/nunjucks-filters/README.md +0 -0
  553. /package/framework/{v0.6.31 → v0.6.32}/lib/nunjucks-filters/package.json +0 -0
  554. /package/framework/{v0.6.31 → v0.6.32}/lib/nunjucks-filters/src/main.js +0 -0
  555. /package/framework/{v0.6.31 → v0.6.32}/lib/nunjucks-resolver/package.json +0 -0
  556. /package/framework/{v0.6.31 → v0.6.32}/lib/nunjucks-resolver/src/main.js +0 -0
  557. /package/framework/{v0.6.31 → v0.6.32}/lib/priority/package.json +0 -0
  558. /package/framework/{v0.6.31 → v0.6.32}/lib/priority/src/main.js +0 -0
  559. /package/framework/{v0.6.31 → v0.6.32}/lib/proc.js +0 -0
  560. /package/framework/{v0.6.31 → v0.6.32}/lib/push/package.json +0 -0
  561. /package/framework/{v0.6.31 → v0.6.32}/lib/push/src/main.js +0 -0
  562. /package/framework/{v0.6.31 → v0.6.32}/lib/rate-limit/package.json +0 -0
  563. /package/framework/{v0.6.31 → v0.6.32}/lib/rate-limit/src/main.js +0 -0
  564. /package/framework/{v0.6.31 → v0.6.32}/lib/release-watch/package.json +0 -0
  565. /package/framework/{v0.6.31 → v0.6.32}/lib/release-watch/src/main.js +0 -0
  566. /package/framework/{v0.6.31 → v0.6.32}/lib/render-cache/package.json +0 -0
  567. /package/framework/{v0.6.31 → v0.6.32}/lib/render-cache/src/main.js +0 -0
  568. /package/framework/{v0.6.31 → v0.6.32}/lib/render-cache-store.js +0 -0
  569. /package/framework/{v0.6.31 → v0.6.32}/lib/routing/README.md +0 -0
  570. /package/framework/{v0.6.31 → v0.6.32}/lib/routing/build.json +0 -0
  571. /package/framework/{v0.6.31 → v0.6.32}/lib/routing/package.json +0 -0
  572. /package/framework/{v0.6.31 → v0.6.32}/lib/routing/src/main.js +0 -0
  573. /package/framework/{v0.6.31 → v0.6.32}/lib/routing/src/radix.js +0 -0
  574. /package/framework/{v0.6.31 → v0.6.32}/lib/routing-introspect/package.json +0 -0
  575. /package/framework/{v0.6.31 → v0.6.32}/lib/routing-introspect/src/main.js +0 -0
  576. /package/framework/{v0.6.31 → v0.6.32}/lib/secrets/package.json +0 -0
  577. /package/framework/{v0.6.31 → v0.6.32}/lib/secrets/src/backends/env.js +0 -0
  578. /package/framework/{v0.6.31 → v0.6.32}/lib/secrets/src/backends/exec.js +0 -0
  579. /package/framework/{v0.6.31 → v0.6.32}/lib/secrets/src/backends/file.js +0 -0
  580. /package/framework/{v0.6.31 → v0.6.32}/lib/secrets/src/declaration.js +0 -0
  581. /package/framework/{v0.6.31 → v0.6.32}/lib/secrets/src/env-file.js +0 -0
  582. /package/framework/{v0.6.31 → v0.6.32}/lib/secrets/src/main.js +0 -0
  583. /package/framework/{v0.6.31 → v0.6.32}/lib/secrets/src/sources.js +0 -0
  584. /package/framework/{v0.6.31 → v0.6.32}/lib/security-headers-emitter/package.json +0 -0
  585. /package/framework/{v0.6.31 → v0.6.32}/lib/security-headers-emitter/src/main.js +0 -0
  586. /package/framework/{v0.6.31 → v0.6.32}/lib/session-store.js +0 -0
  587. /package/framework/{v0.6.31 → v0.6.32}/lib/shell.js +0 -0
  588. /package/framework/{v0.6.31 → v0.6.32}/lib/sqlite-driver.js +0 -0
  589. /package/framework/{v0.6.31 → v0.6.32}/lib/sri/package.json +0 -0
  590. /package/framework/{v0.6.31 → v0.6.32}/lib/sri/src/main.js +0 -0
  591. /package/framework/{v0.6.31 → v0.6.32}/lib/state.js +0 -0
  592. /package/framework/{v0.6.31 → v0.6.32}/lib/storage/package.json +0 -0
  593. /package/framework/{v0.6.31 → v0.6.32}/lib/storage/src/local-cas.js +0 -0
  594. /package/framework/{v0.6.31 → v0.6.32}/lib/storage/src/local-stream.js +0 -0
  595. /package/framework/{v0.6.31 → v0.6.32}/lib/storage/src/local.js +0 -0
  596. /package/framework/{v0.6.31 → v0.6.32}/lib/storage/src/main.js +0 -0
  597. /package/framework/{v0.6.31 → v0.6.32}/lib/storage/src/s3.js +0 -0
  598. /package/framework/{v0.6.31 → v0.6.32}/lib/storage-store.js +0 -0
  599. /package/framework/{v0.6.31 → v0.6.32}/lib/swig-filters/README.md +0 -0
  600. /package/framework/{v0.6.31 → v0.6.32}/lib/swig-filters/package.json +0 -0
  601. /package/framework/{v0.6.31 → v0.6.32}/lib/swig-filters/src/main.js +0 -0
  602. /package/framework/{v0.6.31 → v0.6.32}/lib/swig-resolver/package.json +0 -0
  603. /package/framework/{v0.6.31 → v0.6.32}/lib/swig-resolver/src/main.js +0 -0
  604. /package/framework/{v0.6.31 → v0.6.32}/lib/template-loaders/package.json +0 -0
  605. /package/framework/{v0.6.31 → v0.6.32}/lib/template-loaders/src/loaders/http.js +0 -0
  606. /package/framework/{v0.6.31 → v0.6.32}/lib/template-loaders/src/loaders/memory.js +0 -0
  607. /package/framework/{v0.6.31 → v0.6.32}/lib/template-loaders/src/main.js +0 -0
  608. /package/framework/{v0.6.31 → v0.6.32}/lib/url/README.md +0 -0
  609. /package/framework/{v0.6.31 → v0.6.32}/lib/url/index.js +0 -0
  610. /package/framework/{v0.6.31 → v0.6.32}/lib/url/routing.json +0 -0
  611. /package/framework/{v0.6.31 → v0.6.32}/lib/uuid/package.json +0 -0
  612. /package/framework/{v0.6.31 → v0.6.32}/lib/uuid/src/main.js +0 -0
  613. /package/framework/{v0.6.31 → v0.6.32}/lib/validator.js +0 -0
  614. /package/framework/{v0.6.31 → v0.6.32}/lib/watcher/package.json +0 -0
  615. /package/framework/{v0.6.31 → v0.6.32}/lib/watcher/src/main.js +0 -0
  616. /package/framework/{v0.6.31 → v0.6.32}/lib/ws-framing/package.json +0 -0
  617. /package/framework/{v0.6.31 → v0.6.32}/lib/ws-framing/src/main.js +0 -0
  618. /package/framework/{v0.6.31 → v0.6.32}/lib/ws-query/package.json +0 -0
  619. /package/framework/{v0.6.31 → v0.6.32}/lib/ws-query/src/main.js +0 -0
  620. /package/framework/{v0.6.31 → v0.6.32}/lib/ws-session/package.json +0 -0
  621. /package/framework/{v0.6.31 → v0.6.32}/lib/ws-session/src/main.js +0 -0
@@ -5579,7 +5579,38 @@ function FormValidatorUtil(data, $fields, xhrOptions, fieldsSet, culture) {
5579
5579
  ) {
5580
5580
  date = new Date(NaN);
5581
5581
  }
5582
- this.value = local.data[this.name] = date;
5582
+ // #B558 (2026-09-18) — the FIELD keeps the Date, the PAYLOAD
5583
+ // takes a date-only string. A Date is the one normalised type
5584
+ // that does not round-trip through JSON: JSON.stringify calls
5585
+ // toISOString(), which yields a UTC INSTANT whose date part is
5586
+ // a day EARLIER for every positive UTC offset, so a client east
5587
+ // of UTC submitted the previous day (driven: Europe/Paris
5588
+ // '2026-09-18' -> '2026-09-17T22:00:00.000Z'). Silent, because
5589
+ // the instant is well-formed — only a reader that slices the
5590
+ // string sees it.
5591
+ //
5592
+ // The Date stays on this.value so `format()` and the #B48
5593
+ // chaining contract still work; only local.data (the submitted
5594
+ // payload) changes. The components are read back with the
5595
+ // Date's LOCAL getters, which the round-trip check above has
5596
+ // already proven equal to the parsed input, so no timezone
5597
+ // enters the written value at all.
5598
+ //
5599
+ // The shape is always `yyyy-mm-dd` regardless of the input
5600
+ // mask: gina's own introspection already publishes an isDate
5601
+ // field as {type:'string', format:'date'} (RFC 3339 full-date,
5602
+ // NOT date-time), and the mask governs INPUT parsing, not the
5603
+ // wire. An invalid date is left exactly as it was, so the
5604
+ // error path below still sees an Invalid Date object.
5605
+ // was: this.value = local.data[this.name] = date;
5606
+ this.value = date;
5607
+ if ( isNaN(date.getTime()) ) {
5608
+ local.data[this.name] = date;
5609
+ } else {
5610
+ local.data[this.name] = date.getFullYear()
5611
+ + '-' + ('0' + (date.getMonth() + 1)).slice(-2)
5612
+ + '-' + ('0' + date.getDate()).slice(-2);
5613
+ }
5583
5614
  }
5584
5615
 
5585
5616
  if ( /Invalid Date/i.test(date) || date instanceof Date === false ) {
@@ -8811,28 +8842,35 @@ function handleXhr(xhr, $el, options, require) {
8811
8842
  // if hasPopinHandler & popinIsBinded
8812
8843
  if ( typeof(gina.popin) != 'undefined' && gina.hasPopinHandler ) {
8813
8844
 
8814
- // select popin by id
8815
- var $popin = gina.popin.getActivePopin();
8845
+ // #gh76 — the popin the clicked ANCHOR lives in ($el — never
8846
+ // $target, which is the link plugin's container on this path),
8847
+ // and only while it is open; a page link's HTML answer stays
8848
+ // with its own handler instead of being loaded into whichever
8849
+ // popin happened to be open.
8850
+ var $popin = ( typeof(gina.popin.getPopinContaining) == 'function' )
8851
+ ? gina.popin.getPopinContaining($el)
8852
+ : null;
8853
+ if ( $popin && !$popin.isOpen ) {
8854
+ $popin = null;
8855
+ }
8816
8856
 
8817
8857
  if ($popin) {
8818
8858
 
8819
- XHRData = {};
8859
+ // #B575 — ONE tolerant parse: the hidden inputs are a dev-mode
8860
+ // transport, absent outside dev mode; dereferencing them raised a
8861
+ // false 422 and the popin was never loaded
8862
+ var _parsed = parseXhrHtmlAnswer(result.content);
8863
+ XHRData = _parsed.data;
8864
+ XHRView = _parsed.view;
8820
8865
  // update toolbar
8821
-
8822
8866
  try {
8823
- XHRData = new DOMParser().parseFromString(result.content, 'text/html').getElementById('gina-without-layout-xhr-data');
8824
- XHRData = JSON.parse(decodeURIComponent(XHRData.value));
8825
-
8826
- XHRView = new DOMParser().parseFromString(result.content, 'text/html').getElementById('gina-without-layout-xhr-view');
8827
- XHRView = JSON.parse(decodeURIComponent(XHRView.value));
8828
-
8829
8867
  // update data tab
8830
- if ( gina && typeof(window.ginaToolbar) != 'undefined' && window.ginaToolbar && typeof(XHRData) != 'undefined' ) {
8868
+ if ( gina && typeof(window.ginaToolbar) != 'undefined' && window.ginaToolbar && XHRData ) {
8831
8869
  window.ginaToolbar.update('data-xhr', XHRData);
8832
8870
  }
8833
8871
 
8834
8872
  // update view tab
8835
- if ( gina && typeof(window.ginaToolbar) != 'undefined' && window.ginaToolbar && typeof(XHRView) != 'undefined' ) {
8873
+ if ( gina && typeof(window.ginaToolbar) != 'undefined' && window.ginaToolbar && XHRView ) {
8836
8874
  window.ginaToolbar.update('view-xhr', XHRView);
8837
8875
  }
8838
8876
 
@@ -8842,8 +8880,19 @@ function handleXhr(xhr, $el, options, require) {
8842
8880
 
8843
8881
  $popin.loadContent(result.content);
8844
8882
 
8845
- result = XHRData;
8883
+ // the parsed xhr-data in dev mode, delivered VERBATIM, or an
8884
+ // object carrying the status outside dev mode where the transport
8885
+ // inputs are absent (the handler contract is "an object", never
8886
+ // null). The parsed data is never mutated.
8887
+ result = XHRData || { status: xhr.status };
8846
8888
  triggerEvent(gina, $target, 'success.' + id, result);
8889
+ // #B571 — this branch returned before the tail that emits the
8890
+ // declared-callback companion, so a link's
8891
+ // `data-gina-link-event-on-success` never ran when its HTML
8892
+ // answer was loaded into a popin. `.hlink` only: the `.hform`
8893
+ // half of this handler is dead (no caller passes `$form`).
8894
+ if (hLinkIsRequired)
8895
+ triggerEvent(gina, $link.target, 'success.' + $link.id + '.hlink', result);
8847
8896
 
8848
8897
  return;
8849
8898
  }
@@ -9231,7 +9280,14 @@ function on(event, cb) {
9231
9280
  addListener(gina, $target, event, function(e) {
9232
9281
 
9233
9282
  //if ( typeof(e.defaultPrevented) != 'undefined' && e.defaultPrevented)
9234
- cancelEvent(e);
9283
+ // #gh76 slice 2 — `beforeswap.<id>` is CANCELABLE by design: the listener
9284
+ // decides with `preventDefault()` whether the swap happens, so the blanket
9285
+ // cancel below must not pre-empt it (it would read as "every listener
9286
+ // cancels"). Name-scoped, so no other event sees a difference. Slice 3
9287
+ // adds `oobbeforeswap.<id>`, the per-element out-of-band twin.
9288
+ if ( !/^(oob)?beforeswap\./.test(e.type) ) {
9289
+ cancelEvent(e);
9290
+ }
9235
9291
 
9236
9292
  var data = null;
9237
9293
 
@@ -9261,6 +9317,273 @@ function on(event, cb) {
9261
9317
 
9262
9318
  }
9263
9319
 
9320
+ /**
9321
+ * parseXhrHtmlAnswer
9322
+ *
9323
+ * Parses a `text/html` XHR answer ONCE and reads the two hidden inputs a
9324
+ * `renderWithoutLayout()` action splices in — `gina-without-layout-xhr-data` (the page data)
9325
+ * and `gina-without-layout-xhr-view` — when they are present. They are a DEV-MODE transport:
9326
+ * the server splices them only under `NODE_ENV_IS_DEV`, so outside dev mode both read `null`
9327
+ * (#B575: the popin branches used to dereference the absent input and raise a false `422`).
9328
+ * The inputs are removed from the parsed document so a swap never carries them into the page
9329
+ * (inside a form they would be submitted; repeated swaps would duplicate their ids); the
9330
+ * caller's raw `content` string is untouched.
9331
+ *
9332
+ * @function parseXhrHtmlAnswer
9333
+ * @param {string} content - the answer body
9334
+ * @returns {{doc: Document, data: (object|null), view: (object|null)}}
9335
+ *
9336
+ * @example
9337
+ * var parsed = parseXhrHtmlAnswer(xhr.responseText);
9338
+ * parsed.data; // the action's data in dev mode, null otherwise
9339
+ * parsed.doc.body.innerHTML; // the fragment, hidden inputs stripped
9340
+ */
9341
+ function parseXhrHtmlAnswer(content) {
9342
+ var raw = ( typeof(content) == 'string' ) ? content : '';
9343
+ // #B578 — an answer is a FRAGMENT, not a document. Parsed as a document, the HTML
9344
+ // parser foster-parents table-context elements (`<tr>`, `<td>`, `<tbody>`…) out of
9345
+ // existence, so a row answer came out as its cell text and a `select` on `tr` found
9346
+ // nothing. Parse through a `<template>` — whose contents accept any element; htmx's
9347
+ // `makeFragment` idiom — and MOVE the fragment into `doc.body`: a DOM move never
9348
+ // re-parses, so the row survives and every reader of `doc` (`getElementById`,
9349
+ // `querySelectorAll`, `body.innerHTML`) is unchanged. A full-page answer (a layout
9350
+ // render by mistake) drops its `<head>` first, as htmx does, or its `<title>` would
9351
+ // land in the swap. An author-supplied `<template>` inside the answer is content and
9352
+ // is kept as-is.
9353
+ raw = raw.replace(/<head(\s[^>]*)?>[\s\S]*?<\/head>/i, '');
9354
+ var doc = new DOMParser().parseFromString('<body><template>' + raw + '</template></body>', 'text/html');
9355
+ var $tpl = ( doc.body ) ? doc.body.querySelector('template') : null;
9356
+ if ( $tpl ) {
9357
+ var frag = $tpl.content;
9358
+ $tpl.parentNode.removeChild($tpl);
9359
+ doc.body.appendChild(( typeof(doc.adoptNode) == 'function' ) ? doc.adoptNode(frag) : frag);
9360
+ }
9361
+ var read = function(idName) {
9362
+ var $input = doc.getElementById(idName), value = null;
9363
+ if ( !$input ) {
9364
+ return null;
9365
+ }
9366
+ value = $input.value;
9367
+ try { $input.parentNode.removeChild($input); } catch (rmErr) {}
9368
+ if ( typeof(value) != 'string' || value === '' ) {
9369
+ return null;
9370
+ }
9371
+ try {
9372
+ return JSON.parse(decodeURIComponent(value));
9373
+ } catch (parseErr) {
9374
+ return null;
9375
+ }
9376
+ };
9377
+ return {
9378
+ doc : doc,
9379
+ data : read('gina-without-layout-xhr-data'),
9380
+ view : read('gina-without-layout-xhr-view')
9381
+ };
9382
+ }
9383
+
9384
+ /**
9385
+ * applyOobSwaps
9386
+ *
9387
+ * #gh76 slice 3 — out-of-band swaps (the `hx-swap-oob` contract). Every element of the
9388
+ * parsed answer carrying `data-gina-swap-oob` is swapped into the PAGE element with the
9389
+ * same `id`, independently of where the main answer goes, and is REMOVED from the answer
9390
+ * whether or not it swapped — the main fragment is always clean. Runs BEFORE any
9391
+ * `data-gina-form-select` pick and before the main swap (htmx's order, so an oob element
9392
+ * outside the selection still lands; on overlap the main swap wins).
9393
+ *
9394
+ * Value grammar: `true` / empty ⇒ `outerHTML` (the page element is replaced by the oob
9395
+ * element, attribute stripped); a strategy name ⇒ applied with the oob element's CONTENT
9396
+ * (the wrapper is stripped — `innerHTML` would otherwise nest a duplicate id);
9397
+ * `<strategy>:<selector>` is RESERVED; anything else is unknown. A missing `id`, no page
9398
+ * match, a reserved or unknown value: the element is still removed and reported with a
9399
+ * `reason`, never thrown. An oob element inside ANOTHER oob element is not processed on
9400
+ * its own — it travels with its ancestor and its attribute is stripped there. Elements
9401
+ * inside an author-supplied `<template>` are processed too.
9402
+ *
9403
+ * Per element: `oobbeforeswap.<formId>` (cancelable: `preventDefault()` skips it;
9404
+ * `detail.content` may be rewritten) → the write → the region bound through the shared
9405
+ * `bindRegion()` policy → `oobafterswap.<formId>`, whose `.hform` companion fires when the
9406
+ * declared-callback channel is armed — `oobbeforeswap` has none, mirroring `beforeswap`:
9407
+ * a cancel is a decision, and the declared-callback channel carries notifications. An
9408
+ * author-supplied `<template>` emptied of its out-of-band elements is removed with them,
9409
+ * so an out-of-band-only answer leaves NOTHING behind whether or not it was wrapped. A
9410
+ * swap that replaces or removes the SUBMITTING form
9411
+ * defers its re-binding (`deferFormId`) and is reported through `rebindSelf`, so the
9412
+ * caller's shared tail binds the same-id replacement after the `success` events.
9413
+ *
9414
+ * @param {Document} doc - the parsed answer (`parseXhrHtmlAnswer().doc`), MUTATED: every oob element is removed
9415
+ * @param {object} ctx
9416
+ * @param {HTMLFormElement} ctx.$target - the submitting form element
9417
+ * @param {string} ctx.id - the form id (event channel)
9418
+ * @param {boolean} ctx.hFormIsRequired - whether the `.hform` companions are armed
9419
+ * @param {object} ctx.gina - the framework global (event bus)
9420
+ *
9421
+ * @returns {{ list: Array<{ id: (string|null), strategy: string, swapped: boolean, reason: (string|undefined) }>, rebindSelf: boolean }}
9422
+ *
9423
+ * @example
9424
+ * var run = applyOobSwaps(parsed.doc, { $target: $form, id: 'f', hFormIsRequired: true, gina: gina });
9425
+ * // => { list: [{ id: 'totals', strategy: 'outerHTML', swapped: true }], rebindSelf: false }
9426
+ */
9427
+ function applyOobSwaps(doc, ctx) {
9428
+ var OOB_ATTR = 'data-gina-swap-oob';
9429
+ var STRATEGIES = ['innerHTML', 'outerHTML', 'textContent', 'beforebegin', 'afterbegin', 'beforeend', 'afterend', 'delete', 'none'];
9430
+ var $target = ctx.$target
9431
+ , id = ctx.id
9432
+ , armed = !!ctx.hFormIsRequired
9433
+ , gina = ctx.gina
9434
+ , out = { list: [], rebindSelf: false }
9435
+ , list = []
9436
+ , work = []
9437
+ , i = 0
9438
+ ;
9439
+ if ( !doc || typeof(doc.querySelectorAll) != 'function' ) {
9440
+ return out;
9441
+ }
9442
+ var collect = function(root) {
9443
+ var found = root.querySelectorAll('[' + OOB_ATTR + ']');
9444
+ for (var f = 0; f < found.length; ++f) list.push(found[f]);
9445
+ };
9446
+ collect(doc);
9447
+ var tpls = doc.querySelectorAll('template'), harvested = [];
9448
+ for (i = 0; i < tpls.length; ++i) {
9449
+ if ( !tpls[i].content ) continue;
9450
+ var before = list.length;
9451
+ collect(tpls[i].content);
9452
+ if ( list.length > before ) harvested.push(tpls[i]);
9453
+ }
9454
+ // pre-pass while the tree is intact: an oob element under another oob element travels
9455
+ // with its ancestor — strip its attribute now, so it never fires and never re-arms
9456
+ for (i = 0; i < list.length; ++i) {
9457
+ var $anc = list[i].parentNode, nested = false;
9458
+ while ( $anc && $anc.nodeType === 1 ) {
9459
+ if ( $anc.hasAttribute(OOB_ATTR) ) { nested = true; break; }
9460
+ $anc = $anc.parentNode;
9461
+ }
9462
+ if (nested) {
9463
+ list[i].removeAttribute(OOB_ATTR);
9464
+ } else {
9465
+ work.push(list[i]);
9466
+ }
9467
+ }
9468
+ for (i = 0; i < work.length; ++i) {
9469
+ var $oob = work[i]
9470
+ , oobId = $oob.getAttribute('id') || null
9471
+ , value = ( $oob.getAttribute(OOB_ATTR) || '' ).trim()
9472
+ , strategy = null
9473
+ , reason = null
9474
+ , $page = null
9475
+ , content = null
9476
+ , evt = null
9477
+ , $scope = null
9478
+ , detaches = false
9479
+ , entry = null
9480
+ ;
9481
+ if ( value === '' || /^true$/i.test(value) ) {
9482
+ strategy = 'outerHTML';
9483
+ } else if ( value.indexOf(':') > -1 ) {
9484
+ strategy = value; reason = 'reserved';
9485
+ } else if ( STRATEGIES.indexOf(value) < 0 ) {
9486
+ strategy = value; reason = 'unknownStrategy';
9487
+ } else {
9488
+ strategy = value;
9489
+ }
9490
+ // consumed transport: never lands in the page, never stays in the answer
9491
+ $oob.removeAttribute(OOB_ATTR);
9492
+ if ( $oob.parentNode ) $oob.parentNode.removeChild($oob);
9493
+
9494
+ if ( !reason && !oobId ) reason = 'noId';
9495
+ if ( !reason ) {
9496
+ $page = document.getElementById(oobId);
9497
+ if ( !$page ) reason = 'noTarget';
9498
+ }
9499
+ entry = { id: oobId, strategy: strategy, swapped: false };
9500
+ if ( reason ) {
9501
+ entry.reason = reason;
9502
+ out.list.push(entry);
9503
+ continue;
9504
+ }
9505
+
9506
+ content = ( strategy === 'outerHTML' )
9507
+ ? $oob.outerHTML
9508
+ : ( strategy === 'textContent' ) ? $oob.textContent : $oob.innerHTML;
9509
+
9510
+ if ( strategy !== 'none' ) {
9511
+ // no `.hform` companion, exactly as `beforeswap`: this is a DECISION point and the
9512
+ // declared-callback channel carries notifications — `oobafterswap` has the twin
9513
+ evt = triggerEvent(gina, $target, 'oobbeforeswap.' + id, { target: $page, content: content, strategy: strategy, oob: true, oobId: oobId });
9514
+ if ( evt && evt.defaultPrevented ) {
9515
+ entry.reason = 'cancelled';
9516
+ out.list.push(entry);
9517
+ continue;
9518
+ }
9519
+ if ( evt && evt.detail && typeof(evt.detail.content) == 'string' ) {
9520
+ content = evt.detail.content;
9521
+ }
9522
+ }
9523
+
9524
+ detaches = ( strategy === 'outerHTML' || strategy === 'delete' ) && ( $page === $target || $page.contains($target) );
9525
+
9526
+ switch (strategy) {
9527
+ case 'innerHTML':
9528
+ $page.innerHTML = content;
9529
+ $scope = $page;
9530
+ break;
9531
+ case 'outerHTML':
9532
+ $scope = $page.parentNode;
9533
+ $page.outerHTML = content;
9534
+ break;
9535
+ case 'textContent':
9536
+ $page.textContent = content;
9537
+ break;
9538
+ case 'beforebegin':
9539
+ $scope = $page.parentNode;
9540
+ $page.insertAdjacentHTML('beforebegin', content);
9541
+ break;
9542
+ case 'afterbegin':
9543
+ $page.insertAdjacentHTML('afterbegin', content);
9544
+ $scope = $page;
9545
+ break;
9546
+ case 'beforeend':
9547
+ $page.insertAdjacentHTML('beforeend', content);
9548
+ $scope = $page;
9549
+ break;
9550
+ case 'afterend':
9551
+ $scope = $page.parentNode;
9552
+ $page.insertAdjacentHTML('afterend', content);
9553
+ break;
9554
+ case 'delete':
9555
+ if ( $page.parentNode ) $page.parentNode.removeChild($page);
9556
+ break;
9557
+ case 'none':
9558
+ break;
9559
+ }
9560
+ entry.swapped = ( strategy !== 'none' );
9561
+ if ( detaches ) out.rebindSelf = true;
9562
+
9563
+ if ( $scope && typeof($scope.getElementsByTagName) == 'function' ) {
9564
+ bindRegion($scope, { deferFormId: ( detaches ) ? id : null });
9565
+ }
9566
+ if ( strategy !== 'none' ) {
9567
+ triggerEvent(gina, $target, 'oobafterswap.' + id, { target: $page, strategy: strategy, swapped: entry.swapped, oob: true, oobId: oobId });
9568
+ if ( armed )
9569
+ triggerEvent(gina, $target, 'oobafterswap.' + id + '.hform', { target: $page, strategy: strategy, swapped: entry.swapped, oob: true, oobId: oobId });
9570
+ }
9571
+ out.list.push(entry);
9572
+ }
9573
+ // an author-supplied wrapper that held nothing but out-of-band elements is consumed
9574
+ // transport too: left in place it would defeat the empty-remainder rule downstream
9575
+ // (a popin would be asked to load `<template></template>` and blank itself)
9576
+ for (i = 0; i < harvested.length; ++i) {
9577
+ if ( harvested[i].content.childElementCount === 0
9578
+ && !/\S/.test(harvested[i].content.textContent || '')
9579
+ && harvested[i].parentNode
9580
+ ) {
9581
+ harvested[i].parentNode.removeChild(harvested[i]);
9582
+ }
9583
+ }
9584
+ return out;
9585
+ }
9586
+
9264
9587
  /**
9265
9588
  * listenToXhrEvents
9266
9589
  *
@@ -11701,6 +12024,110 @@ function getElementsByAttribute(attribute) {
11701
12024
  }
11702
12025
  };
11703
12026
  }(DOMParser));
12027
+ /**
12028
+ * bindRegion
12029
+ *
12030
+ * Binds a freshly injected HTML region — ONE policy, shared by fragment navigation
12031
+ * (`gina/nav`) and by the validator's form-answer swaps (#gh76 slice 2), so a third
12032
+ * copy of "what happens to injected content" never has to be written:
12033
+ * - `<script src>` elements the document does not already have OUTSIDE the region are
12034
+ * re-created in `<head>` (a `<script>` inserted through innerHTML never runs); a src the
12035
+ * document already carries elsewhere — the page's own bundles, a copy an earlier swap or a
12036
+ * popin injected — is not re-created, so a fragment may safely re-declare the page's
12037
+ * scripts. Inline scripts are never executed: innerHTML semantics, the contract popin
12038
+ * and nav content have always had;
12039
+ * - `<link rel="stylesheet">` elements are left alone — inserted through innerHTML they are
12040
+ * already live;
12041
+ * - forms that opt in (#B549: a `data-gina-form-*` attribute, an id naming a registered rule,
12042
+ * or a `gina-upload-*` id) are bound through the live validator's own `bindRegion`; a bare
12043
+ * form keeps its native submit, exactly as on the initial page;
12044
+ * - `<a data-gina-link>` anchors are bound through the live link plugin when one is active;
12045
+ * - declarative `data-gina-dialog` triggers are document-delegated (nothing to bind); legacy
12046
+ * `data-gina-popin-*` triggers are inert in injected content; custom elements upgrade
12047
+ * natively.
12048
+ *
12049
+ * @param {HTMLElement} $root - the injected region: the swapped element, or the parent of
12050
+ * nodes inserted beside it
12051
+ * @param {object} [options]
12052
+ * @param {string} [options.deferFormId] - a form id to SKIP: the submitting form of a swap
12053
+ * that replaced it keeps its listeners until the swap's own events are delivered, so
12054
+ * its replacement is bound by the caller afterwards
12055
+ * @param {boolean} [options.forms=true] - bind opted-in forms
12056
+ * @param {boolean} [options.links=true] - bind `data-gina-link` anchors
12057
+ *
12058
+ * @returns {{scripts: number, forms: number, links: number}} what was bound
12059
+ *
12060
+ * @example
12061
+ * $region.innerHTML = fragment;
12062
+ * bindRegion($region); // => { scripts: 1, forms: 2, links: 0 }
12063
+ */
12064
+ function bindRegion($root, options) {
12065
+ var out = { scripts: 0, forms: 0, links: 0 };
12066
+ if ( !$root || typeof($root.getElementsByTagName) != 'function' ) {
12067
+ return out;
12068
+ }
12069
+ options = options || {};
12070
+ var _gina = ( typeof(window) != 'undefined' && window.gina ) ? window.gina : null;
12071
+
12072
+ // scripts — dedup against every src the DOCUMENT carries outside the region: the
12073
+ // fragment's own copies are inert (innerHTML) and must not count as "already loaded"
12074
+ var known = []
12075
+ , docScripts = document.getElementsByTagName('script')
12076
+ , scripts = $root.getElementsByTagName('script')
12077
+ , src = null
12078
+ , $s = null
12079
+ , i = 0
12080
+ , len = docScripts.length
12081
+ ;
12082
+ for (; i < len; ++i) {
12083
+ if ( docScripts[i].src && !$root.contains(docScripts[i]) ) {
12084
+ known.push(docScripts[i].src);
12085
+ }
12086
+ }
12087
+ for (i = 0, len = scripts.length; i < len; ++i) {
12088
+ src = scripts[i].src; // the resolved absolute URL
12089
+ if ( !src || known.indexOf(src) > -1 ) continue;
12090
+ $s = document.createElement('script');
12091
+ $s.src = src;
12092
+ document.head.appendChild($s);
12093
+ known.push(src);
12094
+ out.scripts++;
12095
+ }
12096
+
12097
+ // forms — the validator owns the opt-in gate (#B549)
12098
+ if (
12099
+ options.forms !== false
12100
+ && _gina && _gina.hasValidator && _gina.validator
12101
+ && typeof(_gina.validator.bindRegion) == 'function'
12102
+ ) {
12103
+ try {
12104
+ out.forms = _gina.validator.bindRegion($root, { deferFormId: options.deferFormId || null });
12105
+ } catch (formsErr) {
12106
+ if ( typeof(console) != 'undefined' && console.warn ) {
12107
+ console.warn('[gina][bindRegion] form binding failed: ' + (formsErr.message || formsErr));
12108
+ }
12109
+ }
12110
+ }
12111
+
12112
+ // links — the link plugin binds the anchors that opt in
12113
+ if (
12114
+ options.links !== false
12115
+ && _gina && _gina.hasLinkHandler && _gina.link
12116
+ && typeof(_gina.link.bindLinks) == 'function'
12117
+ ) {
12118
+ try {
12119
+ _gina.link.bindLinks($root);
12120
+ out.links = $root.querySelectorAll('a[data-gina-link]:not([data-gina-link="false"])').length;
12121
+ } catch (linksErr) {
12122
+ if ( typeof(console) != 'undefined' && console.warn ) {
12123
+ console.warn('[gina][bindRegion] link binding failed: ' + (linksErr.message || linksErr));
12124
+ }
12125
+ }
12126
+ }
12127
+
12128
+ return out;
12129
+ }
12130
+ ;
11704
12131
  define("utils/dom", function(){});
11705
12132
 
11706
12133
  /**
@@ -12162,8 +12589,13 @@ function ValidatorPlugin(rules, data, formId, culture) {
12162
12589
  'registered',
12163
12590
  'success',
12164
12591
  'error',
12592
+ 'beforeswap', // #gh76 slice 2 — cancelable, before a form answer is swapped into its target
12593
+ 'afterswap', // #gh76 slice 2 — after the swap and the region binding
12594
+ 'oobbeforeswap', // #gh76 slice 3 — per out-of-band element, cancelable
12595
+ 'oobafterswap', // #gh76 slice 3 — per out-of-band element, after its region binding
12165
12596
  'progress',
12166
12597
  'uploadProgress', // #R8 — upload (client-to-server) wire progress for staged uploads
12598
+ 'abort', // #gh76 slice 4 — a request superseded by `data-gina-form-sync`
12167
12599
  'submit',
12168
12600
  'reset',
12169
12601
  'change',
@@ -12745,15 +13177,1053 @@ function ValidatorPlugin(rules, data, formId, culture) {
12745
13177
  if ( gina.hasPopinHandler && gina.popinIsBinded ) {
12746
13178
  $activePopin = gina.popin.getActivePopin();
12747
13179
  }
12748
-
12749
- if ( $activePopin && $activePopin.isOpen ) {
12750
- isPopinInUse = true;
13180
+
13181
+ if ( $activePopin && $activePopin.isOpen ) {
13182
+ isPopinInUse = true;
13183
+ }
13184
+
13185
+ return isPopinInUse;
13186
+ }
13187
+
13188
+ /**
13189
+ * popinStillContaining
13190
+ *
13191
+ * #gh76 — the settle-time half of the containment rule: the popin captured at
13192
+ * submit, if it is still open and its element still contains the form (a close +
13193
+ * reopen in flight wipes and refetches the content, so the captured form is detached
13194
+ * and the answer must not land in content the user has since navigated to).
13195
+ *
13196
+ * @param {object|null} $captured - the popin captured at submit (`sendCtx.popin`)
13197
+ * @param {HTMLElement} $formEl - the form element
13198
+ *
13199
+ * @returns {object|null} $popin
13200
+ *
13201
+ * @example
13202
+ * var $popin = popinStillContaining(sendCtx.popin, $target); // null => legacy payload
13203
+ */
13204
+ var popinStillContaining = function($captured, $formEl) {
13205
+ var $dialog = null;
13206
+ if ( !$captured || !$captured.isOpen ) {
13207
+ return null;
13208
+ }
13209
+ $dialog = ( $captured.id ) ? document.getElementById($captured.id) : null;
13210
+ return ( $dialog && $dialog.contains($formEl) ) ? $captured : null;
13211
+ }
13212
+
13213
+ /**
13214
+ * warnIfOldRuleRouted
13215
+ *
13216
+ * #gh76 — dev-only notice when the retired "active popin" rule would have routed a
13217
+ * PAGE form's answer or redirect into a popin the form is not inside. The one
13218
+ * conceivable reliance on that routing is a non-modal popin deliberately updated by a
13219
+ * page form; the notice is how such a page finds out. Two messages: the popin was
13220
+ * open (its content would have been replaced) vs loading, not open (a false 422
13221
+ * would have been raised).
13222
+ *
13223
+ * @param {object|null} $captured - the popin captured at submit
13224
+ * @param {object|null} $routed - the popin the answer is actually routed to
13225
+ * @param {HTMLElement} $formEl - the form element
13226
+ * @param {string} what - `answer` or `redirect`, for the message
13227
+ *
13228
+ * @returns {undefined}
13229
+ *
13230
+ * @example
13231
+ * warnIfOldRuleRouted(sendCtx.popin, $popin, $target, 'answer');
13232
+ */
13233
+ var warnIfOldRuleRouted = function($captured, $routed, $formEl, what) {
13234
+ var $old = null, $popins = null, activeId = null;
13235
+ if ( !envIsDev || $routed || $captured || typeof(gina.popin) == 'undefined' || !gina.popin ) {
13236
+ return;
13237
+ }
13238
+ // what getActivePopin() resolved before #gh76: the first open popin, else the
13239
+ // popin `activePopinId` names in ANY state
13240
+ $old = gina.popin.getActivePopin();
13241
+ if ( !$old ) {
13242
+ $popins = gina.popin.$popins || {};
13243
+ activeId = gina.popin.activePopinId;
13244
+ $old = ( activeId && $popins[activeId] ) ? $popins[activeId] : null;
13245
+ }
13246
+ if ( !$old ) {
13247
+ return;
13248
+ }
13249
+ try {
13250
+ console.warn('[FormValidator][popin] the HTML '+ what +' of form `#'+ $formEl.id +'` is delivered to its own handler: the form is not inside popin `'+ $old.name +'`, which is '+ ( $old.isOpen ? 'open (the former routing rule would have replaced its content)' : 'loading, not open (the former routing rule would have raised a false 422 error)' ) +'.');
13251
+ } catch (e) {}
13252
+ }
13253
+
13254
+
13255
+ /**
13256
+ * warnIfOldRulePlacedUpload
13257
+ *
13258
+ * #B572 — dev-only notice, the staged-upload twin of `warnIfOldRuleRouted`: the
13259
+ * retired "active popin" rule placed a PAGE form's virtual upload form — and with it
13260
+ * the preview lookups and the staging request's popin headers — inside whichever popin
13261
+ * happened to be open, where the staged metadata never reached the form. Containment
13262
+ * now decides the placement; this is how a page that relied on the old placement
13263
+ * finds out. Only an OPEN popin is named: the retired gate required `isOpen`.
13264
+ *
13265
+ * @param {object|null} $owner - the popin containing the real form (null for a page form)
13266
+ * @param {HTMLElement} $formEl - the real form the file input belongs to
13267
+ * @param {HTMLElement} $inputEl - the file input
13268
+ *
13269
+ * @returns {undefined}
13270
+ *
13271
+ * @example
13272
+ * warnIfOldRulePlacedUpload($ownerPopin, $el.form, $el);
13273
+ */
13274
+ var warnIfOldRulePlacedUpload = function($owner, $formEl, $inputEl) {
13275
+ var $old = null;
13276
+ if ( !envIsDev || $owner || typeof(gina.popin) == 'undefined' || !gina.popin ) {
13277
+ return;
13278
+ }
13279
+ // what isPopinContext() resolved before #B572: the first OPEN popin
13280
+ $old = ( typeof(gina.popin.getActivePopin) == 'function' ) ? gina.popin.getActivePopin() : null;
13281
+ if ( !$old || !$old.isOpen ) {
13282
+ return;
13283
+ }
13284
+ try {
13285
+ console.warn('[FormValidator][popin] the staged upload of `#'+ ( ( $inputEl && $inputEl.id ) ? $inputEl.id : '' ) +'` (form `#'+ ( ( $formEl && $formEl.id ) ? $formEl.id : '' ) +'`) is placed with its own form: the form is not inside popin `'+ $old.name +'`, which is open (the former rule would have placed the upload inside it and its metadata would never have reached the form).');
13286
+ } catch (e) {}
13287
+ }
13288
+
13289
+ /**
13290
+ * The swap strategies `data-gina-form-swap` accepts (#gh76 slice 2) — htmx's `hx-swap`
13291
+ * values, applied with the matching DOM operation.
13292
+ * @constant {string[]}
13293
+ */
13294
+ var SWAP_STRATEGIES = ['innerHTML', 'outerHTML', 'textContent', 'beforebegin', 'afterbegin', 'beforeend', 'afterend', 'delete', 'none'];
13295
+
13296
+ /**
13297
+ * resolveSwapTarget
13298
+ *
13299
+ * #gh76 slice 2 — resolves a form's `data-gina-form-target` value to an element, at
13300
+ * SUBMIT time, with htmx's `hx-target` grammar: `this` (the form itself),
13301
+ * `closest <selector>` (the closest ancestor OR the form), `find <selector>` (the first
13302
+ * descendant), or a CSS selector (the first match in the document). `next` and
13303
+ * `previous` are reserved: refused with a message naming them, so a later addition is
13304
+ * purely additive.
13305
+ *
13306
+ * @param {HTMLFormElement} $formEl - the submitting form
13307
+ * @param {string} value - the attribute value
13308
+ *
13309
+ * @returns {{target: (HTMLElement|undefined), error: (string|undefined)}}
13310
+ *
13311
+ * @example
13312
+ * resolveSwapTarget($form, 'closest li'); // => { target: <li> }
13313
+ * resolveSwapTarget($form, '#nowhere'); // => { error: 'no element matches `#nowhere`' }
13314
+ */
13315
+ var resolveSwapTarget = function($formEl, value) {
13316
+ var v = ( typeof(value) == 'string' ) ? value.trim() : '';
13317
+ var m = null, keyword = null, selector = null, $el = null;
13318
+ if ( v === '' ) {
13319
+ return { error: 'empty value' };
13320
+ }
13321
+ if ( v === 'this' ) {
13322
+ return { target: $formEl };
13323
+ }
13324
+ try {
13325
+ m = v.match(/^(closest|find|next|previous)(?:\s+(.+))?$/i);
13326
+ if (m) {
13327
+ keyword = m[1].toLowerCase();
13328
+ selector = ( m[2] || '' ).trim();
13329
+ if ( keyword === 'next' || keyword === 'previous' ) {
13330
+ return { error: '`'+ keyword +'` targeting is reserved and not supported yet' };
13331
+ }
13332
+ if ( selector === '' ) {
13333
+ return { error: '`'+ keyword +'` needs a selector' };
13334
+ }
13335
+ $el = ( keyword === 'closest' ) ? $formEl.closest(selector) : $formEl.querySelector(selector);
13336
+ } else {
13337
+ $el = document.querySelector(v);
13338
+ }
13339
+ } catch (selectorErr) {
13340
+ return { error: 'invalid selector `'+ v +'`: '+ (selectorErr.message || selectorErr) };
13341
+ }
13342
+ return ( $el ) ? { target: $el } : { error: 'no element matches `'+ v +'`' };
13343
+ }
13344
+
13345
+ /**
13346
+ * refuseSend
13347
+ *
13348
+ * #gh76 slice 2 — refuses a submit BEFORE any request leaves (a declared target or swap
13349
+ * strategy that cannot be honoured): releases what the submit armed and delivers the
13350
+ * refusal to the form's error channels as `{ status: 422, reason: 'targetError', … }`.
13351
+ * Runs before the request is opened, so no `disabled`/`data-gina-form-loading` lock
13352
+ * exists yet; the #B247 trigger arm does, and is released.
13353
+ *
13354
+ * @param {object} $form - the form's registry entry
13355
+ * @param {HTMLFormElement} $target - the form element
13356
+ * @param {string} id - the form id
13357
+ * @param {boolean} hFormIsRequired - whether the declared-callback channel is armed
13358
+ * @param {string} attribute - the offending attribute name
13359
+ * @param {string} value - its value
13360
+ * @param {string} why - the reason
13361
+ * @param {boolean} [ownedByEarlierSend] - a PREVIOUS request was already in flight when
13362
+ * this attempt reached `send()` (read before it claimed `isSending`). When true the
13363
+ * refusal delivers its error but touches neither `isSending`/`sent` nor the loading
13364
+ * state: both belong to the request still running, whose own settle releases them.
13365
+ * `armSubmitLoading` is first-wins, so this refused attempt armed nothing of its own
13366
+ * — the same ownership rule the validation-rejected path applies (#B247).
13367
+ *
13368
+ * @returns {undefined}
13369
+ *
13370
+ * @example
13371
+ * refuseSend($form, $target, id, hFormIsRequired, 'data-gina-form-target', '#nowhere', 'no element matches `#nowhere`', false);
13372
+ */
13373
+ var refuseSend = function($form, $target, id, hFormIsRequired, attribute, value, why, ownedByEarlierSend) {
13374
+ var result = {
13375
+ status : 422,
13376
+ error : attribute +': '+ why,
13377
+ reason : 'targetError',
13378
+ attribute : attribute,
13379
+ value : value,
13380
+ transportError : false
13381
+ };
13382
+ if ( !ownedByEarlierSend ) {
13383
+ $form.isSending = false;
13384
+ $form.sent = false;
13385
+ if ( instance.$forms[id] ) {
13386
+ instance.$forms[id].isSending = false;
13387
+ instance.$forms[id].sent = false;
13388
+ }
13389
+ disarmSubmitLoading($form);
13390
+ }
13391
+ $form.eventData.error = result;
13392
+ if (envIsDev) {
13393
+ try { console.error('[FormValidator][swap] submit of form `#'+ id +'` refused — '+ result.error); } catch (e) {}
13394
+ }
13395
+ triggerEvent(gina, $target, 'error.' + id, result);
13396
+ if (hFormIsRequired)
13397
+ triggerEvent(gina, $target, 'error.' + id + '.hform', result);
13398
+ }
13399
+
13400
+ /**
13401
+ * #gh76 slice 4 — the three things a submit can do when the region its answer is bound
13402
+ * for is already owned by a request in flight. `data-gina-form-sync` names one of them;
13403
+ * most forms never need to, because the default is DERIVED from the swap strategy they
13404
+ * already declared (see `deriveSync`).
13405
+ * @constant {string[]}
13406
+ */
13407
+ var SYNC_STRATEGIES = ['drop', 'replace', 'queue'];
13408
+
13409
+ /**
13410
+ * #gh76 slice 4 — the swap strategies that REPLACE what a region holds, as opposed to
13411
+ * adding to it. They are what makes the default derivable: an answer about to overwrite
13412
+ * the region makes the answer still on the wire moot, so the newer submit wins. The five
13413
+ * that remain — the four insertion points and `none` — have no conflict to resolve, so
13414
+ * two such answers both land, exactly as they always have.
13415
+ * @constant {string[]}
13416
+ */
13417
+ var REPLACING_SWAPS = ['innerHTML', 'outerHTML', 'textContent', 'delete'];
13418
+
13419
+ /**
13420
+ * #gh76 slice 4 — per-KEY coordination state, keyed on the element a form's answer is
13421
+ * bound for: its resolved swap target, or the form itself when it declares none. A
13422
+ * WeakMap because a swapped-away target is a NEW element — the old entry is collected
13423
+ * with it, and a different element is legitimately a different key.
13424
+ *
13425
+ * There is no sequence counter: `#B175` gives every send its own XHR, so the XHR object
13426
+ * IS the identity a late settle is checked against, exactly and without a counter to
13427
+ * keep in step.
13428
+ *
13429
+ * @type {WeakMap<Element, {xhr: (XMLHttpRequest|null), ctx: (object|null), queue: Array<Function>}>}
13430
+ */
13431
+ var syncRegistry = new WeakMap();
13432
+
13433
+ /**
13434
+ * #gh76 slice 4 — how many in-flight requests currently hold each element disabled
13435
+ * through `data-gina-form-disabled-elt`. Overlapping requests naming the same element
13436
+ * release it once, at the last settle. Only elements WE disabled are ever counted, so an
13437
+ * authored `disabled` can never be cleared by us.
13438
+ *
13439
+ * @type {WeakMap<Element, number>}
13440
+ */
13441
+ var disabledRefs = new WeakMap();
13442
+
13443
+ /**
13444
+ * syncNotice
13445
+ *
13446
+ * #gh76 slice 4 — dev-mode console notice for a coordination decision that silently
13447
+ * changes what a submit did (dropped, queued, superseded). Production stays quiet: these
13448
+ * are the declared outcomes, not errors.
13449
+ *
13450
+ * @param {string} id - the form id
13451
+ * @param {string} msg - what happened
13452
+ *
13453
+ * @returns {undefined}
13454
+ *
13455
+ * @example
13456
+ * syncNotice('add-row', 'submit dropped — a request already owns this target');
13457
+ *
13458
+ * @inner
13459
+ */
13460
+ var syncNotice = function(id, msg) {
13461
+ if (envIsDev) {
13462
+ try { console.warn('[FormValidator][sync] form `#'+ id +'`: '+ msg); } catch (e) {}
13463
+ }
13464
+ }
13465
+
13466
+ /**
13467
+ * parseSync
13468
+ *
13469
+ * #gh76 slice 4 — parses an explicit `data-gina-form-sync` value into `{ strategy }`.
13470
+ *
13471
+ * Three htmx spellings are deliberately REFUSED rather than implemented, each with a
13472
+ * message naming the reason and what to write instead — gina refuses what it will not
13473
+ * honour rather than parsing it into silence:
13474
+ * - `abort` means "anything that comes next may cancel me", htmx's idiom for a
13475
+ * disposable GET. A gina submit is a validated POST with side effects, never
13476
+ * disposable; `replace` and `drop` cover both halves of what it was reaching for.
13477
+ * - the `queue` modifiers exist in htmx because its trigger spec has `queue:` modifiers
13478
+ * too. Gina has no trigger spec, and one submit waiting per region is the only
13479
+ * behaviour the region can act on.
13480
+ * - `<selector>:<strategy>` names the element to coordinate on. Gina already knows it:
13481
+ * it is the resolved swap target.
13482
+ *
13483
+ * @param {string} value - the attribute value
13484
+ *
13485
+ * @returns {{strategy: (string|undefined), error: (string|undefined)}}
13486
+ *
13487
+ * @example
13488
+ * parseSync('replace'); // => { strategy: 'replace' }
13489
+ * parseSync('queue'); // => { strategy: 'queue' }
13490
+ * parseSync('abort'); // => { error: '`abort` is not supported: … Use `replace` …' }
13491
+ *
13492
+ * @inner
13493
+ */
13494
+ var parseSync = function(value) {
13495
+ var v = ( typeof(value) == 'string' ) ? value.trim().toLowerCase() : '';
13496
+ var m = null;
13497
+ if ( v === '' ) {
13498
+ return { error: 'empty value' };
13499
+ }
13500
+ if ( v.indexOf(':') > -1 ) {
13501
+ return { error: '`<selector>:<strategy>` keying is not supported — the key is the resolved `data-gina-form-target`, so there is nothing to name; write the strategy alone' };
13502
+ }
13503
+ if ( /^abort\b/.test(v) ) {
13504
+ return { error: '`abort` is not supported — it means `anything may cancel me`, which fits a disposable GET, not a submit with rules and side effects; use `replace` to let the newer submit take over, or `drop` to yield to the one running' };
13505
+ }
13506
+ m = v.match(/^(drop|replace|queue)(?:\s+(\S+))?$/);
13507
+ if ( !m ) {
13508
+ return { error: 'unknown strategy `'+ v +'` (one of '+ SYNC_STRATEGIES.join(', ') +')' };
13509
+ }
13510
+ if ( m[2] ) {
13511
+ return { error: '`'+ m[1] +'` takes no modifier (found `'+ m[2] +'`) — the key is the resolved swap target, and one submit waits per region, the most recent one' };
13512
+ }
13513
+ return { strategy: m[1] };
13514
+ }
13515
+
13516
+ /**
13517
+ * deriveSync
13518
+ *
13519
+ * #gh76 slice 4 — the DEFAULT, for a form that declares no `data-gina-form-sync`: read
13520
+ * from the swap strategy the form already declared, so the common case coordinates with
13521
+ * nothing written in the markup.
13522
+ *
13523
+ * A form whose answer REPLACES a region coordinates on that region — an answer still on
13524
+ * the wire is about to be overwritten, so it is moot and the newer submit supersedes it.
13525
+ * A form whose answer is INSERTED, or writes nothing, has no conflict to resolve: both
13526
+ * land, exactly as they always have. A form declaring no target coordinates on nothing —
13527
+ * the module-wide one-at-a-time rule is the only thing that governs it, unchanged.
13528
+ *
13529
+ * This is the decision neither htmx nor Turbo can make for you: htmx knows nothing about
13530
+ * the answer until it arrives, and Turbo treats every submit as a navigation, so newest
13531
+ * wins for everything. Gina knows, at submit time, what the answer is going to do.
13532
+ *
13533
+ * The target must RESOLVE here. An unresolvable one is refused by the pre-flight a few
13534
+ * lines below, and a submit already on its way to a refusal must never abort a running
13535
+ * request on the way.
13536
+ *
13537
+ * @param {HTMLFormElement} $formEl - the submitting form
13538
+ *
13539
+ * @returns {?{strategy: string, derived: boolean, key: HTMLElement, swap: string}}
13540
+ *
13541
+ * @example
13542
+ * // <form data-gina-form-target="#rows">
13543
+ * deriveSync($form); // => { strategy: 'replace', derived: true, key: <ul id="rows">, swap: 'innerHTML' }
13544
+ * // <form data-gina-form-target="#rows" data-gina-form-swap="beforeend">
13545
+ * deriveSync($form); // => null — an insertion has nothing to resolve
13546
+ *
13547
+ * @inner
13548
+ */
13549
+ var deriveSync = function($formEl) {
13550
+ var targetAttr = $formEl.getAttribute('data-gina-form-target');
13551
+ var swapAttr = null, strategy = '', resolved = null;
13552
+ if ( targetAttr === null ) {
13553
+ return null;
13554
+ }
13555
+ swapAttr = $formEl.getAttribute('data-gina-form-swap');
13556
+ // read exactly as the pre-flight reads it, case included: a value it would refuse
13557
+ // must not coordinate here either
13558
+ strategy = ( swapAttr === null ) ? 'innerHTML' : swapAttr.trim();
13559
+ if ( REPLACING_SWAPS.indexOf(strategy) < 0 ) {
13560
+ return null;
13561
+ }
13562
+ resolved = resolveSwapTarget($formEl, targetAttr);
13563
+ if ( resolved.error || !resolved.target ) {
13564
+ return null;
13565
+ }
13566
+ return { strategy: 'replace', derived: true, key: resolved.target, swap: strategy };
13567
+ }
13568
+
13569
+ /**
13570
+ * decideSync
13571
+ *
13572
+ * #gh76 slice 4 — the pure overlap decision: what this submit does when the key it
13573
+ * coordinates on is, or is not, already owned by a request in flight.
13574
+ *
13575
+ * A key nobody owns is never a decision: every strategy proceeds. When one IS owned,
13576
+ * `drop` yields, `replace` takes the slot, and `queue` defers until it is free. There is
13577
+ * no state carried on the entry for this — the strategy of the submit ARRIVING decides,
13578
+ * because it is the one whose answer is about to land.
13579
+ *
13580
+ * @param {object} parsed - a parseSync() or deriveSync() result
13581
+ * @param {object} [entry] - the key's registry entry, when it has one
13582
+ *
13583
+ * @returns {{action: string, abortPrevious: (boolean|undefined)}}
13584
+ *
13585
+ * @example
13586
+ * decideSync({ strategy: 'drop' }, { xhr: {} }); // => { action: 'drop' }
13587
+ * decideSync({ strategy: 'replace' }, { xhr: {} }); // => { action: 'proceed', abortPrevious: true }
13588
+ * decideSync({ strategy: 'queue' }, null); // => { action: 'proceed', abortPrevious: false }
13589
+ *
13590
+ * @inner
13591
+ */
13592
+ var decideSync = function(parsed, entry) {
13593
+ var busy = !!( entry && entry.xhr );
13594
+ if ( !busy ) {
13595
+ return { action: 'proceed', abortPrevious: false };
13596
+ }
13597
+ switch ( parsed.strategy ) {
13598
+ case 'drop':
13599
+ return { action: 'drop' };
13600
+ case 'replace':
13601
+ return { action: 'proceed', abortPrevious: true };
13602
+ case 'queue':
13603
+ return { action: 'queue' };
13604
+ }
13605
+ return { action: 'proceed', abortPrevious: false };
13606
+ }
13607
+
13608
+ /**
13609
+ * queueSyncSend
13610
+ *
13611
+ * #gh76 slice 4 — holds a deferred submit until its key's slot is free. Exactly ONE waits
13612
+ * per key: a submit arriving while another already waits replaces it, because what the
13613
+ * user asked for last is what they meant. Drained by the settle chokepoint.
13614
+ *
13615
+ * @param {object} entry - the key's registry entry
13616
+ * @param {Function} replay - re-enters send() with the caller's original arguments
13617
+ *
13618
+ * @returns {undefined}
13619
+ *
13620
+ * @example
13621
+ * queueSyncSend(entry, function () { send.call(receiver, data, options); });
13622
+ *
13623
+ * @inner
13624
+ */
13625
+ var queueSyncSend = function(entry, replay) {
13626
+ entry.queue = [ replay ];
13627
+ }
13628
+
13629
+ /**
13630
+ * shiftSyncQueue
13631
+ *
13632
+ * #gh76 slice 4 — runs the next deferred submit for a key, once that key's slot is free.
13633
+ * Guarded on the slot ACTUALLY being free: a settle that was superseded by a `replace`
13634
+ * never shifts, because the request that replaced it claims the slot immediately
13635
+ * afterwards and would race the one this shift started.
13636
+ *
13637
+ * @param {object} [entry] - the key's registry entry
13638
+ *
13639
+ * @returns {undefined}
13640
+ *
13641
+ * @example
13642
+ * shiftSyncQueue(syncRegistry.get($list));
13643
+ *
13644
+ * @inner
13645
+ */
13646
+ var shiftSyncQueue = function(entry) {
13647
+ var next = null;
13648
+ if ( !entry || entry.xhr || !entry.queue || !entry.queue.length ) {
13649
+ return;
13650
+ }
13651
+ next = entry.queue.shift();
13652
+ try {
13653
+ next();
13654
+ } catch (queueErr) {
13655
+ if (envIsDev) {
13656
+ try { console.error('[FormValidator][sync] a queued submit failed: '+ ( queueErr.message || queueErr )); } catch (e) {}
13657
+ }
13658
+ }
13659
+ }
13660
+
13661
+ /**
13662
+ * resolveSyncKey
13663
+ *
13664
+ * #gh76 slice 4 — the element a form coordinates on: its resolved swap target, or the
13665
+ * form itself when it declares none. Keying on the TARGET is what closes the reorder the
13666
+ * issue describes — two forms answering into one `#list` are invisible to a form-keyed
13667
+ * guard. A declared target that does not resolve falls back to the form, and that submit
13668
+ * is refused a few lines further down anyway, so the key it briefly used is moot.
13669
+ *
13670
+ * @param {HTMLFormElement} $formEl - the submitting form
13671
+ *
13672
+ * @returns {HTMLElement}
13673
+ *
13674
+ * @example
13675
+ * resolveSyncKey($form); // => <ul id="list">
13676
+ *
13677
+ * @inner
13678
+ */
13679
+ var resolveSyncKey = function($formEl) {
13680
+ var attr = $formEl.getAttribute('data-gina-form-target');
13681
+ var resolved = null;
13682
+ if ( attr === null ) {
13683
+ return $formEl;
13684
+ }
13685
+ resolved = resolveSwapTarget($formEl, attr);
13686
+ return ( resolved.target ) ? resolved.target : $formEl;
13687
+ }
13688
+
13689
+ /**
13690
+ * resolveDisabledElts
13691
+ *
13692
+ * #gh76 slice 4 — resolves a `data-gina-form-disabled-elt` value: a comma-separated list,
13693
+ * each part carrying the `data-gina-form-target` grammar (`this`, `closest …`, `find …`,
13694
+ * a CSS selector; `next`/`previous` reserved). A part that resolves to nothing is an
13695
+ * ERROR, not a silent skip: the attribute exists so a user cannot act twice, and a typo
13696
+ * that quietly disables nothing is the very failure it was added to prevent.
13697
+ *
13698
+ * @param {HTMLFormElement} $formEl - the submitting form
13699
+ * @param {string} value - the attribute value
13700
+ *
13701
+ * @returns {{elements: (Array<HTMLElement>|undefined), error: (string|undefined)}}
13702
+ *
13703
+ * @example
13704
+ * resolveDisabledElts($form, 'closest fieldset, #save'); // => { elements: [<fieldset>, <button>] }
13705
+ * resolveDisabledElts($form, '#nope'); // => { error: '`#nope`: no element matches `#nope`' }
13706
+ *
13707
+ * @inner
13708
+ */
13709
+ var resolveDisabledElts = function($formEl, value) {
13710
+ var raw = ( typeof(value) == 'string' ) ? value : '';
13711
+ var parts = raw.split(',');
13712
+ var out = [], i = 0, part = null, resolved = null;
13713
+ if ( raw.trim() === '' ) {
13714
+ return { error: 'empty value' };
13715
+ }
13716
+ for (; i < parts.length; ++i) {
13717
+ part = parts[i].trim();
13718
+ if ( part === '' ) {
13719
+ return { error: 'empty part at position '+ ( i + 1 ) };
13720
+ }
13721
+ resolved = resolveSwapTarget($formEl, part);
13722
+ if ( resolved.error ) {
13723
+ return { error: '`'+ part +'`: '+ resolved.error };
13724
+ }
13725
+ if ( out.indexOf(resolved.target) < 0 ) {
13726
+ out.push(resolved.target);
13727
+ }
13728
+ }
13729
+ return { elements: out };
13730
+ }
13731
+
13732
+ /**
13733
+ * disableForRequest
13734
+ *
13735
+ * #gh76 slice 4 — holds one element disabled for the life of a request, refcounted so
13736
+ * overlapping requests naming the same element release it once. An element ALREADY
13737
+ * disabled without our marker was disabled by the page: it is left untouched and not
13738
+ * counted, so the release can never clear a state we did not set.
13739
+ *
13740
+ * @param {HTMLElement} $el - the element to hold disabled
13741
+ * @param {string} formId - written to `data-gina-disabled-by` as provenance
13742
+ *
13743
+ * @returns {boolean} whether this request now holds the element
13744
+ *
13745
+ * @example
13746
+ * disableForRequest($fieldset, 'add-row'); // => true
13747
+ *
13748
+ * @inner
13749
+ */
13750
+ var disableForRequest = function($el, formId) {
13751
+ var n = disabledRefs.get($el) || 0;
13752
+ if ( n === 0 ) {
13753
+ if ( $el.hasAttribute('disabled') ) {
13754
+ return false;
13755
+ }
13756
+ $el.setAttribute('disabled', '');
13757
+ $el.setAttribute('data-gina-disabled-by', formId);
13758
+ }
13759
+ disabledRefs.set($el, n + 1);
13760
+ return true;
13761
+ }
13762
+
13763
+ /**
13764
+ * releaseDisabledElts
13765
+ *
13766
+ * #gh76 slice 4 — releases the elements one request held, at its settle. Decrements each
13767
+ * refcount and clears `disabled` only at zero, and only on an element still carrying our
13768
+ * own `data-gina-disabled-by` marker.
13769
+ *
13770
+ * @param {Array<HTMLElement>} [elements] - what this request held
13771
+ *
13772
+ * @returns {undefined}
13773
+ *
13774
+ * @example
13775
+ * releaseDisabledElts(sendCtx.disabledElts);
13776
+ *
13777
+ * @inner
13778
+ */
13779
+ var releaseDisabledElts = function(elements) {
13780
+ var i = 0, $el = null, n = 0;
13781
+ if ( !elements || !elements.length ) {
13782
+ return;
13783
+ }
13784
+ for (; i < elements.length; ++i) {
13785
+ $el = elements[i];
13786
+ n = disabledRefs.get($el) || 0;
13787
+ if ( n > 1 ) {
13788
+ disabledRefs.set($el, n - 1);
13789
+ continue;
13790
+ }
13791
+ disabledRefs['delete']($el);
13792
+ if ( $el.getAttribute('data-gina-disabled-by') !== null ) {
13793
+ $el.removeAttribute('disabled');
13794
+ $el.removeAttribute('data-gina-disabled-by');
13795
+ }
13796
+ }
13797
+ }
13798
+
13799
+ /**
13800
+ * warnOobRefusals
13801
+ *
13802
+ * #gh76 slice 3 — dev-mode notice for every out-of-band element that did not swap for a
13803
+ * REASON (no id, no page match, reserved or unknown value, cancelled). `swap="none"`
13804
+ * writes nothing by design and carries no reason, so it is not reported.
13805
+ *
13806
+ * @param {string} id - the form id
13807
+ * @param {Array<{id: (string|null), strategy: string, swapped: boolean, reason: (string|undefined)}>} list - `applyOobSwaps().list`
13808
+ *
13809
+ * @returns {undefined}
13810
+ *
13811
+ * @example
13812
+ * warnOobRefusals(id, run.list);
13813
+ */
13814
+ var warnOobRefusals = function(id, list) {
13815
+ if ( !envIsDev || !list ) return;
13816
+ for (var i = 0; i < list.length; ++i) {
13817
+ if ( !list[i].swapped && list[i].reason ) {
13818
+ try { console.warn('[FormValidator][swap] form `#'+ id +'`: out-of-band element `'+ (list[i].id || '(no id)') +'` not swapped — '+ list[i].reason); } catch (e) {}
13819
+ }
13820
+ }
13821
+ }
13822
+
13823
+ /**
13824
+ * applyResponseOverrides
13825
+ *
13826
+ * #gh76 §6 — the server's last word on where a `text/html` answer goes and how: three
13827
+ * response headers, read at settle AFTER the answer is known to be HTML and BEFORE the
13828
+ * swap-target fork and `beforeswap` (htmx's order, so a listener keeps the final say):
13829
+ * `X-Gina-Retarget` — the `data-gina-form-target` grammar (`this`, `closest`, `find`,
13830
+ * a selector; `next`/`previous` refused), resolved from the submitting form through
13831
+ * resolveSwapTarget. On success it BECOMES the target and `sendCtx.targetAttr` names
13832
+ * the header: a declaration made by the server, so it wins over the popin exactly as
13833
+ * a declared attribute does, and it creates a target where none was declared.
13834
+ * `X-Gina-Reswap` — one of SWAP_STRATEGIES, replacing the declared strategy.
13835
+ * `X-Gina-Reselect` — a selector applied to the answer, replacing `data-gina-form-select`.
13836
+ * The invalid-value rule is ASYMMETRIC, each half consistent with a rule that already
13837
+ * exists. A Retarget that cannot be resolved means NO swap (`applied: false` + the
13838
+ * reason, and the target is dropped): slice 2's settle-time refusal for a target that
13839
+ * left the document — inserting into the declared target could land the answer in the
13840
+ * wrong DOM when the server plainly meant somewhere else. A Reswap or Reselect that
13841
+ * fails validation is IGNORED and the declared value kept: a modifier on a known target,
13842
+ * and htmx degrades an invalid `HX-Reswap` the same way. Either without a target —
13843
+ * declared or retargeted — is ignored too (`reason: 'noTarget'`). Never a 422: a
13844
+ * misconfiguration surfacing as an error after a successful write is the false-422
13845
+ * shape slice 1 removed. Dev-mode console notices name every refused or ignored value.
13846
+ * SAME-ORIGIN ONLY. All three are honoured only from a response whose origin is the
13847
+ * page's own (`xhr.responseURL` — the URL after redirects — against
13848
+ * `window.location.origin`): a responder elsewhere may not choose the element its
13849
+ * answer lands in, nor reshape or trim the swap the author declared. A cross-origin
13850
+ * Retarget is refused exactly as an unresolvable one (no swap, `reason: 'crossOrigin'`);
13851
+ * a cross-origin Reswap/Reselect is ignored and the declared value kept. Fail-closed: a
13852
+ * missing or unparseable `responseURL`, or an opaque origin on either side, reads as
13853
+ * not-same-origin. htmx has no such read because `selfRequestsOnly` refuses the
13854
+ * cross-origin REQUEST; a form here posts to its raw `action`, so the gate is at the read.
13855
+ *
13856
+ * Returns the report — `null` when the answer carried none of the three — and stores it
13857
+ * on `sendCtx.overrides`, from where `beforeswap` and the success payload carry it.
13858
+ *
13859
+ * @param {XMLHttpRequest} xhr - the settled transport (its `getResponseHeader` and `responseURL`)
13860
+ * @param {object} sendCtx - the per-send capture (`target`/`targetAttr`/`swap`/`select` mutated)
13861
+ * @param {HTMLFormElement} $target - the submitting form (the `this`/`closest`/`find` base)
13862
+ * @param {string} id - the form id (notices)
13863
+ *
13864
+ * @returns {(null|{retarget: (object|undefined), reswap: (object|undefined), reselect: (object|undefined)})}
13865
+ * each entry `{ value, applied, reason }` (`reason` only when not applied)
13866
+ *
13867
+ * @example
13868
+ * applyResponseOverrides(xhr, sendCtx, $target, id); // => { retarget: { value: '#totals', applied: true } }
13869
+ */
13870
+ var applyResponseOverrides = function(xhr, sendCtx, $target, id) {
13871
+ var read = function(name) {
13872
+ var v = null;
13873
+ try { v = xhr.getResponseHeader(name); } catch (headerErr) { v = null; }
13874
+ return ( typeof(v) == 'string' ) ? v.trim() : null;
13875
+ };
13876
+ var retarget = read('X-Gina-Retarget')
13877
+ , reswap = read('X-Gina-Reswap')
13878
+ , reselect = read('X-Gina-Reselect')
13879
+ , report = null
13880
+ , resolved = null
13881
+ , hasTarget = false
13882
+ , notice = function(msg) {
13883
+ if (envIsDev) {
13884
+ try { console.warn('[FormValidator][swap] form `#'+ id +'`: '+ msg); } catch (e) {}
13885
+ }
13886
+ }
13887
+ ;
13888
+ if ( retarget === null && reswap === null && reselect === null ) {
13889
+ return null;
13890
+ }
13891
+ // #gh76 §6 — same-origin only. The headers are honoured from a response whose
13892
+ // origin is the page's own: a responder elsewhere may not choose the element its
13893
+ // answer lands in, nor reshape or trim the swap the author declared. htmx never
13894
+ // needs this read (`selfRequestsOnly` refuses the cross-origin REQUEST); a form
13895
+ // here posts to its raw `action`, so the gate belongs at the read. Fail-closed: no
13896
+ // `responseURL` (a transport that cannot say where the answer came from), an
13897
+ // unparseable one, or an opaque origin on either side (`'null'` — two opaque
13898
+ // origins are never the same origin, whatever the string says) all read as
13899
+ // not-same-origin. `responseURL` is the URL AFTER redirects, so a same-origin
13900
+ // action that was redirected elsewhere is refused too.
13901
+ var sameOrigin = false
13902
+ , resOrigin = 'an unknown origin'
13903
+ ;
13904
+ try {
13905
+ var docOrigin = window.location.origin
13906
+ , resURL = xhr.responseURL
13907
+ ;
13908
+ if ( typeof(docOrigin) == 'string' && docOrigin !== '' && docOrigin !== 'null'
13909
+ && typeof(resURL) == 'string' && resURL !== '' ) {
13910
+ resOrigin = new URL(resURL, window.location.href).origin;
13911
+ sameOrigin = ( resOrigin === docOrigin );
13912
+ }
13913
+ } catch (originErr) {
13914
+ sameOrigin = false;
13915
+ }
13916
+ if ( !sameOrigin ) {
13917
+ report = {};
13918
+ if ( retarget !== null ) {
13919
+ report.retarget = { value: retarget, applied: false, reason: 'crossOrigin' };
13920
+ sendCtx.targetAttr = 'X-Gina-Retarget';
13921
+ sendCtx.target = null;
13922
+ notice('`X-Gina-Retarget: '+ retarget +'` refused — the answer came from '+ resOrigin +', not from this page\'s origin; nothing swapped');
13923
+ }
13924
+ if ( reswap !== null ) {
13925
+ report.reswap = { value: reswap, applied: false, reason: 'crossOrigin' };
13926
+ notice('`X-Gina-Reswap: '+ reswap +'` ignored — the answer came from '+ resOrigin +', not from this page\'s origin; `'+ sendCtx.swap +'` kept');
13927
+ }
13928
+ if ( reselect !== null ) {
13929
+ report.reselect = { value: reselect, applied: false, reason: 'crossOrigin' };
13930
+ notice('`X-Gina-Reselect: '+ reselect +'` ignored — the answer came from '+ resOrigin +', not from this page\'s origin; `'+ ( sendCtx.select || 'the whole answer' ) +'` kept');
13931
+ }
13932
+ sendCtx.overrides = report;
13933
+ return report;
13934
+ }
13935
+ report = {};
13936
+ if ( retarget !== null ) {
13937
+ resolved = resolveSwapTarget($target, retarget);
13938
+ sendCtx.targetAttr = 'X-Gina-Retarget';
13939
+ if ( resolved.error ) {
13940
+ report.retarget = { value: retarget, applied: false, reason: resolved.error };
13941
+ sendCtx.target = null;
13942
+ notice('`X-Gina-Retarget: '+ retarget +'` cannot be honoured ('+ resolved.error +') — nothing swapped');
13943
+ } else {
13944
+ report.retarget = { value: retarget, applied: true };
13945
+ sendCtx.target = resolved.target;
13946
+ }
13947
+ }
13948
+ hasTarget = !!sendCtx.target;
13949
+ if ( reswap !== null ) {
13950
+ if ( !hasTarget ) {
13951
+ report.reswap = { value: reswap, applied: false, reason: 'noTarget' };
13952
+ notice('`X-Gina-Reswap: '+ reswap +'` ignored — the answer has no swap target');
13953
+ } else if ( SWAP_STRATEGIES.indexOf(reswap) < 0 ) {
13954
+ report.reswap = { value: reswap, applied: false, reason: 'unknown swap strategy `'+ reswap +'` (one of '+ SWAP_STRATEGIES.join(', ') +')' };
13955
+ notice('`X-Gina-Reswap: '+ reswap +'` ignored — unknown strategy, `'+ sendCtx.swap +'` kept');
13956
+ } else {
13957
+ report.reswap = { value: reswap, applied: true };
13958
+ sendCtx.swap = reswap;
13959
+ }
13960
+ }
13961
+ if ( reselect !== null ) {
13962
+ var selectorOk = ( reselect !== '' );
13963
+ if ( selectorOk ) {
13964
+ try { document.createDocumentFragment().querySelector(reselect); } catch (selectorErr) { selectorOk = false; }
13965
+ }
13966
+ if ( !hasTarget ) {
13967
+ report.reselect = { value: reselect, applied: false, reason: 'noTarget' };
13968
+ notice('`X-Gina-Reselect: '+ reselect +'` ignored — the answer has no swap target');
13969
+ } else if ( !selectorOk ) {
13970
+ report.reselect = { value: reselect, applied: false, reason: 'invalid selector `'+ reselect +'`' };
13971
+ notice('`X-Gina-Reselect: '+ reselect +'` ignored — invalid selector, `'+ ( sendCtx.select || 'the whole answer' ) +'` kept');
13972
+ } else {
13973
+ report.reselect = { value: reselect, applied: true };
13974
+ sendCtx.select = reselect;
13975
+ }
13976
+ }
13977
+ sendCtx.overrides = report;
13978
+ return report;
13979
+ }
13980
+
13981
+ /**
13982
+ * applySwap
13983
+ *
13984
+ * #gh76 slice 2 — swaps a `text/html` answer into the target captured at submit. ONE
13985
+ * parse (`parseXhrHtmlAnswer`, the hidden inputs stripped), the optional
13986
+ * `data-gina-form-select` pick (every match, document order), the attached check, the
13987
+ * cancelable `beforeswap.<id>` event (a listener's `preventDefault()` skips the swap;
13988
+ * `detail.content` is re-read after dispatch, so a listener may rewrite it), the DOM
13989
+ * write by strategy, the region binding through the shared `bindRegion()` policy, then
13990
+ * `afterswap.<id>` (+ `.hform`). Returns the success payload; the caller's shared tail
13991
+ * emits `success`. Fail-soft on a swap that cannot happen (no `select` match, a target
13992
+ * that left the document): `swapped: false`, a dev notice, the payload still delivered
13993
+ * — the data is already saved.
13994
+ *
13995
+ * A swap that replaces or removes the submitting form (`outerHTML`/`delete` on a target
13996
+ * containing it) defers the form's own re-binding: its listeners deliver the events
13997
+ * first, then `finalizeSelfReplacement` binds the same-id replacement.
13998
+ *
13999
+ * @param {object} sendCtx - the per-send capture
14000
+ * @param {object} $form - the form's registry entry
14001
+ * @param {HTMLFormElement} $target - the form element
14002
+ * @param {string} id - the form id
14003
+ * @param {boolean} hFormIsRequired - whether the declared-callback channel is armed
14004
+ * @param {{contentType: string, content: string, status: number}} result - the legacy payload
14005
+ *
14006
+ * @returns {{contentType: string, content: string, status: number, data: (object|null), view: (object|null), target: HTMLElement, swap: string, swapped: boolean}}
14007
+ *
14008
+ * @example
14009
+ * result = applySwap(sendCtx, $form, $target, id, hFormIsRequired, result); // => { …, swapped: true }
14010
+ */
14011
+ var applySwap = function(sendCtx, $form, $target, id, hFormIsRequired, result) {
14012
+ var strategy = sendCtx.swap || 'innerHTML'
14013
+ , $el = sendCtx.target
14014
+ , parsed = parseXhrHtmlAnswer(result.content)
14015
+ , content = null
14016
+ , $nodes = null
14017
+ , evt = null
14018
+ , $scope = null
14019
+ , attached = false
14020
+ , detachesForm = false
14021
+ , payload = {
14022
+ contentType : result.contentType,
14023
+ content : result.content,
14024
+ status : result.status,
14025
+ data : parsed.data,
14026
+ view : parsed.view,
14027
+ target : $el,
14028
+ swap : strategy,
14029
+ swapped : false
14030
+ }
14031
+ ;
14032
+ // #gh76 §6 — present only when the answer carried an override header
14033
+ if ( sendCtx.overrides ) {
14034
+ payload.overrides = sendCtx.overrides;
14035
+ }
14036
+ var warn = function(msg) {
14037
+ if (envIsDev) {
14038
+ try { console.warn('[FormValidator][swap] form `#'+ id +'`: '+ msg); } catch (e) {}
14039
+ }
14040
+ };
14041
+ // dev toolbar
14042
+ try {
14043
+ if ( gina && envIsDev && typeof(window.ginaToolbar) != 'undefined' && window.ginaToolbar ) {
14044
+ if (parsed.data) window.ginaToolbar.update('data-xhr', parsed.data);
14045
+ if (parsed.view) window.ginaToolbar.update('view-xhr', parsed.view);
14046
+ }
14047
+ } catch (toolbarErr) {}
14048
+
14049
+ // #gh76 slice 3 — out-of-band elements first (htmx order: before `select`, before
14050
+ // the main swap), gated on the attribute appearing in the answer so an oob-free
14051
+ // answer pays nothing and stays byte-identical. `content` keeps the RAW answer;
14052
+ // `remainder` is what is left once the oob elements are out.
14053
+ if ( typeof(result.content) == 'string' && result.content.indexOf('data-gina-swap-oob') > -1 ) {
14054
+ var oobRun = applyOobSwaps(parsed.doc, { $target: $target, id: id, hFormIsRequired: hFormIsRequired, gina: gina });
14055
+ warnOobRefusals(id, oobRun.list);
14056
+ payload.oob = oobRun.list;
14057
+ payload.remainder = parsed.doc.body.innerHTML;
14058
+ if ( oobRun.rebindSelf ) sendCtx.rebindSelf = true;
14059
+ }
14060
+
14061
+ if ( strategy === 'textContent' ) {
14062
+ // htmx: the raw answer, not parsed as HTML — but never the out-of-band elements,
14063
+ // which are consumed transport (#gh76 slice 3): once the gate fired, `remainder`
14064
+ // IS the answer without them. Below the gate this is the raw answer, unchanged.
14065
+ content = ( typeof(payload.remainder) == 'string' ) ? payload.remainder : result.content;
14066
+ } else if ( sendCtx.select ) {
14067
+ try {
14068
+ $nodes = parsed.doc.querySelectorAll(sendCtx.select);
14069
+ } catch (selErr) {
14070
+ $nodes = [];
14071
+ }
14072
+ if ( !$nodes.length ) {
14073
+ warn('`data-gina-form-select` matched nothing in the answer (`'+ sendCtx.select +'`) — nothing swapped');
14074
+ return payload;
14075
+ }
14076
+ content = '';
14077
+ for (var n = 0, nLen = $nodes.length; n < nLen; ++n) {
14078
+ content += $nodes[n].outerHTML;
14079
+ }
14080
+ } else {
14081
+ content = parsed.doc.body.innerHTML;
14082
+ }
14083
+
14084
+ attached = ( typeof($el.isConnected) == 'boolean' ) ? $el.isConnected : document.contains($el);
14085
+ if ( !attached ) {
14086
+ warn('the target captured at submit (`'+ sendCtx.targetAttr +'`) is no longer in the document — nothing swapped');
14087
+ return payload;
14088
+ }
14089
+
14090
+ if ( strategy !== 'none' ) {
14091
+ var beforeDetail = { target: $el, content: content, strategy: strategy, select: sendCtx.select };
14092
+ if ( sendCtx.overrides ) {
14093
+ beforeDetail.overrides = sendCtx.overrides;
14094
+ }
14095
+ evt = triggerEvent(gina, $target, 'beforeswap.' + id, beforeDetail);
14096
+ if ( evt && evt.defaultPrevented ) {
14097
+ warn('the swap was cancelled by a `beforeswap` listener');
14098
+ return payload;
14099
+ }
14100
+ if ( evt && evt.detail && typeof(evt.detail.content) == 'string' ) {
14101
+ content = evt.detail.content;
14102
+ }
14103
+ }
14104
+
14105
+ detachesForm = ( strategy === 'outerHTML' || strategy === 'delete' ) && ( $el === $target || $el.contains($target) );
14106
+
14107
+ switch (strategy) {
14108
+ case 'innerHTML':
14109
+ $el.innerHTML = content;
14110
+ $scope = $el;
14111
+ break;
14112
+ case 'outerHTML':
14113
+ $scope = $el.parentNode;
14114
+ $el.outerHTML = content;
14115
+ break;
14116
+ case 'textContent':
14117
+ $el.textContent = content;
14118
+ break;
14119
+ case 'beforebegin':
14120
+ $scope = $el.parentNode;
14121
+ $el.insertAdjacentHTML('beforebegin', content);
14122
+ break;
14123
+ case 'afterbegin':
14124
+ $el.insertAdjacentHTML('afterbegin', content);
14125
+ $scope = $el;
14126
+ break;
14127
+ case 'beforeend':
14128
+ $el.insertAdjacentHTML('beforeend', content);
14129
+ $scope = $el;
14130
+ break;
14131
+ case 'afterend':
14132
+ $scope = $el.parentNode;
14133
+ $el.insertAdjacentHTML('afterend', content);
14134
+ break;
14135
+ case 'delete':
14136
+ if ( $el.parentNode ) $el.parentNode.removeChild($el);
14137
+ break;
14138
+ case 'none':
14139
+ break;
14140
+ }
14141
+ payload.swapped = ( strategy !== 'none' );
14142
+
14143
+ if ( $scope && typeof($scope.getElementsByTagName) == 'function' ) {
14144
+ bindRegion($scope, { deferFormId: ( detachesForm ) ? id : null });
14145
+ }
14146
+ sendCtx.rebindSelf = sendCtx.rebindSelf || detachesForm;
14147
+
14148
+ if ( strategy !== 'none' ) {
14149
+ triggerEvent(gina, $target, 'afterswap.' + id, { target: $el, strategy: strategy, swapped: payload.swapped });
14150
+ if (hFormIsRequired)
14151
+ triggerEvent(gina, $target, 'afterswap.' + id + '.hform', { target: $el, strategy: strategy, swapped: payload.swapped });
14152
+ }
14153
+ return payload;
14154
+ }
14155
+
14156
+ /**
14157
+ * finalizeSelfReplacement
14158
+ *
14159
+ * #gh76 slice 2 — after a swap that replaced or removed the submitting form and after
14160
+ * its events were delivered: retire the stale registry entry (its element left the
14161
+ * document) and bind the same-id replacement when the answer carried one and it opts
14162
+ * in (#B549).
14163
+ *
14164
+ * @param {HTMLFormElement} $oldForm - the submitting form element
14165
+ * @param {string} id - its id
14166
+ *
14167
+ * @returns {undefined}
14168
+ *
14169
+ * @example
14170
+ * finalizeSelfReplacement($target, id);
14171
+ */
14172
+ var finalizeSelfReplacement = function($oldForm, id) {
14173
+ var $new = document.getElementById(id);
14174
+ var oldAttached = ( typeof($oldForm.isConnected) == 'boolean' ) ? $oldForm.isConnected : document.contains($oldForm);
14175
+ if ( !oldAttached ) {
14176
+ try {
14177
+ destroy(id);
14178
+ } catch (destroyErr) {
14179
+ delete instance.$forms[id];
14180
+ }
14181
+ }
14182
+ if ( $new && $new !== $oldForm && /^FORM$/i.test($new.tagName) && isFormOptedIn($new, local.rules) ) {
14183
+ validateFormById.call(instance, id);
14184
+ }
14185
+ }
14186
+
14187
+ /**
14188
+ * #B549 — does the page opt this form into the validator?
14189
+ *
14190
+ * The boot scan binds a `<form>` ONLY when the markup asks for it: a
14191
+ * `data-gina-form-*` attribute (`data-gina-form-rule`, the submit event
14192
+ * handlers, submit-method/action overrides, upload staging…), an EXISTING id
14193
+ * naming a registered rule (`-` read as `.`), or a virtual `gina-upload-*` id.
14194
+ * Anything else is left untouched — no minted id, no `$forms` entry, no submit
14195
+ * proxy — so a plain form on a rules-bearing page keeps its native submit, as
14196
+ * the guide states. Explicit `validateFormById()` / `getFormById()` calls are
14197
+ * not gated: the call is the opt-in.
14198
+ *
14199
+ * @private
14200
+ * @param {HTMLFormElement} $form
14201
+ * @param {object} rules - the registered rule sets, keyed by dotted name
14202
+ * @returns {boolean}
14203
+ * @example
14204
+ * isFormOptedIn(document.querySelector('form[data-gina-form-rule]'), gina.forms.rules); // true
14205
+ * isFormOptedIn(document.querySelector('form[action="/login"]'), gina.forms.rules); // false — bare
14206
+ */
14207
+ function isFormOptedIn($form, rules) {
14208
+ if ( !$form || typeof($form.getAttribute) != 'function' ) {
14209
+ return false;
14210
+ }
14211
+ var existingId = $form.getAttribute('id');
14212
+ if ( existingId && /^gina\-upload/i.test(existingId) ) {
14213
+ return true;
14214
+ }
14215
+ if ( existingId && rules && typeof(rules[existingId.replace(/\-/g, '.')]) != 'undefined' ) {
14216
+ return true;
14217
+ }
14218
+ var attrs = $form.attributes;
14219
+ for (var a = 0, aLen = (attrs) ? attrs.length : 0; a < aLen; ++a) {
14220
+ if ( /^data-gina-form-/i.test(attrs[a].name) ) {
14221
+ return true;
14222
+ }
12751
14223
  }
12752
-
12753
- return isPopinInUse;
14224
+ return false;
12754
14225
  }
12755
14226
 
12756
-
12757
14227
  /**
12758
14228
  * validateFormById
12759
14229
  *
@@ -14251,14 +15721,43 @@ function ValidatorPlugin(rules, data, formId, culture) {
14251
15721
  var XHRData = null;
14252
15722
  var isAttachment = null; // handle download
14253
15723
  var hFormIsRequired = null;
15724
+ // #gh76 — per-send capture of the popin the SUBMITTING form lives in (or null),
15725
+ // read at the settle below. `$form.eventData` is per form and a second send would
15726
+ // overwrite it; this closure is per send (#B175 made the XHR per send).
15727
+ /** @type {{ popin: (object|null), target: (HTMLElement|null), swap: string, select: (string|null), targetAttr: (string|null), rebindSelf: boolean, overrides: (object|undefined), sync: (string|undefined), syncDerived: (boolean|undefined), syncKey: (HTMLElement|undefined), superseded: (boolean|undefined), disabledElts: (Array<HTMLElement>|null|undefined) }} */
15728
+ var sendCtx = { popin: null, target: null, swap: 'innerHTML', select: null, targetAttr: null, rebindSelf: false };
15729
+ // #gh76 slice 4 — the receiver and the caller's ORIGINAL options, captured BEFORE the
15730
+ // merge below stamps headers into them: a `data-gina-form-sync="queue"` submit is
15731
+ // replayed by re-entering send() from the settle chokepoint, and it must re-enter with
15732
+ // what the caller passed, never with this send's merged copy.
15733
+ var syncReceiver = this, syncRawOptions = options, syncEntry = null, syncParsed = null;
14254
15734
 
14255
15735
  options = (typeof (options) != 'undefined') ? merge(options, xhrOptions) : xhrOptions;
14256
15736
 
15737
+ // forward callback to HTML data event attribute through `hform` status
15738
+ // success -> data-gina-form-event-on-submit-success
15739
+ // error -> data-gina-form-event-on-submit-error
15740
+ // afterswap -> data-gina-form-event-on-swap (#gh76 slice 2)
15741
+ // #gh76 slice 4 — read here, above the gate, so the coordination decision below can
15742
+ // refuse a submit into the declared error channel. The binding itself still happens
15743
+ // at its own site further down.
15744
+ hFormIsRequired = ( $target.getAttribute('data-gina-form-event-on-submit-success') || $target.getAttribute('data-gina-form-event-on-submit-error') || $target.getAttribute('data-gina-form-event-on-swap') ) ? true : false;
15745
+
15746
+ // #gh76 slice 4 — `data-gina-form-sync` declares what this submit does when another
15747
+ // request already owns the same key. Read before the gate below, because a form that
15748
+ // declares it OWNS its own overlap decision: the gate is `setOptions`-wide with no
15749
+ // per-form form, so `replace` and `queue` could not be reached otherwise.
15750
+ var syncAttr = $target.getAttribute('data-gina-form-sync');
15751
+
14257
15752
  // Rate Limit to one request at the time
14258
15753
  // Attention: this should be an option
14259
15754
  // the request needs to be completed before another can be made
14260
15755
  // TODO - Check same url
15756
+ // #gh76 slice 4 — yields to `data-gina-form-sync` when that attribute is declared.
15757
+ // Absent ⇒ byte-identical behaviour to before.
14261
15758
  if (
15759
+ syncAttr === null
15760
+ && (
14262
15761
  /^true$/i.test(options.withRateLimit)
14263
15762
  && typeof($form.isSending) != 'undefined'
14264
15763
  && /^true$/i.test($form.isSending)
@@ -14266,6 +15765,7 @@ function ValidatorPlugin(rules, data, formId, culture) {
14266
15765
  /^true$/i.test(options.withRateLimit)
14267
15766
  && typeof($form.sent) != 'undefined'
14268
15767
  && /^true$/i.test($form.sent)
15768
+ )
14269
15769
  ) {
14270
15770
  // #B247 — deliberately NO loading-state release here. This return means a
14271
15771
  // request for this form is already in flight, so the trigger that owns it is
@@ -14275,6 +15775,83 @@ function ValidatorPlugin(rules, data, formId, culture) {
14275
15775
  return;
14276
15776
  }
14277
15777
 
15778
+ // #gh76 slice 4 — the coordination decision. It runs BEFORE `isSending` is claimed
15779
+ // below because `replace` aborts the in-flight request, and an XHR abort settles its
15780
+ // handler SYNCHRONOUSLY: claiming first would let that settle clear the very flag this
15781
+ // send had just taken (the #B332 class). The same ordering is why `ownedByEarlierSend`
15782
+ // is read after this block — after an abort, nothing is owned by an earlier send.
15783
+ // The strategy is the form's own `data-gina-form-sync` when it declares one, and is
15784
+ // otherwise DERIVED from the swap strategy it already declared — so the case this
15785
+ // slice exists for, two forms replacing one region, is coordinated with nothing
15786
+ // written in the markup. The attribute is the override, not the switch.
15787
+ if ( syncAttr !== null ) {
15788
+ syncParsed = parseSync(syncAttr);
15789
+ } else {
15790
+ syncParsed = deriveSync($target);
15791
+ }
15792
+ // An UNREADABLE value coordinates nothing, so there is nothing to decide here — and the
15793
+ // refusal it owes cannot be delivered from this far up, where the declared callbacks are
15794
+ // not bound yet. It is carried to the pre-flight below, beside its `-target`/`-swap`
15795
+ // siblings, and the gate above has already yielded because the attribute is present, so
15796
+ // the submit is guaranteed to reach it.
15797
+ if ( syncParsed && !syncParsed.error ) {
15798
+ var syncKey = syncParsed.key || resolveSyncKey($target);
15799
+ var syncDecision = decideSync(syncParsed, syncRegistry.get(syncKey));
15800
+ if ( syncDecision.action === 'drop' ) {
15801
+ // #B247's ownership rule, re-derived for a TARGET key: a submit that stops here
15802
+ // never reaches an XHR, so no settle will release the loading state its own click
15803
+ // armed. The module-wide gate could return without releasing because it only ever
15804
+ // fires on the form that already owns the request — its settle does the release.
15805
+ // A target key coordinates DIFFERENT forms, so the one turned away here is usually
15806
+ // not the one running, and nothing else would ever release it. It releases only
15807
+ // what it owns: when this form IS the one in flight, the state belongs to that
15808
+ // request and its settle still owns it.
15809
+ if ( !( /^true$/i.test(instance.$forms[id].isSending) || /^true$/i.test($form.isSending) ) ) {
15810
+ disarmSubmitLoading($form);
15811
+ }
15812
+ syncNotice(id, 'submit dropped (`drop`) — a request already owns this target');
15813
+ return;
15814
+ }
15815
+ if ( syncDecision.action === 'queue' ) {
15816
+ syncEntry = syncRegistry.get(syncKey);
15817
+ // a QUEUED submit keeps its loading state: it is genuinely pending, and the
15818
+ // replay re-enters send() with `armSubmitLoading` first-arm-wins, so the state
15819
+ // carries straight through to the real request's settle. Only a submit turned
15820
+ // AWAY releases.
15821
+ queueSyncSend(syncEntry, function replaySyncSend() { send.call(syncReceiver, data, syncRawOptions); });
15822
+ syncNotice(id, 'submit queued (`queue`) — a request already owns this target');
15823
+ return;
15824
+ }
15825
+ if ( syncDecision.abortPrevious ) {
15826
+ syncEntry = syncRegistry.get(syncKey);
15827
+ if ( syncEntry && syncEntry.xhr ) {
15828
+ if ( syncEntry.ctx ) {
15829
+ syncEntry.ctx.superseded = true;
15830
+ }
15831
+ try { syncEntry.xhr.abort(); } catch (abortErr) {}
15832
+ syncNotice(id, ( syncParsed.derived )
15833
+ ? 'the request that owned this target was superseded — a `'+ syncParsed.swap +'` answer replaces the region, so the one still on the wire is moot (declare `data-gina-form-sync` to choose otherwise)'
15834
+ : 'the request that owned this target was superseded (`replace`)');
15835
+ // That settle released the loading state on its way out (`loadend` covers
15836
+ // abort). This send is the one now running, so it takes the state over:
15837
+ // `armSubmitLoading` is first-arm-wins and the stash was just cleared, so
15838
+ // on a same-form replace this is the arm that sticks; on two different
15839
+ // forms the replacing one is already armed and this is a no-op.
15840
+ armSubmitLoading(instance.$forms[id], $submitTrigger);
15841
+ }
15842
+ }
15843
+ sendCtx.sync = syncParsed.strategy;
15844
+ sendCtx.syncDerived = !!syncParsed.derived;
15845
+ sendCtx.syncKey = syncKey;
15846
+ }
15847
+
15848
+ // #gh76 slice 2 — whether a PREVIOUS request still owns `isSending` and the armed
15849
+ // loading state, read BEFORE this attempt claims them. Only reachable with
15850
+ // `withRateLimit: false` (the gate above returns otherwise), and only consulted by
15851
+ // a pre-send refusal below — which must not clear state it does not own.
15852
+ var ownedByEarlierSend = /^true$/i.test(instance.$forms[id].isSending)
15853
+ || /^true$/i.test($form.isSending);
15854
+
14278
15855
  instance.$forms[id].isSending = true;
14279
15856
 
14280
15857
 
@@ -14297,26 +15874,62 @@ function ValidatorPlugin(rules, data, formId, culture) {
14297
15874
  options.headers['X-Gina-Form-Rule'] = $form.target.dataset.ginaFormRule +'@'+ gina.config.bundle;
14298
15875
  }
14299
15876
 
14300
- if (isPopinContext()) {
14301
- // select popin current active popin
14302
- $activePopin = gina.popin.getActivePopin();
14303
- if ( $activePopin.isOpen ) {
14304
- if ( typeof($activePopin.id) != 'undefined' )
14305
- options.headers['X-Gina-Popin-Id'] = $activePopin.id;
15877
+ // #gh76 — the popin context a request carries is the popin the SUBMITTING form is
15878
+ // rendered in, resolved by containment, never "some popin is open" (the former
15879
+ // isPopinContext() gate): a page form submitted while a non-modal popin was open
15880
+ // told the server it came from that popin — `self.isPopinContext()` read true and
15881
+ // the redirect came back as a popin redirect — the request-side half of the same
15882
+ // defect. Gated on the popin handler existing, as the settle site always was.
15883
+ sendCtx.popin = (
15884
+ typeof(gina.popin) != 'undefined'
15885
+ && gina.popin
15886
+ && typeof(gina.popin.getPopinContaining) == 'function'
15887
+ ) ? gina.popin.getPopinContaining($target) : null;
15888
+ if ( sendCtx.popin && sendCtx.popin.isOpen ) {
15889
+ if ( typeof(sendCtx.popin.id) != 'undefined' )
15890
+ options.headers['X-Gina-Popin-Id'] = sendCtx.popin.id;
14306
15891
 
14307
- if ( typeof($activePopin.name) != 'undefined' )
14308
- options.headers['X-Gina-Popin-Name'] = $activePopin.name;
14309
- }
15892
+ if ( typeof(sendCtx.popin.name) != 'undefined' )
15893
+ options.headers['X-Gina-Popin-Name'] = sendCtx.popin.name;
14310
15894
  }
14311
15895
 
14312
15896
 
14313
- // forward callback to HTML data event attribute through `hform` status
14314
- hFormIsRequired = ( $target.getAttribute('data-gina-form-event-on-submit-success') || $target.getAttribute('data-gina-form-event-on-submit-error') ) ? true : false;
14315
- // success -> data-gina-form-event-on-submit-success
14316
- // error -> data-gina-form-event-on-submit-error
15897
+ // #gh76 slice 4 — `hFormIsRequired` itself is now decided at the top of send(): the
15898
+ // coordination decision up there can refuse a submit, and a refusal must reach the
15899
+ // declared error channel. Only the listener binding stays here, where it always was.
14317
15900
  if (hFormIsRequired)
14318
15901
  listenToXhrEvents($form);
14319
15902
 
15903
+ // #gh76 slice 2 — a DECLARED swap target is resolved NOW, from the submitting form,
15904
+ // and carried to the settle in this closure (the htmx contract: the target is a
15905
+ // property of the issuing element, resolved before the request is sent). A target
15906
+ // or strategy that cannot be honoured refuses the submit before anything leaves —
15907
+ // fail-LOUD, deliberately the opposite of `data-gina-dialog-target`'s silent
15908
+ // full-replace fallback.
15909
+ var targetAttr = $target.getAttribute('data-gina-form-target');
15910
+ var swapAttr = $target.getAttribute('data-gina-form-swap');
15911
+ var selectAttr = $target.getAttribute('data-gina-form-select');
15912
+ if ( targetAttr !== null ) {
15913
+ var resolvedTarget = resolveSwapTarget($target, targetAttr);
15914
+ if ( resolvedTarget.error ) {
15915
+ refuseSend($form, $target, id, hFormIsRequired, 'data-gina-form-target', targetAttr, resolvedTarget.error, ownedByEarlierSend);
15916
+ return;
15917
+ }
15918
+ sendCtx.target = resolvedTarget.target;
15919
+ sendCtx.targetAttr = targetAttr;
15920
+ }
15921
+ if ( swapAttr !== null ) {
15922
+ var strategy = swapAttr.trim();
15923
+ if ( SWAP_STRATEGIES.indexOf(strategy) < 0 ) {
15924
+ refuseSend($form, $target, id, hFormIsRequired, 'data-gina-form-swap', swapAttr, 'unknown swap strategy `'+ strategy +'` (one of '+ SWAP_STRATEGIES.join(', ') +')', ownedByEarlierSend);
15925
+ return;
15926
+ }
15927
+ sendCtx.swap = strategy;
15928
+ }
15929
+ if ( selectAttr !== null && selectAttr.trim() !== '' ) {
15930
+ sendCtx.select = selectAttr.trim();
15931
+ }
15932
+
14320
15933
  // #R8 — staged-upload send? (virtual `gina-upload-*` form) The upload-progress
14321
15934
  // channel below only activates for these sends.
14322
15935
  var isUploadXhr = /^gina\-upload/i.test(id);
@@ -14325,6 +15938,35 @@ function ValidatorPlugin(rules, data, formId, culture) {
14325
15938
  // handler); no default: absent attribute = no `.hform` progress channel
14326
15939
  var uploadProgressHFormIsRequired = ( $target.getAttribute('data-gina-form-event-on-upload-progress') ) ? true : false;
14327
15940
 
15941
+ // #gh76 slice 4 — `data-gina-form-disabled-elt`: elements held disabled for the life
15942
+ // of this request, so a user cannot act on what the answer is about to change. Placed
15943
+ // AFTER every pre-send refusal and after the coordination decision, and BEFORE the XHR
15944
+ // is opened, so a refused submit can never strand a disabled control; released at the
15945
+ // `loadend` fail-safe below — the one chokepoint that covers error, abort and timeout
15946
+ // as well as success.
15947
+ // #gh76 slice 4 — the refusal deferred from the coordination decision above: delivered
15948
+ // HERE, where `listenToXhrEvents` has bound the declared error callback, and outside the
15949
+ // `-target`/`-swap` window so that window's shape is untouched.
15950
+ if ( syncParsed && syncParsed.error ) {
15951
+ refuseSend($form, $target, id, hFormIsRequired, 'data-gina-form-sync', syncAttr, syncParsed.error, ownedByEarlierSend);
15952
+ return;
15953
+ }
15954
+
15955
+ var disabledAttr = $target.getAttribute('data-gina-form-disabled-elt');
15956
+ if ( disabledAttr !== null ) {
15957
+ var resolvedDisabled = resolveDisabledElts($target, disabledAttr);
15958
+ if ( resolvedDisabled.error ) {
15959
+ refuseSend($form, $target, id, hFormIsRequired, 'data-gina-form-disabled-elt', disabledAttr, resolvedDisabled.error, ownedByEarlierSend);
15960
+ return;
15961
+ }
15962
+ sendCtx.disabledElts = [];
15963
+ for (var dIdx = 0; dIdx < resolvedDisabled.elements.length; ++dIdx) {
15964
+ if ( disableForRequest(resolvedDisabled.elements[dIdx], id) ) {
15965
+ sendCtx.disabledElts.push(resolvedDisabled.elements[dIdx]);
15966
+ }
15967
+ }
15968
+ }
15969
+
14328
15970
  var url = $target.getAttribute('action') || options.url;
14329
15971
  var method = $target.getAttribute('method') || options.method;
14330
15972
  method = method.toUpperCase();
@@ -14442,8 +16084,43 @@ function ValidatorPlugin(rules, data, formId, culture) {
14442
16084
  // #A11Y4 — last, so focus lands on a trigger that is fully settled:
14443
16085
  // re-enabled above and no longer flagged loading.
14444
16086
  releaseSubmitA11y($form, $submitTrigger);
16087
+ // #gh76 slice 4 — the same one chokepoint releases what this request held
16088
+ // disabled, frees its coordination slot and starts the next deferred
16089
+ // submit. A slot is freed only by the request that owns it (a late settle
16090
+ // must never evict a newer one — the XHR object is that identity), and a
16091
+ // SUPERSEDED settle does not shift: the request that replaced it claims
16092
+ // the slot on the very next statement of the aborting send().
16093
+ releaseDisabledElts(sendCtx.disabledElts);
16094
+ sendCtx.disabledElts = null;
16095
+ if ( sendCtx.syncKey ) {
16096
+ var settledEntry = syncRegistry.get(sendCtx.syncKey);
16097
+ if ( settledEntry && settledEntry.xhr === xhr ) {
16098
+ settledEntry.xhr = null;
16099
+ settledEntry.ctx = null;
16100
+ if ( !sendCtx.superseded ) {
16101
+ shiftSyncQueue(settledEntry);
16102
+ }
16103
+ }
16104
+ }
14445
16105
  });
14446
16106
  }
16107
+ // #gh76 slice 4 — claiming the coordination slot is deliberately deferred to the
16108
+ // line AFTER `xhr.send()` at each of its three sites: claiming here would leave a
16109
+ // key wedged "busy" forever on the one path that opens an XHR and never sends it
16110
+ // (a `processFiles` failure on the binary branch), because `loadend` — the only
16111
+ // release — never fires for a request that never left.
16112
+ var claimSyncSlot = function() {
16113
+ if ( !sendCtx.syncKey ) {
16114
+ return;
16115
+ }
16116
+ var claimed = syncRegistry.get(sendCtx.syncKey);
16117
+ if ( !claimed ) {
16118
+ claimed = { xhr: null, ctx: null, queue: [] };
16119
+ syncRegistry.set(sendCtx.syncKey, claimed);
16120
+ }
16121
+ claimed.xhr = xhr;
16122
+ claimed.ctx = sendCtx;
16123
+ };
14447
16124
  // catching ready state cb
14448
16125
  // Data loading ...
14449
16126
  if ( /^(1|3)$/.test(xhr.readyState) ) {
@@ -14461,7 +16138,13 @@ function ValidatorPlugin(rules, data, formId, culture) {
14461
16138
  }
14462
16139
  //handleXhrResponse(xhr, $target, id, $form, hFormIsRequired);
14463
16140
  xhr.onreadystatechange = function onValidationCallback(event) {
14464
- $form.isSubmitting = false;
16141
+ // #gh76 slice 4 — a request superseded by `data-gina-form-sync="replace"`
16142
+ // settles SYNCHRONOUSLY inside the aborting send()'s own decision, before that
16143
+ // send has claimed this latch: clearing it unconditionally here would clear the
16144
+ // NEW cycle's flag and strand it (the #B332 class).
16145
+ if ( !sendCtx.superseded ) {
16146
+ $form.isSubmitting = false;
16147
+ }
14465
16148
  // #B175: `isSending` is no longer cleared here — this handler
14466
16149
  // first fires at readyState 1 (synchronously at open()), so an
14467
16150
  // early clear made the flag false for almost the whole request
@@ -14528,6 +16211,26 @@ function ValidatorPlugin(rules, data, formId, culture) {
14528
16211
  // with the `loadend` release, which may already have restored it.
14529
16212
  releaseSubmitA11y($form, $submitTrigger);
14530
16213
 
16214
+ // #gh76 slice 4 — this request was superseded by a later submit on the same
16215
+ // `data-gina-form-sync` key. Its RELEASE arms above have run, and must: the
16216
+ // trigger, the loading state and the a11y state may never outlive their
16217
+ // request. Its DISPATCH arms must not run at all — an aborted XHR settles at
16218
+ // readyState 4 with status 0, which the transport arm below reads as a 408
16219
+ // the server never sent (#B447). A deliberate abort is not an error: it
16220
+ // reaches its own `abort.<id>` channel and stops there, so neither `error`
16221
+ // nor `data-gina-form-event-on-submit-error` ever sees it.
16222
+ if ( sendCtx.superseded ) {
16223
+ triggerEvent(gina, $target, 'abort.' + id, {
16224
+ status : 0,
16225
+ reason : 'superseded',
16226
+ sync : sendCtx.sync || null,
16227
+ // the default needs no attribute, so a page seeing this event may
16228
+ // find nothing in its own markup that asked for it: say which it was
16229
+ derived : !!sendCtx.syncDerived
16230
+ });
16231
+ return;
16232
+ }
16233
+
14531
16234
  var $popin = null;
14532
16235
  var blob = null;
14533
16236
  var contentType = xhr.getResponseHeader("Content-Type");
@@ -14603,31 +16306,70 @@ function ValidatorPlugin(rules, data, formId, culture) {
14603
16306
  if ( typeof(result.status) == 'undefined' )
14604
16307
  result.status = xhr.status;
14605
16308
 
16309
+ // #gh76 §6 — the server's last word (X-Gina-Retarget / -Reswap /
16310
+ // -Reselect): read once the answer is known to be HTML, before
16311
+ // the target fork and before `beforeswap`, so a listener still
16312
+ // has the final say. A JSON answer never reaches this read.
16313
+ applyResponseOverrides(xhr, sendCtx, $target, id);
16314
+
16315
+ // #gh76 §6 — a Retarget that cannot be honoured: no swap, nowhere
16316
+ // (the popin included) — the server plainly meant somewhere else,
16317
+ // or the answer came from another origin, which may not choose.
16318
+ // The handler still receives the answer, and out-of-band elements
16319
+ // still land below: they are addressed by id, not by the target.
16320
+ if ( sendCtx.overrides && sendCtx.overrides.retarget && !sendCtx.overrides.retarget.applied ) {
16321
+ var parsedRefused = parseXhrHtmlAnswer(result.content);
16322
+ result = {
16323
+ contentType : result.contentType,
16324
+ content : result.content,
16325
+ status : result.status,
16326
+ data : parsedRefused.data,
16327
+ view : parsedRefused.view,
16328
+ target : null,
16329
+ swap : sendCtx.swap,
16330
+ swapped : false,
16331
+ reason : 'retargetError',
16332
+ overrides : sendCtx.overrides
16333
+ };
16334
+ }
16335
+ // #gh76 slice 2 — a DECLARED target wins over containment: the
16336
+ // answer is swapped into the element captured at submit (inside
16337
+ // or outside a popin), bound, and delivered with the richer
16338
+ // payload; the shared tail below emits `success`.
16339
+ else if ( sendCtx.target ) {
16340
+ result = applySwap(sendCtx, $form, $target, id, hFormIsRequired, result);
16341
+ }
14606
16342
  // if hasPopinHandler & popinIsBinded
14607
- if ( typeof(gina.popin) != 'undefined' && gina.hasPopinHandler ) {
14608
- // select popin current active popin
14609
- $popin = gina.popin.getActivePopin();
16343
+ else if ( typeof(gina.popin) != 'undefined' && gina.hasPopinHandler ) {
16344
+ // #gh76 — the answer goes to the popin the form was captured
16345
+ // in at submit, while it is still open and still contains the
16346
+ // form; anything else is the legacy handler-only payload.
16347
+ // Never "the active popin": that routed a PAGE form's answer
16348
+ // into an open popin (content replaced) or a loading one (a
16349
+ // false 422, `Popin x is not open !`), and with two popins
16350
+ // open picked the first registered one.
16351
+ $popin = popinStillContaining(sendCtx.popin, $target);
16352
+ warnIfOldRuleRouted(sendCtx.popin, $popin, $target, 'answer');
14610
16353
 
14611
16354
  if ($popin) {
14612
16355
 
14613
- XHRData = {};
16356
+ // #B575 — ONE tolerant parse: the hidden inputs are a dev-mode
16357
+ // transport (spliced only under NODE_ENV_IS_DEV), so outside
16358
+ // dev mode they are absent — dereferencing them threw inside
16359
+ // this try and surfaced as a false 422 error callback, and the
16360
+ // popin was never loaded
16361
+ var parsedAnswer = parseXhrHtmlAnswer(result.content);
16362
+ XHRData = parsedAnswer.data;
16363
+ XHRView = parsedAnswer.view;
14614
16364
  // update toolbar
14615
-
14616
16365
  try {
14617
- XHRData = new DOMParser().parseFromString(result.content, 'text/html').getElementById('gina-without-layout-xhr-data');
14618
- XHRData = JSON.parse(decodeURIComponent(XHRData.value));
14619
-
14620
- XHRView = new DOMParser().parseFromString(result.content, 'text/html').getElementById('gina-without-layout-xhr-view');
14621
- XHRView = JSON.parse(decodeURIComponent(XHRView.value));
14622
-
14623
16366
  // update data tab
14624
- if ( gina && envIsDev && typeof(window.ginaToolbar) && typeof(XHRData) != 'undefined' ) {
16367
+ if ( gina && envIsDev && typeof(window.ginaToolbar) && XHRData ) {
14625
16368
  window.ginaToolbar.update("data-xhr", XHRData);
14626
16369
  }
14627
16370
 
14628
16371
  // update view tab
14629
-
14630
- if ( gina && envIsDev && typeof(window.ginaToolbar) && typeof(XHRView) != 'undefined' ) {
16372
+ if ( gina && envIsDev && typeof(window.ginaToolbar) && XHRView ) {
14631
16373
  window.ginaToolbar.update("view-xhr", XHRView);
14632
16374
  }
14633
16375
 
@@ -14635,15 +16377,69 @@ function ValidatorPlugin(rules, data, formId, culture) {
14635
16377
  throw err
14636
16378
  }
14637
16379
 
16380
+ // #gh76 slice 3 — out-of-band elements are swapped into the PAGE
16381
+ // before the popin receives what is left; an EMPTY remainder means
16382
+ // nothing was addressed to the popin, so it is left as it is
16383
+ // (loading '' would blank the open dialog and unbind its form —
16384
+ // measured). The popin payload below is untouched: the oob
16385
+ // results reach the consumer through the events.
16386
+ var popinContent = result.content, oobRunPopin = null;
16387
+ if ( typeof(result.content) == 'string' && result.content.indexOf('data-gina-swap-oob') > -1 ) {
16388
+ oobRunPopin = applyOobSwaps(parsedAnswer.doc, { $target: $target, id: id, hFormIsRequired: hFormIsRequired, gina: gina });
16389
+ warnOobRefusals(id, oobRunPopin.list);
16390
+ popinContent = parsedAnswer.doc.body.innerHTML;
16391
+ if ( oobRunPopin.rebindSelf ) sendCtx.rebindSelf = true;
16392
+ }
16393
+ if ( oobRunPopin === null || popinContent.trim() !== '' ) {
16394
+ $popin.loadContent(popinContent);
16395
+ }
14638
16396
 
14639
- $popin.loadContent(result.content);
14640
-
14641
- result = XHRData;
16397
+ // the parsed xhr-data in dev mode, delivered VERBATIM, or
16398
+ // an object carrying the status outside dev mode where the
16399
+ // transport inputs are absent (the handler contract is "an
16400
+ // object", never null). The parsed data is never mutated:
16401
+ // injecting a `status` key into the action's own data would
16402
+ // change the payload every contained form already receives.
16403
+ result = XHRData || { status: xhr.status };
16404
+ // #B571 — this branch returned after the bare event, so the
16405
+ // DECLARED success callback (bound to the `.hform` channel by
16406
+ // listenToXhrEvents) never ran for a form answering into a
16407
+ // popin; mirror the shared tail below, eventData included.
16408
+ $form.eventData.success = result;
14642
16409
  triggerEvent(gina, $target, 'success.' + id, result);
14643
16410
 
16411
+ if (hFormIsRequired)
16412
+ triggerEvent(gina, $target, 'success.' + id + '.hform', result);
16413
+
16414
+ // #gh76 slice 3 — an oob swap that replaced the submitting form
16415
+ // (the popin left as it was) binds the same-id replacement now,
16416
+ // exactly as the shared tail below does
16417
+ if ( sendCtx.rebindSelf ) {
16418
+ finalizeSelfReplacement($target, id);
16419
+ }
14644
16420
  return;
14645
16421
  }
14646
16422
  }
16423
+
16424
+ // #gh76 slice 3 — the legacy path (no declared target, not inside a
16425
+ // popin): the raw answer still goes to the handler, inserted nowhere,
16426
+ // but its out-of-band elements are swapped into the page first (htmx:
16427
+ // "out of band items will still be processed"). `content` stays raw;
16428
+ // `remainder` is what to insert, `oob` what was handled.
16429
+ if ( !sendCtx.target && typeof(result.content) == 'string' && result.content.indexOf('data-gina-swap-oob') > -1 ) {
16430
+ var parsedLegacy = parseXhrHtmlAnswer(result.content);
16431
+ var oobRunLegacy = applyOobSwaps(parsedLegacy.doc, { $target: $target, id: id, hFormIsRequired: hFormIsRequired, gina: gina });
16432
+ warnOobRefusals(id, oobRunLegacy.list);
16433
+ result.oob = oobRunLegacy.list;
16434
+ result.remainder = parsedLegacy.doc.body.innerHTML;
16435
+ if ( oobRunLegacy.rebindSelf ) sendCtx.rebindSelf = true;
16436
+ }
16437
+ // #gh76 §6 — an override the server sent to a target-less form
16438
+ // (ignored, `reason: 'noTarget'`) is still reported to the handler;
16439
+ // the popin path's parsed data is delivered verbatim and carries none
16440
+ if ( !sendCtx.target && sendCtx.overrides && typeof(result.overrides) == 'undefined' ) {
16441
+ result.overrides = sendCtx.overrides;
16442
+ }
14647
16443
  }
14648
16444
 
14649
16445
  $form.eventData.success = result;
@@ -14684,14 +16480,34 @@ function ValidatorPlugin(rules, data, formId, culture) {
14684
16480
  && typeof(result.location) != 'undefined'
14685
16481
  && isXhrRedirect
14686
16482
  ) {
14687
- $popin = gina.popin.getActivePopin();
16483
+ // #gh76 — route by the SUBMITTING form's popin, never by "the
16484
+ // active popin". A name-less `result.popin` directive (`close`,
16485
+ // `url`) is the one case where the server addresses "the open
16486
+ // popin" explicitly — only a route the app declared as popin
16487
+ // context produces it — so it keeps the open-only fallback; a
16488
+ // plain `location` redirect was chosen by the server BECAUSE it
16489
+ // saw no popin context, so from a page form it falls through to
16490
+ // the page redirect below instead of loading into a popin the
16491
+ // form is not inside.
16492
+ $popin = sendCtx.popin
16493
+ || ( ( typeof(result.popin) != 'undefined' ) ? gina.popin.getActivePopin() : null );
16494
+ if ( !$popin && typeof(result.popin) == 'undefined' ) {
16495
+ warnIfOldRuleRouted(sendCtx.popin, $popin, $target, 'redirect');
16496
+ }
14688
16497
  if ( !$popin && typeof(result.popin) != 'undefined' ) {
14689
- if ( typeof(result.popin) != 'undefined' && typeof(result.popin.name) == 'undefined' ) {
14690
- throw new Error('To get a `$popin` instance, you need at list a `popin.name`.');
14691
- }
14692
- $popin = gina.popin.getPopinByName(result.popin.name);
14693
- if ( !$popin ) {
14694
- throw new Error('Popin with name: `'+ result.popin.name +'` not found.')
16498
+ if ( typeof(result.popin.name) == 'undefined' ) {
16499
+ // a name-less `close` with nothing open to close is a no-op,
16500
+ // not an error: getActivePopin() no longer resolves a not-open
16501
+ // popin for that close() to no-op on (#gh76 §8(a)); only an
16502
+ // actual resolve still throws
16503
+ if ( typeof(result.popin.close) == 'undefined' ) {
16504
+ throw new Error('To get a `$popin` instance, you need at list a `popin.name`.');
16505
+ }
16506
+ } else {
16507
+ $popin = gina.popin.getPopinByName(result.popin.name);
16508
+ if ( !$popin ) {
16509
+ throw new Error('Popin with name: `'+ result.popin.name +'` not found.')
16510
+ }
14695
16511
  }
14696
16512
  }
14697
16513
 
@@ -14699,8 +16515,11 @@ function ValidatorPlugin(rules, data, formId, culture) {
14699
16515
  typeof(result.popin) != 'undefined'
14700
16516
  && typeof(result.popin.close) != 'undefined'
14701
16517
  ) {
14702
- $popin.isRedirecting = false;
14703
- $popin.close();
16518
+ // #gh76 — `$popin` is null when nothing is open to close
16519
+ if ($popin) {
16520
+ $popin.isRedirecting = false;
16521
+ $popin.close();
16522
+ }
14704
16523
  var _reload = (result.popin.reload) ? result.popin.reload : false;
14705
16524
  if ( !result.popin.location && !result.popin.url) {
14706
16525
  delete result.popin;
@@ -14786,6 +16605,13 @@ function ValidatorPlugin(rules, data, formId, culture) {
14786
16605
  if (hFormIsRequired)
14787
16606
  triggerEvent(gina, $target, 'success.' + id + '.hform', result);
14788
16607
 
16608
+ // #gh76 slice 2 — a swap that replaced or removed the SUBMITTING form
16609
+ // kept its listeners until the events above were delivered; its
16610
+ // replacement (same id) is bound only now
16611
+ if ( sendCtx.rebindSelf ) {
16612
+ finalizeSelfReplacement($target, id);
16613
+ }
16614
+
14789
16615
  } catch (err) {
14790
16616
 
14791
16617
  result = {
@@ -15234,6 +17060,13 @@ function ValidatorPlugin(rules, data, formId, culture) {
15234
17060
  if ( /^gina\-upload/i.test(id) )
15235
17061
  onUpload(gina, $target, 'error', id, err);
15236
17062
 
17063
+ // #gh76 slice 4 — this send never reaches the wire, so its
17064
+ // `loadend` never fires: release what it held disabled here.
17065
+ // The coordination slot needs nothing — it is claimed after
17066
+ // `xhr.send()`, which this branch never reaches.
17067
+ releaseDisabledElts(sendCtx.disabledElts);
17068
+ sendCtx.disabledElts = null;
17069
+
15237
17070
  triggerEvent(gina, $target, 'error.' + id, err);
15238
17071
 
15239
17072
  if (hFormIsRequired)
@@ -15243,6 +17076,7 @@ function ValidatorPlugin(rules, data, formId, culture) {
15243
17076
  if (done) {
15244
17077
  xhr.setRequestHeader('Content-Type', 'multipart/form-data; boundary=' + boundary);
15245
17078
  xhr.send(data);
17079
+ claimSyncSlot(); // #gh76 slice 4 — after send(): a throw leaves no wedged key
15246
17080
 
15247
17081
  $form.sent = true;
15248
17082
  if ( envIsDev && isGFFCtx && typeof(window.ginaToolbar) != 'undefined' && window.ginaToolbar ) {
@@ -15298,6 +17132,7 @@ function ValidatorPlugin(rules, data, formId, culture) {
15298
17132
  xhr.setRequestHeader('Content-Type', sendContentType);
15299
17133
  }
15300
17134
  xhr.send(data)
17135
+ claimSyncSlot(); // #gh76 slice 4
15301
17136
  }
15302
17137
 
15303
17138
  } else {
@@ -15306,6 +17141,7 @@ function ValidatorPlugin(rules, data, formId, culture) {
15306
17141
  xhr.setRequestHeader('Content-Type', enctype);
15307
17142
  }
15308
17143
  xhr.send()
17144
+ claimSyncSlot(); // #gh76 slice 4
15309
17145
  }
15310
17146
 
15311
17147
  $form.sent = true;
@@ -16497,6 +18333,16 @@ function ValidatorPlugin(rules, data, formId, culture) {
16497
18333
  $form.on('success.hform', window[htmlSuccesEventCallback])
16498
18334
  }
16499
18335
  }
18336
+ //data-gina-form-event-on-swap (#gh76 slice 2) -> afterswap.<id>.hform
18337
+ var htmlSwapEventCallback = $form.target.getAttribute('data-gina-form-event-on-swap') || null;
18338
+ if (htmlSwapEventCallback != null) {
18339
+ if ( /\((.*)\)/.test(htmlSwapEventCallback) ) {
18340
+ // #M21a — function-call shape unsupported; register a bare handler on window instead
18341
+ try { console.warn('[gina-form-event] function-call shape not supported on data-gina-form-event-on-swap — use a bare identifier and register the handler on window: '+ htmlSwapEventCallback); } catch (e) {}
18342
+ } else {
18343
+ $form.on('afterswap.hform', window[htmlSwapEventCallback])
18344
+ }
18345
+ }
16500
18346
  //data-gina-form-event-on-submit-error
16501
18347
  var htmlErrorEventCallback = $form.target.getAttribute('data-gina-form-event-on-submit-error') || null;
16502
18348
  if (htmlErrorEventCallback != null) {
@@ -16751,6 +18597,7 @@ function ValidatorPlugin(rules, data, formId, culture) {
16751
18597
  $validator.unbind = unbindForm;
16752
18598
  $validator.bind = bindForm;
16753
18599
  $validator.reBind = reBindForm;
18600
+ $validator.bindRegion = bindFormsInRegion;
16754
18601
  $validator.destroy = destroy;
16755
18602
 
16756
18603
  var id = null
@@ -16763,6 +18610,10 @@ function ValidatorPlugin(rules, data, formId, culture) {
16763
18610
  // form has rule ?
16764
18611
  for (var f=0, len = $allForms.length; f<len; ++f) {
16765
18612
  // preparing prototype (need at least an ID for this)
18613
+ // #B549 — bind only what the page opted in; a bare <form> is left
18614
+ // untouched (no minted id, no $forms entry, no submit proxy), as the
18615
+ // guide states. Explicit validateFormById()/getFormById() stay ungated.
18616
+ if ( !isFormOptedIn($allForms[f], local.rules) ) { continue; }
16766
18617
 
16767
18618
  if ($allForms[f].getAttribute) {
16768
18619
  id = $allForms[f].getAttribute('id') || 'form.' + uuid();
@@ -18410,6 +20261,88 @@ function ValidatorPlugin(rules, data, formId, culture) {
18410
20261
  return $form;
18411
20262
  }
18412
20263
 
20264
+ /**
20265
+ * bindFormsInRegion
20266
+ *
20267
+ * #gh76 slice 2 — binds the forms of a freshly injected region through the SAME gate
20268
+ * the boot scan applies (#B549): a form is bound only when the markup opts it in (a
20269
+ * `data-gina-form-*` attribute, an id naming a registered rule, or a `gina-upload-*`
20270
+ * id); a bare form keeps its native submit, exactly as on the initial page. Called by
20271
+ * the shared `bindRegion()` DOM helper for fragment navigation and form-answer swaps,
20272
+ * so swapped content follows the contract the guide states instead of re-entering
20273
+ * the pre-#B549 bind-everything shape.
20274
+ *
20275
+ * An id-less opted-in form is minted an id, as at boot. A registry entry whose
20276
+ * element is no longer the one in the region (a same-id replacement) is destroyed
20277
+ * first, so `validateFormById` binds the NEW element instead of returning the stale
20278
+ * entry. A form already bound to this very element is left alone.
20279
+ *
20280
+ * @param {HTMLElement} $root - the injected region
20281
+ * @param {object} [options]
20282
+ * @param {string|null} [options.deferFormId] - a form id to skip: the submitting form
20283
+ * of a swap that replaced it keeps its listeners until the swap's own events are
20284
+ * delivered, and is (re)bound by the caller afterwards
20285
+ *
20286
+ * @returns {number} the number of forms bound
20287
+ *
20288
+ * @example
20289
+ * $region.innerHTML = fragment;
20290
+ * gina.validator.bindRegion($region); // => 1
20291
+ */
20292
+ // NOTE: deliberately NOT named `bindRegion`. `var` hoists over the whole module,
20293
+ // so a local of that name shadows utils/dom's shared `bindRegion()` policy for
20294
+ // every line of this file — which is how a swapped region's `<script src>` came
20295
+ // to be silently dropped (#gh76 slice 2): `applySwap` called the bare name and
20296
+ // got THIS forms-only binder. The PUBLISHED name stays `gina.validator.bindRegion`.
20297
+ var bindFormsInRegion = function($root, options) {
20298
+ var bound = 0
20299
+ , $v = ( this && typeof(this.$forms) != 'undefined' ) ? this : instance
20300
+ , $forms = null
20301
+ , $f = null
20302
+ , _id = null
20303
+ , existing = null
20304
+ , deferId = ( options && options.deferFormId ) ? options.deferFormId : null
20305
+ ;
20306
+ if ( !$root || typeof($root.getElementsByTagName) != 'function' ) {
20307
+ return bound;
20308
+ }
20309
+ // a static copy: binding mutates nothing structural, but a live collection is
20310
+ // the wrong thing to iterate while ids are minted
20311
+ $forms = Array.prototype.slice.call($root.getElementsByTagName('form'));
20312
+ for (var f = 0, fLen = $forms.length; f < fLen; ++f) {
20313
+ $f = $forms[f];
20314
+ if ( !isFormOptedIn($f, local.rules) ) continue;
20315
+
20316
+ _id = $f.getAttribute('id') || 'form.' + uuid();
20317
+ if ( _id !== $f.getAttribute('id') ) {
20318
+ $f.setAttribute('id', _id);
20319
+ }
20320
+ if ( deferId && _id === deferId ) continue;
20321
+
20322
+ existing = instance.$forms[_id];
20323
+ if ( existing && existing.target === $f ) continue; // already bound to this element
20324
+
20325
+ if ( existing && existing.target && existing.target !== $f ) {
20326
+ // the region replaced a same-id form: retire the stale entry so the new
20327
+ // element binds (validateFormById returns an existing entry untouched)
20328
+ try {
20329
+ destroy(_id);
20330
+ } catch (staleErr) {
20331
+ // one stale entry must never abort the binding of the others — a form
20332
+ // holding a detached virtual upload input throws inside unbindForm
20333
+ if ( typeof(console) != 'undefined' && console.warn ) {
20334
+ console.warn('[gina][validator] bindRegion: could not retire stale form `' + _id + '`: ' + (staleErr.message || staleErr));
20335
+ }
20336
+ delete instance.$forms[_id];
20337
+ }
20338
+ }
20339
+
20340
+ validateFormById.call($v, _id);
20341
+ bound++;
20342
+ }
20343
+ return bound;
20344
+ }
20345
+
18413
20346
  var unbindForm = function($target) {
18414
20347
  var $form = null
18415
20348
  , _id = null
@@ -18448,6 +20381,9 @@ function ValidatorPlugin(rules, data, formId, culture) {
18448
20381
  if ($form.target.getAttribute('data-gina-form-event-on-submit-error'))
18449
20382
  removeListener(gina, $form, 'error.' + _id + '.hform');
18450
20383
 
20384
+ if ($form.target.getAttribute('data-gina-form-event-on-swap'))
20385
+ removeListener(gina, $form, 'afterswap.' + _id + '.hform');
20386
+
18451
20387
  removeListener(gina, $form, 'validate.' + _id);
18452
20388
  removeListener(gina, $form, 'validated.' + _id);
18453
20389
  removeListener(gina, $form, 'submit.' + _id);
@@ -19266,19 +21202,33 @@ function ValidatorPlugin(rules, data, formId, culture) {
19266
21202
  var eventOnError = $el.getAttribute('data-gina-form-upload-on-error');
19267
21203
  var eventOnProgress = $el.getAttribute('data-gina-form-upload-on-progress'); // #R8
19268
21204
  var errorField = null;
21205
+ // #B572 — which popin, if any, this upload belongs to is decided by
21206
+ // CONTAINMENT (the popin the real form is inside), captured ONCE per
21207
+ // selection and read by every site below — never by "some popin is open"
21208
+ // (`isPopinContext()`), which placed a PAGE form's virtual upload form, its
21209
+ // preview lookups and its staging request inside whatever popin happened
21210
+ // to be open, where the staged metadata never reached the form. The same
21211
+ // capture the submit path makes into `sendCtx.popin`; `$el.form` is the
21212
+ // consumer's real form (the file input is never moved into the virtual one).
21213
+ var $ownerPopin = (
21214
+ typeof(gina.popin) != 'undefined'
21215
+ && gina.popin
21216
+ && typeof(gina.popin.getPopinContaining) == 'function'
21217
+ ) ? gina.popin.getPopinContaining($el.form || $el) : null;
21218
+ warnIfOldRulePlacedUpload($ownerPopin, $el.form, $el);
19269
21219
 
19270
21220
  if (files.length > 0) {
19271
21221
 
19272
21222
  // create form if not exists
19273
- var $uploadForm = null, $activePopin = null;
19274
- if ( isPopinContext() ) {
19275
- // getting active popin
19276
- $activePopin = gina.popin.getActivePopin();
19277
- $activePopin.$target = new DOMParser().parseFromString($activePopin.target.outerHTML, 'text/html');
21223
+ var $uploadForm = null;
21224
+ if ( $ownerPopin ) {
21225
+ // the popin CONTAINING the real form (#B572) — its element is where
21226
+ // the virtual form lives and is torn down with it
21227
+ $ownerPopin.$target = new DOMParser().parseFromString($ownerPopin.target.outerHTML, 'text/html');
19278
21228
  // binding to DOM
19279
- $activePopin.$target.getElementById($activePopin.id).innerHTML = document.getElementById($activePopin.id).innerHTML;
21229
+ $ownerPopin.$target.getElementById($ownerPopin.id).innerHTML = document.getElementById($ownerPopin.id).innerHTML;
19280
21230
 
19281
- $uploadForm = $activePopin.$target.getElementById(uploadFormId);
21231
+ $uploadForm = $ownerPopin.$target.getElementById(uploadFormId);
19282
21232
  } else {
19283
21233
  $uploadForm = document.getElementById(uploadFormId);
19284
21234
  }
@@ -19291,8 +21241,8 @@ function ValidatorPlugin(rules, data, formId, culture) {
19291
21241
  }
19292
21242
 
19293
21243
  if (!$uploadForm) {
19294
- $uploadForm = (isPopinContext())
19295
- ? $activePopin.$target.createElement('form')
21244
+ $uploadForm = ($ownerPopin)
21245
+ ? $ownerPopin.$target.createElement('form')
19296
21246
  : document.createElement('form');
19297
21247
  }
19298
21248
 
@@ -19323,8 +21273,8 @@ function ValidatorPlugin(rules, data, formId, culture) {
19323
21273
  // no default id (see bindUploadDropzone)
19324
21274
  var dropzoneContainer = $el.getAttribute('data-gina-form-upload-dropzone') || null;
19325
21275
  var previewContainer = $el.getAttribute('data-gina-form-upload-preview') || fieldId + '-preview';
19326
- previewContainer = (isPopinContext())
19327
- ? $activePopin.$target.getElementById(previewContainer)
21276
+ previewContainer = ($ownerPopin)
21277
+ ? $ownerPopin.$target.getElementById(previewContainer)
19328
21278
  : document.getElementById(previewContainer);
19329
21279
 
19330
21280
  // #B147 — previewContainer is a getElementById RESULT
@@ -19477,7 +21427,7 @@ function ValidatorPlugin(rules, data, formId, culture) {
19477
21427
  mandatoryFields : mandatoryFields,
19478
21428
  uploadFields : hiddenFields,
19479
21429
  hasPreviewContainer : hasPreviewContainer,
19480
- isPopinContext : isPopinContext()
21430
+ isPopinContext : !!$ownerPopin // #B572 — true iff the file's form is inside a popin; nothing reads it, the key is kept for shape
19481
21431
  };
19482
21432
  if (hasPreviewContainer) {
19483
21433
  $uploadForm.uploadProperties.previewContainer = previewContainer;
@@ -19504,9 +21454,9 @@ function ValidatorPlugin(rules, data, formId, culture) {
19504
21454
 
19505
21455
 
19506
21456
  // adding for to current document
19507
- if (isPopinContext()) {
19508
- //$activePopin.$target.appendChild($uploadForm)
19509
- document.getElementById($activePopin.id).appendChild($uploadForm)
21457
+ if ($ownerPopin) {
21458
+ //$ownerPopin.$target.appendChild($uploadForm)
21459
+ document.getElementById($ownerPopin.id).appendChild($uploadForm)
19510
21460
  } else {
19511
21461
  document.body.appendChild($uploadForm)
19512
21462
  }
@@ -23520,6 +25470,7 @@ function ValidatorPlugin(rules, data, formId, culture) {
23520
25470
  instance.setOptions = setOptions;
23521
25471
  instance.getFormById = getFormById;
23522
25472
  instance.validateFormById = validateFormById;
25473
+ instance.bindRegion = bindFormsInRegion;
23523
25474
  instance.resetErrorsDisplay = resetErrorsDisplay;
23524
25475
  instance.resetFields = resetFields;
23525
25476
  instance.handleErrorsDisplay = handleErrorsDisplay;
@@ -23706,6 +25657,7 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
23706
25657
  '$popins' : _sharedPopins, // #B90 — module-shared registry (one object for all instances)
23707
25658
  activePopinId : null,
23708
25659
  getActivePopin : null, // returns the active $popin
25660
+ getPopinContaining : null, // the popin whose element contains a given element (#gh76)
23709
25661
  target : document, // by default
23710
25662
  isReady : false,
23711
25663
  initialized : false
@@ -23723,6 +25675,15 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
23723
25675
  'loadContent' : null,
23724
25676
  'open' : null,
23725
25677
  'isOpen' : false,
25678
+ // #gh76 §8 — true from the moment a `preOpen` popin shows its loading shell
25679
+ // until its real open (or its close / failed load); a default popin shows
25680
+ // nothing while it loads, so it never enters this state
25681
+ 'isLoading' : false,
25682
+ // #gh76 §8 — this popin's load sequence and the transport of the load in
25683
+ // flight: a close during the load bumps the sequence, so the landing result
25684
+ // is dropped instead of re-opening a dismissed dialog
25685
+ '_loadSeq' : 0,
25686
+ '_loadXhr' : null,
23726
25687
  'isRedirecting' : false,
23727
25688
  'close' : null,
23728
25689
  '$forms' : [],
@@ -23833,21 +25794,78 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
23833
25794
  return $popin;
23834
25795
  }
23835
25796
 
25797
+ /**
25798
+ * getActivePopin
25799
+ *
25800
+ * The popin the framework considers active: the one `activePopinId` names WHEN IT
25801
+ * IS OPEN, else the first open popin in registration order, else `null`.
25802
+ *
25803
+ * #gh76 §8(a) — a registered-but-not-open popin is never returned. The former
25804
+ * any-registered fallback on `activePopinId` returned a popin during its own
25805
+ * click-time load window (`popinLoad` sets the id before the content lands), which
25806
+ * routed a page form's HTML answer into a popin the framework itself considered
25807
+ * closed, and `popinLoadContent` then threw on it. With two popins open the id —
25808
+ * the most recently opened one — now wins over registration order.
25809
+ *
25810
+ * @returns {object|null} $popin
25811
+ *
25812
+ * @example
25813
+ * var $popin = gina.popin.getActivePopin();
25814
+ * if ($popin) { $popin.close(); }
25815
+ */
23836
25816
  function getActivePopin() {
23837
- var $popin = null;
25817
+ var $popins = gina.popin.$popins, activeId = gina.popin.activePopinId;
23838
25818
 
23839
- for (var p in gina.popin.$popins) {
23840
- if ( typeof(gina.popin.$popins[p].isOpen) != 'undefined' && gina.popin.$popins[p].isOpen ) {
23841
- $popin = gina.popin.$popins[p];
23842
- break;
25819
+ if ( activeId && $popins[activeId] && $popins[activeId].isOpen ) {
25820
+ return $popins[activeId];
25821
+ }
25822
+ for (var p in $popins) {
25823
+ if ( typeof($popins[p].isOpen) != 'undefined' && $popins[p].isOpen ) {
25824
+ return $popins[p];
23843
25825
  }
23844
25826
  }
25827
+ return null;
25828
+ }
23845
25829
 
23846
- if (!$popin && gina.popin.activePopinId) {
23847
- $popin = gina.popin.$popins[gina.popin.activePopinId]
23848
- }
25830
+ /**
25831
+ * getPopinContaining
25832
+ *
25833
+ * The registered popin whose dialog element contains `$el`, or `null` when `$el`
25834
+ * is not inside any popin. Innermost wins when popins nest (an in-page dialog
25835
+ * registered from inside another popin's loaded content).
25836
+ *
25837
+ * #gh76 — the routing test for a form's or a link's HTML answer: the answer goes
25838
+ * to the popin the SUBMITTING element lives in, never to "the active popin". The
25839
+ * element id is the popin id on every flavour (dialog, div, in-page), so the walk
25840
+ * is one `getElementById` + `contains` per registered popin — measured at 0.2 µs
25841
+ * worst case per submit.
25842
+ *
25843
+ * @param {HTMLElement} $el - the form, anchor or any element to locate
25844
+ *
25845
+ * @returns {object|null} $popin
25846
+ *
25847
+ * @example
25848
+ * var $popin = gina.popin.getPopinContaining(document.getElementById('my-form'));
25849
+ * // -> the popin the form is rendered in, or null for a page form
25850
+ */
25851
+ function getPopinContaining($el) {
25852
+ var $popins = gina.popin.$popins, $found = null, $foundEl = null, $dialog = null;
23849
25853
 
23850
- return $popin;
25854
+ if ( !$el || typeof($el.nodeType) == 'undefined' ) {
25855
+ return null;
25856
+ }
25857
+ for (var p in $popins) {
25858
+ $dialog = ( $popins[p].id ) ? document.getElementById($popins[p].id) : null;
25859
+ if ( !$dialog || !$dialog.contains($el) ) {
25860
+ continue;
25861
+ }
25862
+ // innermost wins: take the candidate the previous one contains
25863
+ if ( !$found || $foundEl.contains($dialog) ) {
25864
+ $found = $popins[p];
25865
+ $foundEl = $dialog;
25866
+ }
25867
+ }
25868
+ return $found;
23851
25869
  }
23852
25870
 
23853
25871
 
@@ -24101,29 +26119,95 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
24101
26119
  }
24102
26120
  }
24103
26121
 
26122
+ /**
26123
+ * warnPartialTarget
26124
+ *
26125
+ * Dev-mode notice for the partial-swap fallbacks (#B580). Gated on
26126
+ * `gina.config.envIsDev` like the CORS trace below, so production stays
26127
+ * silent exactly as the guide promises; plain `console.warn`, the shape
26128
+ * warnDeprecatedOnce uses. One line per event, naming the popin, the
26129
+ * selector and which fallback ran — the three silent decisions this file
26130
+ * used to make with no trace at all.
26131
+ *
26132
+ * @inner
26133
+ * @param {object|null} $popin - the popin (its `name` goes in the message)
26134
+ * @param {string} partialTarget - the declared `data-gina-dialog-target`
26135
+ * @param {string} what - `invalid` | `noSlot` | `noRegion`
26136
+ * @param {Error} [err] - the engine's error, for `invalid`
26137
+ *
26138
+ * @returns {undefined}
26139
+ */
26140
+ function warnPartialTarget($popin, partialTarget, what, err) {
26141
+ var isDev = ( typeof(gina) != 'undefined' && gina && gina.config && gina.config.envIsDev ) ? true : false;
26142
+ if ( !isDev || typeof(console) == 'undefined' || typeof(console.warn) != 'function' ) {
26143
+ return;
26144
+ }
26145
+ var name = ( $popin && $popin.name ) ? $popin.name : '(unknown)';
26146
+ var why = ( what === 'invalid' )
26147
+ ? 'is not a valid selector ('+ ( err && err.message ? err.message : String(err) ) +') — the whole dialog was replaced instead'
26148
+ : ( what === 'noSlot' )
26149
+ ? 'matched nothing in the open dialog — the whole dialog was replaced instead'
26150
+ : 'matched nothing in the answer — the whole answer went into the slot';
26151
+ try {
26152
+ console.warn('[gina/popin] popin `'+ name +'`: `data-gina-dialog-target="'+ partialTarget +'"` '+ why +'.');
26153
+ } catch (e) {}
26154
+ }
26155
+
24104
26156
  /**
24105
26157
  * applyContent
24106
26158
  *
24107
- * Full (default): replace the whole element — byte-identical to the legacy
24108
- * `$el.innerHTML = html.trim()`, so legacy popins are unaffected.
24109
- * Partial (`partialTarget` set): parse the fetched HTML with DOMParser and swap
24110
- * only the `partialTarget` region, so chrome (close button, header/footer) and
24111
- * its bindings survive. Falls back to full-replace if the slot is absent.
26159
+ * Writes a loaded HTML body into the dialog element.
26160
+ *
26161
+ * Full (`partialTarget` absent): `$el.innerHTML = html.trim()`.
26162
+ *
26163
+ * Partial (`partialTarget` set): ONE plain CSS selector, applied on BOTH
26164
+ * sides — it locates the slot in the OPEN DIALOG and the region in the
26165
+ * parsed answer — and only the slot's CONTENTS are swapped, so chrome
26166
+ * (close button, header/footer) and its bindings survive. That makes the
26167
+ * attribute the dialog-scoped sibling of `data-gina-form-select`, not of
26168
+ * `data-gina-form-target`: there is nothing here to resolve relative to an
26169
+ * element, so the `this` / `closest` / `find` grammar does not apply.
26170
+ *
26171
+ * Three fallbacks, all deliberate (a popin open is a read the user can
26172
+ * retry, so working-but-wrong beats refusing) and each now announced in
26173
+ * dev mode (#B580 — before, the first THREW uncaught and the other two were
26174
+ * silent):
26175
+ * - a selector the engine refuses → full replace;
26176
+ * - a selector matching nothing in the dialog → full replace;
26177
+ * - a selector matching nothing in the answer → the whole answer body into the slot.
24112
26178
  *
24113
26179
  * @inner
26180
+ * @param {HTMLElement} $el - the dialog element
26181
+ * @param {string} html - the loaded body
26182
+ * @param {object|null} $popin - the popin (for the notice)
26183
+ * @param {string|null} partialTarget - the declared selector, or null
26184
+ *
26185
+ * @returns {undefined}
24114
26186
  */
24115
26187
  function applyContent($el, html, $popin, partialTarget) {
24116
26188
  if ( !partialTarget ) {
24117
26189
  $el.innerHTML = ( typeof(html) == 'string' ) ? html.trim() : '';
24118
26190
  return;
24119
26191
  }
24120
- var $slot = $el.querySelector(partialTarget);
26192
+ var $slot = null, parsed = null, $incoming = null;
26193
+ try {
26194
+ $slot = $el.querySelector(partialTarget);
26195
+ } catch (badSelector) {
26196
+ warnPartialTarget($popin, partialTarget, 'invalid', badSelector);
26197
+ $el.innerHTML = ( typeof(html) == 'string' ) ? html.trim() : '';
26198
+ return;
26199
+ }
24121
26200
  if ( !$slot ) {
26201
+ warnPartialTarget($popin, partialTarget, 'noSlot');
24122
26202
  $el.innerHTML = ( typeof(html) == 'string' ) ? html.trim() : '';
24123
26203
  return;
24124
26204
  }
24125
- var parsed = new DOMParser().parseFromString(html, 'text/html');
24126
- var $incoming = parsed.querySelector(partialTarget) || parsed.body;
26205
+ parsed = new DOMParser().parseFromString(html, 'text/html');
26206
+ $incoming = parsed.querySelector(partialTarget);
26207
+ if ( !$incoming ) {
26208
+ warnPartialTarget($popin, partialTarget, 'noRegion');
26209
+ $incoming = parsed.body;
26210
+ }
24127
26211
  $slot.innerHTML = $incoming.innerHTML;
24128
26212
  }
24129
26213
 
@@ -24289,6 +26373,9 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
24289
26373
  // A preOpen popin still gets its instant born-modal skeleton now; the
24290
26374
  // adopted preload swaps in the real content on arrival — matching the
24291
26375
  // popinLoad preOpen flow this consume path bypasses (#B54).
26376
+ // #gh76 §8 — this adoption's sequence, on the same counter popinLoad uses:
26377
+ // a close during the wait bumps it and the body is dropped on arrival
26378
+ var seq = ( $popin ) ? ( $popin._loadSeq = ( $popin._loadSeq || 0 ) + 1 ) : null;
24292
26379
  if ( $popin && $popin.options && $popin.options.preOpen ) {
24293
26380
  showLoadingShell($popin, ensurePopinDialog($popin));
24294
26381
  }
@@ -24299,6 +26386,10 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
24299
26386
  // #B285 — the adopted wait is over (body or failure): settle FIRST,
24300
26387
  // matching the cold path's release-before-apply order.
24301
26388
  _settle();
26389
+ // #gh76 §8 — cancelled while it waited: released above, body dropped
26390
+ if ( seq !== null && seq !== $popin._loadSeq ) {
26391
+ return;
26392
+ }
24302
26393
  if ( body == null ) {
24303
26394
  if ( typeof(onMiss) == 'function' ) { onMiss(); }
24304
26395
  } else {
@@ -24650,6 +26741,13 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
24650
26741
  if ( typeof(gina.events[loadedEvt]) == 'undefined' ) {
24651
26742
  addListener(gina, existing.target, loadedEvt, function (loadedEvent) {
24652
26743
  loadedEvent.preventDefault();
26744
+ // #B579 — popinLoadContent's redirect emit fires this same event with the
26745
+ // POPIN OBJECT as detail (the legacy listener only binds + opens on it): a
26746
+ // load landing on an already-open popin routes through it, and applying a
26747
+ // non-string would blank the dialog whose content that call just wrote
26748
+ if ( typeof(loadedEvent.detail) != 'string' ) {
26749
+ return;
26750
+ }
24653
26751
  handleLoadedBody(loadedEvent.detail, existing, ensurePopinDialog(existing));
24654
26752
  });
24655
26753
  }
@@ -24984,7 +27082,7 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
24984
27082
 
24985
27083
  if (!fired) {
24986
27084
  fired = true;
24987
- popinLoadContent(e.detail);
27085
+ popinLoadContent.call($popin, e.detail);
24988
27086
  }
24989
27087
  });
24990
27088
  }
@@ -25564,6 +27662,106 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
25564
27662
  $overlay.classList.add('gina-popin-is-active');
25565
27663
  }
25566
27664
  }
27665
+
27666
+ // #gh76 §8 — the shell is showing and the load is in flight: an explicit state.
27667
+ // `isOpen` stays false, so the two `!isOpen ⇒ popinOpen` consumers still run the
27668
+ // real open when the content lands. A dialog shell gets the native `close` sync
27669
+ // NOW, not only at popinOpen: an Escape during the load must route through
27670
+ // popinClose, or the landing content would re-open a dismissed dialog.
27671
+ $popin.isLoading = true;
27672
+ bindNativeCloseSync($popin, $el);
27673
+ }
27674
+
27675
+ /**
27676
+ * bindNativeCloseSync
27677
+ *
27678
+ * #gh76 §8 — one native `close` listener per dialog element, bound at the loading
27679
+ * shell AND at popinOpen, whichever comes first (`_ginaCloseSyncBound` keeps it to
27680
+ * one across element reuse). Listens to `close`, not `cancel`: `close` fires for
27681
+ * BOTH Escape and method="dialog". De-dup with the plugin's own close: popinClose
27682
+ * (and closeLoadingShell) set isOpen / isLoading false SYNCHRONOUSLY before the
27683
+ * queued `close` event fires, so the guard sees neither and no-ops; a `$el.close()`
27684
+ * on an already-closed dialog is a spec no-op, so there is no recursion. Native
27685
+ * addEventListener — NOT gina's addListener (the custom event bus). Gated on
27686
+ * useDialogMode: a <div> has no `close` event.
27687
+ *
27688
+ * @param {object} $popin - the registered popin
27689
+ * @param {HTMLElement} $el - its dialog element
27690
+ *
27691
+ * @returns {undefined}
27692
+ *
27693
+ * @example
27694
+ * bindNativeCloseSync($popin, $el);
27695
+ */
27696
+ function bindNativeCloseSync($popin, $el) {
27697
+ if ( self.options.useDialogMode && $el && !$el._ginaCloseSyncBound ) {
27698
+ $el._ginaCloseSyncBound = true;
27699
+ $el.addEventListener('close', function () {
27700
+ if ( $popin.isOpen || $popin.isLoading ) {
27701
+ popinClose($popin.name);
27702
+ }
27703
+ });
27704
+ }
27705
+ }
27706
+
27707
+ /**
27708
+ * closeLoadingShell
27709
+ *
27710
+ * #gh76 §8 — closes a popin whose loading shell is showing but whose real open never
27711
+ * happened (`isLoading` true, `isOpen` false). The load itself is CANCELLED: the
27712
+ * sequence is bumped so the landing result — or an adopted preload's body — is
27713
+ * dropped instead of re-opening a dismissed dialog, and a transport still in flight
27714
+ * is aborted (its own readyState-4 handler sees the bumped sequence and returns
27715
+ * right after its release block, so no `error` fires for a cancel). Tears down what
27716
+ * the shell built BY THE SHELL'S OWN STATE — not by the `gina-popin-is-active` class
27717
+ * popinClose's teardown keys on, which a dialog shell never carries — releases the
27718
+ * trigger, and fires `close.<id>`: the honest "this dialog went away" signal, even
27719
+ * though `open.<id>` never fired.
27720
+ *
27721
+ * @param {object} $popin - the loading popin
27722
+ *
27723
+ * @returns {undefined}
27724
+ *
27725
+ * @example
27726
+ * if ( $popin.isLoading && !$popin.isOpen ) closeLoadingShell($popin);
27727
+ */
27728
+ function closeLoadingShell($popin) {
27729
+ var $el = document.getElementById($popin.id) || null;
27730
+ var $popinTrigger = document.getElementById($popin.openTrigger) || null;
27731
+ // cancel the load: the landing result is dropped by the sequence check
27732
+ $popin._loadSeq = ( $popin._loadSeq || 0 ) + 1;
27733
+ if ( $popin._loadXhr && $popin._loadXhr.readyState !== 4 ) {
27734
+ try { $popin._loadXhr.abort(); } catch (abortErr) { /* already dead */ }
27735
+ }
27736
+ $popin._loadXhr = null;
27737
+ // BEFORE the native close below: its queued `close` event must find no state
27738
+ $popin.isLoading = false;
27739
+ if ( $el ) {
27740
+ if ( $el.tagName === 'DIALOG' ) {
27741
+ if ( typeof($el.close) === 'function' && $el.hasAttribute('open') ) {
27742
+ try { $el.close(); } catch (closeErr) { $el.removeAttribute('open'); }
27743
+ } else {
27744
+ $el.removeAttribute('open');
27745
+ }
27746
+ } else {
27747
+ $el.classList.remove('gina-popin-is-active');
27748
+ var $overlay = $el.parentElement;
27749
+ if ( $overlay ) { $overlay.classList.remove('gina-popin-is-active'); }
27750
+ }
27751
+ $el.innerHTML = '';
27752
+ $el.removeAttribute('data-gina-popin-loading');
27753
+ }
27754
+ if ( $popin.target && typeof($popin.target.removeAttribute) == 'function' ) {
27755
+ $popin.target.removeAttribute('data-gina-popin-loading');
27756
+ }
27757
+ releasePopinTrigger($popinTrigger);
27758
+ // operands reversed on purpose: popin-registry.test.js counts the raw WRITE
27759
+ // shape of the active id (assignment to the instance field) at exactly one
27760
+ // site, and a `===` read written the other way round carries that prefix
27761
+ if ( $popin.id === instance.activePopinId ) {
27762
+ setActivePopinId(null);
27763
+ }
27764
+ triggerEvent(gina, $popin.target, 'close.'+ $popin.id, $popin);
25567
27765
  }
25568
27766
 
25569
27767
  /**
@@ -25652,6 +27850,9 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
25652
27850
  var $popin = getPopinByName(name);
25653
27851
  var id = $popin.id;
25654
27852
  var $popinTrigger = document.getElementById($popin.openTrigger) || null;
27853
+ // #gh76 §8 — this load's sequence: a close during the load bumps it, and the
27854
+ // landing result below is dropped instead of re-opening a dismissed dialog
27855
+ var seq = $popin._loadSeq = ( $popin._loadSeq || 0 ) + 1;
25655
27856
 
25656
27857
  // #B139 — record the content's source URL for the close-time cache
25657
27858
  // clear (covers every click-time load path, incl. validator redirects).
@@ -25752,6 +27953,9 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
25752
27953
  catch (e) { try { xhr = new ActiveXObject("Microsoft.XMLHTTP"); } catch (e) {} }
25753
27954
  }
25754
27955
 
27956
+ // #gh76 §8 — stashed so a close during the load can abort it
27957
+ $popin._loadXhr = xhr;
27958
+
25755
27959
  if ( options.withCredentials ) { // Preflighted requests
25756
27960
  if ('withCredentials' in xhr) {
25757
27961
  // XHR for Chrome/Firefox/Opera/Safari.
@@ -25832,6 +28036,13 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
25832
28036
  }
25833
28037
  loadingState.disarm($popinTrigger);
25834
28038
  }
28039
+ // #gh76 §8 — a load cancelled by a close (or superseded by a newer
28040
+ // load of the same popin) has released above; its result is dropped
28041
+ // here, so landing content cannot re-open a dismissed dialog
28042
+ if ( seq !== $popin._loadSeq ) {
28043
+ return;
28044
+ }
28045
+ $popin._loadXhr = null;
25835
28046
  // 200, 201, 201' etc ...
25836
28047
  var result = null;
25837
28048
 
@@ -25865,7 +28076,7 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
25865
28076
  !isJsonContent && $popin.isOpen && isRedirecting && !$popin.partialTarget
25866
28077
  ) {
25867
28078
  // console.debug('Popin now redirecting [1]');
25868
- popinLoadContent(result, isRedirecting);
28079
+ popinLoadContent.call($popin, result, isRedirecting);
25869
28080
  } else {
25870
28081
 
25871
28082
  if (
@@ -26025,6 +28236,13 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26025
28236
  updateToolbar(result, resultIsObject);
26026
28237
 
26027
28238
  triggerEvent(gina, $el, 'error.' + id, result)
28239
+ // #gh76 §8 — a failed load never leaves the shell behind: the
28240
+ // `error` listener above ran FIRST (it may have loaded content,
28241
+ // which completes the open and keeps the dialog); if nobody did,
28242
+ // close the shell — a spinner on a load that will never land
28243
+ if ( $popin.isLoading && !$popin.isOpen ) {
28244
+ closeLoadingShell($popin);
28245
+ }
26028
28246
  }
26029
28247
  }
26030
28248
  };
@@ -26089,21 +28307,43 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26089
28307
  /**
26090
28308
  * popinLoadContent
26091
28309
  *
26092
- * @param {string} html - plain/text
26093
- * @param {boolean} [isRedirecting] - to handle link inside popin without form
28310
+ * Replaces the popin's content with `stringContent` and re-binds it. Called as a
28311
+ * METHOD (`$popin.loadContent(html)`, or the internal `.call($popin, …)` sites) it
28312
+ * loads into THAT popin; called on the manager (`gina.popin.loadContent(html)`) it
28313
+ * loads into the active popin. Throws when the resolved popin is not open.
28314
+ *
28315
+ * #gh76 precision 4 — the former unconditional `getActivePopin()` re-resolve landed
28316
+ * a form's answer in whichever open popin was registered first when two were open
28317
+ * (measured); honouring `this` is what lets the validator load into the popin it
28318
+ * captured at submit.
28319
+ *
28320
+ * @this {object} [$popin] - when a registry entry, the popin to load into
28321
+ * @param {string} stringContent - the HTML to inject (plain text)
28322
+ * @param {boolean} [isRedirecting] - to handle a link inside a popin without a form
28323
+ *
28324
+ * @example
28325
+ * gina.popin.getPopinByName('details').loadContent('<p>updated</p>');
26094
28326
  */
26095
28327
  function popinLoadContent(stringContent, isRedirecting) {
26096
28328
 
26097
- var $popin = getActivePopin();
28329
+ var $popin = ( this && this.id && instance.$popins[this.id] === this )
28330
+ ? this
28331
+ : getActivePopin();
26098
28332
  if ( !$popin ) {
26099
28333
  return;
26100
28334
  }
26101
- if (!$popin.isOpen)
28335
+ // #gh76 §8 — a popin whose loading shell is showing accepts content: it lands
28336
+ // in the shell and the open is completed below (a load still in flight lands
28337
+ // afterwards as any second loadContent would — last write wins)
28338
+ var completingOpen = ( !$popin.isOpen && $popin.isLoading );
28339
+ if ( !$popin.isOpen && !completingOpen )
26102
28340
  throw new Error('Popin `'+$popin.name+'` is not open !');
26103
28341
 
26104
28342
  $popin.isRedirecting = ( typeof(isRedirecting) != 'undefined' ) ? isRedirecting : false;
26105
28343
 
26106
- var $el = $popin.target;
28344
+ // while loading, `target` still names the shared container (popinOpen re-points
28345
+ // it at the dialog) — inject into the dialog element itself
28346
+ var $el = ( completingOpen ) ? ( document.getElementById($popin.id) || $popin.target ) : $popin.target;
26107
28347
  // if (
26108
28348
  // typeof(stringContent) != 'undefined'
26109
28349
  // && typeof(stringContent.trim) == 'function'
@@ -26120,7 +28360,12 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26120
28360
  refreshCSS();
26121
28361
  }
26122
28362
 
26123
- if ( !$popin.isRedirecting ) {
28363
+ if ( completingOpen ) {
28364
+ // the real open: the a11y label, isOpen, the trigger re-point, `open.<id>` —
28365
+ // the bind above is deduped, and the shell already showed the dialog so
28366
+ // popinOpen's own showModal() is skipped
28367
+ popinOpen($popin.name);
28368
+ } else if ( !$popin.isRedirecting ) {
26124
28369
  triggerEvent(gina, instance.target, 'open.'+ $popin.id, $popin);
26125
28370
  } else {
26126
28371
  // console.debug('Popin now redirecting [1-b]');
@@ -26292,7 +28537,7 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26292
28537
  // Fix today's name-based aria-labelledby: associate a REAL title element.
26293
28538
  associateLabel($el);
26294
28539
 
26295
- if ( self.options.useDialogMode && !$el.getAttribute('open') ) {
28540
+ if ( self.options.useDialogMode && !$el.hasAttribute('open') ) {
26296
28541
  // Modal vs non-modal. The new `data-gina-dialog` API defaults to
26297
28542
  // non-modal — openFromTrigger sets `$popin.modal`. Any path that did NOT
26298
28543
  // set it (legacy `data-gina-popin-*` triggers, direct popinOpen() calls)
@@ -26303,8 +28548,12 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26303
28548
  // showModal() promotes the dialog to the top layer with a native
26304
28549
  // ::backdrop and inerts the rest of the page. Consumers that
26305
28550
  // preemptively open the dialog (skeleton-loading) MUST also use
26306
- // showModal() so it is born modal; the !getAttribute('open') guard
28551
+ // showModal() so it is born modal; the !hasAttribute('open') guard
26307
28552
  // above then skips this call (re-showModal on an open dialog throws).
28553
+ // #B574 — it has to be hasAttribute: showModal() sets `open` to the
28554
+ // EMPTY string, which getAttribute() returned as a falsy value, so the
28555
+ // guard never skipped a shell-opened dialog and the modeless branch
28556
+ // below called show() on a modal dialog (InvalidStateError).
26308
28557
  $el.showModal();
26309
28558
  } else {
26310
28559
  // Non-modal: .show() loses the native ::backdrop / Escape /
@@ -26333,16 +28582,11 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26333
28582
  // there is no recursion. `_ginaCloseSyncBound` keeps it to one listener across
26334
28583
  // element reuse; gated on useDialogMode (non-dialog mode is a <div>, no `close`
26335
28584
  // event). Native addEventListener — NOT gina's addListener (the custom event bus).
26336
- if ( self.options.useDialogMode && $el && !$el._ginaCloseSyncBound ) {
26337
- $el._ginaCloseSyncBound = true;
26338
- $el.addEventListener('close', function () {
26339
- if ( $popin.isOpen ) {
26340
- popinClose($popin.name);
26341
- }
26342
- });
26343
- }
28585
+ // #gh76 §8 — shared with the loading shell, which may have bound it already
28586
+ bindNativeCloseSync($popin, $el);
26344
28587
 
26345
28588
  $popin.isOpen = true;
28589
+ $popin.isLoading = false;
26346
28590
  // so it can be forwarded to the handler who is listening
26347
28591
  $popin.target = $el;
26348
28592
 
@@ -26481,7 +28725,7 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26481
28725
  function popinClose(name) {
26482
28726
 
26483
28727
  var $popin = null;
26484
- if ( typeof(name) == 'undefined' && /^true$/.test(this.isOpen) ) {
28728
+ if ( typeof(name) == 'undefined' && ( /^true$/.test(this.isOpen) || /^true$/.test(this.isLoading) ) ) {
26485
28729
  name = this.name;
26486
28730
  $popin = this;
26487
28731
  } else {
@@ -26496,6 +28740,16 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26496
28740
  if ( !$popin && typeof(name) != 'undefined' ) {
26497
28741
  throw new Error('Popin `'+name+'` not found !');
26498
28742
  }
28743
+ // #gh76 §8 — a popin whose loading shell is showing closes too: the load is
28744
+ // cancelled and the shell torn down. The `!isOpen` return below would otherwise
28745
+ // leave a preOpen shell un-closeable for the whole round trip.
28746
+ if ( !$popin.isOpen && $popin.isLoading ) {
28747
+ if ( $popin.isRedirecting ) {
28748
+ return;
28749
+ }
28750
+ closeLoadingShell($popin);
28751
+ return;
28752
+ }
26499
28753
  if (!$popin.isOpen)
26500
28754
  return;
26501
28755
 
@@ -26593,8 +28847,9 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26593
28847
  id = $popin.id;
26594
28848
  name = $popin.name;
26595
28849
 
26596
- // Close first if still open (handles listener cleanup, form unbinding, header removal)
26597
- if ( $popin.isOpen ) {
28850
+ // Close first if still open — or still LOADING (#gh76 §8): the loading close
28851
+ // cancels the load and tears the shell down
28852
+ if ( $popin.isOpen || $popin.isLoading ) {
26598
28853
  $popin.isRedirecting = false;
26599
28854
  popinClose(name);
26600
28855
  }
@@ -26760,6 +29015,7 @@ define('gina/popin', [ 'require', 'lib/domain', 'lib/loading-state', 'lib/merge'
26760
29015
  instance.load = popinLoad;
26761
29016
  instance.loadContent = popinLoadContent;
26762
29017
  instance.getActivePopin = getActivePopin;
29018
+ instance.getPopinContaining = getPopinContaining;
26763
29019
  instance.open = popinOpen;
26764
29020
  instance.close = popinClose;
26765
29021
  instance.destroy = popinDestroy;
@@ -27218,7 +29474,7 @@ if ( ( typeof(module) !== 'undefined' ) && module.exports ) {
27218
29474
  // Publish as AMD module
27219
29475
  define('gina/storage', ['helpers/dateFormat', 'helpers/prototypes'],function() { return StoragePlugin })
27220
29476
  };
27221
- define('gina/nav', [ 'require', 'lib/merge', 'lib/uuid', 'utils/events' ], function (require) {
29477
+ define('gina/nav', [ 'require', 'lib/merge', 'lib/uuid', 'utils/events', 'utils/dom' ], function (require) {
27222
29478
 
27223
29479
  var merge = require('lib/merge');
27224
29480
  var uuid = require('lib/uuid');
@@ -27254,12 +29510,15 @@ define('gina/nav', [ 'require', 'lib/merge', 'lib/uuid', 'utils/events' ], funct
27254
29510
  * `X-Requested-With: XMLHttpRequest` (without it the render path
27255
29511
  * re-wraps a layoutless body into a full `<html>` shell);
27256
29512
  * 4. a genuine negotiated answer — 2xx, HTML, `Vary` advertising
27257
- * `X-Gina-Navigate` — replaces the region's content, re-injects the
27258
- * fragment's src-bearing scripts the document does not already have
27259
- * (inline scripts are NOT executed — same contract as popin content),
27260
- * rebinds id-bearing forms through the live validator, closes any open
29513
+ * `X-Gina-Navigate` — replaces the region's content, closes any open
27261
29514
  * popin (a full navigation would have unloaded it), pushes a history
27262
- * entry, restores focus/scroll, and fires `success`;
29515
+ * entry, restores focus/scroll, binds the new region through the shared
29516
+ * `bindRegion()` policy (utils/dom: the fragment's src-bearing scripts
29517
+ * the document does not already have are re-created — inline scripts
29518
+ * are NOT executed, same contract as popin content — the forms that opt
29519
+ * in are bound through the live validator (#B549: a bare form keeps its
29520
+ * native submit, as on the initial page), `data-gina-link` anchors
29521
+ * through the live link plugin), and fires `success`;
27263
29522
  * 5. ANY other answer falls back to a full-page navigation: non-2xx,
27264
29523
  * timeout, network error, an un-negotiated 2xx (the `Vary` check —
27265
29524
  * a client/server matching disagreement degrades to a normal page
@@ -27322,11 +29581,6 @@ define('gina/nav', [ 'require', 'lib/merge', 'lib/uuid', 'utils/events' ], funct
27322
29581
  // arrives from an async fetch and can land after `isFrameworkLoaded`.
27323
29582
  var _compiled = null;
27324
29583
  var _compiledFor = null;
27325
- // Scripts present at activation + scripts nav injected across swaps
27326
- // (the popin `parentScripts` model): a fragment's src-bearing script is
27327
- // injected once, never re-executed on later swaps.
27328
- var _parentScripts = [];
27329
- var _injectedScripts = [];
27330
29584
  // pathname+search last rendered — popstate events that do not change it
27331
29585
  // are hash-only moves and are left to the browser.
27332
29586
  var _currentUrl = null;
@@ -27533,75 +29787,6 @@ define('gina/nav', [ 'require', 'lib/merge', 'lib/uuid', 'utils/events' ], funct
27533
29787
  return null;
27534
29788
  };
27535
29789
 
27536
- /**
27537
- * Snapshots the src of every script present in the document at
27538
- * activation — the popin `parentScripts` model. Scripts already loaded
27539
- * with the page are never re-injected from a fragment.
27540
- *
27541
- * @inner
27542
- * @private
27543
- */
27544
- var snapshotParentScripts = function() {
27545
- var scripts = document.getElementsByTagName('script');
27546
- for (var i = 0, len = scripts.length; i < len; ++i) {
27547
- if ( scripts[i].src ) {
27548
- _parentScripts.push(scripts[i].src);
27549
- }
27550
- }
27551
- };
27552
-
27553
- /**
27554
- * Re-injects the fragment's src-bearing scripts the document does not
27555
- * already have (mirrors popinOpen's external-resource loading). Inline
27556
- * scripts are NOT executed — innerHTML semantics, the same contract
27557
- * popin content has always had. Injected scripts persist across later
27558
- * swaps (a page never "closes"); the registry keeps them single-shot.
27559
- *
27560
- * @inner
27561
- * @private
27562
- * @param {object} $target - the swapped region
27563
- */
27564
- var injectFragmentScripts = function($target) {
27565
- var scripts = $target.getElementsByTagName('script');
27566
- for (var i = 0, len = scripts.length; i < len; ++i) {
27567
- var src = scripts[i].src; // resolved absolute URL
27568
- if ( !src ) continue;
27569
- if ( _parentScripts.indexOf(src) > -1 || _injectedScripts.indexOf(src) > -1 ) continue;
27570
- var s = document.createElement('script');
27571
- s.src = src;
27572
- document.head.appendChild(s);
27573
- _injectedScripts.push(src);
27574
- }
27575
- };
27576
-
27577
- /**
27578
- * Rebinds id-bearing forms of the swapped region through the live
27579
- * validator (`gina.validator.validateFormById` — the popinBind model).
27580
- * Id-less forms are skipped: validation rules are keyed by form id, so
27581
- * no rule can target them. A missing/ruleless validator is a no-op.
27582
- *
27583
- * @inner
27584
- * @private
27585
- * @param {object} $target - the swapped region
27586
- */
27587
- var rebindValidator = function($target) {
27588
- try {
27589
- if ( !gina.hasValidator || !gina.validator || typeof(gina.validator.validateFormById) != 'function' ) {
27590
- return;
27591
- }
27592
- var $forms = $target.getElementsByTagName('form');
27593
- for (var i = 0, len = $forms.length; i < len; ++i) {
27594
- var _id = $forms[i].getAttribute('id');
27595
- if ( !_id ) continue;
27596
- try {
27597
- gina.validator.validateFormById(_id);
27598
- } catch (bindErr) {
27599
- console.warn('[gina][nav] validator rebind skipped for `'+ _id +'`: '+ (bindErr.message || bindErr));
27600
- }
27601
- }
27602
- } catch (validatorErr) {}
27603
- };
27604
-
27605
29790
  /**
27606
29791
  * Closes the active popin when one is open: a full-page navigation
27607
29792
  * would have unloaded it, and after a fragment swap it would sit over
@@ -27639,10 +29824,11 @@ define('gina/nav', [ 'require', 'lib/merge', 'lib/uuid', 'utils/events' ], funct
27639
29824
  };
27640
29825
 
27641
29826
  /**
27642
- * Applies a fetched fragment: swaps the region content, re-injects
27643
- * missing src scripts, applies the optional `[data-gina-nav-title]`
27644
- * document title, pushes/updates history, restores scroll and focus,
27645
- * closes any open popin, rebinds validator forms and fires `success`.
29827
+ * Applies a fetched fragment: swaps the region content, applies the
29828
+ * optional `[data-gina-nav-title]` document title, pushes/updates
29829
+ * history, restores scroll and focus, closes any open popin, binds the
29830
+ * new region (scripts, opted-in forms, links — the shared `bindRegion()`
29831
+ * policy) and fires `success`.
27646
29832
  *
27647
29833
  * @inner
27648
29834
  * @private
@@ -27655,8 +29841,6 @@ define('gina/nav', [ 'require', 'lib/merge', 'lib/uuid', 'utils/events' ], funct
27655
29841
  var $target = instance.target;
27656
29842
  $target.innerHTML = ( typeof(html) == 'string' ) ? html.trim() : '';
27657
29843
 
27658
- injectFragmentScripts($target);
27659
-
27660
29844
  // Optional per-page document title: the first element of the
27661
29845
  // fragment carrying `data-gina-nav-title` (the fragment is
27662
29846
  // layoutless, so it cannot carry a <title> of its own).
@@ -27702,7 +29886,11 @@ define('gina/nav', [ 'require', 'lib/merge', 'lib/uuid', 'utils/events' ], funct
27702
29886
  }
27703
29887
 
27704
29888
  closeActivePopin();
27705
- rebindValidator($target);
29889
+ // #gh76 — the one region-binding policy nav shares with the validator's
29890
+ // form-answer swaps (utils/dom). Forms follow the #B549 opt-in gate here
29891
+ // too: a bare id-bearing form in a fragment used to be bound — and its
29892
+ // submit turned into an XHR — by a rebind that bypassed the gate.
29893
+ bindRegion($target);
27706
29894
 
27707
29895
  instance.eventData.success = { 'url': url, 'target': $target, 'isPopState': isPopState };
27708
29896
  triggerEvent(gina, instance.target, 'success.' + instance.id, instance.eventData.success);
@@ -27954,7 +30142,6 @@ define('gina/nav', [ 'require', 'lib/merge', 'lib/uuid', 'utils/events' ], funct
27954
30142
 
27955
30143
  instance.target = $marker;
27956
30144
  _currentUrl = window.location.pathname + window.location.search;
27957
- snapshotParentScripts();
27958
30145
  try {
27959
30146
  if ( window.history && 'scrollRestoration' in window.history ) {
27960
30147
  window.history.scrollRestoration = 'manual';