haori 0.13.0 → 0.13.2

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 (307) hide show
  1. package/README.ja.md +1 -1
  2. package/README.md +1 -1
  3. package/dist/haori.cjs.js +10 -10
  4. package/dist/haori.es.js +191 -165
  5. package/dist/haori.iife.js +10 -10
  6. package/dist/index.d.ts +1 -1
  7. package/package.json +11 -3
  8. package/dist/haori.cjs.js.map +0 -1
  9. package/dist/haori.es.js.map +0 -1
  10. package/dist/haori.iife.js.map +0 -1
  11. package/dist/package.json +0 -70
  12. package/dist/src/builtins.d.ts +0 -100
  13. package/dist/src/builtins.d.ts.map +0 -1
  14. package/dist/src/builtins.js +0 -194
  15. package/dist/src/builtins.js.map +0 -1
  16. package/dist/src/core.d.ts +0 -505
  17. package/dist/src/core.d.ts.map +0 -1
  18. package/dist/src/core.js +0 -1817
  19. package/dist/src/core.js.map +0 -1
  20. package/dist/src/dev.d.ts +0 -35
  21. package/dist/src/dev.d.ts.map +0 -1
  22. package/dist/src/dev.js +0 -44
  23. package/dist/src/dev.js.map +0 -1
  24. package/dist/src/env.d.ts +0 -43
  25. package/dist/src/env.d.ts.map +0 -1
  26. package/dist/src/env.js +0 -108
  27. package/dist/src/env.js.map +0 -1
  28. package/dist/src/event.d.ts +0 -171
  29. package/dist/src/event.d.ts.map +0 -1
  30. package/dist/src/event.js +0 -235
  31. package/dist/src/event.js.map +0 -1
  32. package/dist/src/event_dispatcher.d.ts +0 -66
  33. package/dist/src/event_dispatcher.d.ts.map +0 -1
  34. package/dist/src/event_dispatcher.js +0 -167
  35. package/dist/src/event_dispatcher.js.map +0 -1
  36. package/dist/src/expression.d.ts +0 -260
  37. package/dist/src/expression.d.ts.map +0 -1
  38. package/dist/src/expression.js +0 -975
  39. package/dist/src/expression.js.map +0 -1
  40. package/dist/src/form.d.ts +0 -148
  41. package/dist/src/form.d.ts.map +0 -1
  42. package/dist/src/form.js +0 -428
  43. package/dist/src/form.js.map +0 -1
  44. package/dist/src/fragment.d.ts +0 -621
  45. package/dist/src/fragment.d.ts.map +0 -1
  46. package/dist/src/fragment.js +0 -1963
  47. package/dist/src/fragment.js.map +0 -1
  48. package/dist/src/haori.d.ts +0 -130
  49. package/dist/src/haori.d.ts.map +0 -1
  50. package/dist/src/haori.js +0 -215
  51. package/dist/src/haori.js.map +0 -1
  52. package/dist/src/import.d.ts +0 -19
  53. package/dist/src/import.d.ts.map +0 -1
  54. package/dist/src/import.js +0 -64
  55. package/dist/src/import.js.map +0 -1
  56. package/dist/src/index.d.ts +0 -27
  57. package/dist/src/index.d.ts.map +0 -1
  58. package/dist/src/index.js +0 -30
  59. package/dist/src/index.js.map +0 -1
  60. package/dist/src/intersect.d.ts +0 -28
  61. package/dist/src/intersect.d.ts.map +0 -1
  62. package/dist/src/intersect.js +0 -201
  63. package/dist/src/intersect.js.map +0 -1
  64. package/dist/src/log.d.ts +0 -32
  65. package/dist/src/log.d.ts.map +0 -1
  66. package/dist/src/log.js +0 -43
  67. package/dist/src/log.js.map +0 -1
  68. package/dist/src/observer.d.ts +0 -24
  69. package/dist/src/observer.d.ts.map +0 -1
  70. package/dist/src/observer.js +0 -131
  71. package/dist/src/observer.js.map +0 -1
  72. package/dist/src/procedure.d.ts +0 -447
  73. package/dist/src/procedure.d.ts.map +0 -1
  74. package/dist/src/procedure.js +0 -1921
  75. package/dist/src/procedure.js.map +0 -1
  76. package/dist/src/queue.d.ts +0 -35
  77. package/dist/src/queue.d.ts.map +0 -1
  78. package/dist/src/queue.js +0 -202
  79. package/dist/src/queue.js.map +0 -1
  80. package/dist/src/url.d.ts +0 -14
  81. package/dist/src/url.d.ts.map +0 -1
  82. package/dist/src/url.js +0 -22
  83. package/dist/src/url.js.map +0 -1
  84. package/dist/tests/bind-arg-own-binding.test.d.ts +0 -2
  85. package/dist/tests/bind-arg-own-binding.test.d.ts.map +0 -1
  86. package/dist/tests/bind-arg-own-binding.test.js +0 -63
  87. package/dist/tests/bind-arg-own-binding.test.js.map +0 -1
  88. package/dist/tests/bindcomplete-and-scope.test.d.ts +0 -2
  89. package/dist/tests/bindcomplete-and-scope.test.d.ts.map +0 -1
  90. package/dist/tests/bindcomplete-and-scope.test.js +0 -115
  91. package/dist/tests/bindcomplete-and-scope.test.js.map +0 -1
  92. package/dist/tests/builtins.test.d.ts +0 -2
  93. package/dist/tests/builtins.test.d.ts.map +0 -1
  94. package/dist/tests/builtins.test.js +0 -152
  95. package/dist/tests/builtins.test.js.map +0 -1
  96. package/dist/tests/click-attributes.test.d.ts +0 -2
  97. package/dist/tests/click-attributes.test.d.ts.map +0 -1
  98. package/dist/tests/click-attributes.test.js +0 -113
  99. package/dist/tests/click-attributes.test.js.map +0 -1
  100. package/dist/tests/click-no-disabled.test.d.ts +0 -2
  101. package/dist/tests/click-no-disabled.test.d.ts.map +0 -1
  102. package/dist/tests/click-no-disabled.test.js +0 -67
  103. package/dist/tests/click-no-disabled.test.js.map +0 -1
  104. package/dist/tests/core.test.d.ts +0 -5
  105. package/dist/tests/core.test.d.ts.map +0 -1
  106. package/dist/tests/core.test.js +0 -1056
  107. package/dist/tests/core.test.js.map +0 -1
  108. package/dist/tests/data-bind-arg-reeval.test.d.ts +0 -2
  109. package/dist/tests/data-bind-arg-reeval.test.d.ts.map +0 -1
  110. package/dist/tests/data-bind-arg-reeval.test.js +0 -119
  111. package/dist/tests/data-bind-arg-reeval.test.js.map +0 -1
  112. package/dist/tests/data-bind-merge.test.d.ts +0 -2
  113. package/dist/tests/data-bind-merge.test.d.ts.map +0 -1
  114. package/dist/tests/data-bind-merge.test.js +0 -86
  115. package/dist/tests/data-bind-merge.test.js.map +0 -1
  116. package/dist/tests/data-click-defer.test.d.ts +0 -2
  117. package/dist/tests/data-click-defer.test.d.ts.map +0 -1
  118. package/dist/tests/data-click-defer.test.js +0 -57
  119. package/dist/tests/data-click-defer.test.js.map +0 -1
  120. package/dist/tests/data-click-run.test.d.ts +0 -5
  121. package/dist/tests/data-click-run.test.d.ts.map +0 -1
  122. package/dist/tests/data-click-run.test.js +0 -109
  123. package/dist/tests/data-click-run.test.js.map +0 -1
  124. package/dist/tests/data-derive.test.d.ts +0 -2
  125. package/dist/tests/data-derive.test.d.ts.map +0 -1
  126. package/dist/tests/data-derive.test.js +0 -999
  127. package/dist/tests/data-derive.test.js.map +0 -1
  128. package/dist/tests/data-each-browserlike.test.d.ts +0 -2
  129. package/dist/tests/data-each-browserlike.test.d.ts.map +0 -1
  130. package/dist/tests/data-each-browserlike.test.js +0 -48
  131. package/dist/tests/data-each-browserlike.test.js.map +0 -1
  132. package/dist/tests/data-each-fragment-debug.test.d.ts +0 -2
  133. package/dist/tests/data-each-fragment-debug.test.d.ts.map +0 -1
  134. package/dist/tests/data-each-fragment-debug.test.js +0 -119
  135. package/dist/tests/data-each-fragment-debug.test.js.map +0 -1
  136. package/dist/tests/data-each-table.test.d.ts +0 -2
  137. package/dist/tests/data-each-table.test.d.ts.map +0 -1
  138. package/dist/tests/data-each-table.test.js +0 -131
  139. package/dist/tests/data-each-table.test.js.map +0 -1
  140. package/dist/tests/data-each-template-recovery.test.d.ts +0 -2
  141. package/dist/tests/data-each-template-recovery.test.d.ts.map +0 -1
  142. package/dist/tests/data-each-template-recovery.test.js +0 -87
  143. package/dist/tests/data-each-template-recovery.test.js.map +0 -1
  144. package/dist/tests/data-event-prevent.test.d.ts +0 -2
  145. package/dist/tests/data-event-prevent.test.d.ts.map +0 -1
  146. package/dist/tests/data-event-prevent.test.js +0 -76
  147. package/dist/tests/data-event-prevent.test.js.map +0 -1
  148. package/dist/tests/data-fetch-form-input-binding.test.d.ts +0 -2
  149. package/dist/tests/data-fetch-form-input-binding.test.d.ts.map +0 -1
  150. package/dist/tests/data-fetch-form-input-binding.test.js +0 -202
  151. package/dist/tests/data-fetch-form-input-binding.test.js.map +0 -1
  152. package/dist/tests/data-fetch-tbody-dom.test.d.ts +0 -2
  153. package/dist/tests/data-fetch-tbody-dom.test.d.ts.map +0 -1
  154. package/dist/tests/data-fetch-tbody-dom.test.js +0 -82
  155. package/dist/tests/data-fetch-tbody-dom.test.js.map +0 -1
  156. package/dist/tests/data-form-container.test.d.ts +0 -2
  157. package/dist/tests/data-form-container.test.d.ts.map +0 -1
  158. package/dist/tests/data-form-container.test.js +0 -88
  159. package/dist/tests/data-form-container.test.js.map +0 -1
  160. package/dist/tests/data-if-dev-diagnostics.test.d.ts +0 -2
  161. package/dist/tests/data-if-dev-diagnostics.test.d.ts.map +0 -1
  162. package/dist/tests/data-if-dev-diagnostics.test.js +0 -55
  163. package/dist/tests/data-if-dev-diagnostics.test.js.map +0 -1
  164. package/dist/tests/data-if-falsy.test.d.ts +0 -2
  165. package/dist/tests/data-if-falsy.test.d.ts.map +0 -1
  166. package/dist/tests/data-if-falsy.test.js +0 -73
  167. package/dist/tests/data-if-falsy.test.js.map +0 -1
  168. package/dist/tests/data-load-on-show.test.d.ts +0 -2
  169. package/dist/tests/data-load-on-show.test.d.ts.map +0 -1
  170. package/dist/tests/data-load-on-show.test.js +0 -98
  171. package/dist/tests/data-load-on-show.test.js.map +0 -1
  172. package/dist/tests/derive-each-render-wait.test.d.ts +0 -2
  173. package/dist/tests/derive-each-render-wait.test.d.ts.map +0 -1
  174. package/dist/tests/derive-each-render-wait.test.js +0 -90
  175. package/dist/tests/derive-each-render-wait.test.js.map +0 -1
  176. package/dist/tests/dev.test.d.ts +0 -2
  177. package/dist/tests/dev.test.d.ts.map +0 -1
  178. package/dist/tests/dev.test.js +0 -51
  179. package/dist/tests/dev.test.js.map +0 -1
  180. package/dist/tests/each-update-event.test.d.ts +0 -2
  181. package/dist/tests/each-update-event.test.d.ts.map +0 -1
  182. package/dist/tests/each-update-event.test.js +0 -83
  183. package/dist/tests/each-update-event.test.js.map +0 -1
  184. package/dist/tests/each_arg.test.d.ts +0 -2
  185. package/dist/tests/each_arg.test.d.ts.map +0 -1
  186. package/dist/tests/each_arg.test.js +0 -41
  187. package/dist/tests/each_arg.test.js.map +0 -1
  188. package/dist/tests/env.test.d.ts +0 -2
  189. package/dist/tests/env.test.d.ts.map +0 -1
  190. package/dist/tests/env.test.js +0 -124
  191. package/dist/tests/env.test.js.map +0 -1
  192. package/dist/tests/evaluation-profile.test.d.ts +0 -2
  193. package/dist/tests/evaluation-profile.test.d.ts.map +0 -1
  194. package/dist/tests/evaluation-profile.test.js +0 -92
  195. package/dist/tests/evaluation-profile.test.js.map +0 -1
  196. package/dist/tests/event.test.d.ts +0 -2
  197. package/dist/tests/event.test.d.ts.map +0 -1
  198. package/dist/tests/event.test.js +0 -300
  199. package/dist/tests/event.test.js.map +0 -1
  200. package/dist/tests/expression-disallowed-keyword-hint.test.d.ts +0 -2
  201. package/dist/tests/expression-disallowed-keyword-hint.test.d.ts.map +0 -1
  202. package/dist/tests/expression-disallowed-keyword-hint.test.js +0 -45
  203. package/dist/tests/expression-disallowed-keyword-hint.test.js.map +0 -1
  204. package/dist/tests/expression-forbidden-hint.test.d.ts +0 -2
  205. package/dist/tests/expression-forbidden-hint.test.d.ts.map +0 -1
  206. package/dist/tests/expression-forbidden-hint.test.js +0 -37
  207. package/dist/tests/expression-forbidden-hint.test.js.map +0 -1
  208. package/dist/tests/expression.test.d.ts +0 -2
  209. package/dist/tests/expression.test.d.ts.map +0 -1
  210. package/dist/tests/expression.test.js +0 -482
  211. package/dist/tests/expression.test.js.map +0 -1
  212. package/dist/tests/fetch-and-procedure-scenarios.test.d.ts +0 -2
  213. package/dist/tests/fetch-and-procedure-scenarios.test.d.ts.map +0 -1
  214. package/dist/tests/fetch-and-procedure-scenarios.test.js +0 -195
  215. package/dist/tests/fetch-and-procedure-scenarios.test.js.map +0 -1
  216. package/dist/tests/form-number-type.test.d.ts +0 -2
  217. package/dist/tests/form-number-type.test.d.ts.map +0 -1
  218. package/dist/tests/form-number-type.test.js +0 -83
  219. package/dist/tests/form-number-type.test.js.map +0 -1
  220. package/dist/tests/form.test.d.ts +0 -2
  221. package/dist/tests/form.test.d.ts.map +0 -1
  222. package/dist/tests/form.test.js +0 -636
  223. package/dist/tests/form.test.js.map +0 -1
  224. package/dist/tests/fragment.test.d.ts +0 -2
  225. package/dist/tests/fragment.test.d.ts.map +0 -1
  226. package/dist/tests/fragment.test.js +0 -284
  227. package/dist/tests/fragment.test.js.map +0 -1
  228. package/dist/tests/haori.test.d.ts +0 -2
  229. package/dist/tests/haori.test.d.ts.map +0 -1
  230. package/dist/tests/haori.test.js +0 -149
  231. package/dist/tests/haori.test.js.map +0 -1
  232. package/dist/tests/helpers/async.d.ts +0 -20
  233. package/dist/tests/helpers/async.d.ts.map +0 -1
  234. package/dist/tests/helpers/async.js +0 -35
  235. package/dist/tests/helpers/async.js.map +0 -1
  236. package/dist/tests/import.test.d.ts +0 -2
  237. package/dist/tests/import.test.d.ts.map +0 -1
  238. package/dist/tests/import.test.js +0 -148
  239. package/dist/tests/import.test.js.map +0 -1
  240. package/dist/tests/intersect.test.d.ts +0 -2
  241. package/dist/tests/intersect.test.d.ts.map +0 -1
  242. package/dist/tests/intersect.test.js +0 -173
  243. package/dist/tests/intersect.test.js.map +0 -1
  244. package/dist/tests/log.test.d.ts +0 -2
  245. package/dist/tests/log.test.d.ts.map +0 -1
  246. package/dist/tests/log.test.js +0 -58
  247. package/dist/tests/log.test.js.map +0 -1
  248. package/dist/tests/observer.test.d.ts +0 -2
  249. package/dist/tests/observer.test.d.ts.map +0 -1
  250. package/dist/tests/observer.test.js +0 -64
  251. package/dist/tests/observer.test.js.map +0 -1
  252. package/dist/tests/placeholder-resolution.test.d.ts +0 -2
  253. package/dist/tests/placeholder-resolution.test.d.ts.map +0 -1
  254. package/dist/tests/placeholder-resolution.test.js +0 -463
  255. package/dist/tests/placeholder-resolution.test.js.map +0 -1
  256. package/dist/tests/procedure-action-operations.test.d.ts +0 -2
  257. package/dist/tests/procedure-action-operations.test.d.ts.map +0 -1
  258. package/dist/tests/procedure-action-operations.test.js +0 -1140
  259. package/dist/tests/procedure-action-operations.test.js.map +0 -1
  260. package/dist/tests/procedure-bind-append.test.d.ts +0 -2
  261. package/dist/tests/procedure-bind-append.test.d.ts.map +0 -1
  262. package/dist/tests/procedure-bind-append.test.js +0 -80
  263. package/dist/tests/procedure-bind-append.test.js.map +0 -1
  264. package/dist/tests/procedure-fetch-options.test.d.ts +0 -2
  265. package/dist/tests/procedure-fetch-options.test.d.ts.map +0 -1
  266. package/dist/tests/procedure-fetch-options.test.js +0 -255
  267. package/dist/tests/procedure-fetch-options.test.js.map +0 -1
  268. package/dist/tests/procedure.test.d.ts +0 -2
  269. package/dist/tests/procedure.test.d.ts.map +0 -1
  270. package/dist/tests/procedure.test.js +0 -137
  271. package/dist/tests/procedure.test.js.map +0 -1
  272. package/dist/tests/procedure_events.test.d.ts +0 -7
  273. package/dist/tests/procedure_events.test.d.ts.map +0 -1
  274. package/dist/tests/procedure_events.test.js +0 -261
  275. package/dist/tests/procedure_events.test.js.map +0 -1
  276. package/dist/tests/render-wait.test.d.ts +0 -2
  277. package/dist/tests/render-wait.test.d.ts.map +0 -1
  278. package/dist/tests/render-wait.test.js +0 -68
  279. package/dist/tests/render-wait.test.js.map +0 -1
  280. package/dist/tests/reset_each.test.d.ts +0 -2
  281. package/dist/tests/reset_each.test.d.ts.map +0 -1
  282. package/dist/tests/reset_each.test.js +0 -215
  283. package/dist/tests/reset_each.test.js.map +0 -1
  284. package/dist/tests/row-move.test.d.ts +0 -2
  285. package/dist/tests/row-move.test.d.ts.map +0 -1
  286. package/dist/tests/row-move.test.js +0 -220
  287. package/dist/tests/row-move.test.js.map +0 -1
  288. package/dist/tests/row-operations.test.d.ts +0 -2
  289. package/dist/tests/row-operations.test.d.ts.map +0 -1
  290. package/dist/tests/row-operations.test.js +0 -259
  291. package/dist/tests/row-operations.test.js.map +0 -1
  292. package/dist/tests/scheduled-evaluate-wait.test.d.ts +0 -2
  293. package/dist/tests/scheduled-evaluate-wait.test.d.ts.map +0 -1
  294. package/dist/tests/scheduled-evaluate-wait.test.js +0 -68
  295. package/dist/tests/scheduled-evaluate-wait.test.js.map +0 -1
  296. package/dist/tests/url.test.d.ts +0 -2
  297. package/dist/tests/url.test.d.ts.map +0 -1
  298. package/dist/tests/url.test.js +0 -150
  299. package/dist/tests/url.test.js.map +0 -1
  300. package/dist/vite.config.d.ts +0 -3
  301. package/dist/vite.config.d.ts.map +0 -1
  302. package/dist/vite.config.js +0 -28
  303. package/dist/vite.config.js.map +0 -1
  304. package/dist/vitest.config.d.ts +0 -3
  305. package/dist/vitest.config.d.ts.map +0 -1
  306. package/dist/vitest.config.js +0 -19
  307. package/dist/vitest.config.js.map +0 -1
@@ -1,1921 +0,0 @@
1
- /**
2
- * @fileoverview 手続き的処理管理機能
3
- *
4
- * イベントに基づく手続き的な処理を提供します。
5
- */
6
- import Core from './core';
7
- import Env from './env';
8
- import Expression from './expression';
9
- import Form from './form';
10
- import Fragment from './fragment';
11
- import Haori from './haori';
12
- import Log from './log';
13
- import HaoriEvent from './event';
14
- const PROCEDURE_HAORI_METHOD_NAMES = [
15
- 'addErrorMessage',
16
- 'closeDialog',
17
- 'confirm',
18
- 'dialog',
19
- 'openDialog',
20
- 'toast',
21
- ];
22
- const PROCEDURE_HISTORY_STATE_KEY = '__haoriHistoryState__';
23
- /** click ロック中であることを示す内部マーカー属性名 */
24
- const PROCEDURE_CLICK_LOCK_MARKER = 'data-haori-click-lock';
25
- /**
26
- * Procedure から利用する Haori API を解決します。
27
- * window.Haori が差し替えられている場合はそちらを優先します。
28
- *
29
- * @returns Procedure が使用する Haori API。
30
- */
31
- function resolveProcedureHaoriApi() {
32
- const scope = globalThis;
33
- const candidate = scope.window?.Haori;
34
- const hasRequiredMethods = PROCEDURE_HAORI_METHOD_NAMES.every(methodName => typeof candidate?.[methodName] === 'function');
35
- return hasRequiredMethods ? candidate : Haori;
36
- }
37
- const QUERY_TRANSPORT_METHODS = new Set(['GET', 'HEAD', 'OPTIONS']);
38
- /**
39
- * URL クエリ化の対象メソッドかどうかを判定します。
40
- *
41
- * @param method 判定対象の HTTP メソッド。
42
- * @return クエリ送信対象なら true。
43
- */
44
- function isQueryTransportMethod(method) {
45
- return QUERY_TRANSPORT_METHODS.has(method.toUpperCase());
46
- }
47
- /**
48
- * 送信データを URLSearchParams に追加します。
49
- *
50
- * @param params 追加先の URLSearchParams。
51
- * @param payload 追加対象の送信データ。
52
- * @return 戻り値はありません。
53
- */
54
- function appendPayloadToSearchParams(params, payload) {
55
- for (const [key, value] of Object.entries(payload)) {
56
- if (value === undefined) {
57
- continue;
58
- }
59
- if (value === null) {
60
- params.append(key, '');
61
- }
62
- else if (Array.isArray(value)) {
63
- value.forEach(item => {
64
- params.append(key, String(item));
65
- });
66
- }
67
- else if (typeof value === 'object' || typeof value === 'function') {
68
- params.append(key, JSON.stringify(value));
69
- }
70
- else {
71
- params.append(key, String(value));
72
- }
73
- }
74
- }
75
- /**
76
- * 送信データをクエリ文字列へ付加した URL を返します。
77
- *
78
- * @param fetchUrl 元のフェッチ URL。
79
- * @param payload 追加対象の送信データ。
80
- * @return クエリ文字列を付加した URL。
81
- */
82
- function appendPayloadToUrl(fetchUrl, payload) {
83
- const url = new URL(fetchUrl, window.location.href);
84
- const params = new URLSearchParams(url.search);
85
- appendPayloadToSearchParams(params, payload);
86
- url.search = params.toString();
87
- return url.toString();
88
- }
89
- function normalizeRequestBody(body) {
90
- if (body === undefined || body === null) {
91
- return null;
92
- }
93
- if (typeof body === 'string') {
94
- return body;
95
- }
96
- if (body instanceof URLSearchParams) {
97
- return body.toString();
98
- }
99
- if (body instanceof FormData) {
100
- return Array.from(body.entries()).map(([key, value]) => {
101
- if (value instanceof File) {
102
- return [
103
- key,
104
- {
105
- type: 'file',
106
- name: value.name,
107
- size: value.size,
108
- mimeType: value.type,
109
- },
110
- ];
111
- }
112
- return [key, String(value)];
113
- });
114
- }
115
- return String(body);
116
- }
117
- function buildFetchSignature(url, options) {
118
- const headers = new Headers(options.headers || undefined);
119
- const normalizedHeaders = Array.from(headers.entries()).sort(([a], [b]) => a.localeCompare(b));
120
- return JSON.stringify({
121
- url,
122
- method: String(options.method || 'GET').toUpperCase(),
123
- headers: normalizedHeaders,
124
- body: normalizeRequestBody(options.body || null),
125
- });
126
- }
127
- /**
128
- * 手続き的処理管理クラスです。
129
- */
130
- class Procedure {
131
- /**
132
- * イベント属性名を正しく生成します。
133
- * 例: ("click", "fetch") => "data-click-fetch"
134
- * (null, "fetch") => "data-fetch"
135
- * ("change", "bind-arg") => "data-change-bind-arg"
136
- * 非イベント変種が "data-fetch-xxx" として存在するものについては、event が null の場合にそちらを返します。
137
- */
138
- static attrName(event, key, hasFetchFallback = false) {
139
- if (event) {
140
- return `${Env.prefix}${event}-${key}`;
141
- }
142
- return hasFetchFallback
143
- ? `${Env.prefix}fetch-${key}`
144
- : `${Env.prefix}${key}`;
145
- }
146
- /**
147
- * data 属性のテンプレート式評価結果を URLSearchParams 向けに組み立てます。
148
- *
149
- * @param rawAttribute 生の属性値
150
- * @param bindingValues バインディング値
151
- * @returns パラメータ形式として扱える文字列
152
- */
153
- static resolveDataParamString(rawAttribute, bindingValues) {
154
- return Procedure.resolveDataParamStringDetailed(rawAttribute, bindingValues)
155
- .value;
156
- }
157
- /**
158
- * data 属性のテンプレート式評価結果を URLSearchParams 向けに組み立てます。
159
- *
160
- * @param rawAttribute 生の属性値
161
- * @param bindingValues バインディング値
162
- * @returns パラメータ形式として扱える文字列と未解決参照の有無
163
- */
164
- static resolveDataParamStringDetailed(rawAttribute, bindingValues) {
165
- let hasUnresolvedReference = false;
166
- const value = rawAttribute.replace(Procedure.DATA_PLACEHOLDER_REGEX, (_matched, rawExpression, expression) => {
167
- const result = Expression.evaluateDetailed(rawExpression ?? expression ?? '', bindingValues);
168
- hasUnresolvedReference =
169
- hasUnresolvedReference || result.unresolvedReference;
170
- if (result.value === null ||
171
- result.value === undefined ||
172
- Number.isNaN(result.value)) {
173
- return '';
174
- }
175
- if (typeof result.value === 'object') {
176
- return encodeURIComponent(JSON.stringify(result.value));
177
- }
178
- return encodeURIComponent(String(result.value));
179
- });
180
- return { value, hasUnresolvedReference };
181
- }
182
- /**
183
- * JSON 文字列中のテンプレート式かどうかを判定します。
184
- *
185
- * @param source 生の属性値
186
- * @param offset プレースホルダ開始位置
187
- * @returns JSON 文字列中なら true
188
- */
189
- static isJsonStringContext(source, offset) {
190
- let inString = false;
191
- let escaped = false;
192
- for (let index = 0; index < offset; index += 1) {
193
- const char = source[index];
194
- if (escaped) {
195
- escaped = false;
196
- continue;
197
- }
198
- if (char === '\\') {
199
- escaped = true;
200
- continue;
201
- }
202
- if (char === '"') {
203
- inString = !inString;
204
- }
205
- }
206
- return inString;
207
- }
208
- /**
209
- * JSON 値コンテキスト向けにテンプレート式の評価結果を直列化します。
210
- *
211
- * @param result テンプレート式の評価結果
212
- * @returns JSON 値として埋め込める文字列
213
- */
214
- static stringifyJsonTemplateValue(result) {
215
- if (result === undefined || Number.isNaN(result)) {
216
- return 'null';
217
- }
218
- try {
219
- const serialized = JSON.stringify(result);
220
- return serialized ?? JSON.stringify(String(result));
221
- }
222
- catch {
223
- return JSON.stringify(String(result));
224
- }
225
- }
226
- /**
227
- * JSON 文字列コンテキスト向けにテンプレート式の評価結果を直列化します。
228
- *
229
- * @param result テンプレート式の評価結果
230
- * @returns JSON 文字列へ安全に埋め込める文字列
231
- */
232
- static stringifyJsonTemplateStringContent(result) {
233
- if (result === null || result === undefined || Number.isNaN(result)) {
234
- return '';
235
- }
236
- const value = typeof result === 'object'
237
- ? Procedure.stringifyJsonTemplateValue(result)
238
- : String(result);
239
- return JSON.stringify(value).slice(1, -1);
240
- }
241
- /**
242
- * JSON 形式 data 属性内のテンプレート式を安全に解決します。
243
- *
244
- * @param rawAttribute 生の属性値
245
- * @param bindingValues バインディング値
246
- * @returns JSON として解釈可能な文字列
247
- */
248
- static resolveDataJsonString(rawAttribute, bindingValues) {
249
- return Procedure.resolveDataJsonStringDetailed(rawAttribute, bindingValues)
250
- .value;
251
- }
252
- /**
253
- * JSON 形式 data 属性内のテンプレート式を安全に解決します。
254
- *
255
- * @param rawAttribute 生の属性値
256
- * @param bindingValues バインディング値
257
- * @returns JSON として解釈可能な文字列と未解決参照の有無
258
- */
259
- static resolveDataJsonStringDetailed(rawAttribute, bindingValues) {
260
- let hasUnresolvedReference = false;
261
- const value = rawAttribute.replace(Procedure.DATA_PLACEHOLDER_REGEX, (_matched, rawExpression, expression, offset) => {
262
- const result = Expression.evaluateDetailed(rawExpression ?? expression ?? '', bindingValues);
263
- hasUnresolvedReference =
264
- hasUnresolvedReference || result.unresolvedReference;
265
- return Procedure.isJsonStringContext(rawAttribute, offset)
266
- ? Procedure.stringifyJsonTemplateStringContent(result.value)
267
- : Procedure.stringifyJsonTemplateValue(result.value);
268
- });
269
- return { value, hasUnresolvedReference };
270
- }
271
- /**
272
- * data 属性を評価済みの値として取得します。
273
- *
274
- * @param fragment フラグメント
275
- * @param attrName 属性名
276
- * @returns 送信データ
277
- */
278
- static resolveDataAttribute(fragment, attrName) {
279
- return Procedure.resolveDataAttributeDetailed(fragment, attrName).value;
280
- }
281
- /**
282
- * data 属性を評価済みの値として取得し、未解決参照の有無を返します。
283
- *
284
- * @param fragment フラグメント
285
- * @param attrName 属性名
286
- * @returns 送信データと未解決参照の有無
287
- */
288
- static resolveDataAttributeDetailed(fragment, attrName) {
289
- const rawAttribute = fragment.getRawAttribute(attrName);
290
- const attributeEvaluation = fragment.getAttributeEvaluation(attrName);
291
- const dataAttribute = attributeEvaluation?.value ?? null;
292
- const hasUnresolvedReference = attributeEvaluation?.hasUnresolvedReference ?? false;
293
- if (dataAttribute &&
294
- typeof dataAttribute === 'object' &&
295
- !Array.isArray(dataAttribute)) {
296
- return {
297
- value: dataAttribute,
298
- hasUnresolvedReference,
299
- };
300
- }
301
- if (typeof dataAttribute !== 'string' || rawAttribute === null) {
302
- return { value: null, hasUnresolvedReference };
303
- }
304
- const trimmed = rawAttribute.trim();
305
- if (Procedure.SINGLE_PLACEHOLDER_REGEX.test(trimmed)) {
306
- return {
307
- value: Core.parseDataBind(dataAttribute),
308
- hasUnresolvedReference,
309
- };
310
- }
311
- if (trimmed.startsWith('{') || trimmed.startsWith('[')) {
312
- const resolved = Procedure.resolveDataJsonStringDetailed(rawAttribute, fragment.getBindingData());
313
- return {
314
- value: Core.parseDataBind(resolved.value),
315
- hasUnresolvedReference: hasUnresolvedReference || resolved.hasUnresolvedReference,
316
- };
317
- }
318
- const resolved = Procedure.resolveDataParamStringDetailed(rawAttribute, fragment.getBindingData());
319
- return {
320
- value: Core.parseDataBind(resolved.value),
321
- hasUnresolvedReference: hasUnresolvedReference || resolved.hasUnresolvedReference,
322
- };
323
- }
324
- /**
325
- * オプションをフラグメントの属性から構築します。
326
- *
327
- * @param fragment フラグメント
328
- * @param event イベント名
329
- * @return 構築されたオプション
330
- */
331
- static buildOptions(fragment, event) {
332
- const options = {
333
- targetFragment: fragment,
334
- };
335
- if (event) {
336
- // validate(spec: data-???-validate)
337
- if (fragment.hasAttribute(Procedure.attrName(event, 'validate'))) {
338
- options.valid = true;
339
- }
340
- // confirm
341
- if (fragment.hasAttribute(Procedure.attrName(event, 'confirm'))) {
342
- options.confirmMessage = fragment.getAttribute(Procedure.attrName(event, 'confirm')).replace(/\\n/g, '\n');
343
- }
344
- // data(イベント)
345
- if (fragment.hasAttribute(Procedure.attrName(event, 'data'))) {
346
- options.dataAttrName = Procedure.attrName(event, 'data');
347
- }
348
- // form(イベント)
349
- if (fragment.hasAttribute(Procedure.attrName(event, 'form'))) {
350
- const formSelector = fragment.getRawAttribute(Procedure.attrName(event, 'form'));
351
- if (formSelector) {
352
- const formElement = document.body.querySelector(formSelector);
353
- if (formElement !== null) {
354
- options.formFragment = Form.getFormFragment(Fragment.get(formElement));
355
- }
356
- else {
357
- Log.error('Haori', `Form element not found: ${formSelector}` +
358
- ` (${Procedure.attrName(event, 'form')})`);
359
- }
360
- }
361
- else {
362
- // 属性はあるが値が省略された場合は自要素もしくは先祖の form を対象
363
- options.formFragment = Form.getFormFragment(fragment);
364
- }
365
- }
366
- else if (event === 'change') {
367
- // changeイベントの場合、data-change-form属性がなくても自動的にフォームを検索
368
- options.formFragment = Form.getFormFragment(fragment);
369
- }
370
- if (fragment.hasAttribute(`${Env.prefix}${event}-before-run`)) {
371
- const body = fragment.getRawAttribute(`${Env.prefix}${event}-before-run`);
372
- try {
373
- options.beforeCallback = new Function('fetchUrl', 'fetchOptions', `
374
- "use strict";
375
- ${body}
376
- `);
377
- }
378
- catch (e) {
379
- Log.error('Haori', `Invalid before script: ${e}`);
380
- }
381
- }
382
- // data-{event}-run: フェッチを伴わない任意 JS をクリック等で実行する。
383
- // before-run と異なり {{...}} を展開した値(getAttribute)を本体にする。
384
- const runAttrName = Procedure.attrName(event, 'run');
385
- if (fragment.hasAttribute(runAttrName)) {
386
- const body = String(fragment.getAttribute(runAttrName) ?? '');
387
- try {
388
- options.runScript = new Function('event', `
389
- "use strict";
390
- ${body}
391
- `);
392
- }
393
- catch (e) {
394
- Log.error('Haori', `Invalid run script: ${e}`);
395
- }
396
- }
397
- }
398
- // fetch URL(イベントあり/なし)
399
- const fetchAttrName = Procedure.attrName(event, 'fetch');
400
- const hasFetchAttr = fragment.hasAttribute(fetchAttrName);
401
- if (hasFetchAttr) {
402
- const fetchEvaluation = fragment.getAttributeEvaluation(fetchAttrName);
403
- if (fetchEvaluation) {
404
- options.fetchHasUnresolvedReference =
405
- fetchEvaluation.hasUnresolvedReference;
406
- options.fetchUrl = fetchEvaluation.hasUnresolvedReference
407
- ? null
408
- : fetchEvaluation.value;
409
- }
410
- }
411
- const fetchOptions = {};
412
- // fetch-method(イベントあり/なし)
413
- // event: data-{event}-fetch-method, non-event: data-fetch-method
414
- if (event) {
415
- const fetchMethodAttrEvent = Procedure.attrName(event, 'fetch-method');
416
- if (fragment.hasAttribute(fetchMethodAttrEvent)) {
417
- const fetchMethodEvaluation = fragment.getAttributeEvaluation(fetchMethodAttrEvent);
418
- if (fetchMethodEvaluation?.hasUnresolvedReference) {
419
- options.fetchHasUnresolvedReference = true;
420
- }
421
- else {
422
- fetchOptions.method = fetchMethodEvaluation?.value;
423
- }
424
- }
425
- }
426
- else {
427
- const fetchMethodAttrNonEvent = Procedure.attrName(null, 'method', true);
428
- if (fragment.hasAttribute(fetchMethodAttrNonEvent)) {
429
- const fetchMethodEvaluation = fragment.getAttributeEvaluation(fetchMethodAttrNonEvent);
430
- if (fetchMethodEvaluation?.hasUnresolvedReference) {
431
- options.fetchHasUnresolvedReference = true;
432
- }
433
- else {
434
- fetchOptions.method = fetchMethodEvaluation?.value;
435
- }
436
- }
437
- }
438
- // fetch-headers(イベントあり/なし)
439
- // event: data-{event}-fetch-headers, non-event: data-fetch-headers
440
- if (event) {
441
- const fetchHeadersAttrEvent = Procedure.attrName(event, 'fetch-headers');
442
- if (fragment.hasAttribute(fetchHeadersAttrEvent)) {
443
- const headersString = fragment.getRawAttribute(fetchHeadersAttrEvent);
444
- try {
445
- fetchOptions.headers = Core.parseDataBind(headersString);
446
- }
447
- catch (e) {
448
- Log.error('Haori', `Invalid fetch headers: ${e}`);
449
- }
450
- }
451
- }
452
- else {
453
- const fetchHeadersAttrNonEvent = Procedure.attrName(null, 'headers', true);
454
- if (fragment.hasAttribute(fetchHeadersAttrNonEvent)) {
455
- const headersString = fragment.getRawAttribute(fetchHeadersAttrNonEvent);
456
- try {
457
- fetchOptions.headers = Core.parseDataBind(headersString);
458
- }
459
- catch (e) {
460
- Log.error('Haori', `Invalid fetch headers: ${e}`);
461
- }
462
- }
463
- }
464
- // fetch-content-type(イベントあり/なし)
465
- // event: data-{event}-fetch-content-type
466
- // non-event: data-fetch-content-type
467
- if (event) {
468
- const fetchCTAttrEvent = Procedure.attrName(event, 'fetch-content-type');
469
- if (fragment.hasAttribute(fetchCTAttrEvent)) {
470
- const fetchContentTypeEvaluation = fragment.getAttributeEvaluation(fetchCTAttrEvent);
471
- if (fetchContentTypeEvaluation?.hasUnresolvedReference) {
472
- options.fetchHasUnresolvedReference = true;
473
- }
474
- fetchOptions.headers = {
475
- ...fetchOptions.headers,
476
- 'Content-Type': fetchContentTypeEvaluation?.value,
477
- };
478
- }
479
- else if (fetchOptions.method &&
480
- fetchOptions.method !== 'GET' &&
481
- fetchOptions.method !== 'HEAD' &&
482
- fetchOptions.method !== 'OPTIONS') {
483
- // only set default Content-Type when one is not already provided
484
- let hasContentType = false;
485
- if (fetchOptions.headers && typeof fetchOptions.headers === 'object') {
486
- const headersObj = fetchOptions.headers;
487
- hasContentType = 'Content-Type' in headersObj;
488
- }
489
- if (!hasContentType) {
490
- fetchOptions.headers = {
491
- ...fetchOptions.headers,
492
- 'Content-Type': 'application/json',
493
- };
494
- }
495
- }
496
- else if (fetchOptions.method &&
497
- (fetchOptions.method === 'GET' ||
498
- fetchOptions.method === 'HEAD' ||
499
- fetchOptions.method === 'OPTIONS')) {
500
- // 仕様: GET/HEAD/OPTIONS 既定は application/x-www-form-urlencoded
501
- fetchOptions.headers = {
502
- ...fetchOptions.headers,
503
- 'Content-Type': 'application/x-www-form-urlencoded',
504
- };
505
- }
506
- }
507
- else {
508
- const fetchCTAttrNonEvent = Procedure.attrName(null, 'content-type', true);
509
- if (fragment.hasAttribute(fetchCTAttrNonEvent)) {
510
- const fetchContentTypeEvaluation = fragment.getAttributeEvaluation(fetchCTAttrNonEvent);
511
- if (fetchContentTypeEvaluation?.hasUnresolvedReference) {
512
- options.fetchHasUnresolvedReference = true;
513
- }
514
- fetchOptions.headers = {
515
- ...fetchOptions.headers,
516
- 'Content-Type': fetchContentTypeEvaluation?.value,
517
- };
518
- }
519
- else if (fetchOptions.method &&
520
- fetchOptions.method !== 'GET' &&
521
- fetchOptions.method !== 'HEAD' &&
522
- fetchOptions.method !== 'OPTIONS') {
523
- // only set default Content-Type when one is not already provided
524
- let hasContentType = false;
525
- if (fetchOptions.headers && typeof fetchOptions.headers === 'object') {
526
- const headersObj = fetchOptions.headers;
527
- hasContentType = 'Content-Type' in headersObj;
528
- }
529
- if (!hasContentType) {
530
- fetchOptions.headers = {
531
- ...fetchOptions.headers,
532
- 'Content-Type': 'application/json',
533
- };
534
- }
535
- }
536
- else if (fetchOptions.method &&
537
- (fetchOptions.method === 'GET' ||
538
- fetchOptions.method === 'HEAD' ||
539
- fetchOptions.method === 'OPTIONS')) {
540
- // 仕様: GET/HEAD/OPTIONS 既定は application/x-www-form-urlencoded
541
- fetchOptions.headers = {
542
- ...fetchOptions.headers,
543
- 'Content-Type': 'application/x-www-form-urlencoded',
544
- };
545
- }
546
- }
547
- if (Object.keys(fetchOptions).length > 0) {
548
- options.fetchOptions = fetchOptions;
549
- }
550
- // bind(イベントあり/なし: 非イベントは data-fetch-bind)
551
- const bindAttr = event
552
- ? Procedure.attrName(event, 'bind')
553
- : Procedure.attrName(null, 'bind', true);
554
- if (fragment.hasAttribute(bindAttr)) {
555
- const bindSelector = fragment.getRawAttribute(bindAttr);
556
- if (bindSelector) {
557
- const bindElements = document.body.querySelectorAll(bindSelector);
558
- if (bindElements.length > 0) {
559
- options.bindFragments = [];
560
- bindElements.forEach(element => {
561
- const fragment = Fragment.get(element);
562
- if (fragment) {
563
- options.bindFragments.push(fragment);
564
- }
565
- });
566
- }
567
- else {
568
- Log.error('Haori', `Bind element not found: ${bindSelector} (${bindAttr})`);
569
- }
570
- }
571
- }
572
- const bindArgAttrEvent = Procedure.attrName(event, 'bind-arg');
573
- const bindArgAttrNonEventLegacy = Procedure.attrName(null, 'arg', true); // data-fetch-arg
574
- const bindArgAttrNonEventNew = Procedure.attrName(null, 'bind-arg', true); // data-fetch-bind-arg (less common)
575
- if (event) {
576
- if (fragment.hasAttribute(bindArgAttrEvent)) {
577
- options.bindArg = fragment.getRawAttribute(bindArgAttrEvent);
578
- }
579
- }
580
- else {
581
- // Prefer legacy `data-fetch-arg` for non-event usage.
582
- // Fallback to `data-fetch-bind-arg` if legacy is not present.
583
- if (fragment.hasAttribute(bindArgAttrNonEventLegacy)) {
584
- options.bindArg = fragment.getRawAttribute(bindArgAttrNonEventLegacy);
585
- }
586
- else if (fragment.hasAttribute(bindArgAttrNonEventNew)) {
587
- options.bindArg = fragment.getRawAttribute(bindArgAttrNonEventNew);
588
- }
589
- }
590
- const bindParamsAttr = event
591
- ? Procedure.attrName(event, 'bind-params')
592
- : Procedure.attrName(null, 'bind-params', true);
593
- if (fragment.hasAttribute(bindParamsAttr)) {
594
- const paramsString = fragment.getRawAttribute(bindParamsAttr);
595
- options.bindParams = paramsString.split('&').map(p => p.trim());
596
- }
597
- const bindAppendAttr = event
598
- ? Procedure.attrName(event, 'bind-append')
599
- : Procedure.attrName(null, 'bind-append', true);
600
- if (fragment.hasAttribute(bindAppendAttr)) {
601
- const paramsString = fragment.getRawAttribute(bindAppendAttr);
602
- options.bindAppendParams = paramsString
603
- .split('&')
604
- .map(p => p.trim())
605
- .filter(Boolean);
606
- }
607
- const bindMergeAttr = event
608
- ? Procedure.attrName(event, 'bind-merge')
609
- : Procedure.attrName(null, 'bind-merge', true);
610
- if (fragment.hasAttribute(bindMergeAttr)) {
611
- options.bindMerge = true;
612
- }
613
- const copyParamsAttr = event
614
- ? Procedure.attrName(event, 'copy-params')
615
- : null;
616
- if (copyParamsAttr && fragment.hasAttribute(copyParamsAttr)) {
617
- const paramsString = fragment.getRawAttribute(copyParamsAttr);
618
- options.copyParams = paramsString
619
- .split('&')
620
- .map(param => param.trim())
621
- .filter(Boolean);
622
- }
623
- if (event) {
624
- if (fragment.hasAttribute(Procedure.attrName(event, 'adjust'))) {
625
- const adjustSelector = fragment.getRawAttribute(Procedure.attrName(event, 'adjust'));
626
- if (adjustSelector) {
627
- const adjustElements = document.body.querySelectorAll(adjustSelector);
628
- if (adjustElements.length > 0) {
629
- options.adjustFragments = [];
630
- adjustElements.forEach(element => {
631
- const fragment = Fragment.get(element);
632
- if (fragment) {
633
- options.adjustFragments.push(fragment);
634
- }
635
- });
636
- }
637
- else {
638
- Log.error('Haori', `Adjust element not found: ${adjustSelector}` +
639
- ` (${Procedure.attrName(event, 'adjust')})`);
640
- }
641
- }
642
- if (fragment.hasAttribute(Procedure.attrName(event, 'adjust-value'))) {
643
- const valueString = fragment.getRawAttribute(Procedure.attrName(event, 'adjust-value'));
644
- const value = Number(valueString);
645
- if (!isNaN(value)) {
646
- options.adjustValue = value;
647
- }
648
- }
649
- }
650
- if (fragment.hasAttribute(Procedure.attrName(event, 'row-add'))) {
651
- options.rowAdd = true;
652
- }
653
- if (fragment.hasAttribute(Procedure.attrName(event, 'row-remove'))) {
654
- options.rowRemove = true;
655
- }
656
- if (fragment.hasAttribute(Procedure.attrName(event, 'row-prev'))) {
657
- options.rowMovePrev = true;
658
- }
659
- if (fragment.hasAttribute(Procedure.attrName(event, 'row-next'))) {
660
- options.rowMoveNext = true;
661
- }
662
- if (fragment.hasAttribute(`${Env.prefix}${event}-after-run`)) {
663
- const body = fragment.getRawAttribute(`${Env.prefix}${event}-after-run`);
664
- try {
665
- options.afterCallback = new Function('response', `
666
- "use strict";
667
- ${body}
668
- `);
669
- }
670
- catch (e) {
671
- Log.error('Haori', `Invalid after script: ${e}`);
672
- }
673
- }
674
- if (fragment.hasAttribute(Procedure.attrName(event, 'dialog'))) {
675
- options.dialogMessage = fragment.getAttribute(Procedure.attrName(event, 'dialog')).replace(/\\n/g, '\n');
676
- }
677
- if (fragment.hasAttribute(Procedure.attrName(event, 'toast'))) {
678
- options.toastMessage = fragment.getAttribute(Procedure.attrName(event, 'toast'));
679
- const rawLevel = fragment.getRawAttribute(Procedure.attrName(event, 'toast-level'));
680
- const validLevels = ['info', 'warning', 'error', 'success'];
681
- const isValidLevel = validLevels.includes(rawLevel);
682
- options.toastLevel = isValidLevel ? rawLevel : null;
683
- }
684
- if (fragment.hasAttribute(Procedure.attrName(event, 'redirect'))) {
685
- options.redirectUrl = fragment.getAttribute(Procedure.attrName(event, 'redirect'));
686
- }
687
- if (fragment.hasAttribute(Procedure.attrName(event, 'scroll-error'))) {
688
- options.scrollOnError = true;
689
- }
690
- if (fragment.hasAttribute(Procedure.attrName(event, 'scroll'))) {
691
- options.scrollTarget = fragment.getAttribute(Procedure.attrName(event, 'scroll'));
692
- }
693
- // history(data-{event}-history / history-data / history-form)
694
- if (fragment.hasAttribute(Procedure.attrName(event, 'history'))) {
695
- options.historyUrl = fragment.getAttribute(Procedure.attrName(event, 'history'));
696
- }
697
- if (fragment.hasAttribute(Procedure.attrName(event, 'history-data'))) {
698
- options.historyDataAttrName = Procedure.attrName(event, 'history-data');
699
- }
700
- if (fragment.hasAttribute(Procedure.attrName(event, 'history-form'))) {
701
- const historyFormSelector = fragment.getRawAttribute(Procedure.attrName(event, 'history-form'));
702
- if (historyFormSelector) {
703
- const historyFormElement = document.body.querySelector(historyFormSelector);
704
- if (historyFormElement !== null) {
705
- options.historyFormFragment = Form.getFormFragment(Fragment.get(historyFormElement));
706
- }
707
- else {
708
- Log.error('Haori', `Form element not found: ${historyFormSelector}` +
709
- ` (${Procedure.attrName(event, 'history-form')})`);
710
- }
711
- }
712
- else {
713
- options.historyFormFragment = Form.getFormFragment(fragment);
714
- }
715
- }
716
- // reset/refetch/click/open/close(イベント、CSSセレクタ)
717
- const selectorAttrs = [
718
- 'reset-before',
719
- 'reset',
720
- 'refetch',
721
- 'click',
722
- 'copy',
723
- 'open',
724
- 'close',
725
- ];
726
- selectorAttrs.forEach(attrKey => {
727
- const attrName = Procedure.attrName(event, attrKey);
728
- if (!fragment.hasAttribute(attrName)) {
729
- return;
730
- }
731
- const selector = fragment.getRawAttribute(attrName);
732
- const list = [];
733
- if (selector) {
734
- const elements = document.body.querySelectorAll(selector);
735
- elements.forEach(el => {
736
- const frag = Fragment.get(el);
737
- if (frag) {
738
- list.push(frag);
739
- }
740
- });
741
- if (list.length === 0) {
742
- Log.error('Haori', `Element not found: ${selector} (${attrName})`);
743
- }
744
- }
745
- else {
746
- // 値が省略されている場合は自要素を対象
747
- list.push(fragment);
748
- }
749
- if (list.length > 0) {
750
- switch (attrKey) {
751
- case 'reset-before':
752
- options.resetBeforeFragments = list;
753
- break;
754
- case 'reset':
755
- options.resetFragments = list;
756
- break;
757
- case 'refetch':
758
- options.refetchFragments = list;
759
- break;
760
- case 'click':
761
- options.clickFragments = list;
762
- break;
763
- case 'copy':
764
- options.copyFragments = list;
765
- break;
766
- case 'open':
767
- options.openFragments = list;
768
- break;
769
- case 'close':
770
- options.closeFragments = list;
771
- break;
772
- }
773
- }
774
- });
775
- // copy-source(単一セレクタ)
776
- const copySourceAttrName = Procedure.attrName(event, 'copy-source');
777
- if (fragment.hasAttribute(copySourceAttrName)) {
778
- const selector = fragment.getRawAttribute(copySourceAttrName);
779
- if (selector) {
780
- const el = document.body.querySelector(selector);
781
- if (el !== null) {
782
- const frag = Fragment.get(el);
783
- if (frag) {
784
- options.copySourceFragment = frag;
785
- }
786
- else {
787
- Log.error('Haori', `Element is not managed by Haori: ${selector}` +
788
- ` (${copySourceAttrName})`);
789
- }
790
- }
791
- else {
792
- Log.error('Haori', `Element not found: ${selector} (${copySourceAttrName})`);
793
- }
794
- }
795
- else {
796
- // 値が省略されている場合は自要素を対象
797
- options.copySourceFragment = fragment;
798
- }
799
- }
800
- }
801
- // 非イベントの data / form(data-fetch-data / data-fetch-form)も取り込む
802
- if (!event) {
803
- if (fragment.hasAttribute(Procedure.attrName(null, 'data', true))) {
804
- options.dataAttrName = Procedure.attrName(null, 'data', true);
805
- }
806
- if (fragment.hasAttribute(Procedure.attrName(null, 'form', true))) {
807
- const formSelector = fragment.getRawAttribute(Procedure.attrName(null, 'form', true));
808
- if (formSelector) {
809
- const formElement = document.body.querySelector(formSelector);
810
- if (formElement !== null) {
811
- options.formFragment = Form.getFormFragment(Fragment.get(formElement));
812
- }
813
- else {
814
- Log.error('Haori', `Form element not found: ${formSelector} (` +
815
- `${Procedure.attrName(null, 'fetch-form', true)})`);
816
- }
817
- }
818
- else {
819
- // 属性はあるが値が省略された場合は自要素もしくは先祖の form を対象
820
- options.formFragment = Form.getFormFragment(fragment);
821
- }
822
- }
823
- }
824
- // fetch が指定されているのにバインド先が無い場合、デフォルトで自要素にバインド
825
- if (hasFetchAttr &&
826
- (!options.bindFragments || options.bindFragments.length === 0)) {
827
- options.bindFragments = [fragment];
828
- }
829
- return options;
830
- }
831
- /**
832
- * ElementFragment の構造的タイプガード。
833
- *
834
- * @param value チェックする値
835
- * @returns ElementFragment である場合は true、それ以外は false
836
- */
837
- static isElementFragment(value) {
838
- if (typeof value !== 'object' || value === null) {
839
- return false;
840
- }
841
- const obj = value;
842
- return (typeof obj.getTarget === 'function' &&
843
- typeof obj.getChildElementFragments === 'function');
844
- }
845
- /**
846
- * コンストラクタ。
847
- *
848
- * @param arg1 オプションもしくはフラグメント
849
- * @param arg2 イベント名
850
- * @param domEvent 起点となった DOM イベント
851
- */
852
- constructor(arg1, arg2 = null, domEvent = null) {
853
- if (Procedure.isElementFragment(arg1)) {
854
- this.options = Procedure.buildOptions(arg1, arg2);
855
- this.eventType = arg2;
856
- }
857
- else {
858
- this.options = arg1;
859
- this.eventType = null;
860
- }
861
- this.domEvent = domEvent;
862
- }
863
- /**
864
- * 非イベント data-fetch の自動再評価用シグネチャを解決します。
865
- *
866
- * @param fragment 対象フラグメント
867
- * @returns フェッチシグネチャと未解決参照の有無
868
- */
869
- static resolveAutoFetchSignature(fragment) {
870
- return new Procedure(fragment, null).resolveFetchSignature();
871
- }
872
- /**
873
- * 一連の処理を実行します。オプションが空の場合は即座にresolveされます。
874
- *
875
- * @returns 実行結果のPromise
876
- */
877
- run() {
878
- return this.runWithResult().then(() => undefined);
879
- }
880
- /**
881
- * 一連の処理を実行し、成功したかどうかを返します。
882
- *
883
- * @returns 成功した場合は true、途中停止や失敗時は false
884
- */
885
- runWithResult() {
886
- return this.execute();
887
- }
888
- /**
889
- * 一連の処理を実行します。成功結果を内部で扱うための実体です。
890
- *
891
- * @returns 実行成功時は true、停止や失敗時は false
892
- */
893
- async execute() {
894
- const executionLock = this.acquireExecutionLock();
895
- if (executionLock === false) {
896
- return false;
897
- }
898
- try {
899
- if (Object.keys(this.options).length === 0) {
900
- return false;
901
- }
902
- if (this.options.formFragment &&
903
- this.validate(this.options.formFragment) === false) {
904
- return false;
905
- }
906
- // data-{event}-run: 任意 JS を同期実行する。await を挟む前に実行することで、
907
- // クリックイベント中の event.preventDefault() が間に合う。本体が false を
908
- // 返した場合はデフォルト動作(リンク遷移・フォーム送信)を抑止する。
909
- if (this.options.runScript) {
910
- const sourceElement = this.options.targetFragment?.getTarget() ?? null;
911
- try {
912
- const result = this.options.runScript.call(sourceElement, this.domEvent);
913
- if (result === false && this.domEvent) {
914
- this.domEvent.preventDefault();
915
- }
916
- }
917
- catch (e) {
918
- Log.error('Haori', `Run script execution error: ${e}`);
919
- }
920
- }
921
- const confirmed = await this.confirm();
922
- if (!confirmed) {
923
- return false;
924
- }
925
- if (this.options.resetBeforeFragments &&
926
- this.options.resetBeforeFragments.length > 0) {
927
- await Promise.all(this.options.resetBeforeFragments.map(fragment => Form.reset(fragment)));
928
- this.captureHistorySnapshots();
929
- }
930
- const preparedRequest = this.prepareFetchRequest();
931
- const payload = preparedRequest.payload;
932
- let fetchUrl = preparedRequest.url;
933
- let fetchOptions = preparedRequest.options;
934
- if (this.options.beforeCallback) {
935
- const result = this.options.beforeCallback(fetchUrl || null, fetchOptions || null);
936
- if (result !== undefined && result !== null) {
937
- if (result === false || (typeof result === 'object' && result.stop)) {
938
- return false;
939
- }
940
- if (typeof result === 'object') {
941
- fetchUrl = ('fetchUrl' in result ? result.fetchUrl : fetchUrl);
942
- fetchOptions = ('fetchOptions' in result ? result.fetchOptions : fetchOptions);
943
- }
944
- }
945
- }
946
- const hasPayload = Object.keys(payload).length > 0;
947
- if (fetchUrl) {
948
- const finalOptions = { ...(fetchOptions || {}) };
949
- const requestedMethod = preparedRequest.requestedMethod;
950
- const method = preparedRequest.effectiveMethod;
951
- const isDemoQueryNormalization = preparedRequest.transportMode === 'query-get';
952
- const queryString = preparedRequest.queryString;
953
- if (isDemoQueryNormalization) {
954
- Log.info('Haori demo fetch normalization', {
955
- runtime: Env.runtime,
956
- requestedMethod,
957
- effectiveMethod: method,
958
- transportMode: 'query-get',
959
- url: fetchUrl,
960
- payload: hasPayload ? payload : undefined,
961
- queryString,
962
- });
963
- }
964
- // fetchstartイベントを発火
965
- if (this.options.targetFragment && fetchUrl) {
966
- const startedAt = performance.now();
967
- const fetchStartMetadata = {
968
- runtime: Env.runtime,
969
- requestedMethod,
970
- effectiveMethod: method,
971
- transportMode: isDemoQueryNormalization ? 'query-get' : 'http',
972
- ...(isDemoQueryNormalization ? { queryString } : {}),
973
- };
974
- HaoriEvent.fetchStart(this.options.targetFragment.getTarget(), fetchUrl, finalOptions, hasPayload ? payload : undefined, fetchStartMetadata);
975
- return fetch(fetchUrl, finalOptions)
976
- .then(response => {
977
- return this.handleFetchResult(response, fetchUrl || undefined, startedAt);
978
- })
979
- .catch(error => {
980
- if (fetchUrl) {
981
- HaoriEvent.fetchError(this.options.targetFragment.getTarget(), fetchUrl, error);
982
- }
983
- throw error;
984
- });
985
- }
986
- return fetch(fetchUrl, finalOptions).then(response => {
987
- return this.handleFetchResult(response, fetchUrl || undefined);
988
- });
989
- }
990
- // fetchUrlが無い場合(changeイベント等)、bindFragmentsが無ければformFragmentにバインド
991
- if ((!this.options.bindFragments ||
992
- this.options.bindFragments.length === 0) &&
993
- this.options.formFragment &&
994
- hasPayload) {
995
- // 双方向バインディング: フォーム値を自動的にバインディングデータに反映
996
- const formFragment = this.options.formFragment;
997
- const formElement = formFragment.getTarget();
998
- const skipFragments = new Set();
999
- if (executionLock &&
1000
- executionLock.appliedDisabledAttribute &&
1001
- this.options.targetFragment) {
1002
- skipFragments.add(this.options.targetFragment);
1003
- }
1004
- formElement.setAttribute(`${Env.prefix}bind`, JSON.stringify(payload));
1005
- const bindingData = formFragment.getBindingData();
1006
- Object.assign(bindingData, payload);
1007
- await Core.setBindingData(formElement, bindingData, skipFragments);
1008
- }
1009
- const merged = hasPayload ? payload : {};
1010
- const response = new Response(JSON.stringify(merged), {
1011
- headers: { 'Content-Type': 'application/json' },
1012
- });
1013
- return this.handleFetchResult(response);
1014
- }
1015
- finally {
1016
- this.releaseExecutionLock(executionLock);
1017
- }
1018
- }
1019
- /**
1020
- * click 手続きの重複実行を防ぐためのロックを取得します。
1021
- *
1022
- * @returns ロック情報。取得不要なら null、取得失敗なら false。
1023
- */
1024
- acquireExecutionLock() {
1025
- if (this.eventType !== 'click' || !this.options.targetFragment) {
1026
- return null;
1027
- }
1028
- const targetFragment = this.options.targetFragment;
1029
- const target = targetFragment.getTarget();
1030
- if (Procedure.RUNNING_CLICK_TARGETS.has(target) ||
1031
- target.matches(':disabled') ||
1032
- target.hasAttribute('disabled') ||
1033
- target.hasAttribute(PROCEDURE_CLICK_LOCK_MARKER)) {
1034
- return false;
1035
- }
1036
- // data-click-no-disabled が指定されている場合は native disabled を付与しない。
1037
- // Bootstrap など他ライブラリの click ハンドラや CSS が disabled 要素を無視する
1038
- // 問題を避けつつ、内部マーカーと RUNNING_CLICK_TARGETS で多重実行は防止する。
1039
- const skipDisabled = target.hasAttribute(`${Env.prefix}click-no-disabled`);
1040
- Procedure.RUNNING_CLICK_TARGETS.add(target);
1041
- target.setAttribute(PROCEDURE_CLICK_LOCK_MARKER, '');
1042
- if (!skipDisabled) {
1043
- target.setAttribute('disabled', '');
1044
- }
1045
- return {
1046
- target,
1047
- appliedDisabledAttribute: !skipDisabled,
1048
- };
1049
- }
1050
- /**
1051
- * 取得済みの実行ロックを解放します。
1052
- *
1053
- * @param executionLock 解放対象のロック情報。
1054
- * @returns 戻り値はありません。
1055
- */
1056
- releaseExecutionLock(executionLock) {
1057
- if (!executionLock) {
1058
- return;
1059
- }
1060
- Procedure.RUNNING_CLICK_TARGETS.delete(executionLock.target);
1061
- // マーカーは常に解除する(解除し損ねると再クリックできなくなるため)。
1062
- executionLock.target.removeAttribute(PROCEDURE_CLICK_LOCK_MARKER);
1063
- if (executionLock.appliedDisabledAttribute) {
1064
- executionLock.target.removeAttribute('disabled');
1065
- }
1066
- }
1067
- /**
1068
- * フェッチ後の処理を実行します。
1069
- */
1070
- async handleFetchResult(response, url, startedAt) {
1071
- const activeHaori = resolveProcedureHaoriApi();
1072
- // エラー応答時は以後の処理を停止し、メッセージを伝播
1073
- if (!response.ok) {
1074
- if (this.options.targetFragment && url) {
1075
- HaoriEvent.fetchError(this.options.targetFragment.getTarget(), url, new Error(`${response.status} ${response.statusText}`), response.status, startedAt);
1076
- }
1077
- await this.handleFetchError(response);
1078
- return false;
1079
- }
1080
- // fetchendイベントを発火
1081
- if (this.options.targetFragment && url && startedAt) {
1082
- HaoriEvent.fetchEnd(this.options.targetFragment.getTarget(), url, response.status, startedAt);
1083
- }
1084
- if (this.options.afterCallback) {
1085
- const result = this.options.afterCallback(response);
1086
- if (result !== undefined && result !== null) {
1087
- if (result === false || (typeof result === 'object' && result.stop)) {
1088
- return false;
1089
- }
1090
- if (typeof result === 'object' && 'response' in result) {
1091
- response = ('response' in result ? result.response : response);
1092
- }
1093
- }
1094
- }
1095
- const promises = [];
1096
- promises.push(this.bindResult(response));
1097
- promises.push(this.adjust());
1098
- promises.push(this.addRow());
1099
- promises.push(this.removeRow());
1100
- promises.push(this.movePrevRow());
1101
- promises.push(this.moveNextRow());
1102
- await Promise.all(promises);
1103
- if (this.options.resetFragments && this.options.resetFragments.length > 0) {
1104
- await Promise.all(this.options.resetFragments.map(fragment => Form.reset(fragment)));
1105
- }
1106
- await this.copy();
1107
- const deferredPromises = [];
1108
- if (this.options.refetchFragments &&
1109
- this.options.refetchFragments.length > 0) {
1110
- this.options.refetchFragments.forEach(fragment => {
1111
- deferredPromises.push(new Procedure(fragment, null).run());
1112
- });
1113
- }
1114
- if (this.options.clickFragments && this.options.clickFragments.length > 0) {
1115
- // bind 後の最新 DOM を参照させるため click 前に再評価する。
1116
- // 複数フラグメントは直列実行:各 click が前の evaluateAll 完了後に発火する。
1117
- for (const fragment of this.options.clickFragments) {
1118
- await Core.evaluateAll(fragment);
1119
- const target = fragment.getTarget();
1120
- if (typeof target.click === 'function') {
1121
- target.click();
1122
- }
1123
- else {
1124
- target.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true }));
1125
- }
1126
- }
1127
- }
1128
- if (this.options.openFragments && this.options.openFragments.length > 0) {
1129
- this.options.openFragments.forEach(fragment => {
1130
- const target = fragment.getTarget();
1131
- if (target instanceof HTMLElement) {
1132
- deferredPromises.push(activeHaori.openDialog(target));
1133
- }
1134
- else {
1135
- Log.error('Haori', 'Element is not an HTML element: ', target);
1136
- }
1137
- });
1138
- }
1139
- if (this.options.closeFragments && this.options.closeFragments.length > 0) {
1140
- this.options.closeFragments.forEach(fragment => {
1141
- const target = fragment.getTarget();
1142
- if (target instanceof HTMLElement) {
1143
- deferredPromises.push(activeHaori.closeDialog(target));
1144
- }
1145
- else {
1146
- Log.error('Haori', 'Element is not an HTML element: ', target);
1147
- }
1148
- });
1149
- }
1150
- // 仕様順序: 先に各種操作(bind/adjust/row/reset/refetch/click/open/close)を完了
1151
- await Promise.all(deferredPromises);
1152
- // その後にダイアログ/トーストを表示
1153
- if (this.options.dialogMessage) {
1154
- await activeHaori.dialog(this.options.dialogMessage);
1155
- }
1156
- if (this.options.toastMessage) {
1157
- await activeHaori.toast(this.options.toastMessage, this.options.toastLevel ?? 'info');
1158
- }
1159
- this.pushHistory();
1160
- if (this.options.scrollTarget) {
1161
- const el = document.querySelector(this.options.scrollTarget);
1162
- el?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1163
- }
1164
- if (this.options.redirectUrl) {
1165
- window.location.href = this.options.redirectUrl;
1166
- }
1167
- return true;
1168
- }
1169
- /**
1170
- * history.pushState を実行します。
1171
- *
1172
- * `historyUrl` / `historyData` / `historyFormFragment` の内容を基に URL を組み立て、
1173
- * `history.pushState()` を呼び出します。いずれも未指定の場合は何もしません。
1174
- * 不正 URL・オリジン違反・例外は `Log.error` でログ出力してスキップし、後続処理は継続します。
1175
- */
1176
- pushHistory() {
1177
- const hasHistoryUrl = this.options.historyUrl !== undefined && this.options.historyUrl !== null;
1178
- const historyDataValues = this.resolveHistoryDataValues();
1179
- const historyFormValues = this.resolveHistoryFormValues();
1180
- const hasHistoryData = historyDataValues !== undefined && historyDataValues !== null;
1181
- const hasHistoryForm = historyFormValues !== undefined && historyFormValues !== null;
1182
- if (!hasHistoryUrl && !hasHistoryData && !hasHistoryForm) {
1183
- return;
1184
- }
1185
- try {
1186
- const baseUrlString = hasHistoryUrl
1187
- ? this.options.historyUrl
1188
- : window.location.pathname;
1189
- const url = new URL(baseUrlString, window.location.href);
1190
- if (url.origin !== window.location.origin) {
1191
- const errorMessage = 'history.pushState: cross-origin URL is not allowed: ' +
1192
- url.toString();
1193
- Log.error('Haori', errorMessage);
1194
- return;
1195
- }
1196
- const appendParams = (values) => {
1197
- for (const [k, v] of Object.entries(values)) {
1198
- if (v === undefined || v === null) {
1199
- continue;
1200
- }
1201
- if (Array.isArray(v)) {
1202
- v.forEach(item => url.searchParams.append(k, String(item)));
1203
- }
1204
- else if (typeof v === 'object') {
1205
- url.searchParams.set(k, JSON.stringify(v));
1206
- }
1207
- else {
1208
- url.searchParams.set(k, String(v));
1209
- }
1210
- }
1211
- };
1212
- if (hasHistoryData) {
1213
- appendParams(historyDataValues);
1214
- }
1215
- if (hasHistoryForm) {
1216
- appendParams(historyFormValues);
1217
- }
1218
- history.pushState({ [PROCEDURE_HISTORY_STATE_KEY]: true }, '', url.toString());
1219
- }
1220
- catch (e) {
1221
- Log.error('Haori', `history.pushState failed: ${e}`);
1222
- }
1223
- }
1224
- /**
1225
- * フェッチエラー応答のメッセージを適切な要素へ伝播します。
1226
- */
1227
- async handleFetchError(response) {
1228
- // ベースとなるフォーム/フラグメントを決定
1229
- let baseFragment = null;
1230
- if (this.options.formFragment) {
1231
- baseFragment = this.options.formFragment;
1232
- }
1233
- else if (this.options.targetFragment) {
1234
- baseFragment =
1235
- Form.getFormFragment(this.options.targetFragment) ||
1236
- this.options.targetFragment;
1237
- }
1238
- const addGeneralMessage = async (message) => {
1239
- const targetEl = baseFragment ? baseFragment.getTarget() : document.body;
1240
- await resolveProcedureHaoriApi().addErrorMessage(targetEl, message);
1241
- };
1242
- const scrollToFirstError = () => {
1243
- if (!this.options.scrollOnError) {
1244
- return;
1245
- }
1246
- const root = baseFragment ? baseFragment.getTarget() : document.body;
1247
- // addErrorMessage はフォーム以外の target に対して parentElement へエラーを付与するため、
1248
- // root 自身・parentElement・root 配下の順で探索する
1249
- const errorTarget = root.getAttribute('data-message-level') === 'error'
1250
- ? root
1251
- : root.parentElement?.getAttribute('data-message-level') === 'error'
1252
- ? root.parentElement
1253
- : root.querySelector('[data-message-level="error"]');
1254
- errorTarget?.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1255
- };
1256
- // コンテンツタイプに応じて解析
1257
- const contentType = response.headers.get('Content-Type') || '';
1258
- if (contentType.includes('application/json')) {
1259
- try {
1260
- const data = await response.json();
1261
- // 代表的な形式に対応
1262
- const entries = [];
1263
- if (data && typeof data === 'object') {
1264
- if (typeof data.message === 'string') {
1265
- entries.push({ message: data.message });
1266
- }
1267
- if (Array.isArray(data.messages)) {
1268
- for (const m of data.messages) {
1269
- if (typeof m === 'string') {
1270
- entries.push({ message: m });
1271
- }
1272
- }
1273
- }
1274
- if (data.errors && typeof data.errors === 'object') {
1275
- for (const [k, v] of Object.entries(data.errors)) {
1276
- if (Array.isArray(v)) {
1277
- entries.push({ key: k, message: v.join('\n') });
1278
- }
1279
- else if (typeof v === 'string') {
1280
- entries.push({ key: k, message: v });
1281
- }
1282
- else if (v != null) {
1283
- entries.push({ key: k, message: String(v) });
1284
- }
1285
- }
1286
- }
1287
- // キー: 値(文字列/配列)形式にフォールバック
1288
- if (entries.length === 0) {
1289
- for (const [k, v] of Object.entries(data)) {
1290
- if (k === 'message' || k === 'messages' || k === 'errors') {
1291
- continue;
1292
- }
1293
- if (Array.isArray(v)) {
1294
- entries.push({ key: k, message: v.join('\n') });
1295
- }
1296
- else if (typeof v === 'string') {
1297
- entries.push({ key: k, message: v });
1298
- }
1299
- }
1300
- }
1301
- }
1302
- if (entries.length === 0) {
1303
- // 汎用メッセージ
1304
- await addGeneralMessage(`${response.status} ${response.statusText}`);
1305
- scrollToFirstError();
1306
- return false;
1307
- }
1308
- // メッセージを反映
1309
- for (const e of entries) {
1310
- if (e.key && baseFragment) {
1311
- await Form.addErrorMessage(baseFragment, e.key, e.message);
1312
- }
1313
- else {
1314
- await addGeneralMessage(e.message);
1315
- }
1316
- }
1317
- scrollToFirstError();
1318
- return false;
1319
- }
1320
- catch {
1321
- // JSON 解析失敗時はテキストにフォールバック
1322
- }
1323
- }
1324
- // テキストとして処理
1325
- try {
1326
- const text = await response.text();
1327
- if (text && text.trim().length > 0) {
1328
- await addGeneralMessage(text.trim());
1329
- }
1330
- else {
1331
- await addGeneralMessage(`${response.status} ${response.statusText}`);
1332
- }
1333
- }
1334
- catch {
1335
- await addGeneralMessage(`${response.status} ${response.statusText}`);
1336
- }
1337
- scrollToFirstError();
1338
- return false;
1339
- }
1340
- /**
1341
- * 対象のフラグメント以下の入力要素に対してバリデーションを実行します。
1342
- * バリデーションエラーがある場合は、最初のエラー要素にフォーカスを移動します。
1343
- *
1344
- * @param fragment 対象のフラグメント
1345
- * @returns バリデーション結果(true: 成功, false: 失敗)
1346
- */
1347
- validate(fragment) {
1348
- if (this.options.valid !== true) {
1349
- return true;
1350
- }
1351
- const firstInvalid = this.findFirstInvalid(fragment);
1352
- if (firstInvalid === null) {
1353
- return true;
1354
- }
1355
- // 検出フェーズ(findFirstInvalid)は checkValidity で副作用なく走査済み。
1356
- // reportValidity と focus は確定した 1 要素にだけ呼び出す。
1357
- firstInvalid.reportValidity();
1358
- firstInvalid.focus();
1359
- if (this.options.scrollOnError) {
1360
- firstInvalid.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
1361
- }
1362
- return false;
1363
- }
1364
- /**
1365
- * 対象フラグメント以下で DOM 順の最上部にある invalid 要素を返します。
1366
- * 副作用のない checkValidity のみを使用し、検出のみを行います。
1367
- *
1368
- * @param fragment 対象のフラグメント
1369
- * @returns 最初の invalid 要素、なければ null
1370
- */
1371
- findFirstInvalid(fragment) {
1372
- // 子要素を逆順に処理することで、DOM 順の先頭要素が最後に found を上書きし、
1373
- // 最終的に最上部の invalid 要素が返る
1374
- let found = null;
1375
- for (const child of fragment.getChildElementFragments().reverse()) {
1376
- const result = this.findFirstInvalid(child);
1377
- if (result !== null) {
1378
- found = result;
1379
- }
1380
- }
1381
- // 自身は子より DOM 上位にあるため、invalid なら子の結果を上書きする
1382
- if (!this.checkOne(fragment)) {
1383
- return fragment.getTarget();
1384
- }
1385
- return found;
1386
- }
1387
- /**
1388
- * 対象のフラグメントに対して、副作用なく有効性を検査します。
1389
- * reportValidity は使わず checkValidity のみ呼び出します。
1390
- *
1391
- * @param fragment 対象のフラグメント
1392
- * @returns 有効なら true、無効なら false
1393
- */
1394
- checkOne(fragment) {
1395
- const target = fragment.getTarget();
1396
- if (target instanceof HTMLInputElement ||
1397
- target instanceof HTMLSelectElement ||
1398
- target instanceof HTMLTextAreaElement) {
1399
- return target.checkValidity();
1400
- }
1401
- return true;
1402
- }
1403
- /**
1404
- * 確認メッセージを表示し、ユーザーの確認を求めます。
1405
- * メッセージが設定されていない場合は、即座に成功とみなします。
1406
- *
1407
- * @returns ユーザーの確認結果を含むPromise(true: 確認, false: キャンセル)
1408
- */
1409
- confirm() {
1410
- const message = this.options.confirmMessage;
1411
- if (message === null || message === undefined) {
1412
- return Promise.resolve(true);
1413
- }
1414
- return resolveProcedureHaoriApi().confirm(message);
1415
- }
1416
- /**
1417
- * 結果データを対象のフラグメントにバインドします。
1418
- *
1419
- * @param response フェッチのレスポンスオブジェクト
1420
- */
1421
- bindResult(response) {
1422
- if (!this.options.bindFragments ||
1423
- this.options.bindFragments.length === 0) {
1424
- return Promise.resolve();
1425
- }
1426
- const promise = response.headers
1427
- .get('Content-Type')
1428
- ?.includes('application/json')
1429
- ? response.json()
1430
- : response.text();
1431
- return promise.then(data => {
1432
- if (this.options.bindParams) {
1433
- const newData = {};
1434
- this.options.bindParams.forEach(param => {
1435
- if (data && typeof data === 'object' && param in data) {
1436
- newData[param] = data[param];
1437
- }
1438
- });
1439
- data = newData;
1440
- }
1441
- const promises = [];
1442
- if (this.options.bindArg) {
1443
- this.options.bindFragments.forEach(fragment => {
1444
- // バインド先の「自身の」最新 binding(getRawBindingData)を基底にして
1445
- // bindArg キーだけを更新する。getBindingData()(継承込み)を基底にすると
1446
- // 継承キーが own の data-bind に混入してしまうため、own のみを対象にする。
1447
- // 読み取り〜書き込み(fragment.setBindingData)は await を挟まず同期で行われ、
1448
- // 並行・リアクティブな複数 bind-arg が重なっても呼び出し単位で原子的に
1449
- // 反映される(各呼び出しは直前の更新後の最新 own を読む)。
1450
- const bindingData = { ...(fragment.getRawBindingData() ?? {}) };
1451
- const bindArg = this.options.bindArg;
1452
- if (data && typeof data === 'object' && !Array.isArray(data)) {
1453
- const currentValue = bindingData[bindArg];
1454
- const currentObject = currentValue &&
1455
- typeof currentValue === 'object' &&
1456
- !Array.isArray(currentValue)
1457
- ? currentValue
1458
- : {};
1459
- bindingData[bindArg] = this.mergeAppendBindingData(fragment, data, currentObject);
1460
- }
1461
- else {
1462
- bindingData[bindArg] = data;
1463
- }
1464
- promises.push(Core.setBindingData(fragment.getTarget(), bindingData));
1465
- });
1466
- }
1467
- else if (typeof data === 'string') {
1468
- Log.error('Haori', 'string data cannot be bound without a bindArg.');
1469
- return Promise.reject(new Error('string data cannot be bound without a bindArg.'));
1470
- }
1471
- else {
1472
- this.options.bindFragments.forEach(fragment => {
1473
- const resolvedData = this.mergeAppendBindingData(fragment, data);
1474
- // bind-merge 指定時は全置換せず、対象要素自身の既存 binding data へ
1475
- // 浅くマージして未指定キー(例: 一覧の items)を保持する。
1476
- const finalData = this.options.bindMerge
1477
- ? { ...(fragment.getRawBindingData() ?? {}), ...resolvedData }
1478
- : resolvedData;
1479
- promises.push(Core.setBindingData(fragment.getTarget(), finalData));
1480
- });
1481
- }
1482
- return Promise.all(promises).then(() => {
1483
- // バインドと対象配下の再評価(data-if / data-each 等)の完了後に
1484
- // bindcomplete を発火し、外部スクリプトが同期処理を行えるようにする。
1485
- const bindArg = this.options.bindArg ?? null;
1486
- this.options.bindFragments.forEach(fragment => {
1487
- HaoriEvent.bindComplete(fragment.getTarget(), bindArg);
1488
- });
1489
- return undefined;
1490
- });
1491
- });
1492
- }
1493
- /**
1494
- * bind-append 指定があるキーについて、既存配列と結合したデータを返します。
1495
- */
1496
- mergeAppendBindingData(fragment, data, currentData = fragment.getBindingData()) {
1497
- if (!this.options.bindAppendParams ||
1498
- this.options.bindAppendParams.length === 0) {
1499
- return data;
1500
- }
1501
- const merged = { ...data };
1502
- const current = currentData;
1503
- for (const key of this.options.bindAppendParams) {
1504
- const incoming = merged[key];
1505
- const existing = current[key];
1506
- if (Array.isArray(existing) && Array.isArray(incoming)) {
1507
- merged[key] = existing.concat(incoming);
1508
- }
1509
- }
1510
- return merged;
1511
- }
1512
- /**
1513
- * 指定されたフラグメントへバインディングデータをコピーします。
1514
- */
1515
- copy() {
1516
- if (!this.options.copyFragments ||
1517
- this.options.copyFragments.length === 0) {
1518
- return Promise.resolve();
1519
- }
1520
- const sourceData = this.resolveCopySourceData();
1521
- const copyData = this.pickCopyData(sourceData);
1522
- const promises = this.options.copyFragments.map(fragment => {
1523
- const bindingData = {
1524
- ...fragment.getBindingData(),
1525
- ...copyData,
1526
- };
1527
- return Core.setBindingData(fragment.getTarget(), bindingData);
1528
- });
1529
- return Promise.all(promises).then(() => undefined);
1530
- }
1531
- /**
1532
- * copy のコピー元データを取得します。
1533
- */
1534
- resolveCopySourceData() {
1535
- if (this.options.copySourceFragment) {
1536
- const sourceTarget = this.options.copySourceFragment.getTarget();
1537
- if (sourceTarget.tagName === 'FORM') {
1538
- return Form.getValues(this.options.copySourceFragment);
1539
- }
1540
- return { ...this.options.copySourceFragment.getBindingData() };
1541
- }
1542
- if (this.options.formFragment) {
1543
- return Form.getValues(this.options.formFragment);
1544
- }
1545
- if (this.options.targetFragment) {
1546
- return { ...this.options.targetFragment.getBindingData() };
1547
- }
1548
- return {};
1549
- }
1550
- /**
1551
- * data 属性とフォーム値を統合した送信データを作成します。
1552
- *
1553
- * @returns 送信データ。
1554
- */
1555
- buildPayload() {
1556
- return this.buildPayloadResolution().payload;
1557
- }
1558
- /**
1559
- * data 属性とフォーム値を統合した送信データを作成し、未解決参照の有無を返します。
1560
- *
1561
- * @returns 送信データと未解決参照の有無。
1562
- */
1563
- buildPayloadResolution() {
1564
- const payload = {};
1565
- let hasUnresolvedReference = false;
1566
- if (this.options.formFragment) {
1567
- Object.assign(payload, Form.getValues(this.options.formFragment));
1568
- }
1569
- if (this.options.data && typeof this.options.data === 'object') {
1570
- Object.assign(payload, this.options.data);
1571
- }
1572
- if (this.options.targetFragment && this.options.dataAttrName) {
1573
- const resolvedData = Procedure.resolveDataAttributeDetailed(this.options.targetFragment, this.options.dataAttrName);
1574
- hasUnresolvedReference =
1575
- hasUnresolvedReference || resolvedData.hasUnresolvedReference;
1576
- if (resolvedData.value) {
1577
- Object.assign(payload, resolvedData.value);
1578
- }
1579
- }
1580
- return { payload, hasUnresolvedReference };
1581
- }
1582
- /**
1583
- * 現在の data-fetch 実行内容を比較用シグネチャへ正規化します。
1584
- *
1585
- * @returns フェッチシグネチャと未解決参照の有無。
1586
- */
1587
- resolveFetchSignature() {
1588
- const preparedRequest = this.prepareFetchRequest();
1589
- return {
1590
- signature: preparedRequest.signature,
1591
- hasUnresolvedReference: preparedRequest.hasUnresolvedReference,
1592
- };
1593
- }
1594
- /**
1595
- * 現在のオプションから送信前の fetch リクエストを組み立てます。
1596
- *
1597
- * @returns リクエスト情報。
1598
- */
1599
- prepareFetchRequest() {
1600
- const payloadResolution = this.buildPayloadResolution();
1601
- const payload = payloadResolution.payload;
1602
- const hasUnresolvedReference = Boolean(this.options.fetchHasUnresolvedReference) ||
1603
- payloadResolution.hasUnresolvedReference;
1604
- if (!this.options.fetchUrl || hasUnresolvedReference) {
1605
- return {
1606
- url: null,
1607
- options: null,
1608
- payload,
1609
- hasUnresolvedReference,
1610
- requestedMethod: 'GET',
1611
- effectiveMethod: 'GET',
1612
- transportMode: 'http',
1613
- signature: null,
1614
- };
1615
- }
1616
- let fetchUrl = this.options.fetchUrl;
1617
- const finalOptions = { ...(this.options.fetchOptions || {}) };
1618
- const headers = new Headers(finalOptions.headers || undefined);
1619
- const requestedMethod = (finalOptions.method || 'GET').toUpperCase();
1620
- const isDemoQueryNormalization = Env.runtime === 'demo' && !isQueryTransportMethod(requestedMethod);
1621
- const method = isDemoQueryNormalization ? 'GET' : requestedMethod;
1622
- finalOptions.method = method;
1623
- if (method === 'GET' || method === 'HEAD' || method === 'OPTIONS') {
1624
- if (Object.keys(payload).length > 0) {
1625
- fetchUrl = appendPayloadToUrl(fetchUrl, payload);
1626
- }
1627
- }
1628
- else if (Object.keys(payload).length > 0) {
1629
- const contentType = headers.get('Content-Type') || '';
1630
- if (/multipart\/form-data/i.test(contentType)) {
1631
- headers.delete('Content-Type');
1632
- const formData = new FormData();
1633
- for (const [k, v] of Object.entries(payload)) {
1634
- if (v === undefined || v === null) {
1635
- formData.append(k, '');
1636
- }
1637
- else if (v instanceof Blob) {
1638
- formData.append(k, v);
1639
- }
1640
- else if (Array.isArray(v)) {
1641
- v.forEach(item => formData.append(k, String(item)));
1642
- }
1643
- else if (typeof v === 'object') {
1644
- formData.append(k, JSON.stringify(v));
1645
- }
1646
- else {
1647
- formData.append(k, String(v));
1648
- }
1649
- }
1650
- finalOptions.body = formData;
1651
- }
1652
- else if (/application\/x-www-form-urlencoded/i.test(contentType)) {
1653
- const params = new URLSearchParams();
1654
- for (const [k, v] of Object.entries(payload)) {
1655
- if (v === undefined) {
1656
- continue;
1657
- }
1658
- if (v === null) {
1659
- params.append(k, '');
1660
- }
1661
- else if (Array.isArray(v)) {
1662
- v.forEach(item => params.append(k, String(item)));
1663
- }
1664
- else if (typeof v === 'object') {
1665
- params.append(k, JSON.stringify(v));
1666
- }
1667
- else {
1668
- params.append(k, String(v));
1669
- }
1670
- }
1671
- finalOptions.body = params;
1672
- }
1673
- else {
1674
- headers.set('Content-Type', 'application/json');
1675
- finalOptions.body = JSON.stringify(payload);
1676
- }
1677
- }
1678
- finalOptions.headers = headers;
1679
- let queryString;
1680
- if (isDemoQueryNormalization) {
1681
- queryString = new URL(fetchUrl, window.location.href).search || undefined;
1682
- headers.delete('Content-Type');
1683
- }
1684
- return {
1685
- url: fetchUrl,
1686
- options: finalOptions,
1687
- payload,
1688
- hasUnresolvedReference: false,
1689
- requestedMethod,
1690
- effectiveMethod: method,
1691
- queryString,
1692
- transportMode: isDemoQueryNormalization ? 'query-get' : 'http',
1693
- signature: buildFetchSignature(fetchUrl, finalOptions),
1694
- };
1695
- }
1696
- /**
1697
- * reset-before 後の history 用スナップショットを保存します。
1698
- */
1699
- captureHistorySnapshots() {
1700
- if (this.options.targetFragment && this.options.historyDataAttrName) {
1701
- this.historyDataSnapshot = Procedure.resolveDataAttribute(this.options.targetFragment, this.options.historyDataAttrName);
1702
- }
1703
- else {
1704
- this.historyDataSnapshot = undefined;
1705
- }
1706
- this.historyFormSnapshot = this.options.historyFormFragment
1707
- ? Form.getValues(this.options.historyFormFragment)
1708
- : undefined;
1709
- }
1710
- /**
1711
- * history-data の評価値を取得します。
1712
- *
1713
- * @returns history-data の評価値。
1714
- */
1715
- resolveHistoryDataValues() {
1716
- if (this.historyDataSnapshot !== undefined) {
1717
- return this.historyDataSnapshot;
1718
- }
1719
- if (this.options.targetFragment && this.options.historyDataAttrName) {
1720
- return Procedure.resolveDataAttribute(this.options.targetFragment, this.options.historyDataAttrName);
1721
- }
1722
- return this.options.historyData;
1723
- }
1724
- /**
1725
- * history-form の評価値を取得します。
1726
- *
1727
- * @returns history-form の評価値。
1728
- */
1729
- resolveHistoryFormValues() {
1730
- if (this.historyFormSnapshot !== undefined) {
1731
- return this.historyFormSnapshot;
1732
- }
1733
- if (this.options.historyFormFragment) {
1734
- return Form.getValues(this.options.historyFormFragment);
1735
- }
1736
- return undefined;
1737
- }
1738
- /**
1739
- * copy-params が指定されている場合は include / exclude を考慮して抽出します。
1740
- */
1741
- pickCopyData(sourceData) {
1742
- if (!this.options.copyParams || this.options.copyParams.length === 0) {
1743
- return sourceData;
1744
- }
1745
- const includeParams = new Set();
1746
- const excludeParams = new Set();
1747
- this.options.copyParams.forEach(param => {
1748
- const trimmedParam = param.trim();
1749
- if (!trimmedParam) {
1750
- return;
1751
- }
1752
- if (trimmedParam.startsWith('!')) {
1753
- const excludedParam = trimmedParam.slice(1).trim();
1754
- if (excludedParam) {
1755
- excludeParams.add(excludedParam);
1756
- }
1757
- return;
1758
- }
1759
- includeParams.add(trimmedParam);
1760
- });
1761
- const filtered = {};
1762
- const sourceKeys = includeParams.size > 0
1763
- ? Array.from(includeParams)
1764
- : Object.keys(sourceData);
1765
- sourceKeys.forEach(param => {
1766
- if (!(param in sourceData)) {
1767
- return;
1768
- }
1769
- if (excludeParams.has(param)) {
1770
- return;
1771
- }
1772
- filtered[param] = sourceData[param];
1773
- });
1774
- return filtered;
1775
- }
1776
- /**
1777
- * 値の増減を行います。
1778
- */
1779
- adjust() {
1780
- if (!this.options.adjustFragments ||
1781
- this.options.adjustFragments.length === 0) {
1782
- return Promise.resolve();
1783
- }
1784
- const adjustValue = this.options.adjustValue ?? 0;
1785
- const promises = [];
1786
- for (const fragment of this.options.adjustFragments) {
1787
- let valueString = fragment.getValue();
1788
- if (valueString === null ||
1789
- valueString === undefined ||
1790
- valueString === '') {
1791
- valueString = '0';
1792
- }
1793
- let value = Number(valueString);
1794
- if (isNaN(value)) {
1795
- value = 0;
1796
- }
1797
- value += adjustValue;
1798
- promises.push(fragment.setValue(String(value)));
1799
- }
1800
- return Promise.all(promises).then(() => undefined);
1801
- }
1802
- /**
1803
- * 行フラグメントを取得します。
1804
- *
1805
- * @returns 行フラグメントまたはnull
1806
- */
1807
- getRowFragment() {
1808
- if (!this.options.targetFragment) {
1809
- Log.error('Haori', 'Target fragment is not specified for row operation.');
1810
- return null;
1811
- }
1812
- const rowFragment = this.options.targetFragment.closestByAttribute(`${Env.prefix}row`);
1813
- if (!rowFragment) {
1814
- Log.error('Haori', 'Row fragment not found.');
1815
- return null;
1816
- }
1817
- return rowFragment;
1818
- }
1819
- /**
1820
- * 行を追加します。
1821
- *
1822
- * @returns 処理結果のPromise
1823
- */
1824
- addRow() {
1825
- if (this.options.rowAdd !== true) {
1826
- return Promise.resolve();
1827
- }
1828
- const rowFragment = this.getRowFragment();
1829
- if (!rowFragment) {
1830
- return Promise.reject(new Error('Row fragment not found.'));
1831
- }
1832
- const promises = [];
1833
- const newFragment = rowFragment.clone();
1834
- promises.push(rowFragment.getParent().insertAfter(newFragment, rowFragment));
1835
- promises.push(Core.evaluateAll(newFragment));
1836
- // 追加された行のフォーム要素をリセット
1837
- promises.push(Form.reset(newFragment));
1838
- return Promise.all(promises).then(() => undefined);
1839
- }
1840
- /**
1841
- * 行を削除します。
1842
- *
1843
- * @returns 処理結果のPromise
1844
- */
1845
- removeRow() {
1846
- if (this.options.rowRemove !== true) {
1847
- return Promise.resolve();
1848
- }
1849
- const rowFragment = this.getRowFragment();
1850
- if (!rowFragment) {
1851
- return Promise.reject(new Error('Row fragment not found.'));
1852
- }
1853
- // 1行だった場合は削除しない
1854
- const parent = rowFragment.getParent();
1855
- if (parent) {
1856
- const siblings = parent.getChildElementFragments().filter(child => {
1857
- // data-each-before と data-each-after を除外
1858
- return (!child.hasAttribute(`${Env.prefix}each-before`) &&
1859
- !child.hasAttribute(`${Env.prefix}each-after`));
1860
- });
1861
- if (siblings.length <= 1) {
1862
- return Promise.resolve();
1863
- }
1864
- }
1865
- return rowFragment.remove();
1866
- }
1867
- /**
1868
- * 前の行へ移動します。
1869
- *
1870
- * @returns 処理結果のPromise
1871
- */
1872
- movePrevRow() {
1873
- if (this.options.rowMovePrev !== true) {
1874
- return Promise.resolve();
1875
- }
1876
- const rowFragment = this.getRowFragment();
1877
- if (!rowFragment) {
1878
- return Promise.reject(new Error('Row fragment not found.'));
1879
- }
1880
- const prevFragment = rowFragment.getPrevious();
1881
- if (!prevFragment) {
1882
- return Promise.resolve();
1883
- }
1884
- const parent = rowFragment.getParent();
1885
- if (!parent) {
1886
- return Promise.resolve();
1887
- }
1888
- return parent.insertBefore(rowFragment, prevFragment);
1889
- }
1890
- /**
1891
- * 次の行へ移動します。
1892
- *
1893
- * @returns 処理結果のPromise
1894
- */
1895
- moveNextRow() {
1896
- if (this.options.rowMoveNext !== true) {
1897
- return Promise.resolve();
1898
- }
1899
- const rowFragment = this.getRowFragment();
1900
- if (!rowFragment) {
1901
- return Promise.reject(new Error('Row fragment not found.'));
1902
- }
1903
- const nextFragment = rowFragment.getNext();
1904
- if (!nextFragment) {
1905
- return Promise.resolve();
1906
- }
1907
- const parent = rowFragment.getParent();
1908
- if (!parent) {
1909
- return Promise.resolve();
1910
- }
1911
- return parent.insertAfter(rowFragment, nextFragment);
1912
- }
1913
- }
1914
- /** data 属性内のテンプレート式検出用正規表現 */
1915
- Procedure.DATA_PLACEHOLDER_REGEX = /\{\{\{([\s\S]+?)\}\}\}|\{\{([\s\S]+?)\}\}/g;
1916
- /** 属性全体が単一テンプレート式かを判定する正規表現 */
1917
- Procedure.SINGLE_PLACEHOLDER_REGEX = /^(\{\{\{[\s\S]+?\}\}\}|\{\{[\s\S]+?\}\})$/;
1918
- /** click 手続きの再入を防ぐ対象要素の集合 */
1919
- Procedure.RUNNING_CLICK_TARGETS = new WeakSet();
1920
- export default Procedure;
1921
- //# sourceMappingURL=procedure.js.map