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.
- package/README.ja.md +1 -1
- package/README.md +1 -1
- package/dist/haori.cjs.js +10 -10
- package/dist/haori.es.js +191 -165
- package/dist/haori.iife.js +10 -10
- package/dist/index.d.ts +1 -1
- package/package.json +11 -3
- package/dist/haori.cjs.js.map +0 -1
- package/dist/haori.es.js.map +0 -1
- package/dist/haori.iife.js.map +0 -1
- package/dist/package.json +0 -70
- package/dist/src/builtins.d.ts +0 -100
- package/dist/src/builtins.d.ts.map +0 -1
- package/dist/src/builtins.js +0 -194
- package/dist/src/builtins.js.map +0 -1
- package/dist/src/core.d.ts +0 -505
- package/dist/src/core.d.ts.map +0 -1
- package/dist/src/core.js +0 -1817
- package/dist/src/core.js.map +0 -1
- package/dist/src/dev.d.ts +0 -35
- package/dist/src/dev.d.ts.map +0 -1
- package/dist/src/dev.js +0 -44
- package/dist/src/dev.js.map +0 -1
- package/dist/src/env.d.ts +0 -43
- package/dist/src/env.d.ts.map +0 -1
- package/dist/src/env.js +0 -108
- package/dist/src/env.js.map +0 -1
- package/dist/src/event.d.ts +0 -171
- package/dist/src/event.d.ts.map +0 -1
- package/dist/src/event.js +0 -235
- package/dist/src/event.js.map +0 -1
- package/dist/src/event_dispatcher.d.ts +0 -66
- package/dist/src/event_dispatcher.d.ts.map +0 -1
- package/dist/src/event_dispatcher.js +0 -167
- package/dist/src/event_dispatcher.js.map +0 -1
- package/dist/src/expression.d.ts +0 -260
- package/dist/src/expression.d.ts.map +0 -1
- package/dist/src/expression.js +0 -975
- package/dist/src/expression.js.map +0 -1
- package/dist/src/form.d.ts +0 -148
- package/dist/src/form.d.ts.map +0 -1
- package/dist/src/form.js +0 -428
- package/dist/src/form.js.map +0 -1
- package/dist/src/fragment.d.ts +0 -621
- package/dist/src/fragment.d.ts.map +0 -1
- package/dist/src/fragment.js +0 -1963
- package/dist/src/fragment.js.map +0 -1
- package/dist/src/haori.d.ts +0 -130
- package/dist/src/haori.d.ts.map +0 -1
- package/dist/src/haori.js +0 -215
- package/dist/src/haori.js.map +0 -1
- package/dist/src/import.d.ts +0 -19
- package/dist/src/import.d.ts.map +0 -1
- package/dist/src/import.js +0 -64
- package/dist/src/import.js.map +0 -1
- package/dist/src/index.d.ts +0 -27
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -30
- package/dist/src/index.js.map +0 -1
- package/dist/src/intersect.d.ts +0 -28
- package/dist/src/intersect.d.ts.map +0 -1
- package/dist/src/intersect.js +0 -201
- package/dist/src/intersect.js.map +0 -1
- package/dist/src/log.d.ts +0 -32
- package/dist/src/log.d.ts.map +0 -1
- package/dist/src/log.js +0 -43
- package/dist/src/log.js.map +0 -1
- package/dist/src/observer.d.ts +0 -24
- package/dist/src/observer.d.ts.map +0 -1
- package/dist/src/observer.js +0 -131
- package/dist/src/observer.js.map +0 -1
- package/dist/src/procedure.d.ts +0 -447
- package/dist/src/procedure.d.ts.map +0 -1
- package/dist/src/procedure.js +0 -1921
- package/dist/src/procedure.js.map +0 -1
- package/dist/src/queue.d.ts +0 -35
- package/dist/src/queue.d.ts.map +0 -1
- package/dist/src/queue.js +0 -202
- package/dist/src/queue.js.map +0 -1
- package/dist/src/url.d.ts +0 -14
- package/dist/src/url.d.ts.map +0 -1
- package/dist/src/url.js +0 -22
- package/dist/src/url.js.map +0 -1
- package/dist/tests/bind-arg-own-binding.test.d.ts +0 -2
- package/dist/tests/bind-arg-own-binding.test.d.ts.map +0 -1
- package/dist/tests/bind-arg-own-binding.test.js +0 -63
- package/dist/tests/bind-arg-own-binding.test.js.map +0 -1
- package/dist/tests/bindcomplete-and-scope.test.d.ts +0 -2
- package/dist/tests/bindcomplete-and-scope.test.d.ts.map +0 -1
- package/dist/tests/bindcomplete-and-scope.test.js +0 -115
- package/dist/tests/bindcomplete-and-scope.test.js.map +0 -1
- package/dist/tests/builtins.test.d.ts +0 -2
- package/dist/tests/builtins.test.d.ts.map +0 -1
- package/dist/tests/builtins.test.js +0 -152
- package/dist/tests/builtins.test.js.map +0 -1
- package/dist/tests/click-attributes.test.d.ts +0 -2
- package/dist/tests/click-attributes.test.d.ts.map +0 -1
- package/dist/tests/click-attributes.test.js +0 -113
- package/dist/tests/click-attributes.test.js.map +0 -1
- package/dist/tests/click-no-disabled.test.d.ts +0 -2
- package/dist/tests/click-no-disabled.test.d.ts.map +0 -1
- package/dist/tests/click-no-disabled.test.js +0 -67
- package/dist/tests/click-no-disabled.test.js.map +0 -1
- package/dist/tests/core.test.d.ts +0 -5
- package/dist/tests/core.test.d.ts.map +0 -1
- package/dist/tests/core.test.js +0 -1056
- package/dist/tests/core.test.js.map +0 -1
- package/dist/tests/data-bind-arg-reeval.test.d.ts +0 -2
- package/dist/tests/data-bind-arg-reeval.test.d.ts.map +0 -1
- package/dist/tests/data-bind-arg-reeval.test.js +0 -119
- package/dist/tests/data-bind-arg-reeval.test.js.map +0 -1
- package/dist/tests/data-bind-merge.test.d.ts +0 -2
- package/dist/tests/data-bind-merge.test.d.ts.map +0 -1
- package/dist/tests/data-bind-merge.test.js +0 -86
- package/dist/tests/data-bind-merge.test.js.map +0 -1
- package/dist/tests/data-click-defer.test.d.ts +0 -2
- package/dist/tests/data-click-defer.test.d.ts.map +0 -1
- package/dist/tests/data-click-defer.test.js +0 -57
- package/dist/tests/data-click-defer.test.js.map +0 -1
- package/dist/tests/data-click-run.test.d.ts +0 -5
- package/dist/tests/data-click-run.test.d.ts.map +0 -1
- package/dist/tests/data-click-run.test.js +0 -109
- package/dist/tests/data-click-run.test.js.map +0 -1
- package/dist/tests/data-derive.test.d.ts +0 -2
- package/dist/tests/data-derive.test.d.ts.map +0 -1
- package/dist/tests/data-derive.test.js +0 -999
- package/dist/tests/data-derive.test.js.map +0 -1
- package/dist/tests/data-each-browserlike.test.d.ts +0 -2
- package/dist/tests/data-each-browserlike.test.d.ts.map +0 -1
- package/dist/tests/data-each-browserlike.test.js +0 -48
- package/dist/tests/data-each-browserlike.test.js.map +0 -1
- package/dist/tests/data-each-fragment-debug.test.d.ts +0 -2
- package/dist/tests/data-each-fragment-debug.test.d.ts.map +0 -1
- package/dist/tests/data-each-fragment-debug.test.js +0 -119
- package/dist/tests/data-each-fragment-debug.test.js.map +0 -1
- package/dist/tests/data-each-table.test.d.ts +0 -2
- package/dist/tests/data-each-table.test.d.ts.map +0 -1
- package/dist/tests/data-each-table.test.js +0 -131
- package/dist/tests/data-each-table.test.js.map +0 -1
- package/dist/tests/data-each-template-recovery.test.d.ts +0 -2
- package/dist/tests/data-each-template-recovery.test.d.ts.map +0 -1
- package/dist/tests/data-each-template-recovery.test.js +0 -87
- package/dist/tests/data-each-template-recovery.test.js.map +0 -1
- package/dist/tests/data-event-prevent.test.d.ts +0 -2
- package/dist/tests/data-event-prevent.test.d.ts.map +0 -1
- package/dist/tests/data-event-prevent.test.js +0 -76
- package/dist/tests/data-event-prevent.test.js.map +0 -1
- package/dist/tests/data-fetch-form-input-binding.test.d.ts +0 -2
- package/dist/tests/data-fetch-form-input-binding.test.d.ts.map +0 -1
- package/dist/tests/data-fetch-form-input-binding.test.js +0 -202
- package/dist/tests/data-fetch-form-input-binding.test.js.map +0 -1
- package/dist/tests/data-fetch-tbody-dom.test.d.ts +0 -2
- package/dist/tests/data-fetch-tbody-dom.test.d.ts.map +0 -1
- package/dist/tests/data-fetch-tbody-dom.test.js +0 -82
- package/dist/tests/data-fetch-tbody-dom.test.js.map +0 -1
- package/dist/tests/data-form-container.test.d.ts +0 -2
- package/dist/tests/data-form-container.test.d.ts.map +0 -1
- package/dist/tests/data-form-container.test.js +0 -88
- package/dist/tests/data-form-container.test.js.map +0 -1
- package/dist/tests/data-if-dev-diagnostics.test.d.ts +0 -2
- package/dist/tests/data-if-dev-diagnostics.test.d.ts.map +0 -1
- package/dist/tests/data-if-dev-diagnostics.test.js +0 -55
- package/dist/tests/data-if-dev-diagnostics.test.js.map +0 -1
- package/dist/tests/data-if-falsy.test.d.ts +0 -2
- package/dist/tests/data-if-falsy.test.d.ts.map +0 -1
- package/dist/tests/data-if-falsy.test.js +0 -73
- package/dist/tests/data-if-falsy.test.js.map +0 -1
- package/dist/tests/data-load-on-show.test.d.ts +0 -2
- package/dist/tests/data-load-on-show.test.d.ts.map +0 -1
- package/dist/tests/data-load-on-show.test.js +0 -98
- package/dist/tests/data-load-on-show.test.js.map +0 -1
- package/dist/tests/derive-each-render-wait.test.d.ts +0 -2
- package/dist/tests/derive-each-render-wait.test.d.ts.map +0 -1
- package/dist/tests/derive-each-render-wait.test.js +0 -90
- package/dist/tests/derive-each-render-wait.test.js.map +0 -1
- package/dist/tests/dev.test.d.ts +0 -2
- package/dist/tests/dev.test.d.ts.map +0 -1
- package/dist/tests/dev.test.js +0 -51
- package/dist/tests/dev.test.js.map +0 -1
- package/dist/tests/each-update-event.test.d.ts +0 -2
- package/dist/tests/each-update-event.test.d.ts.map +0 -1
- package/dist/tests/each-update-event.test.js +0 -83
- package/dist/tests/each-update-event.test.js.map +0 -1
- package/dist/tests/each_arg.test.d.ts +0 -2
- package/dist/tests/each_arg.test.d.ts.map +0 -1
- package/dist/tests/each_arg.test.js +0 -41
- package/dist/tests/each_arg.test.js.map +0 -1
- package/dist/tests/env.test.d.ts +0 -2
- package/dist/tests/env.test.d.ts.map +0 -1
- package/dist/tests/env.test.js +0 -124
- package/dist/tests/env.test.js.map +0 -1
- package/dist/tests/evaluation-profile.test.d.ts +0 -2
- package/dist/tests/evaluation-profile.test.d.ts.map +0 -1
- package/dist/tests/evaluation-profile.test.js +0 -92
- package/dist/tests/evaluation-profile.test.js.map +0 -1
- package/dist/tests/event.test.d.ts +0 -2
- package/dist/tests/event.test.d.ts.map +0 -1
- package/dist/tests/event.test.js +0 -300
- package/dist/tests/event.test.js.map +0 -1
- package/dist/tests/expression-disallowed-keyword-hint.test.d.ts +0 -2
- package/dist/tests/expression-disallowed-keyword-hint.test.d.ts.map +0 -1
- package/dist/tests/expression-disallowed-keyword-hint.test.js +0 -45
- package/dist/tests/expression-disallowed-keyword-hint.test.js.map +0 -1
- package/dist/tests/expression-forbidden-hint.test.d.ts +0 -2
- package/dist/tests/expression-forbidden-hint.test.d.ts.map +0 -1
- package/dist/tests/expression-forbidden-hint.test.js +0 -37
- package/dist/tests/expression-forbidden-hint.test.js.map +0 -1
- package/dist/tests/expression.test.d.ts +0 -2
- package/dist/tests/expression.test.d.ts.map +0 -1
- package/dist/tests/expression.test.js +0 -482
- package/dist/tests/expression.test.js.map +0 -1
- package/dist/tests/fetch-and-procedure-scenarios.test.d.ts +0 -2
- package/dist/tests/fetch-and-procedure-scenarios.test.d.ts.map +0 -1
- package/dist/tests/fetch-and-procedure-scenarios.test.js +0 -195
- package/dist/tests/fetch-and-procedure-scenarios.test.js.map +0 -1
- package/dist/tests/form-number-type.test.d.ts +0 -2
- package/dist/tests/form-number-type.test.d.ts.map +0 -1
- package/dist/tests/form-number-type.test.js +0 -83
- package/dist/tests/form-number-type.test.js.map +0 -1
- package/dist/tests/form.test.d.ts +0 -2
- package/dist/tests/form.test.d.ts.map +0 -1
- package/dist/tests/form.test.js +0 -636
- package/dist/tests/form.test.js.map +0 -1
- package/dist/tests/fragment.test.d.ts +0 -2
- package/dist/tests/fragment.test.d.ts.map +0 -1
- package/dist/tests/fragment.test.js +0 -284
- package/dist/tests/fragment.test.js.map +0 -1
- package/dist/tests/haori.test.d.ts +0 -2
- package/dist/tests/haori.test.d.ts.map +0 -1
- package/dist/tests/haori.test.js +0 -149
- package/dist/tests/haori.test.js.map +0 -1
- package/dist/tests/helpers/async.d.ts +0 -20
- package/dist/tests/helpers/async.d.ts.map +0 -1
- package/dist/tests/helpers/async.js +0 -35
- package/dist/tests/helpers/async.js.map +0 -1
- package/dist/tests/import.test.d.ts +0 -2
- package/dist/tests/import.test.d.ts.map +0 -1
- package/dist/tests/import.test.js +0 -148
- package/dist/tests/import.test.js.map +0 -1
- package/dist/tests/intersect.test.d.ts +0 -2
- package/dist/tests/intersect.test.d.ts.map +0 -1
- package/dist/tests/intersect.test.js +0 -173
- package/dist/tests/intersect.test.js.map +0 -1
- package/dist/tests/log.test.d.ts +0 -2
- package/dist/tests/log.test.d.ts.map +0 -1
- package/dist/tests/log.test.js +0 -58
- package/dist/tests/log.test.js.map +0 -1
- package/dist/tests/observer.test.d.ts +0 -2
- package/dist/tests/observer.test.d.ts.map +0 -1
- package/dist/tests/observer.test.js +0 -64
- package/dist/tests/observer.test.js.map +0 -1
- package/dist/tests/placeholder-resolution.test.d.ts +0 -2
- package/dist/tests/placeholder-resolution.test.d.ts.map +0 -1
- package/dist/tests/placeholder-resolution.test.js +0 -463
- package/dist/tests/placeholder-resolution.test.js.map +0 -1
- package/dist/tests/procedure-action-operations.test.d.ts +0 -2
- package/dist/tests/procedure-action-operations.test.d.ts.map +0 -1
- package/dist/tests/procedure-action-operations.test.js +0 -1140
- package/dist/tests/procedure-action-operations.test.js.map +0 -1
- package/dist/tests/procedure-bind-append.test.d.ts +0 -2
- package/dist/tests/procedure-bind-append.test.d.ts.map +0 -1
- package/dist/tests/procedure-bind-append.test.js +0 -80
- package/dist/tests/procedure-bind-append.test.js.map +0 -1
- package/dist/tests/procedure-fetch-options.test.d.ts +0 -2
- package/dist/tests/procedure-fetch-options.test.d.ts.map +0 -1
- package/dist/tests/procedure-fetch-options.test.js +0 -255
- package/dist/tests/procedure-fetch-options.test.js.map +0 -1
- package/dist/tests/procedure.test.d.ts +0 -2
- package/dist/tests/procedure.test.d.ts.map +0 -1
- package/dist/tests/procedure.test.js +0 -137
- package/dist/tests/procedure.test.js.map +0 -1
- package/dist/tests/procedure_events.test.d.ts +0 -7
- package/dist/tests/procedure_events.test.d.ts.map +0 -1
- package/dist/tests/procedure_events.test.js +0 -261
- package/dist/tests/procedure_events.test.js.map +0 -1
- package/dist/tests/render-wait.test.d.ts +0 -2
- package/dist/tests/render-wait.test.d.ts.map +0 -1
- package/dist/tests/render-wait.test.js +0 -68
- package/dist/tests/render-wait.test.js.map +0 -1
- package/dist/tests/reset_each.test.d.ts +0 -2
- package/dist/tests/reset_each.test.d.ts.map +0 -1
- package/dist/tests/reset_each.test.js +0 -215
- package/dist/tests/reset_each.test.js.map +0 -1
- package/dist/tests/row-move.test.d.ts +0 -2
- package/dist/tests/row-move.test.d.ts.map +0 -1
- package/dist/tests/row-move.test.js +0 -220
- package/dist/tests/row-move.test.js.map +0 -1
- package/dist/tests/row-operations.test.d.ts +0 -2
- package/dist/tests/row-operations.test.d.ts.map +0 -1
- package/dist/tests/row-operations.test.js +0 -259
- package/dist/tests/row-operations.test.js.map +0 -1
- package/dist/tests/scheduled-evaluate-wait.test.d.ts +0 -2
- package/dist/tests/scheduled-evaluate-wait.test.d.ts.map +0 -1
- package/dist/tests/scheduled-evaluate-wait.test.js +0 -68
- package/dist/tests/scheduled-evaluate-wait.test.js.map +0 -1
- package/dist/tests/url.test.d.ts +0 -2
- package/dist/tests/url.test.d.ts.map +0 -1
- package/dist/tests/url.test.js +0 -150
- package/dist/tests/url.test.js.map +0 -1
- package/dist/vite.config.d.ts +0 -3
- package/dist/vite.config.d.ts.map +0 -1
- package/dist/vite.config.js +0 -28
- package/dist/vite.config.js.map +0 -1
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
- package/dist/vitest.config.js +0 -19
- 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
|