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
package/dist/src/core.js DELETED
@@ -1,1817 +0,0 @@
1
- /**
2
- * @fileoverview Core機能
3
- *
4
- * Fragmentの管理、属性変化の監視、条件分岐・繰り返し処理など、
5
- * アプリケーションの中心的な機能を提供します。
6
- */
7
- import Env from './env';
8
- import Dev from './dev';
9
- import Expression from './expression';
10
- import Form from './form';
11
- import Fragment, { ElementFragment, TextFragment } from './fragment';
12
- import Log from './log';
13
- import Procedure from './procedure';
14
- import Url from './url';
15
- import { Import } from './import';
16
- import Queue from './queue';
17
- import HaoriEvent from './event';
18
- /**
19
- * アプリケーションの中心的な制御を行うクラスです。
20
- * Fragment の初期化、属性変化の処理、条件分岐・繰り返し処理を管理します。
21
- */
22
- class Core {
23
- /**
24
- * 遅延属性かどうか(完全名で判定)を判定します。
25
- *
26
- * @param name 属性名
27
- * @returns 遅延属性かどうか
28
- */
29
- static isDeferredAttributeName(name) {
30
- return Core.DEFERRED_ATTRIBUTE_SUFFIXES.some(suffix => name === `${Env.prefix}${suffix}`);
31
- }
32
- /**
33
- * evaluateAll で再評価対象から除外する特殊属性かどうかを判定します。
34
- *
35
- * @param name 属性名
36
- * @returns 除外対象かどうか
37
- */
38
- static isEvaluateAllExcludedAttributeName(name) {
39
- return Core.EVALUATE_ALL_EXCLUDED_ATTRIBUTE_SUFFIXES.some(suffix => name === `${Env.prefix}${suffix}`);
40
- }
41
- /**
42
- * evaluateAll で通常属性を再評価すべきかを判定します。
43
- *
44
- * @param name 属性名
45
- * @param value 属性の生値
46
- * @returns 再評価する場合は true
47
- */
48
- static shouldReevaluateAttribute(name, value) {
49
- return (value !== null &&
50
- !Core.isEvaluateAllExcludedAttributeName(name) &&
51
- Core.ATTRIBUTE_PLACEHOLDER_REGEX.test(value));
52
- }
53
- /**
54
- * data-attr-* 形式の属性名から実際に更新する属性名を取得します。
55
- *
56
- * @param name 属性名
57
- * @returns 実際の属性名。data-attr-* でない場合は null
58
- */
59
- static getAliasedAttributeName(name) {
60
- const aliasPrefix = `${Env.prefix}${Core.ATTRIBUTE_ALIAS_SUFFIX}`;
61
- if (!name.startsWith(aliasPrefix) || name.length <= aliasPrefix.length) {
62
- return null;
63
- }
64
- return name.slice(aliasPrefix.length);
65
- }
66
- /**
67
- * 実属性の変更が data-attr-* の内部反映かどうかを判定します。
68
- *
69
- * @param element 対象要素
70
- * @param name 変更された属性名
71
- * @returns data-attr-* の内部反映なら true
72
- */
73
- static isAliasedAttributeReflection(element, name) {
74
- const fragment = Fragment.get(element);
75
- if (!(fragment instanceof ElementFragment)) {
76
- return false;
77
- }
78
- return fragment.hasAttribute(`${Env.prefix}${Core.ATTRIBUTE_ALIAS_SUFFIX}${name}`);
79
- }
80
- /**
81
- * プレースホルダを含む通常属性を再評価します。
82
- * 内部状態の更新は同期的に行い、DOM 反映は fragment 側の非同期更新に委ねます。
83
- *
84
- * @param fragment 対象フラグメント
85
- * @returns 再評価完了の Promise
86
- */
87
- static reevaluateInterpolatedAttributes(fragment) {
88
- let chain = Promise.resolve();
89
- for (const name of fragment.getAttributeNames()) {
90
- const rawValue = fragment.getRawAttribute(name);
91
- if (!Core.shouldReevaluateAttribute(name, rawValue)) {
92
- continue;
93
- }
94
- chain = chain.then(() => Core.setAttribute(fragment.getTarget(), name, rawValue));
95
- }
96
- return chain.then(() => undefined);
97
- }
98
- /**
99
- * 指定フラグメントの直下の子孫評価を再実行します。
100
- *
101
- * @param fragment 対象フラグメント
102
- * @returns 再評価完了の Promise
103
- */
104
- static reevaluateChildren(fragment) {
105
- const promises = [];
106
- fragment.getChildren().forEach(child => {
107
- if (child instanceof ElementFragment) {
108
- promises.push(Core.evaluateAll(child));
109
- }
110
- else if (child instanceof TextFragment) {
111
- promises.push(Core.evaluateText(child));
112
- }
113
- });
114
- return Promise.all(promises).then(() => undefined);
115
- }
116
- /**
117
- * data-fetch の再評価状態を取得します。
118
- *
119
- * @param element 対象要素
120
- * @returns 再評価状態
121
- */
122
- static getReactiveFetchState(element) {
123
- const existing = Core.REACTIVE_FETCH_STATES.get(element);
124
- if (existing) {
125
- return existing;
126
- }
127
- const state = {
128
- lastSignature: null,
129
- running: false,
130
- rerunRequested: false,
131
- };
132
- Core.REACTIVE_FETCH_STATES.set(element, state);
133
- return state;
134
- }
135
- /**
136
- * data-import の再評価状態を取得します。
137
- *
138
- * @param element 対象要素
139
- * @returns 再評価状態
140
- */
141
- static getReactiveImportState(element) {
142
- const existing = Core.REACTIVE_IMPORT_STATES.get(element);
143
- if (existing) {
144
- return existing;
145
- }
146
- const state = {
147
- lastUrl: null,
148
- running: false,
149
- rerunRequested: false,
150
- };
151
- Core.REACTIVE_IMPORT_STATES.set(element, state);
152
- return state;
153
- }
154
- /**
155
- * bind 更新時に data-fetch / data-import を専用ルートで再評価します。
156
- *
157
- * @param fragment 対象フラグメント
158
- * @param skipFragments 再評価をスキップするフラグメント集合
159
- * @returns 再評価完了の Promise
160
- */
161
- static reevaluateReactiveSpecialAttributes(fragment, skipFragments = new Set()) {
162
- if (skipFragments.has(fragment)) {
163
- return Promise.resolve();
164
- }
165
- const promises = [];
166
- if (fragment.hasAttribute(`${Env.prefix}fetch`)) {
167
- promises.push(Core.executeManagedFetch(fragment));
168
- }
169
- if (fragment.hasAttribute(`${Env.prefix}import`)) {
170
- promises.push(Core.executeManagedImport(fragment));
171
- }
172
- fragment.getChildren().forEach(child => {
173
- if (child instanceof ElementFragment) {
174
- promises.push(Core.reevaluateReactiveSpecialAttributes(child, skipFragments));
175
- }
176
- });
177
- return Promise.all(promises).then(() => undefined);
178
- }
179
- /**
180
- * data-fetch をシグネチャ比較付きで実行します。
181
- *
182
- * @param fragment 対象フラグメント
183
- * @returns 実行完了の Promise
184
- */
185
- static executeManagedFetch(fragment) {
186
- const target = fragment.getTarget();
187
- const state = Core.getReactiveFetchState(target);
188
- const resolved = Procedure.resolveAutoFetchSignature(fragment);
189
- if (state.running) {
190
- if (resolved.hasUnresolvedReference ||
191
- resolved.signature !== state.lastSignature) {
192
- state.rerunRequested = true;
193
- }
194
- return Promise.resolve();
195
- }
196
- if (resolved.hasUnresolvedReference || resolved.signature === null) {
197
- state.lastSignature = null;
198
- return Promise.resolve();
199
- }
200
- if (state.lastSignature === resolved.signature) {
201
- return Promise.resolve();
202
- }
203
- state.lastSignature = resolved.signature;
204
- state.running = true;
205
- return new Procedure(fragment, null)
206
- .runWithResult()
207
- .then(() => undefined)
208
- .finally(() => {
209
- state.running = false;
210
- if (state.rerunRequested) {
211
- state.rerunRequested = false;
212
- return Core.executeManagedFetch(fragment);
213
- }
214
- return undefined;
215
- });
216
- }
217
- /**
218
- * data-import を URL 比較付きで実行します。
219
- *
220
- * @param fragment 対象フラグメント
221
- * @returns 実行完了の Promise
222
- */
223
- static executeManagedImport(fragment) {
224
- const target = fragment.getTarget();
225
- const state = Core.getReactiveImportState(target);
226
- const importEvaluation = fragment.getAttributeEvaluation(`${Env.prefix}import`);
227
- const resolvedUrl = importEvaluation &&
228
- !importEvaluation.hasUnresolvedReference &&
229
- typeof importEvaluation.value === 'string' &&
230
- importEvaluation.value !== ''
231
- ? importEvaluation.value
232
- : null;
233
- if (state.running) {
234
- if (resolvedUrl !== state.lastUrl) {
235
- state.rerunRequested = true;
236
- }
237
- return Promise.resolve();
238
- }
239
- if (resolvedUrl === null) {
240
- state.lastUrl = null;
241
- return Promise.resolve();
242
- }
243
- if (state.lastUrl === resolvedUrl) {
244
- return Promise.resolve();
245
- }
246
- state.lastUrl = resolvedUrl;
247
- state.running = true;
248
- const startedAt = performance.now();
249
- target.setAttribute(`${Env.prefix}importing`, '');
250
- HaoriEvent.importStart(target, resolvedUrl);
251
- return Import.load(resolvedUrl)
252
- .then(html => {
253
- const bytes = new TextEncoder().encode(html).length;
254
- return Queue.enqueue(() => {
255
- target.innerHTML = html;
256
- }).then(() => {
257
- target.removeAttribute(`${Env.prefix}importing`);
258
- HaoriEvent.importEnd(target, resolvedUrl, bytes, startedAt);
259
- if (!document.body.hasAttribute('data-haori-ready')) {
260
- const childPromises = [];
261
- target.childNodes.forEach(node => {
262
- const child = Fragment.get(node);
263
- if (child instanceof ElementFragment) {
264
- childPromises.push(Core.scan(child.getTarget()));
265
- }
266
- else if (child instanceof TextFragment) {
267
- childPromises.push(Core.evaluateText(child));
268
- }
269
- });
270
- return Promise.all(childPromises).then(() => undefined);
271
- }
272
- return undefined;
273
- });
274
- })
275
- .catch(error => {
276
- target.removeAttribute(`${Env.prefix}importing`);
277
- HaoriEvent.importError(target, resolvedUrl, error);
278
- Log.error('[Haori]', 'Failed to import HTML:', resolvedUrl, error);
279
- })
280
- .finally(() => {
281
- state.running = false;
282
- if (state.rerunRequested) {
283
- state.rerunRequested = false;
284
- return Core.executeManagedImport(fragment);
285
- }
286
- return undefined;
287
- });
288
- }
289
- /**
290
- * 指定された要素と、その子要素をスキャンし、Fragmentを生成します。
291
- *
292
- * @param element スキャン対象の要素
293
- * @returns Promise (スキャンが完了したときに解決される)
294
- */
295
- static scan(element) {
296
- const fragment = Fragment.get(element);
297
- if (!fragment) {
298
- return Promise.resolve();
299
- }
300
- return Core.initializeElementFragment(fragment, false);
301
- }
302
- /**
303
- * 新規 each 行を局所初期化します。
304
- * 既存 scan の属性順序を保ちつつ、Fragment 木を直接たどります。
305
- *
306
- * @param fragment 新規挿入された行フラグメント
307
- * @returns 初期化完了の Promise
308
- */
309
- static initializeFreshEachRow(fragment) {
310
- return Core.initializeElementFragment(fragment, true).then(() => {
311
- if (Core.needsScheduledEvaluateAll(fragment)) {
312
- Core.scheduleEvaluateAll(fragment);
313
- }
314
- return undefined;
315
- });
316
- }
317
- /**
318
- * ElementFragment とその子孫を初期化します。
319
- *
320
- * @param fragment 対象フラグメント
321
- * @param stopAtEach true の場合、data-each 要素では通常再帰を止める
322
- * @returns 初期化完了の Promise
323
- */
324
- static initializeElementFragment(fragment, stopAtEach) {
325
- Core.syncMountedState(fragment);
326
- if (stopAtEach && fragment.isFreshInitializationSkippable()) {
327
- return Promise.resolve();
328
- }
329
- return Core.initializeElementAttributes(fragment).then(() => {
330
- if (Core.shouldSkipChildInitialization(fragment, stopAtEach)) {
331
- Core.refreshDerivedSubtreeSignature(fragment);
332
- return undefined;
333
- }
334
- const childPromises = [];
335
- fragment.getChildren().forEach(child => {
336
- if (child instanceof ElementFragment) {
337
- childPromises.push(Core.initializeElementFragment(child, stopAtEach));
338
- }
339
- else if (child instanceof TextFragment) {
340
- childPromises.push(Core.evaluateText(child));
341
- }
342
- });
343
- return Promise.all(childPromises).then(() => {
344
- Core.refreshDerivedSubtreeSignature(fragment);
345
- return undefined;
346
- });
347
- });
348
- }
349
- /**
350
- * 要素初期化時の mounted 状態を同期します。
351
- *
352
- * @param fragment 対象フラグメント
353
- */
354
- static syncMountedState(fragment) {
355
- const parent = fragment.getParent();
356
- if (parent?.isMounted()) {
357
- fragment.setMounted(true);
358
- return;
359
- }
360
- const target = fragment.getTarget();
361
- if (target.parentNode && document.body.contains(target)) {
362
- fragment.setMounted(true);
363
- return;
364
- }
365
- fragment.setMounted(false);
366
- }
367
- /**
368
- * scan と fresh clone 初期化で共有する属性初期化を行います。
369
- *
370
- * @param fragment 対象フラグメント
371
- * @returns 属性初期化完了の Promise
372
- */
373
- static initializeElementAttributes(fragment) {
374
- let attributeChain = Promise.resolve();
375
- const processedAttributes = new Set();
376
- for (const suffix of Core.PRIORITY_ATTRIBUTE_SUFFIXES) {
377
- // 優先属性の処理
378
- const name = Env.prefix + suffix;
379
- if (fragment.hasAttribute(name)) {
380
- attributeChain = attributeChain.then(() => Core.setAttribute(fragment.getTarget(), name, fragment.getRawAttribute(name)));
381
- processedAttributes.add(name);
382
- }
383
- }
384
- for (const name of fragment.getAttributeNames()) {
385
- if (processedAttributes.has(name) || Core.isDeferredAttributeName(name)) {
386
- // すでに処理済みもしくは遅延処理の属性はスキップ
387
- continue;
388
- }
389
- const value = fragment.getRawAttribute(name);
390
- if (value !== null) {
391
- attributeChain = attributeChain.then(() => Core.setAttribute(fragment.getTarget(), name, value));
392
- }
393
- }
394
- for (const suffix of Core.DEFERRED_ATTRIBUTE_SUFFIXES) {
395
- // 遅延属性の処理
396
- const name = Env.prefix + suffix;
397
- if (fragment.hasAttribute(name)) {
398
- attributeChain = attributeChain.then(() => Core.setAttribute(fragment.getTarget(), name, fragment.getRawAttribute(name)));
399
- processedAttributes.add(name);
400
- }
401
- }
402
- return attributeChain.then(() => undefined);
403
- }
404
- /**
405
- * 子孫初期化をスキップすべきかどうかを返します。
406
- *
407
- * @param fragment 対象フラグメント
408
- * @param stopAtEach true の場合、data-each 要素で通常再帰を止める
409
- * @returns 子孫初期化をスキップするなら true
410
- */
411
- static shouldSkipChildInitialization(fragment, stopAtEach) {
412
- const condition = fragment.getAttribute(`${Env.prefix}if`);
413
- if (fragment.hasAttribute(`${Env.prefix}if`) &&
414
- Core.isHiddenIfCondition(condition)) {
415
- return true;
416
- }
417
- return stopAtEach && fragment.hasAttribute(`${Env.prefix}each`);
418
- }
419
- /**
420
- * エレメントに属性を設定します。
421
- * 属性固有の処理も行います。
422
- *
423
- * @param element エレメント
424
- * @param name 属性名
425
- * @param value 属性値
426
- * @returns Promise (DOM操作が完了したときに解決される)
427
- */
428
- static setAttribute(element, name, value, fromObserver = false) {
429
- const fragment = Fragment.get(element);
430
- const aliasedAttributeName = Core.getAliasedAttributeName(name);
431
- if (aliasedAttributeName !== null) {
432
- if (value === null) {
433
- return fragment.removeAliasedAttribute(name, aliasedAttributeName);
434
- }
435
- return fragment.setAliasedAttribute(name, aliasedAttributeName, value, fromObserver);
436
- }
437
- const promises = [];
438
- let deriveChangedPromise = null;
439
- let nextDeriveInputSignature = null;
440
- switch (name) {
441
- case `${Env.prefix}bind`: {
442
- if (value === null) {
443
- fragment.clearBindingDataCache();
444
- fragment.setBindingData({});
445
- }
446
- else {
447
- fragment.setBindingData(Core.parseDataBind(value));
448
- }
449
- break;
450
- }
451
- case `${Env.prefix}derive`:
452
- nextDeriveInputSignature = Core.createDeriveInputSignature(fragment, value, fragment.getRawAttribute(`${Env.prefix}derive-name`));
453
- deriveChangedPromise = Core.evaluateDerive(fragment, value, fragment.getRawAttribute(`${Env.prefix}derive-name`));
454
- promises.push(deriveChangedPromise.then(() => undefined));
455
- break;
456
- case `${Env.prefix}derive-name`:
457
- nextDeriveInputSignature = Core.createDeriveInputSignature(fragment, fragment.getRawAttribute(`${Env.prefix}derive`), value);
458
- deriveChangedPromise = Core.evaluateDerive(fragment, fragment.getRawAttribute(`${Env.prefix}derive`), value);
459
- promises.push(deriveChangedPromise.then(() => undefined));
460
- break;
461
- case `${Env.prefix}if`:
462
- promises.push(Core.evaluateIf(fragment));
463
- break;
464
- case `${Env.prefix}each`:
465
- promises.push(Core.evaluateEach(fragment));
466
- break;
467
- case `${Env.prefix}fetch`:
468
- promises.push(Core.executeManagedFetch(fragment));
469
- break;
470
- case `${Env.prefix}import`:
471
- if (typeof value === 'string') {
472
- promises.push(Core.executeManagedImport(fragment));
473
- }
474
- break;
475
- case `${Env.prefix}url-param`: {
476
- const arg = fragment.getAttribute(`${Env.prefix}url-arg`);
477
- const params = Url.readParams();
478
- if (arg === null) {
479
- promises.push(Core.setBindingData(element, params));
480
- }
481
- else {
482
- const data = fragment.getRawBindingData() || {};
483
- data[String(arg)] = params;
484
- promises.push(Core.setBindingData(element, data));
485
- }
486
- break;
487
- }
488
- }
489
- if (value === null) {
490
- promises.push(fragment.removeAttribute(name));
491
- }
492
- else {
493
- promises.push(fragment.setAttribute(name, value, fromObserver));
494
- }
495
- return Promise.all(promises)
496
- .then(() => {
497
- if (deriveChangedPromise !== null) {
498
- fragment.setDeriveInputSignature(nextDeriveInputSignature);
499
- return deriveChangedPromise.then(changed => {
500
- if (!changed) {
501
- return undefined;
502
- }
503
- return Core.reevaluateChildren(fragment);
504
- });
505
- }
506
- return undefined;
507
- })
508
- .then(() => undefined);
509
- }
510
- /**
511
- * エレメントに属性を設定し、評価を行います。
512
- *
513
- * @param element エレメント
514
- * @param name 属性名
515
- * @param value 属性値
516
- * @returns Promise (DOM操作が完了したときに解決される)
517
- */
518
- static setBindingData(element, data, skipFragments = new Set()) {
519
- const fragment = Fragment.get(element);
520
- const previous = fragment.getRawBindingData();
521
- fragment.setBindingData(data);
522
- let chain = fragment.setAttribute(`${Env.prefix}bind`, JSON.stringify(data));
523
- if (element.tagName === 'FORM') {
524
- const arg = fragment.getAttribute(`${Env.prefix}form-arg`);
525
- const formValues = arg &&
526
- data[String(arg)] &&
527
- typeof data[String(arg)] === 'object' &&
528
- !Array.isArray(data[String(arg)])
529
- ? data[String(arg)]
530
- : arg
531
- ? {}
532
- : data;
533
- chain = chain.then(() => Form.syncValues(fragment, formValues));
534
- }
535
- chain = chain.then(() => Core.evaluateAll(fragment, skipFragments));
536
- chain = chain.then(() => Core.reevaluateReactiveSpecialAttributes(fragment, skipFragments));
537
- // bindchangeイベントを発火
538
- HaoriEvent.bindChange(element, previous, data, 'manual');
539
- return chain.then(() => undefined);
540
- }
541
- /**
542
- * 指定要素の式評価で見えるバインディングスコープをダンプします(デバッグ用)。
543
- *
544
- * 式(`data-if` / `{{...}}` など)の識別子は、その要素を起点に DOM のネストを
545
- * たどって解決されます(内側のスコープが外側を上書き)。本メソッドは解決済みの
546
- * スコープ(`resolved`)と、各キーがどの要素・どの種類(`bind` または `derive`)に
547
- * 由来するか(`sources`)を返します。開発モード時はコンソールにも出力します。
548
- *
549
- * 注意: フォームの入力値(`name` 属性)は、変更(change)や明示的な同期が行われる
550
- * までフォームの binding data に反映されません。したがって初期表示時点では、入力名と
551
- * 同名の識別子は**外側のスコープ**にフォールバックして解決されます。
552
- *
553
- * @param element 対象要素
554
- * @return 解決済みスコープと各キーの由来情報
555
- */
556
- static dumpScope(element) {
557
- const fragment = Fragment.get(element);
558
- if (!fragment) {
559
- return { resolved: {}, sources: {} };
560
- }
561
- const resolved = fragment.getBindingData();
562
- const sources = {};
563
- const describe = (frag) => {
564
- const target = frag.getTarget();
565
- if (target.id) {
566
- return `#${target.id}`;
567
- }
568
- return target.tagName.toLowerCase();
569
- };
570
- const record = (data, frag, kind, depth) => {
571
- if (!data) {
572
- return;
573
- }
574
- for (const key of Object.keys(data)) {
575
- if (!(key in sources)) {
576
- sources[key] = {
577
- value: data[key],
578
- source: describe(frag),
579
- kind,
580
- depth,
581
- };
582
- }
583
- }
584
- };
585
- let current = fragment;
586
- let depth = 0;
587
- while (current) {
588
- // 子孫から見ると derive は同要素の bind より優先されるため derive を先に記録する。
589
- // ただし起点要素自身の derive は自身のスコープには現れない(子孫にのみ公開)。
590
- if (current !== fragment) {
591
- record(current.getRawDerivedBindingData(), current, 'derive', depth);
592
- }
593
- record(current.getRawBindingData(), current, 'bind', depth);
594
- current = current.getParent();
595
- depth += 1;
596
- }
597
- if (Dev.isEnabled()) {
598
- Log.info('[Haori]', 'scope dump for', element, { resolved, sources });
599
- }
600
- return { resolved, sources };
601
- }
602
- /**
603
- * data-bind 属性の値をパースします。
604
- *
605
- * @param data data-bind 属性の値
606
- * @returns パースされたデータオブジェクト
607
- */
608
- static parseDataBind(data) {
609
- if (data.startsWith('{') || data.startsWith('[')) {
610
- // JSONとしてパース
611
- try {
612
- return JSON.parse(data);
613
- }
614
- catch (e) {
615
- Log.error('[Haori]', 'Invalid JSON in data-bind:', e);
616
- return {};
617
- }
618
- }
619
- else {
620
- // URLSearchParamsでパース
621
- const params = new URLSearchParams(data);
622
- const result = {};
623
- for (const [key, value] of params.entries()) {
624
- if (result[key] !== undefined) {
625
- // すでに値がある場合は配列化
626
- if (Array.isArray(result[key])) {
627
- result[key].push(value);
628
- }
629
- else {
630
- result[key] = [result[key], value];
631
- }
632
- }
633
- else {
634
- result[key] = value;
635
- }
636
- }
637
- return result;
638
- }
639
- }
640
- /**
641
- * ノードを親要素に追加し評価を行います。
642
- *
643
- * @param parentElement 親エレメント
644
- * @param node 追加するノード
645
- */
646
- static addNode(parentElement, node) {
647
- const parent = Fragment.get(parentElement);
648
- // skipMutationNodesが設定されている場合は処理をスキップ
649
- if (parent.isSkipMutationNodes()) {
650
- return;
651
- }
652
- const next = Fragment.get(node.nextSibling);
653
- const fragment = Fragment.get(node);
654
- if (fragment) {
655
- parent.insertBefore(fragment, next);
656
- if (fragment instanceof ElementFragment) {
657
- // 新規追加ノードは属性評価(bind/if/each/import など含む)のフルスキャンを行う。
658
- // これにより、取り込まれた断片内の data-import の入れ子や data-bind も正しく処理される。
659
- Core.scan(fragment.getTarget());
660
- }
661
- else if (fragment instanceof TextFragment) {
662
- Core.evaluateText(fragment);
663
- }
664
- }
665
- }
666
- /**
667
- * ノードを親要素から削除します。
668
- *
669
- * @param node 削除するノード
670
- */
671
- static removeNode(node) {
672
- const fragment = Fragment.get(node);
673
- if (fragment) {
674
- const parent = fragment.getParent();
675
- // skipMutationNodesが設定されている場合は処理をスキップ
676
- if (parent && parent.isSkipMutationNodes()) {
677
- return;
678
- }
679
- fragment.remove();
680
- }
681
- }
682
- /**
683
- * ノードのテキストを変更します。
684
- *
685
- * @param node 変更するノード
686
- * @param text 新しいテキスト
687
- */
688
- static changeText(node, text) {
689
- const fragment = Fragment.get(node);
690
- if (fragment) {
691
- fragment.setContent(text);
692
- }
693
- }
694
- /**
695
- * エレメントの値を変更します。
696
- * フォームの双方向バインディングを考慮し、フォームのバインドデータも更新します。
697
- *
698
- * @param element 変更するエレメント
699
- * @param value 新しい値
700
- * @returns Promise (DOM操作が完了したときに解決される)
701
- */
702
- static changeValue(element, value) {
703
- const fragment = Fragment.get(element);
704
- if (fragment.getValue() === value) {
705
- return Promise.resolve();
706
- }
707
- const promises = [];
708
- promises.push(fragment.setValue(value));
709
- const formFragment = Core.getFormFragment(fragment);
710
- if (formFragment) {
711
- const values = Form.getValues(formFragment);
712
- const arg = formFragment.getAttribute(`${Env.prefix}form-arg`);
713
- let bindingData;
714
- if (arg) {
715
- bindingData = formFragment.getRawBindingData();
716
- if (!bindingData) {
717
- bindingData = {};
718
- }
719
- bindingData[String(arg)] = values;
720
- }
721
- else {
722
- bindingData = values;
723
- }
724
- promises.push(Core.setBindingData(formFragment.getTarget(), bindingData));
725
- }
726
- return Promise.all(promises).then(() => undefined);
727
- }
728
- /**
729
- * フォームフラグメントを取得します。
730
- *
731
- * @param fragment フラグメント
732
- * @returns フォームフラグメントまたはnull
733
- */
734
- static getFormFragment(fragment) {
735
- if (fragment.getTarget() instanceof HTMLFormElement) {
736
- return fragment;
737
- }
738
- const parent = fragment.getParent();
739
- if (parent) {
740
- return Core.getFormFragment(parent);
741
- }
742
- return null;
743
- }
744
- /**
745
- * フラグメントとその子要素を評価します。
746
- *
747
- * @param fragment 対象フラグメント
748
- * @return Promise (DOM操作が完了したときに解決される)
749
- */
750
- static evaluateAll(fragment, skipFragments = new Set()) {
751
- if (skipFragments.has(fragment)) {
752
- return Promise.resolve();
753
- }
754
- let chain = Core.reevaluateInterpolatedAttributes(fragment);
755
- const hasDerive = fragment.hasAttribute(`${Env.prefix}derive`);
756
- const hasIf = fragment.hasAttribute(`${Env.prefix}if`);
757
- const hasEach = fragment.hasAttribute(`${Env.prefix}each`);
758
- const deriveExpression = fragment.getRawAttribute(`${Env.prefix}derive`);
759
- const deriveName = fragment.getRawAttribute(`${Env.prefix}derive-name`);
760
- let shouldSkipDerivedSubtree = false;
761
- let shouldRecordDerivedSubtreeSignature = false;
762
- let nextDerivedSubtreeSignature = null;
763
- if (!hasDerive && fragment.getDeriveSubtreeSignature() !== null) {
764
- fragment.setDeriveSubtreeSignature(null);
765
- }
766
- if (!hasDerive && fragment.getDeriveInputSignature() !== null) {
767
- fragment.setDeriveInputSignature(null);
768
- }
769
- if (hasDerive) {
770
- const nextDeriveInputSignature = Core.createDeriveInputSignature(fragment, deriveExpression, deriveName);
771
- if (nextDeriveInputSignature === null) {
772
- if (fragment.getDeriveInputSignature() !== null) {
773
- fragment.setDeriveInputSignature(null);
774
- }
775
- chain = chain.then(() => Core.evaluateDerive(fragment, deriveExpression, deriveName).then(() => undefined));
776
- }
777
- else if (fragment.getDeriveInputSignature() !== nextDeriveInputSignature) {
778
- chain = chain.then(() => {
779
- return Core.evaluateDerive(fragment, deriveExpression, deriveName).then(() => {
780
- fragment.setDeriveInputSignature(nextDeriveInputSignature);
781
- return undefined;
782
- });
783
- });
784
- }
785
- }
786
- if (hasIf) {
787
- chain = chain.then(() => Core.evaluateIf(fragment));
788
- }
789
- if (hasEach) {
790
- if (fragment.getDeriveSubtreeSignature() !== null) {
791
- fragment.setDeriveSubtreeSignature(null);
792
- }
793
- return chain.then(() => Core.evaluateEach(fragment));
794
- }
795
- if (hasIf) {
796
- if (fragment.getDeriveSubtreeSignature() !== null) {
797
- fragment.setDeriveSubtreeSignature(null);
798
- }
799
- return chain.then(() => undefined);
800
- }
801
- if (hasDerive) {
802
- chain = chain.then(() => {
803
- if (!Core.canSkipStableDerivedSubtree(fragment)) {
804
- fragment.setDeriveSubtreeSignature(null);
805
- Core.logDerivedSubtreeProfileSnapshot(fragment, 'skip-ineligible');
806
- return;
807
- }
808
- nextDerivedSubtreeSignature = Core.createDescendantBindingSignature(fragment, 'evaluateAll');
809
- shouldRecordDerivedSubtreeSignature = true;
810
- shouldSkipDerivedSubtree =
811
- fragment.getDeriveSubtreeSignature() !== null &&
812
- fragment.getDeriveSubtreeSignature() === nextDerivedSubtreeSignature;
813
- Core.logDerivedSubtreeProfileSnapshot(fragment, shouldSkipDerivedSubtree ? 'skip-hit' : 'skip-miss');
814
- });
815
- }
816
- return chain
817
- .then(() => {
818
- if (shouldSkipDerivedSubtree) {
819
- return undefined;
820
- }
821
- const promises = [];
822
- fragment.getChildren().forEach(child => {
823
- if (child instanceof ElementFragment) {
824
- if (Core.canSkipUnchangedNestedEach(child)) {
825
- return;
826
- }
827
- promises.push(Core.evaluateAll(child, skipFragments));
828
- }
829
- else if (child instanceof TextFragment) {
830
- promises.push(Core.evaluateText(child));
831
- }
832
- });
833
- return Promise.all(promises).then(() => undefined);
834
- })
835
- .then(() => {
836
- if (shouldRecordDerivedSubtreeSignature &&
837
- nextDerivedSubtreeSignature !== null) {
838
- fragment.setDeriveSubtreeSignature(nextDerivedSubtreeSignature);
839
- }
840
- return undefined;
841
- });
842
- }
843
- /**
844
- * data-derive / data-derive-name を評価し、子孫要素向けの派生値を更新します。
845
- *
846
- * @param fragment 対象フラグメント
847
- * @param deriveExpression 上書きする導出式
848
- * @param deriveName 上書きする導出名
849
- * @returns Promise (評価完了時に解決)
850
- */
851
- static evaluateDerive(fragment, deriveExpression = fragment.getRawAttribute(`${Env.prefix}derive`), deriveName = fragment.getRawAttribute(`${Env.prefix}derive-name`)) {
852
- const previousDerivedBindingData = fragment.getRawDerivedBindingData();
853
- const normalizedName = typeof deriveName === 'string' ? deriveName.trim() : '';
854
- if (!deriveExpression || normalizedName === '') {
855
- if (previousDerivedBindingData === null) {
856
- return Promise.resolve(false);
857
- }
858
- fragment.setDerivedBindingData(null);
859
- return Promise.resolve(true);
860
- }
861
- const result = Expression.evaluateDetailed(deriveExpression, fragment.getBindingData());
862
- if (result.unresolvedReference) {
863
- if (previousDerivedBindingData === null) {
864
- return Promise.resolve(false);
865
- }
866
- fragment.setDerivedBindingData(null);
867
- return Promise.resolve(true);
868
- }
869
- const nextDerivedBindingData = {
870
- [normalizedName]: result.value,
871
- };
872
- if (Core.createBindingSignature(previousDerivedBindingData) ===
873
- Core.createBindingSignature(nextDerivedBindingData)) {
874
- return Promise.resolve(false);
875
- }
876
- fragment.setDerivedBindingData(nextDerivedBindingData);
877
- return Promise.resolve(true);
878
- }
879
- /**
880
- * テキストフラグメントを評価します。
881
- *
882
- * @param fragment 対象フラグメント
883
- * @returns Promise (DOM操作が完了したときに解決される)
884
- */
885
- static evaluateText(fragment) {
886
- return fragment.evaluate();
887
- }
888
- /**
889
- * data-if の評価値が「非表示」とみなされるかどうかを判定します。
890
- *
891
- * JavaScript の falsy 判定に準拠し、`false`・`null`・`undefined`・`NaN` に加えて
892
- * `0`・空文字列 `''` も非表示とします(例: `data-if="items.length"` は要素数 0 で
893
- * 非表示)。空配列 `[]` や空オブジェクト `{}` は JavaScript 同様 truthy なので表示されます。
894
- *
895
- * @param condition data-if の評価結果
896
- * @return 非表示とみなす場合は true
897
- */
898
- static isHiddenIfCondition(condition) {
899
- return !condition;
900
- }
901
- /**
902
- * 開発モードで、falsy により非表示になった `data-if` 式の診断情報を出力します。
903
- *
904
- * 式が参照するトップレベル識別子について、解決値と由来要素(`dumpScope` の
905
- * `sources`)を併記します。フォームの `name` 由来の値が想定外のスコープから
906
- * 解決される等のスコープ競合をデバッグするための補助です。
907
- *
908
- * @param fragment data-if フラグメント
909
- * @return 戻り値はありません。
910
- */
911
- static logFalsyIfDiagnostics(fragment) {
912
- const expression = fragment.getRawAttribute(`${Env.prefix}if`);
913
- if (typeof expression !== 'string' || expression.indexOf('{{') >= 0) {
914
- // テンプレート式({{...}})を含む属性は対象外(純粋な data-if 式のみ)。
915
- return;
916
- }
917
- const { sources } = Core.dumpScope(fragment.getTarget());
918
- // 式に現れるトップレベル識別子(プロパティアクセスの末尾などは除く)を抽出する。
919
- const identifiers = new Set();
920
- const matches = expression.match(/[A-Za-z_$][\w$]*/g) ?? [];
921
- let prevCharIndex = 0;
922
- matches.forEach(name => {
923
- const at = expression.indexOf(name, prevCharIndex);
924
- prevCharIndex = at + name.length;
925
- // 直前が `.` の場合はプロパティ名なのでトップレベル識別子ではない。
926
- if (at > 0 && expression[at - 1] === '.') {
927
- return;
928
- }
929
- identifiers.add(name);
930
- });
931
- const used = {};
932
- identifiers.forEach(name => {
933
- if (name in sources) {
934
- used[name] = sources[name];
935
- }
936
- });
937
- Log.info('[Haori]', 'data-if is falsy (hidden):', expression, '— referenced scope:', used);
938
- }
939
- /**
940
- * if要素を評価します。
941
- * 値が falsy(false・null・undefined・NaN・0・空文字列)の場合は非表示にし、
942
- * それ以外の場合は表示します。
943
- *
944
- * @param fragment 対象フラグメント
945
- * @return Promise (DOM操作が完了したときに解決される)
946
- */
947
- static evaluateIf(fragment) {
948
- const promises = [];
949
- const condition = fragment.getAttribute(`${Env.prefix}if`);
950
- if (Core.isHiddenIfCondition(condition)) {
951
- // 開発モードでは、falsy で非表示になった data-if 式と、その式が参照する
952
- // 識別子の解決値・由来(どの要素の bind/derive か)を出力する。
953
- // 「フォームの name 由来の値が想定外に解決される」等のスコープ競合の特定に役立つ。
954
- if (Dev.isEnabled()) {
955
- Core.logFalsyIfDiagnostics(fragment);
956
- }
957
- promises.push(fragment.hide().then(() => {
958
- HaoriEvent.hide(fragment.getTarget());
959
- }));
960
- }
961
- else {
962
- // 非表示→表示への遷移を検出するため、show() 前の表示状態を退避する。
963
- const wasVisible = fragment.isVisible();
964
- const childPromises = [];
965
- fragment.getChildren().forEach(child => {
966
- if (child instanceof ElementFragment) {
967
- // 未スキャンの子は scan で初期化し、既に表示済みの子は再評価だけ行う。
968
- childPromises.push(child.isMounted()
969
- ? Core.evaluateAll(child)
970
- : Core.scan(child.getTarget()));
971
- }
972
- else if (child instanceof TextFragment) {
973
- childPromises.push(Core.evaluateText(child));
974
- }
975
- });
976
- promises.push(fragment.show().then(() => {
977
- HaoriEvent.show(fragment.getTarget());
978
- // 非表示→表示へ遷移したときだけ data-load-* を発火する。
979
- // ボタンや div などネイティブの load イベントが発生しない要素でも、
980
- // data-if による表示(haori:show)を契機に data-load-* を実行できるようにする。
981
- // 毎回の再評価で発火させると無限ループや過剰実行を招くため、遷移時に限定する。
982
- if (!wasVisible) {
983
- Core.triggerLoadOnShow(fragment);
984
- }
985
- }));
986
- promises.push(Promise.all(childPromises).then(() => undefined));
987
- }
988
- return Promise.all(promises).then(() => undefined);
989
- }
990
- /**
991
- * data-if 表示時に data-load-* 手続きを発火します。
992
- *
993
- * 対象要素が data-load-* 属性を持つ場合のみ、load 種別の Procedure を1回実行します。
994
- * 結果は待機せず(fire-and-forget)、表示処理の完了をブロックしません。
995
- *
996
- * @param fragment 対象フラグメント
997
- * @return 戻り値はありません。
998
- */
999
- static triggerLoadOnShow(fragment) {
1000
- const loadPrefix = `${Env.prefix}load-`;
1001
- const hasLoadAttribute = fragment
1002
- .getTarget()
1003
- .getAttributeNames()
1004
- .some(name => name.startsWith(loadPrefix));
1005
- if (!hasLoadAttribute) {
1006
- return;
1007
- }
1008
- void new Procedure(fragment, 'load').run().catch(error => {
1009
- Log.error('[Haori]', 'data-load procedure error (on show):', error);
1010
- });
1011
- }
1012
- /**
1013
- * data-each フラグメントの差分更新の再入制御状態を取得します。
1014
- *
1015
- * @param fragment 対象フラグメント
1016
- * @return 再入制御状態
1017
- */
1018
- static getEachUpdateState(fragment) {
1019
- let state = Core.EACH_UPDATE_STATES.get(fragment);
1020
- if (!state) {
1021
- state = { running: false, rerunRequested: false, settled: null };
1022
- Core.EACH_UPDATE_STATES.set(fragment, state);
1023
- }
1024
- return state;
1025
- }
1026
- /**
1027
- * each要素を評価します。
1028
- * 非表示または未マウントの場合は処理をスキップします。
1029
- *
1030
- * 同一フラグメントに対する差分更新が並行・再入しないように直列化します。
1031
- * 実行中に再度呼び出された場合は再評価要求を記録し、現在進行中の更新の完了
1032
- * Promise(後続の再実行も含む)を返します。これにより、bind 直後のリアクティブ
1033
- * 再評価が重なっても data-each の描画が破壊されず、かつ呼び出し元(`evaluateAll`→
1034
- * `setBindingData`→`haori:bindcomplete`)が**最終的な DOM 反映まで確実に待機**できます。
1035
- *
1036
- * @param fragment 対象フラグメント
1037
- * @return 差分更新(再実行を含む)の完了 Promise
1038
- */
1039
- static evaluateEach(fragment) {
1040
- if (!fragment.isVisible() || !fragment.isMounted()) {
1041
- return Promise.resolve();
1042
- }
1043
- const state = Core.getEachUpdateState(fragment);
1044
- if (state.running) {
1045
- // 実行中は再評価要求を記録し、進行中の settle Promise を待つ
1046
- // (最新データの描画完了まで待てるようにする)。
1047
- state.rerunRequested = true;
1048
- return state.settled ?? Promise.resolve();
1049
- }
1050
- return Core.runEachUpdateLoop(fragment, state);
1051
- }
1052
- /**
1053
- * data-each の差分更新を、再評価要求が無くなるまで直列に繰り返し実行します。
1054
- * 進行中・後続の再実行を含む完了 Promise を state に保持し、再入した呼び出し元が
1055
- * 同じ Promise を待てるようにします。
1056
- *
1057
- * @param fragment 対象フラグメント
1058
- * @param state 再入制御状態
1059
- * @return すべての差分更新が安定するまでの完了 Promise
1060
- */
1061
- static runEachUpdateLoop(fragment, state) {
1062
- state.running = true;
1063
- // 新しい描画サイクルの開始時に完了マーカーを外す。
1064
- fragment.getTarget().removeAttribute(`${Env.prefix}each-done`);
1065
- const settled = (async () => {
1066
- try {
1067
- do {
1068
- state.rerunRequested = false;
1069
- await Core.performEachUpdate(fragment);
1070
- } while (state.rerunRequested);
1071
- // 全行の描画が安定して完了したことを示すマーカーを付与する。
1072
- // 外部テストは `[data-each-done]` を待機して描画完了を検知できる。
1073
- fragment.getTarget().setAttribute(`${Env.prefix}each-done`, '');
1074
- }
1075
- finally {
1076
- state.running = false;
1077
- state.settled = null;
1078
- }
1079
- })();
1080
- state.settled = settled;
1081
- return settled;
1082
- }
1083
- /**
1084
- * data-each の差分更新本体を実行します(再入制御は呼び出し側で行います)。
1085
- *
1086
- * @param fragment 対象フラグメント
1087
- * @return 差分更新完了の Promise
1088
- */
1089
- static performEachUpdate(fragment) {
1090
- const data = Core.resolveEachItems(fragment);
1091
- if (data === null) {
1092
- return Promise.reject(new Error('Invalid each attribute.'));
1093
- }
1094
- let template = fragment.getTemplate();
1095
- const keyArg = fragment.getAttribute(`${Env.prefix}each-key`);
1096
- const nextEachInputSignature = Core.createBindingSignature({
1097
- key: keyArg ? String(keyArg) : null,
1098
- items: data,
1099
- });
1100
- if (template === null) {
1101
- // テンプレートの作成
1102
- let found = false;
1103
- fragment.getChildren().forEach(child => {
1104
- if (found) {
1105
- return;
1106
- }
1107
- if (child instanceof ElementFragment) {
1108
- if (child.hasAttribute(`${Env.prefix}each-before`) ||
1109
- child.hasAttribute(`${Env.prefix}each-after`)) {
1110
- return;
1111
- }
1112
- // 最初のElementFragmentをテンプレートとして採用
1113
- template = child.clone();
1114
- Core.markFreshInitializationSkippable(template);
1115
- fragment.setTemplate(template);
1116
- found = true;
1117
- // 元のchildはchildrenから除外
1118
- fragment.removeChild(child);
1119
- // DOMからも必ず除去
1120
- const templateTarget = child.getTarget();
1121
- if (templateTarget.parentNode) {
1122
- templateTarget.parentNode.removeChild(templateTarget);
1123
- }
1124
- child.setMounted(false);
1125
- }
1126
- // TextNodeやCommentNodeはテンプレートにならないので無視
1127
- });
1128
- if (!found) {
1129
- // フォールバック: フラグメントの子に要素が無いが DOM には要素子がある場合
1130
- // (タブ表示やネスト data-if など特定フローでフラグメント木と DOM の子が
1131
- // 同期しない状況の復旧)、DOM の要素子からテンプレートを復旧する。
1132
- // この分岐は template===null かつフラグメント子テンプレートも無い「復旧専用
1133
- // パス」であり、each-before/after 以外の要素子はすべて each に未管理の残留物
1134
- // とみなせる。先頭要素子をテンプレートとして採用し、残りの該当要素子も
1135
- // まとめて除去して、後続の updateDiff をクリーンな状態から開始する。
1136
- const target = fragment.getTarget();
1137
- const staleChildren = Array.from(target.children).filter(domChild => !domChild.hasAttribute(`${Env.prefix}each-before`) &&
1138
- !domChild.hasAttribute(`${Env.prefix}each-after`));
1139
- staleChildren.forEach(domChild => {
1140
- // 最初の要素子をテンプレートとして採用する。
1141
- if (!found) {
1142
- const childFragment = Fragment.get(domChild);
1143
- if (childFragment instanceof ElementFragment) {
1144
- template = childFragment.clone();
1145
- Core.markFreshInitializationSkippable(template);
1146
- fragment.setTemplate(template);
1147
- found = true;
1148
- }
1149
- }
1150
- // フラグメントに紐づく残留子があれば children から除外する。
1151
- const frag = Fragment.get(domChild);
1152
- if (frag instanceof ElementFragment &&
1153
- fragment.getChildren().includes(frag)) {
1154
- fragment.removeChild(frag);
1155
- frag.setMounted(false);
1156
- }
1157
- // 未追跡の素の DOM ノードも含め、残留要素子は DOM から除去する。
1158
- if (domChild.parentNode) {
1159
- domChild.parentNode.removeChild(domChild);
1160
- }
1161
- });
1162
- }
1163
- // テンプレートのunmount完了後にupdateDiffを実行
1164
- return this.updateDiff(fragment, data).then(() => {
1165
- fragment.setEachInputSignature(nextEachInputSignature);
1166
- });
1167
- }
1168
- if (fragment.getEachInputSignature() === nextEachInputSignature) {
1169
- return Promise.resolve();
1170
- }
1171
- return this.updateDiff(fragment, data).then(() => {
1172
- fragment.setEachInputSignature(nextEachInputSignature);
1173
- });
1174
- }
1175
- /**
1176
- * data-each 属性値を仕様に従って配列へ正規化します。
1177
- *
1178
- * @param fragment 対象フラグメント
1179
- * @returns 配列。無効な場合は null
1180
- */
1181
- static resolveEachItems(fragment) {
1182
- const evaluation = fragment.getAttributeEvaluation(`${Env.prefix}each`);
1183
- const data = evaluation?.value;
1184
- if (evaluation?.hasUnresolvedReference ||
1185
- data === false ||
1186
- data === null ||
1187
- data === undefined) {
1188
- return [];
1189
- }
1190
- if (Array.isArray(data)) {
1191
- return data;
1192
- }
1193
- Log.error('[Haori]', 'Invalid each attribute:', data);
1194
- return null;
1195
- }
1196
- /**
1197
- * nested data-each の入力が同値で、要素自身に他の動的要素が無い場合は
1198
- * evaluateAll の子走査を省略できるかどうかを返します。
1199
- *
1200
- * @param fragment 判定対象フラグメント
1201
- * @returns 省略可能なら true
1202
- */
1203
- static canSkipUnchangedNestedEach(fragment) {
1204
- if (!fragment.hasAttribute(`${Env.prefix}each`)) {
1205
- return false;
1206
- }
1207
- if (fragment.getEachInputSignature() === null) {
1208
- return false;
1209
- }
1210
- const parent = fragment.getParent();
1211
- if (parent?.closestByAttribute(`${Env.prefix}derive`) ||
1212
- parent?.closestByAttribute(`${Env.prefix}derive-name`) ||
1213
- parent?.closestByAttribute(`${Env.prefix}if`) ||
1214
- parent?.closestByAttribute(`${Env.prefix}fetch`) ||
1215
- parent?.closestByAttribute(`${Env.prefix}import`)) {
1216
- return false;
1217
- }
1218
- if (Core.hasNonEachDynamicElementState(fragment)) {
1219
- return false;
1220
- }
1221
- const data = Core.resolveEachItems(fragment);
1222
- if (data === null) {
1223
- return false;
1224
- }
1225
- const keyArg = fragment.getAttribute(`${Env.prefix}each-key`);
1226
- const nextEachInputSignature = Core.createBindingSignature({
1227
- key: keyArg ? String(keyArg) : null,
1228
- items: data,
1229
- });
1230
- return fragment.getEachInputSignature() === nextEachInputSignature;
1231
- }
1232
- /**
1233
- * data-derive subtree の入力が同値で、保守条件も満たす場合に
1234
- * 子走査を省略できるかどうかを返します。
1235
- *
1236
- * @param fragment 判定対象フラグメント
1237
- * @returns 省略可能なら true
1238
- */
1239
- static canSkipStableDerivedSubtree(fragment) {
1240
- if (!fragment.hasAttribute(`${Env.prefix}derive`)) {
1241
- return false;
1242
- }
1243
- if (fragment.hasAttribute(`${Env.prefix}if`) ||
1244
- fragment.hasAttribute(`${Env.prefix}each`) ||
1245
- fragment.hasAttribute(`${Env.prefix}fetch`) ||
1246
- fragment.hasAttribute(`${Env.prefix}import`)) {
1247
- return false;
1248
- }
1249
- return !Core.hasDisallowedDerivedSubtreeDescendant(fragment);
1250
- }
1251
- /**
1252
- * data-derive subtree skip の初期 PoC で扱わない子孫要素を含むかを返します。
1253
- *
1254
- * @param fragment 判定対象フラグメント
1255
- * @returns 含むなら true
1256
- */
1257
- static hasDisallowedDerivedSubtreeDescendant(fragment) {
1258
- return fragment.getChildren().some(child => {
1259
- if (!(child instanceof ElementFragment)) {
1260
- return false;
1261
- }
1262
- if (child.hasAttribute(`${Env.prefix}derive`) ||
1263
- child.hasAttribute(`${Env.prefix}derive-name`) ||
1264
- child.hasAttribute(`${Env.prefix}fetch`) ||
1265
- child.hasAttribute(`${Env.prefix}import`)) {
1266
- return true;
1267
- }
1268
- return Core.hasDisallowedDerivedSubtreeDescendant(child);
1269
- });
1270
- }
1271
- /**
1272
- * data-derive host が子孫要素へ公開している binding の署名を返します。
1273
- *
1274
- * @param fragment 対象フラグメント
1275
- * @returns binding 署名
1276
- */
1277
- static createDescendantBindingSignature(fragment, source) {
1278
- Core.recordDerivedSubtreeSignatureComputation(fragment, source);
1279
- return Core.createBindingSignature(fragment.getDescendantBindingData());
1280
- }
1281
- /**
1282
- * data-derive 実行前の入力署名を返します。
1283
- *
1284
- * @param fragment 対象フラグメント
1285
- * @param deriveExpression 導出式
1286
- * @param deriveName 導出名
1287
- * @returns 入力署名。導出が無効なら null
1288
- */
1289
- static createDeriveInputSignature(fragment, deriveExpression, deriveName) {
1290
- const normalizedName = typeof deriveName === 'string' ? deriveName.trim() : '';
1291
- if (!deriveExpression || normalizedName === '') {
1292
- return null;
1293
- }
1294
- return Core.createBindingSignature({
1295
- expression: deriveExpression,
1296
- name: normalizedName,
1297
- scope: fragment.getBindingData(),
1298
- });
1299
- }
1300
- /**
1301
- * data-derive subtree skip 用の署名を現在状態で更新します。
1302
- *
1303
- * @param fragment 対象フラグメント
1304
- */
1305
- static refreshDerivedSubtreeSignature(fragment) {
1306
- if (!Core.canSkipStableDerivedSubtree(fragment)) {
1307
- fragment.setDeriveSubtreeSignature(null);
1308
- Core.logDerivedSubtreeProfileSnapshot(fragment, 'skip-ineligible');
1309
- return;
1310
- }
1311
- fragment.setDeriveSubtreeSignature(Core.createDescendantBindingSignature(fragment, 'refresh'));
1312
- Core.logDerivedSubtreeProfileSnapshot(fragment, 'refresh');
1313
- }
1314
- /**
1315
- * data-derive subtree skip のプロファイルを取得または初期化します。
1316
- *
1317
- * @param fragment 対象フラグメント
1318
- * @returns プロファイル
1319
- */
1320
- static getOrCreateDerivedSubtreeProfile(fragment) {
1321
- if (!Dev.isEnabled() || !fragment.hasAttribute(`${Env.prefix}derive`)) {
1322
- return null;
1323
- }
1324
- const existing = Core.DERIVE_SUBTREE_PROFILES.get(fragment);
1325
- if (existing) {
1326
- return existing;
1327
- }
1328
- const profile = {
1329
- hostId: Core.createDerivedSubtreeHostId(fragment),
1330
- signatureComputeTotal: 0,
1331
- signatureComputeFromEvaluateAll: 0,
1332
- signatureComputeFromRefresh: 0,
1333
- skipHitCount: 0,
1334
- skipMissCount: 0,
1335
- skipIneligibleCount: 0,
1336
- };
1337
- Core.DERIVE_SUBTREE_PROFILES.set(fragment, profile);
1338
- return profile;
1339
- }
1340
- /**
1341
- * data-derive subtree host の識別子を作成します。
1342
- *
1343
- * @param fragment 対象フラグメント
1344
- * @returns host 識別子
1345
- */
1346
- static createDerivedSubtreeHostId(fragment) {
1347
- const segments = [];
1348
- let current = fragment;
1349
- while (current) {
1350
- const target = current.getTarget();
1351
- if (!(target instanceof HTMLElement)) {
1352
- break;
1353
- }
1354
- let segment = target.tagName.toLowerCase();
1355
- if (target.id.trim() !== '') {
1356
- segment += `#${target.id.trim()}`;
1357
- segments.unshift(segment);
1358
- break;
1359
- }
1360
- const deriveName = current.getRawAttribute(`${Env.prefix}derive-name`);
1361
- if (typeof deriveName === 'string' && deriveName.trim() !== '') {
1362
- segment += `[${Env.prefix}derive-name="${deriveName.trim()}"]`;
1363
- }
1364
- const parent = current.getParent();
1365
- if (parent) {
1366
- const siblingIndex = parent
1367
- .getChildren()
1368
- .filter(child => child instanceof ElementFragment)
1369
- .findIndex(child => child === current);
1370
- segment += `:nth-child(${siblingIndex + 1})`;
1371
- }
1372
- segments.unshift(segment);
1373
- current = parent;
1374
- }
1375
- return segments.join(' > ');
1376
- }
1377
- /**
1378
- * data-derive subtree の署名計算回数を記録します。
1379
- *
1380
- * @param fragment 対象フラグメント
1381
- * @param source 計算元
1382
- */
1383
- static recordDerivedSubtreeSignatureComputation(fragment, source) {
1384
- const profile = Core.getOrCreateDerivedSubtreeProfile(fragment);
1385
- if (profile === null) {
1386
- return;
1387
- }
1388
- profile.signatureComputeTotal += 1;
1389
- if (source === 'refresh') {
1390
- profile.signatureComputeFromRefresh += 1;
1391
- return;
1392
- }
1393
- profile.signatureComputeFromEvaluateAll += 1;
1394
- }
1395
- /**
1396
- * data-derive subtree の現在プロファイルをログ出力します。
1397
- *
1398
- * @param fragment 対象フラグメント
1399
- * @param reason ログ理由
1400
- */
1401
- static logDerivedSubtreeProfileSnapshot(fragment, reason) {
1402
- const profile = Core.getOrCreateDerivedSubtreeProfile(fragment);
1403
- if (profile === null) {
1404
- return;
1405
- }
1406
- if (reason === 'skip-hit') {
1407
- profile.skipHitCount += 1;
1408
- }
1409
- else if (reason === 'skip-miss') {
1410
- profile.skipMissCount += 1;
1411
- }
1412
- else if (reason === 'skip-ineligible') {
1413
- profile.skipIneligibleCount += 1;
1414
- }
1415
- Log.info('[Haori][derive-profile]', {
1416
- reason,
1417
- hostId: profile.hostId,
1418
- signatureComputeTotal: profile.signatureComputeTotal,
1419
- signatureComputeFromEvaluateAll: profile.signatureComputeFromEvaluateAll,
1420
- signatureComputeFromRefresh: profile.signatureComputeFromRefresh,
1421
- skipHitCount: profile.skipHitCount,
1422
- skipMissCount: profile.skipMissCount,
1423
- skipIneligibleCount: profile.skipIneligibleCount,
1424
- });
1425
- }
1426
- /**
1427
- * data-each 以外の動的要素状態を持つかどうかを返します。
1428
- *
1429
- * @param fragment 判定対象フラグメント
1430
- * @returns 該当するなら true
1431
- */
1432
- static hasNonEachDynamicElementState(fragment) {
1433
- const allowedEachAttributes = new Set([
1434
- `${Env.prefix}each`,
1435
- `${Env.prefix}each-key`,
1436
- `${Env.prefix}each-arg`,
1437
- `${Env.prefix}each-index`,
1438
- ]);
1439
- const hasDynamicAttributes = fragment.getAttributeNames().some(name => {
1440
- if (allowedEachAttributes.has(name)) {
1441
- return false;
1442
- }
1443
- if (name.startsWith(`${Env.prefix}attr-`)) {
1444
- return true;
1445
- }
1446
- if (name.startsWith(Env.prefix)) {
1447
- return true;
1448
- }
1449
- const value = fragment.getRawAttribute(name);
1450
- return typeof value === 'string' && value.includes('{{');
1451
- });
1452
- if (hasDynamicAttributes) {
1453
- return true;
1454
- }
1455
- return fragment.getChildren().some(child => child instanceof TextFragment && child.hasDynamicContent());
1456
- }
1457
- /**
1458
- * fresh clone 初期化を subtree ごと省略できるかどうかを事前計算します。
1459
- *
1460
- * @param fragment 判定対象フラグメント
1461
- * @returns subtree 全体を省略可能なら true
1462
- */
1463
- static markFreshInitializationSkippable(fragment) {
1464
- const hasDynamicAttributes = fragment
1465
- .getAttributeNames()
1466
- .some(name => Core.isFreshInitializationDynamicAttribute(fragment, name));
1467
- const hasDynamicChildren = fragment.getChildren().some(child => {
1468
- if (child instanceof ElementFragment) {
1469
- return !Core.markFreshInitializationSkippable(child);
1470
- }
1471
- if (child instanceof TextFragment) {
1472
- return child.hasDynamicContent();
1473
- }
1474
- return false;
1475
- });
1476
- const skippable = !hasDynamicAttributes && !hasDynamicChildren;
1477
- fragment.setFreshInitializationSkippable(skippable);
1478
- return skippable;
1479
- }
1480
- /**
1481
- * fresh clone 初期化で再評価が必要な属性かどうかを返します。
1482
- *
1483
- * @param fragment 判定対象フラグメント
1484
- * @param name 属性名
1485
- * @returns 再評価が必要なら true
1486
- */
1487
- static isFreshInitializationDynamicAttribute(fragment, name) {
1488
- if (name.startsWith(`${Env.prefix}attr-`)) {
1489
- return true;
1490
- }
1491
- if (name.startsWith(Env.prefix)) {
1492
- return true;
1493
- }
1494
- const value = fragment.getRawAttribute(name);
1495
- return typeof value === 'string' && value.includes('{{');
1496
- }
1497
- /**
1498
- * 差分を更新します。
1499
- *
1500
- * @param parent 親フラグメント
1501
- * @param newList 新しいリスト
1502
- */
1503
- static updateDiff(parent, newList) {
1504
- const template = parent.getTemplate();
1505
- if (template === null) {
1506
- Log.error('[Haori]', 'Template is not set for each element.');
1507
- return Promise.resolve();
1508
- }
1509
- let indexKey = parent.getAttribute(`${Env.prefix}each-index`);
1510
- if (indexKey) {
1511
- indexKey = String(indexKey);
1512
- }
1513
- const keyArg = parent.getAttribute(`${Env.prefix}each-key`);
1514
- const itemArg = parent.getAttribute(`${Env.prefix}each-arg`);
1515
- const keyDataMap = new Map();
1516
- const newKeys = [];
1517
- newList.forEach((item, itemIndex) => {
1518
- const listKey = Core.createListKey(item, keyArg ? String(keyArg) : null, itemIndex);
1519
- newKeys.push(listKey);
1520
- keyDataMap.set(listKey, { item, itemIndex });
1521
- });
1522
- const newKeySet = new Set(newKeys);
1523
- const removalPromises = [];
1524
- let childElements = parent
1525
- .getChildren()
1526
- .filter(child => child instanceof ElementFragment)
1527
- .filter(child => !child.hasAttribute(`${Env.prefix}each-before`) &&
1528
- !child.hasAttribute(`${Env.prefix}each-after`));
1529
- const previousKeys = childElements.map(child => child.getListKey());
1530
- childElements = childElements.filter(child => {
1531
- if (!newKeySet.has(String(child.getListKey()))) {
1532
- removalPromises.push(child.remove());
1533
- return false;
1534
- }
1535
- return true;
1536
- });
1537
- const srcKeys = childElements.map(child => child.getListKey());
1538
- const childElementsByKey = new Map();
1539
- childElements.forEach(child => {
1540
- const listKey = child.getListKey();
1541
- if (listKey !== null && !childElementsByKey.has(listKey)) {
1542
- childElementsByKey.set(listKey, child);
1543
- }
1544
- });
1545
- const insertTargets = parent.getChildElementFragments().slice();
1546
- const baseInsertIndex = insertTargets.filter(child => child.hasAttribute(`${Env.prefix}each-before`)).length;
1547
- let chain = Promise.resolve();
1548
- newKeys.forEach((newKey, loopIndex) => {
1549
- const { item, itemIndex } = keyDataMap.get(newKey);
1550
- let child;
1551
- const reusedChild = childElementsByKey.get(newKey);
1552
- if (reusedChild) {
1553
- // 既存の要素を再利用
1554
- child = reusedChild;
1555
- // 行の入力が同一なら子孫の再評価をスキップする。
1556
- chain = chain.then(() => Core.updateRowFragment(child, item, indexKey, itemIndex, itemArg ? String(itemArg) : null, newKey).then(changed => {
1557
- if (!changed) {
1558
- return undefined;
1559
- }
1560
- return Core.evaluateAll(child);
1561
- }));
1562
- }
1563
- else {
1564
- // 新しい要素を追加
1565
- child = template.clone();
1566
- const currentInsertIndex = baseInsertIndex + loopIndex;
1567
- chain = chain.then(() => Core.updateRowFragment(child, item, indexKey, itemIndex, itemArg ? String(itemArg) : null, newKey).then(() => {
1568
- const referenceChild = insertTargets[currentInsertIndex] ?? null;
1569
- return parent
1570
- .insertBefore(child, referenceChild)
1571
- .then(() => {
1572
- insertTargets.splice(currentInsertIndex, 0, child);
1573
- })
1574
- .then(() => Core.initializeFreshEachRow(child));
1575
- }));
1576
- }
1577
- });
1578
- return Promise.all(removalPromises)
1579
- .then(() => chain)
1580
- .then(() => {
1581
- // eachupdate イベントを発火する。
1582
- // chain は全新規行の initializeFreshEachRow(= 行内容の描画)完了まで await
1583
- // しているため、本イベントは「今回の差分で追加・削除・並べ替えされた全行が
1584
- // DOM に反映され、各行の {{...}} 補間などの内容描画が完了した後」に発火する。
1585
- // これにより外部から data-each の描画完了を検知できる(仕様上の保証)。
1586
- const validNewKeys = newKeys.filter((key) => key !== null);
1587
- const validSrcKeys = srcKeys.filter((key) => key !== null);
1588
- const validSrcKeySet = new Set(validSrcKeys);
1589
- const addedKeys = validNewKeys.filter(key => !validSrcKeySet.has(key));
1590
- const previousValidKeys = previousKeys.filter((key) => key !== null);
1591
- const removedKeys = previousValidKeys.filter(key => !newKeySet.has(key));
1592
- HaoriEvent.eachUpdate(parent.getTarget(), addedKeys, removedKeys, validNewKeys);
1593
- return undefined;
1594
- });
1595
- }
1596
- /**
1597
- * リスト比較用のキーを生成します。
1598
- *
1599
- * @param item 対象オブジェクト
1600
- * @param keyArg リストキーに使用するプロパティ名
1601
- * @param index 配列のインデックス
1602
- * @returns リストキー
1603
- */
1604
- static createListKey(item, keyArg, index) {
1605
- let listKey;
1606
- if (typeof item === 'object' && item !== null) {
1607
- if (keyArg) {
1608
- const key = item[keyArg];
1609
- if (key === null || key === undefined) {
1610
- listKey = `__index_${index}`;
1611
- }
1612
- else if (typeof key == 'object') {
1613
- listKey = JSON.stringify(key);
1614
- }
1615
- else {
1616
- listKey = String(key);
1617
- }
1618
- }
1619
- else {
1620
- // data-each-key がない場合はインデックスをキーとして使用
1621
- listKey = `__index_${index}`;
1622
- }
1623
- }
1624
- else {
1625
- listKey = String(item);
1626
- }
1627
- return listKey;
1628
- }
1629
- /**
1630
- * 行フラグメントにデータを設定します。
1631
- *
1632
- * @param rowFragment 行フラグメント
1633
- * @param data 行データ
1634
- * @param indexKey インデックスキー
1635
- * @param index インデックス番号
1636
- * @param arg バインドデータパラメータ名
1637
- * @param listKey リストキー
1638
- * @returns 行メタデータの更新完了 Promise
1639
- */
1640
- static updateRowFragment(rowFragment, data, indexKey, index, arg, listKey) {
1641
- let bindingData = data;
1642
- if (typeof data === 'object' && data !== null) {
1643
- bindingData = { ...data };
1644
- if (indexKey) {
1645
- bindingData[indexKey] = index;
1646
- }
1647
- if (arg) {
1648
- bindingData = {
1649
- [arg]: bindingData,
1650
- };
1651
- }
1652
- }
1653
- else {
1654
- if (arg) {
1655
- bindingData = {
1656
- [arg]: data,
1657
- };
1658
- if (indexKey) {
1659
- bindingData[indexKey] = index;
1660
- }
1661
- }
1662
- else {
1663
- Log.error('[Haori]', `Primitive value requires '${Env.prefix}each-arg' attribute: ${data}`);
1664
- return Promise.resolve(false);
1665
- }
1666
- }
1667
- const normalizedBindingData = bindingData;
1668
- const nextRenderSignature = Core.createBindingSignature({
1669
- listKey,
1670
- bindingData: normalizedBindingData,
1671
- });
1672
- if (rowFragment.getListKey() === listKey &&
1673
- rowFragment.getRenderSignature() === nextRenderSignature) {
1674
- return Promise.resolve(false);
1675
- }
1676
- rowFragment.setListKey(listKey);
1677
- rowFragment.setRenderSignature(nextRenderSignature);
1678
- rowFragment.setBindingData(normalizedBindingData);
1679
- return rowFragment
1680
- .setAttribute(`${Env.prefix}row`, listKey)
1681
- .then(() => true);
1682
- }
1683
- /**
1684
- * 新規挿入行に遅延再評価が必要かどうかを判定します。
1685
- *
1686
- * @param fragment 判定対象の行フラグメント
1687
- * @returns 遅延再評価が必要なら true
1688
- */
1689
- static needsScheduledEvaluateAll(fragment) {
1690
- const stack = [fragment];
1691
- while (stack.length > 0) {
1692
- const current = stack.pop();
1693
- current.getChildElementFragments().forEach(child => {
1694
- stack.push(child);
1695
- });
1696
- if (current !== fragment &&
1697
- !current.isMounted() &&
1698
- Core.hasMountSensitiveAttribute(current)) {
1699
- return true;
1700
- }
1701
- }
1702
- return false;
1703
- }
1704
- /**
1705
- * mounted 状態に依存して再評価が必要になりやすい属性を持つかどうかを返します。
1706
- *
1707
- * @param fragment 判定対象フラグメント
1708
- * @returns 該当属性を持つなら true
1709
- */
1710
- static hasMountSensitiveAttribute(fragment) {
1711
- return ['fetch', 'import'].some(suffix => fragment.hasAttribute(`${Env.prefix}${suffix}`));
1712
- }
1713
- /**
1714
- * バインド値が同一かどうかを再帰的に判定します。
1715
- *
1716
- * @param left 比較元の値
1717
- * @param right 比較先の値
1718
- * @param visited 循環参照対策用の訪問済みペア
1719
- * @returns 同一なら true
1720
- */
1721
- static createBindingSignature(value, seen = new WeakMap(), nextId = { value: 0 }) {
1722
- if (value === null) {
1723
- return 'null';
1724
- }
1725
- if (value === undefined) {
1726
- return 'undefined';
1727
- }
1728
- if (typeof value === 'string') {
1729
- return JSON.stringify(value);
1730
- }
1731
- if (typeof value === 'number' ||
1732
- typeof value === 'boolean' ||
1733
- typeof value === 'bigint') {
1734
- return String(value);
1735
- }
1736
- if (typeof value === 'function') {
1737
- return `[Function:${value.name || 'anonymous'}]`;
1738
- }
1739
- if (typeof value === 'symbol') {
1740
- return value.toString();
1741
- }
1742
- if (value instanceof Date) {
1743
- return `[Date:${value.toISOString()}]`;
1744
- }
1745
- if (Array.isArray(value)) {
1746
- if (seen.has(value)) {
1747
- return `[Circular:${seen.get(value)}]`;
1748
- }
1749
- const marker = `array-${nextId.value}`;
1750
- nextId.value += 1;
1751
- seen.set(value, marker);
1752
- return `[${value
1753
- .map(item => Core.createBindingSignature(item, seen, nextId))
1754
- .join(',')}]`;
1755
- }
1756
- if (typeof value === 'object') {
1757
- if (seen.has(value)) {
1758
- return `[Circular:${seen.get(value)}]`;
1759
- }
1760
- const marker = `object-${nextId.value}`;
1761
- nextId.value += 1;
1762
- seen.set(value, marker);
1763
- const record = value;
1764
- return `{${Object.keys(record)
1765
- .sort()
1766
- .map(key => `${JSON.stringify(key)}:${Core.createBindingSignature(record[key], seen, nextId)}`)
1767
- .join(',')}}`;
1768
- }
1769
- return String(value);
1770
- }
1771
- /**
1772
- * フラグメントの再評価を次のイベントループで実行します。
1773
- *
1774
- * @param fragment 再評価対象のフラグメント
1775
- */
1776
- static scheduleEvaluateAll(fragment) {
1777
- setTimeout(() => {
1778
- void Core.evaluateAll(fragment);
1779
- }, 100);
1780
- }
1781
- }
1782
- /** 属性エイリアスのサフィックス */
1783
- Core.ATTRIBUTE_ALIAS_SUFFIX = 'attr-';
1784
- /** 優先処理する属性のサフィックス(処理順序で定義) */
1785
- Core.PRIORITY_ATTRIBUTE_SUFFIXES = [
1786
- 'bind',
1787
- 'url-param',
1788
- 'derive-name',
1789
- 'derive',
1790
- 'if',
1791
- 'each',
1792
- ];
1793
- /** 遅延処理する属性のサフィックス */
1794
- Core.DEFERRED_ATTRIBUTE_SUFFIXES = ['fetch'];
1795
- /** evaluateAll で再評価対象から除外する特殊属性のサフィックス */
1796
- Core.EVALUATE_ALL_EXCLUDED_ATTRIBUTE_SUFFIXES = [
1797
- 'bind',
1798
- 'derive',
1799
- 'derive-name',
1800
- 'if',
1801
- 'each',
1802
- 'fetch',
1803
- 'import',
1804
- 'url-param',
1805
- ];
1806
- /** 属性内プレースホルダ検出用の正規表現 */
1807
- Core.ATTRIBUTE_PLACEHOLDER_REGEX = /\{\{\{[\s\S]+?\}\}\}|\{\{[\s\S]+?\}\}/;
1808
- /** data-fetch の自動再評価状態 */
1809
- Core.REACTIVE_FETCH_STATES = new WeakMap();
1810
- /** data-import の自動再評価状態 */
1811
- Core.REACTIVE_IMPORT_STATES = new WeakMap();
1812
- /** data-derive subtree skip の開発用プロファイル */
1813
- Core.DERIVE_SUBTREE_PROFILES = new WeakMap();
1814
- /** data-each の差分更新の再入制御状態 */
1815
- Core.EACH_UPDATE_STATES = new WeakMap();
1816
- export default Core;
1817
- //# sourceMappingURL=core.js.map