haori 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ja.md +157 -0
- package/README.md +158 -0
- package/dist/haori.cjs.js +13 -0
- package/dist/haori.cjs.js.map +1 -0
- package/dist/haori.es.js +2929 -0
- package/dist/haori.es.js.map +1 -0
- package/dist/haori.iife.js +13 -0
- package/dist/haori.iife.js.map +1 -0
- package/dist/index.d.ts +824 -0
- package/dist/src/core.d.ts +144 -0
- package/dist/src/core.d.ts.map +1 -0
- package/dist/src/core.js +605 -0
- package/dist/src/core.js.map +1 -0
- package/dist/src/dev.d.ts +35 -0
- package/dist/src/dev.d.ts.map +1 -0
- package/dist/src/dev.js +44 -0
- package/dist/src/dev.js.map +1 -0
- package/dist/src/env.d.ts +25 -0
- package/dist/src/env.d.ts.map +1 -0
- package/dist/src/env.js +64 -0
- package/dist/src/env.js.map +1 -0
- package/dist/src/event.d.ts +144 -0
- package/dist/src/event.d.ts.map +1 -0
- package/dist/src/event.js +221 -0
- package/dist/src/event.js.map +1 -0
- package/dist/src/event_dispatcher.d.ts +50 -0
- package/dist/src/event_dispatcher.d.ts.map +1 -0
- package/dist/src/event_dispatcher.js +99 -0
- package/dist/src/event_dispatcher.js.map +1 -0
- package/dist/src/expression.d.ts +39 -0
- package/dist/src/expression.d.ts.map +1 -0
- package/dist/src/expression.js +148 -0
- package/dist/src/expression.js.map +1 -0
- package/dist/src/form.d.ts +113 -0
- package/dist/src/form.d.ts.map +1 -0
- package/dist/src/form.js +361 -0
- package/dist/src/form.js.map +1 -0
- package/dist/src/fragment.d.ts +427 -0
- package/dist/src/fragment.d.ts.map +1 -0
- package/dist/src/fragment.js +1168 -0
- package/dist/src/fragment.js.map +1 -0
- package/dist/src/haori.d.ts +54 -0
- package/dist/src/haori.d.ts.map +1 -0
- package/dist/src/haori.js +120 -0
- package/dist/src/haori.js.map +1 -0
- package/dist/src/import.d.ts +19 -0
- package/dist/src/import.d.ts.map +1 -0
- package/dist/src/import.js +64 -0
- package/dist/src/import.js.map +1 -0
- package/dist/src/index.d.ts +17 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +21 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/log.d.ts +32 -0
- package/dist/src/log.d.ts.map +1 -0
- package/dist/src/log.js +43 -0
- package/dist/src/log.js.map +1 -0
- package/dist/src/observer.d.ts +17 -0
- package/dist/src/observer.d.ts.map +1 -0
- package/dist/src/observer.js +102 -0
- package/dist/src/observer.js.map +1 -0
- package/dist/src/procedure.d.ts +203 -0
- package/dist/src/procedure.d.ts.map +1 -0
- package/dist/src/procedure.js +1040 -0
- package/dist/src/procedure.js.map +1 -0
- package/dist/src/queue.d.ts +28 -0
- package/dist/src/queue.d.ts.map +1 -0
- package/dist/src/queue.js +150 -0
- package/dist/src/queue.js.map +1 -0
- package/dist/src/url.d.ts +14 -0
- package/dist/src/url.d.ts.map +1 -0
- package/dist/src/url.js +22 -0
- package/dist/src/url.js.map +1 -0
- package/dist/tests/click-attributes.test.d.ts +2 -0
- package/dist/tests/click-attributes.test.d.ts.map +1 -0
- package/dist/tests/click-attributes.test.js +95 -0
- package/dist/tests/click-attributes.test.js.map +1 -0
- package/dist/tests/core.test.d.ts +5 -0
- package/dist/tests/core.test.d.ts.map +1 -0
- package/dist/tests/core.test.js +158 -0
- package/dist/tests/core.test.js.map +1 -0
- package/dist/tests/data-each-browserlike.test.d.ts +2 -0
- package/dist/tests/data-each-browserlike.test.d.ts.map +1 -0
- package/dist/tests/data-each-browserlike.test.js +48 -0
- package/dist/tests/data-each-browserlike.test.js.map +1 -0
- package/dist/tests/data-each-fragment-debug.test.d.ts +2 -0
- package/dist/tests/data-each-fragment-debug.test.d.ts.map +1 -0
- package/dist/tests/data-each-fragment-debug.test.js +119 -0
- package/dist/tests/data-each-fragment-debug.test.js.map +1 -0
- package/dist/tests/data-each-table.test.d.ts +2 -0
- package/dist/tests/data-each-table.test.d.ts.map +1 -0
- package/dist/tests/data-each-table.test.js +63 -0
- package/dist/tests/data-each-table.test.js.map +1 -0
- package/dist/tests/dev.test.d.ts +2 -0
- package/dist/tests/dev.test.d.ts.map +1 -0
- package/dist/tests/dev.test.js +51 -0
- package/dist/tests/dev.test.js.map +1 -0
- package/dist/tests/each_arg.test.d.ts +2 -0
- package/dist/tests/each_arg.test.d.ts.map +1 -0
- package/dist/tests/each_arg.test.js +41 -0
- package/dist/tests/each_arg.test.js.map +1 -0
- package/dist/tests/env.test.d.ts +2 -0
- package/dist/tests/env.test.d.ts.map +1 -0
- package/dist/tests/env.test.js +96 -0
- package/dist/tests/env.test.js.map +1 -0
- package/dist/tests/event.test.d.ts +2 -0
- package/dist/tests/event.test.d.ts.map +1 -0
- package/dist/tests/event.test.js +287 -0
- package/dist/tests/event.test.js.map +1 -0
- package/dist/tests/expression.test.d.ts +2 -0
- package/dist/tests/expression.test.d.ts.map +1 -0
- package/dist/tests/expression.test.js +281 -0
- package/dist/tests/expression.test.js.map +1 -0
- package/dist/tests/fetch-and-procedure-scenarios.test.d.ts +2 -0
- package/dist/tests/fetch-and-procedure-scenarios.test.d.ts.map +1 -0
- package/dist/tests/fetch-and-procedure-scenarios.test.js +133 -0
- package/dist/tests/fetch-and-procedure-scenarios.test.js.map +1 -0
- package/dist/tests/form.test.d.ts +2 -0
- package/dist/tests/form.test.d.ts.map +1 -0
- package/dist/tests/form.test.js +607 -0
- package/dist/tests/form.test.js.map +1 -0
- package/dist/tests/fragment.test.d.ts +2 -0
- package/dist/tests/fragment.test.d.ts.map +1 -0
- package/dist/tests/fragment.test.js +164 -0
- package/dist/tests/fragment.test.js.map +1 -0
- package/dist/tests/import.test.d.ts +2 -0
- package/dist/tests/import.test.d.ts.map +1 -0
- package/dist/tests/import.test.js +148 -0
- package/dist/tests/import.test.js.map +1 -0
- package/dist/tests/log.test.d.ts +2 -0
- package/dist/tests/log.test.d.ts.map +1 -0
- package/dist/tests/log.test.js +58 -0
- package/dist/tests/log.test.js.map +1 -0
- package/dist/tests/procedure-action-operations.test.d.ts +2 -0
- package/dist/tests/procedure-action-operations.test.d.ts.map +1 -0
- package/dist/tests/procedure-action-operations.test.js +148 -0
- package/dist/tests/procedure-action-operations.test.js.map +1 -0
- package/dist/tests/procedure-fetch-options.test.d.ts +2 -0
- package/dist/tests/procedure-fetch-options.test.d.ts.map +1 -0
- package/dist/tests/procedure-fetch-options.test.js +131 -0
- package/dist/tests/procedure-fetch-options.test.js.map +1 -0
- package/dist/tests/procedure.test.d.ts +2 -0
- package/dist/tests/procedure.test.d.ts.map +1 -0
- package/dist/tests/procedure.test.js +136 -0
- package/dist/tests/procedure.test.js.map +1 -0
- package/dist/tests/procedure_events.test.d.ts +7 -0
- package/dist/tests/procedure_events.test.d.ts.map +1 -0
- package/dist/tests/procedure_events.test.js +96 -0
- package/dist/tests/procedure_events.test.js.map +1 -0
- package/dist/tests/reset_each.test.d.ts +2 -0
- package/dist/tests/reset_each.test.d.ts.map +1 -0
- package/dist/tests/reset_each.test.js +215 -0
- package/dist/tests/reset_each.test.js.map +1 -0
- package/dist/tests/row-move.test.d.ts +2 -0
- package/dist/tests/row-move.test.d.ts.map +1 -0
- package/dist/tests/row-move.test.js +197 -0
- package/dist/tests/row-move.test.js.map +1 -0
- package/dist/tests/row-operations.test.d.ts +2 -0
- package/dist/tests/row-operations.test.d.ts.map +1 -0
- package/dist/tests/row-operations.test.js +238 -0
- package/dist/tests/row-operations.test.js.map +1 -0
- package/dist/tests/url.test.d.ts +2 -0
- package/dist/tests/url.test.d.ts.map +1 -0
- package/dist/tests/url.test.js +150 -0
- package/dist/tests/url.test.js.map +1 -0
- package/dist/vite.config.d.ts +3 -0
- package/dist/vite.config.d.ts.map +1 -0
- package/dist/vite.config.js +28 -0
- package/dist/vite.config.js.map +1 -0
- package/dist/vitest.config.d.ts +3 -0
- package/dist/vitest.config.d.ts.map +1 -0
- package/dist/vitest.config.js +19 -0
- package/dist/vitest.config.js.map +1 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview 仮想DOM実装
|
|
3
|
+
*
|
|
4
|
+
* メモリ上にノードツリーを保持し、DOMへの反映を非同期で行います。
|
|
5
|
+
* DOMからの読み込みは行わず、オブザーバーとchangeイベントで更新されます。
|
|
6
|
+
*/
|
|
7
|
+
import Queue from './queue';
|
|
8
|
+
import Log from './log';
|
|
9
|
+
import Expression from './expression';
|
|
10
|
+
import Env from './env';
|
|
11
|
+
/**
|
|
12
|
+
* 仮想DOMのフラグメントの抽象クラス。
|
|
13
|
+
*/
|
|
14
|
+
class Fragment {
|
|
15
|
+
static get(node) {
|
|
16
|
+
if (node == null) {
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
if (Fragment.FRAGMENT_CACHE.has(node)) {
|
|
20
|
+
return Fragment.FRAGMENT_CACHE.get(node);
|
|
21
|
+
}
|
|
22
|
+
let fragment;
|
|
23
|
+
switch (node.nodeType) {
|
|
24
|
+
case Node.ELEMENT_NODE:
|
|
25
|
+
fragment = new ElementFragment(node);
|
|
26
|
+
break;
|
|
27
|
+
case Node.TEXT_NODE:
|
|
28
|
+
fragment = new TextFragment(node);
|
|
29
|
+
break;
|
|
30
|
+
case Node.COMMENT_NODE:
|
|
31
|
+
fragment = new CommentFragment(node);
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
Log.warn('[Haori]', 'Unsupported node type:', node.nodeType);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
return fragment;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* フラグメントのコンストラクタ。
|
|
41
|
+
*
|
|
42
|
+
* @param target 対象ノード
|
|
43
|
+
*/
|
|
44
|
+
constructor(target) {
|
|
45
|
+
/** 親フラグメント */
|
|
46
|
+
this.parent = null;
|
|
47
|
+
/** フラグメントがDOMにマウントされているかどうか */
|
|
48
|
+
this.mounted = false;
|
|
49
|
+
/** ノード更新スキップフラグ(オブザーバーによる無限ループ対応) */
|
|
50
|
+
this.skipMutationNodes = false;
|
|
51
|
+
this.target = target;
|
|
52
|
+
Fragment.FRAGMENT_CACHE.set(target, this);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* skipMutationNodesフラグの値を取得します。
|
|
56
|
+
*
|
|
57
|
+
* @returns skipMutationNodesの値
|
|
58
|
+
*/
|
|
59
|
+
isSkipMutationNodes() {
|
|
60
|
+
return this.skipMutationNodes;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* フラグメントをDOMから除去します。
|
|
64
|
+
*
|
|
65
|
+
* @return 除去のPromise
|
|
66
|
+
*/
|
|
67
|
+
unmount() {
|
|
68
|
+
if (!this.mounted || this.skipMutationNodes) {
|
|
69
|
+
return Promise.resolve();
|
|
70
|
+
}
|
|
71
|
+
if (this.parent) {
|
|
72
|
+
const parent = this.parent;
|
|
73
|
+
const prevSkip = parent.skipMutationNodes;
|
|
74
|
+
return Queue.enqueue(() => {
|
|
75
|
+
parent.skipMutationNodes = true;
|
|
76
|
+
if (this.target.parentNode === parent.getTarget()) {
|
|
77
|
+
parent.getTarget().removeChild(this.target);
|
|
78
|
+
}
|
|
79
|
+
this.mounted = false;
|
|
80
|
+
}).finally(() => {
|
|
81
|
+
parent.skipMutationNodes = prevSkip;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// 親フラグメント情報が無くても、DOM 上に親ノードが存在する場合は安全に除去する。
|
|
86
|
+
const host = this.target.parentNode;
|
|
87
|
+
if (host) {
|
|
88
|
+
return Queue.enqueue(() => {
|
|
89
|
+
if (this.target.parentNode === host) {
|
|
90
|
+
host.removeChild(this.target);
|
|
91
|
+
}
|
|
92
|
+
this.mounted = false;
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
this.mounted = false;
|
|
96
|
+
}
|
|
97
|
+
return Promise.resolve();
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* フラグメントをDOMに追加します。
|
|
101
|
+
*
|
|
102
|
+
* @return 追加のPromise
|
|
103
|
+
*/
|
|
104
|
+
mount() {
|
|
105
|
+
if (this.mounted || this.skipMutationNodes) {
|
|
106
|
+
return Promise.resolve();
|
|
107
|
+
}
|
|
108
|
+
if (this.parent) {
|
|
109
|
+
const parent = this.parent;
|
|
110
|
+
const prevSkip = parent.skipMutationNodes;
|
|
111
|
+
return Queue.enqueue(() => {
|
|
112
|
+
parent.skipMutationNodes = true;
|
|
113
|
+
if (this.target.parentNode !== parent.getTarget()) {
|
|
114
|
+
// 既に同じ親なら何もしない
|
|
115
|
+
parent.getTarget().appendChild(this.target);
|
|
116
|
+
}
|
|
117
|
+
this.mounted = true;
|
|
118
|
+
}).finally(() => {
|
|
119
|
+
parent.skipMutationNodes = prevSkip;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return Promise.resolve();
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* フラグメントのマウント状態を取得します。
|
|
126
|
+
*
|
|
127
|
+
* @returns マウント状態
|
|
128
|
+
*/
|
|
129
|
+
isMounted() {
|
|
130
|
+
return this.mounted;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* フラグメントのマウント状態を設定します。
|
|
134
|
+
*
|
|
135
|
+
* @param mounted マウント状態
|
|
136
|
+
*/
|
|
137
|
+
setMounted(mounted) {
|
|
138
|
+
this.mounted = mounted;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* フラグメントとノードを削除します。
|
|
142
|
+
*
|
|
143
|
+
* @param unmount DOMからの除去を行うかどうか(内部の子呼び出しの場合のみfalseとする)
|
|
144
|
+
* @return 除去のPromise
|
|
145
|
+
*/
|
|
146
|
+
remove(unmount = true) {
|
|
147
|
+
if (this.parent) {
|
|
148
|
+
this.parent.removeChild(this);
|
|
149
|
+
}
|
|
150
|
+
Fragment.FRAGMENT_CACHE.delete(this.target);
|
|
151
|
+
if (unmount) {
|
|
152
|
+
return this.unmount();
|
|
153
|
+
}
|
|
154
|
+
return Promise.resolve();
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* 対象ノードを取得します。
|
|
158
|
+
*
|
|
159
|
+
* @returns 対象ノード
|
|
160
|
+
*/
|
|
161
|
+
getTarget() {
|
|
162
|
+
return this.target;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 親フラグメントを取得します。
|
|
166
|
+
*
|
|
167
|
+
* @returns 親フラグメント
|
|
168
|
+
*/
|
|
169
|
+
getParent() {
|
|
170
|
+
return this.parent;
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* 親フラグメントを設定します。
|
|
174
|
+
*
|
|
175
|
+
* @param parent 親フラグメント
|
|
176
|
+
*/
|
|
177
|
+
setParent(parent) {
|
|
178
|
+
this.parent = parent;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/** フラグメントの対象ノードに対するキャッシュ */
|
|
182
|
+
Fragment.FRAGMENT_CACHE = new WeakMap();
|
|
183
|
+
export default Fragment;
|
|
184
|
+
/**
|
|
185
|
+
* エレメントフラグメント。
|
|
186
|
+
* DOM要素を表現し、子ノードを持つことができます。
|
|
187
|
+
*/
|
|
188
|
+
export class ElementFragment extends Fragment {
|
|
189
|
+
/**
|
|
190
|
+
* エレメントフラグメントのコンストラクタ。
|
|
191
|
+
* アトリビュートや子フラグメントの作成も行います。
|
|
192
|
+
*
|
|
193
|
+
* @param target 対象エレメント
|
|
194
|
+
*/
|
|
195
|
+
constructor(target) {
|
|
196
|
+
super(target);
|
|
197
|
+
/** inputイベントを発生させるタイプ */
|
|
198
|
+
this.INPUT_EVENT_TYPES = [
|
|
199
|
+
'text',
|
|
200
|
+
'password',
|
|
201
|
+
'email',
|
|
202
|
+
'url',
|
|
203
|
+
'tel',
|
|
204
|
+
'search',
|
|
205
|
+
'number',
|
|
206
|
+
'range',
|
|
207
|
+
'color',
|
|
208
|
+
'date',
|
|
209
|
+
'datetime-local',
|
|
210
|
+
'month',
|
|
211
|
+
'time',
|
|
212
|
+
'week',
|
|
213
|
+
];
|
|
214
|
+
/** 子フラグメントのリスト */
|
|
215
|
+
this.children = [];
|
|
216
|
+
/** 属性名に対する属性情報のマップ */
|
|
217
|
+
this.attributeMap = new Map();
|
|
218
|
+
/** バインドデータ */
|
|
219
|
+
this.bindingData = null;
|
|
220
|
+
/** バインドデータのキャッシュ */
|
|
221
|
+
this.bindingDataCache = null;
|
|
222
|
+
/** 表示状態 */
|
|
223
|
+
this.visible = true;
|
|
224
|
+
/** 元の display 値 */
|
|
225
|
+
this.display = null;
|
|
226
|
+
/** each用のテンプレート */
|
|
227
|
+
this.template = null;
|
|
228
|
+
/** each比較用のキー */
|
|
229
|
+
this.listKey = null;
|
|
230
|
+
/** valueプロパティの値 */
|
|
231
|
+
this.value = null;
|
|
232
|
+
/** 属性更新スキップフラグ(オブザーバーによる無限ループ対応) */
|
|
233
|
+
this.skipMutationAttributes = false;
|
|
234
|
+
/** 値変更スキップフラグ(更新イベントによる無限ループ対応) */
|
|
235
|
+
this.skipChangeValue = false;
|
|
236
|
+
this.syncValue();
|
|
237
|
+
target.getAttributeNames().forEach(name => {
|
|
238
|
+
const value = target.getAttribute(name);
|
|
239
|
+
if (value !== null && !this.attributeMap.has(name)) {
|
|
240
|
+
const contents = new AttributeContents(name, value);
|
|
241
|
+
this.attributeMap.set(name, contents);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
target.childNodes.forEach(node => {
|
|
245
|
+
const childFragment = Fragment.get(node);
|
|
246
|
+
childFragment.setParent(this);
|
|
247
|
+
this.children.push(childFragment);
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* 子フラグメントのリストを取得します。
|
|
252
|
+
*
|
|
253
|
+
* @returns 子フラグメントのリスト
|
|
254
|
+
*/
|
|
255
|
+
getChildren() {
|
|
256
|
+
return this.children;
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* 子エレメントフラグメントのリストを取得します。
|
|
260
|
+
*
|
|
261
|
+
* @returns 子エレメントフラグメントのリスト
|
|
262
|
+
*/
|
|
263
|
+
getChildElementFragments() {
|
|
264
|
+
return this.children.filter(child => child instanceof ElementFragment);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* 子フラグメントをリストに追加します。
|
|
268
|
+
* DOMの追加は行いません。
|
|
269
|
+
*
|
|
270
|
+
* @param child 追加する子フラグメント
|
|
271
|
+
*/
|
|
272
|
+
pushChild(child) {
|
|
273
|
+
this.children.push(child);
|
|
274
|
+
child.setParent(this);
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* 子フラグメントをリストから削除します。
|
|
278
|
+
* DOMからの削除は行いません。
|
|
279
|
+
*
|
|
280
|
+
* @param child 削除する子フラグメント
|
|
281
|
+
*/
|
|
282
|
+
removeChild(child) {
|
|
283
|
+
const index = this.children.indexOf(child);
|
|
284
|
+
if (index < 0) {
|
|
285
|
+
Log.warn('[Haori]', 'Child fragment not found.', child);
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
this.children.splice(index, 1);
|
|
289
|
+
child.setParent(null);
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* フラグメントをクローンします。
|
|
293
|
+
*
|
|
294
|
+
* @returns クローンされたフラグメント
|
|
295
|
+
*/
|
|
296
|
+
clone() {
|
|
297
|
+
const clone = new ElementFragment(this.target.cloneNode(false));
|
|
298
|
+
this.children.forEach(child => {
|
|
299
|
+
const childClone = child.clone();
|
|
300
|
+
clone.getTarget().appendChild(childClone.getTarget());
|
|
301
|
+
clone.pushChild(childClone);
|
|
302
|
+
});
|
|
303
|
+
clone.mounted = false;
|
|
304
|
+
clone.bindingData = this.bindingData;
|
|
305
|
+
clone.clearBindingDataCache();
|
|
306
|
+
clone.visible = this.visible;
|
|
307
|
+
clone.display = this.display;
|
|
308
|
+
clone.template = this.template;
|
|
309
|
+
return clone;
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* フラグメントとノードを削除します。
|
|
313
|
+
*
|
|
314
|
+
* @param unmount DOMからの除去を行うかどうか(内部の子呼び出しの場合のみfalseとする)
|
|
315
|
+
* @return 除去のPromise
|
|
316
|
+
*/
|
|
317
|
+
remove(unmount = true) {
|
|
318
|
+
const promises = [];
|
|
319
|
+
this.children.forEach(child => {
|
|
320
|
+
promises.push(child.remove(false));
|
|
321
|
+
});
|
|
322
|
+
this.children.length = 0;
|
|
323
|
+
this.attributeMap.clear();
|
|
324
|
+
this.bindingData = null;
|
|
325
|
+
this.bindingDataCache = null;
|
|
326
|
+
if (this.template) {
|
|
327
|
+
promises.push(this.template.remove(false));
|
|
328
|
+
this.template = null;
|
|
329
|
+
}
|
|
330
|
+
promises.push(super.remove(unmount));
|
|
331
|
+
return Promise.all(promises).then(() => undefined);
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* フラグメントの対象エレメントを取得します。
|
|
335
|
+
*
|
|
336
|
+
* @returns フラグメントの対象エレメント
|
|
337
|
+
*/
|
|
338
|
+
getTarget() {
|
|
339
|
+
return this.target;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* 継承を考慮したバインドデータを取得します。
|
|
343
|
+
*
|
|
344
|
+
* @returns バインドデータのオブジェクト
|
|
345
|
+
*/
|
|
346
|
+
getBindingData() {
|
|
347
|
+
if (this.bindingDataCache) {
|
|
348
|
+
return this.bindingDataCache;
|
|
349
|
+
}
|
|
350
|
+
this.bindingDataCache = {};
|
|
351
|
+
if (this.parent) {
|
|
352
|
+
Object.assign(this.bindingDataCache, this.parent.getBindingData());
|
|
353
|
+
}
|
|
354
|
+
if (this.bindingData) {
|
|
355
|
+
Object.assign(this.bindingDataCache, this.bindingData);
|
|
356
|
+
}
|
|
357
|
+
return this.bindingDataCache;
|
|
358
|
+
}
|
|
359
|
+
/**
|
|
360
|
+
* 生のバインドデータを取得します。
|
|
361
|
+
*
|
|
362
|
+
* @returns 生のバインドデータ
|
|
363
|
+
*/
|
|
364
|
+
getRawBindingData() {
|
|
365
|
+
return this.bindingData;
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* バインドデータを設定します。
|
|
369
|
+
*
|
|
370
|
+
* @param data バインドデータ
|
|
371
|
+
*/
|
|
372
|
+
setBindingData(data) {
|
|
373
|
+
this.bindingData = data;
|
|
374
|
+
this.clearBindingDataCache();
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* バインドデータのキャッシュをクリアします。
|
|
378
|
+
*/
|
|
379
|
+
clearBindingDataCache() {
|
|
380
|
+
this.bindingDataCache = null;
|
|
381
|
+
this.children.forEach(child => {
|
|
382
|
+
if (child instanceof ElementFragment) {
|
|
383
|
+
child.clearBindingDataCache();
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* フラグメントのテンプレートを取得します。
|
|
389
|
+
*
|
|
390
|
+
* @returns テンプレート
|
|
391
|
+
*/
|
|
392
|
+
getTemplate() {
|
|
393
|
+
return this.template;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* フラグメントのテンプレートを設定します。
|
|
397
|
+
*
|
|
398
|
+
* @param template フラグメントのテンプレート
|
|
399
|
+
*/
|
|
400
|
+
setTemplate(template) {
|
|
401
|
+
this.template = template;
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* 比較用リストキーを設定します。
|
|
405
|
+
*
|
|
406
|
+
* @param key 比較用リストキー
|
|
407
|
+
*/
|
|
408
|
+
setListKey(key) {
|
|
409
|
+
this.listKey = key;
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* 比較用リストキーを取得します。
|
|
413
|
+
*
|
|
414
|
+
* @returns 比較用リストキー
|
|
415
|
+
*/
|
|
416
|
+
getListKey() {
|
|
417
|
+
return this.listKey;
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* 入力エレメントに値を設定します。
|
|
421
|
+
* チェックボックとラジオボタンの場合は値に一致するかどうかでチェック状態を変更します。
|
|
422
|
+
*
|
|
423
|
+
* @param value 値
|
|
424
|
+
* @returns エレメントの更新のPromise
|
|
425
|
+
*/
|
|
426
|
+
setValue(value) {
|
|
427
|
+
if (this.skipChangeValue) {
|
|
428
|
+
return Promise.resolve();
|
|
429
|
+
}
|
|
430
|
+
if (this.value === value) {
|
|
431
|
+
return Promise.resolve();
|
|
432
|
+
}
|
|
433
|
+
const element = this.getTarget();
|
|
434
|
+
if (element instanceof HTMLInputElement &&
|
|
435
|
+
(element.type === 'checkbox' || element.type === 'radio')) {
|
|
436
|
+
const result = this.getAttribute('value');
|
|
437
|
+
let newChecked;
|
|
438
|
+
if (result === 'true') {
|
|
439
|
+
newChecked = value === true;
|
|
440
|
+
}
|
|
441
|
+
else if (result === 'false') {
|
|
442
|
+
newChecked = value === false;
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
newChecked = result === String(value);
|
|
446
|
+
}
|
|
447
|
+
this.value = newChecked ? value : null;
|
|
448
|
+
if (element.checked === newChecked) {
|
|
449
|
+
return Promise.resolve();
|
|
450
|
+
}
|
|
451
|
+
this.skipChangeValue = true;
|
|
452
|
+
return Queue.enqueue(() => {
|
|
453
|
+
element.checked = newChecked;
|
|
454
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
455
|
+
}).finally(() => {
|
|
456
|
+
this.skipChangeValue = false;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
else if (element instanceof HTMLInputElement ||
|
|
460
|
+
element instanceof HTMLTextAreaElement ||
|
|
461
|
+
element instanceof HTMLSelectElement) {
|
|
462
|
+
this.value = value;
|
|
463
|
+
this.skipChangeValue = true;
|
|
464
|
+
return Queue.enqueue(() => {
|
|
465
|
+
element.value = value === null ? '' : String(value);
|
|
466
|
+
if ((element instanceof HTMLInputElement &&
|
|
467
|
+
this.INPUT_EVENT_TYPES.includes(element.type)) ||
|
|
468
|
+
element instanceof HTMLTextAreaElement) {
|
|
469
|
+
element.dispatchEvent(new Event('input', { bubbles: true }));
|
|
470
|
+
}
|
|
471
|
+
element.dispatchEvent(new Event('change', { bubbles: true }));
|
|
472
|
+
}).finally(() => {
|
|
473
|
+
this.skipChangeValue = false;
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
Log.warn('[Haori]', 'setValue is not supported for this element type.', element);
|
|
478
|
+
return Promise.resolve();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* 入力エレメントの値を取得します。
|
|
483
|
+
* DOM要素の現在の値と同期します。
|
|
484
|
+
*
|
|
485
|
+
* @returns 入力エレメントの値
|
|
486
|
+
*/
|
|
487
|
+
getValue() {
|
|
488
|
+
return this.value;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* 内部の値をクリアします。エレメントのvalue値は変化しません。
|
|
492
|
+
*/
|
|
493
|
+
clearValue() {
|
|
494
|
+
this.value = null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* 内部の値をDOMの値と同期します。
|
|
498
|
+
* changeイベント時など、DOM値が変更された後に呼び出されます。
|
|
499
|
+
*/
|
|
500
|
+
syncValue() {
|
|
501
|
+
const element = this.getTarget();
|
|
502
|
+
if (element instanceof HTMLInputElement) {
|
|
503
|
+
if (element.type === 'checkbox' || element.type === 'radio') {
|
|
504
|
+
if (element.checked) {
|
|
505
|
+
const value = element.value;
|
|
506
|
+
if (value === 'true') {
|
|
507
|
+
this.value = true;
|
|
508
|
+
}
|
|
509
|
+
else if (value === 'false') {
|
|
510
|
+
this.value = false;
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
this.value = value;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
else {
|
|
517
|
+
// チェックボックスがOFFの場合
|
|
518
|
+
const value = element.value;
|
|
519
|
+
if (value === 'true') {
|
|
520
|
+
this.value = false;
|
|
521
|
+
}
|
|
522
|
+
else if (value === 'false') {
|
|
523
|
+
this.value = true;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
this.value = null;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
this.value = element.value;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
else if (element instanceof HTMLTextAreaElement) {
|
|
535
|
+
this.value = element.value;
|
|
536
|
+
}
|
|
537
|
+
else if (element instanceof HTMLSelectElement) {
|
|
538
|
+
this.value = element.value;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
/**
|
|
542
|
+
* 属性の値を評価して設定します。
|
|
543
|
+
* 評価値がfalseの場合は属性を削除します。
|
|
544
|
+
* 矯正評価属性の場合は元の値を設定します。
|
|
545
|
+
*
|
|
546
|
+
* @param name 属性名
|
|
547
|
+
* @param value 属性値
|
|
548
|
+
* @returns 属性の更新のPromise
|
|
549
|
+
*/
|
|
550
|
+
setAttribute(name, value) {
|
|
551
|
+
if (this.skipMutationAttributes) {
|
|
552
|
+
return Promise.resolve();
|
|
553
|
+
}
|
|
554
|
+
if (value === null) {
|
|
555
|
+
return this.removeAttribute(name);
|
|
556
|
+
}
|
|
557
|
+
const contents = new AttributeContents(name, value);
|
|
558
|
+
this.attributeMap.set(name, contents);
|
|
559
|
+
this.skipMutationAttributes = true;
|
|
560
|
+
const element = this.getTarget();
|
|
561
|
+
const result = contents.isForceEvaluation()
|
|
562
|
+
? value
|
|
563
|
+
: this.getAttribute(name);
|
|
564
|
+
return Queue.enqueue(() => {
|
|
565
|
+
if (result === null || result === false) {
|
|
566
|
+
element.removeAttribute(name);
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
const string = String(result);
|
|
570
|
+
if (element.getAttribute(name) !== string) {
|
|
571
|
+
element.setAttribute(name, string);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}).finally(() => {
|
|
575
|
+
this.skipMutationAttributes = false;
|
|
576
|
+
});
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* 属性の値を削除します。
|
|
580
|
+
*
|
|
581
|
+
* @param name 属性名
|
|
582
|
+
* @returns 属性の削除のPromise
|
|
583
|
+
*/
|
|
584
|
+
removeAttribute(name) {
|
|
585
|
+
if (this.skipMutationAttributes) {
|
|
586
|
+
return Promise.resolve();
|
|
587
|
+
}
|
|
588
|
+
this.attributeMap.delete(name);
|
|
589
|
+
this.skipMutationAttributes = true;
|
|
590
|
+
const element = this.getTarget();
|
|
591
|
+
return Queue.enqueue(() => {
|
|
592
|
+
element.removeAttribute(name);
|
|
593
|
+
}).finally(() => {
|
|
594
|
+
this.skipMutationAttributes = false;
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
/**
|
|
598
|
+
* 属性の評価された値を取得します。
|
|
599
|
+
* 複数の評価値がある場合は結合して返します。
|
|
600
|
+
*
|
|
601
|
+
* @param name 属性名
|
|
602
|
+
* @returns 評価された値
|
|
603
|
+
*/
|
|
604
|
+
getAttribute(name) {
|
|
605
|
+
const contents = this.attributeMap.get(name);
|
|
606
|
+
if (contents === undefined) {
|
|
607
|
+
return null;
|
|
608
|
+
}
|
|
609
|
+
const results = contents.evaluate(this.getBindingData());
|
|
610
|
+
if (results.length === 1) {
|
|
611
|
+
return results[0];
|
|
612
|
+
}
|
|
613
|
+
return TextContents.joinEvaluateResults(results);
|
|
614
|
+
}
|
|
615
|
+
/**
|
|
616
|
+
* 属性の生の値を取得します。
|
|
617
|
+
*
|
|
618
|
+
* @param name 属性名
|
|
619
|
+
* @returns 生の属性値
|
|
620
|
+
*/
|
|
621
|
+
getRawAttribute(name) {
|
|
622
|
+
const contents = this.attributeMap.get(name);
|
|
623
|
+
if (contents === undefined) {
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
return contents.getValue();
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* 属性名のリストを取得します。
|
|
630
|
+
*
|
|
631
|
+
* @return 属性名のリスト
|
|
632
|
+
*/
|
|
633
|
+
getAttributeNames() {
|
|
634
|
+
return Array.from(this.attributeMap.keys());
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* 属性の有無を確認します。
|
|
638
|
+
*
|
|
639
|
+
* @param name 属性名
|
|
640
|
+
* @returns 属性の有無
|
|
641
|
+
*/
|
|
642
|
+
hasAttribute(name) {
|
|
643
|
+
return this.attributeMap.has(name);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* 子ノードを参照ノードの前に挿入します。
|
|
647
|
+
* 参照ノードがnullの場合、親の最後に追加されます。
|
|
648
|
+
*
|
|
649
|
+
* @param newChild 新しい子ノード
|
|
650
|
+
* @param referenceChild 参照ノード
|
|
651
|
+
* @return 挿入のPromise
|
|
652
|
+
*/
|
|
653
|
+
insertBefore(newChild, referenceChild) {
|
|
654
|
+
if (this.skipMutationNodes) {
|
|
655
|
+
return Promise.resolve();
|
|
656
|
+
}
|
|
657
|
+
// 循環参照チェック
|
|
658
|
+
if (newChild === this) {
|
|
659
|
+
Log.error('[Haori]', 'Cannot insert element as child of itself');
|
|
660
|
+
return Promise.reject(new Error('Self-insertion not allowed'));
|
|
661
|
+
}
|
|
662
|
+
// 祖先チェック
|
|
663
|
+
const ancestors = new Set();
|
|
664
|
+
let ancestor = this.parent;
|
|
665
|
+
while (ancestor) {
|
|
666
|
+
ancestors.add(ancestor);
|
|
667
|
+
ancestor = ancestor.getParent();
|
|
668
|
+
}
|
|
669
|
+
if (ancestors.has(newChild)) {
|
|
670
|
+
Log.error('[Haori]', 'Cannot create circular reference');
|
|
671
|
+
return Promise.reject(new Error('Circular reference detected'));
|
|
672
|
+
}
|
|
673
|
+
// 同じ親内での移動かどうかを確認
|
|
674
|
+
const isSameParent = newChild.getParent() === this;
|
|
675
|
+
let newChildIndex = -1;
|
|
676
|
+
let referenceIndex = -1;
|
|
677
|
+
if (isSameParent) {
|
|
678
|
+
newChildIndex = this.children.indexOf(newChild);
|
|
679
|
+
if (referenceChild !== null) {
|
|
680
|
+
referenceIndex = this.children.indexOf(referenceChild);
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
const newChildParent = newChild.getParent();
|
|
684
|
+
if (newChildParent !== null) {
|
|
685
|
+
// 既存の親から削除
|
|
686
|
+
newChildParent.removeChild(newChild);
|
|
687
|
+
}
|
|
688
|
+
if (referenceChild === null) {
|
|
689
|
+
this.children.push(newChild);
|
|
690
|
+
}
|
|
691
|
+
else {
|
|
692
|
+
let index;
|
|
693
|
+
if (isSameParent) {
|
|
694
|
+
// 同じ親内での移動の場合、削除後のインデックスを調整
|
|
695
|
+
if (newChildIndex !== -1 && newChildIndex < referenceIndex) {
|
|
696
|
+
// 削除する要素が参照要素より前にあった場合、インデックスは1つ減る
|
|
697
|
+
index = referenceIndex - 1;
|
|
698
|
+
}
|
|
699
|
+
else {
|
|
700
|
+
index = referenceIndex;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
else {
|
|
704
|
+
index = this.children.indexOf(referenceChild);
|
|
705
|
+
}
|
|
706
|
+
if (index === -1) {
|
|
707
|
+
Log.warn('[Haori]', 'Reference child not found in children.', referenceChild);
|
|
708
|
+
this.children.push(newChild);
|
|
709
|
+
}
|
|
710
|
+
else {
|
|
711
|
+
this.children.splice(index, 0, newChild);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
newChild.setParent(this);
|
|
715
|
+
newChild.setMounted(this.mounted);
|
|
716
|
+
const prevSkip = this.skipMutationNodes;
|
|
717
|
+
this.skipMutationNodes = true;
|
|
718
|
+
return Queue.enqueue(() => {
|
|
719
|
+
this.target.insertBefore(newChild.getTarget(), referenceChild?.getTarget() || null);
|
|
720
|
+
}).finally(() => {
|
|
721
|
+
this.skipMutationNodes = prevSkip;
|
|
722
|
+
});
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* 指定した参照ノードの後に子ノードを挿入します。
|
|
726
|
+
*
|
|
727
|
+
* @param newChild 子ノード
|
|
728
|
+
* @param referenceChild 参照ノード
|
|
729
|
+
* @returns 挿入のPromise
|
|
730
|
+
*/
|
|
731
|
+
insertAfter(newChild, referenceChild) {
|
|
732
|
+
if (referenceChild == null) {
|
|
733
|
+
return this.insertBefore(newChild, null);
|
|
734
|
+
}
|
|
735
|
+
const index = this.children.indexOf(referenceChild);
|
|
736
|
+
if (index === -1) {
|
|
737
|
+
Log.warn('[Haori]', 'Reference child not found in children.', referenceChild);
|
|
738
|
+
return this.insertBefore(newChild, null);
|
|
739
|
+
}
|
|
740
|
+
return this.insertBefore(newChild, this.children[index + 1] || null);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* 前のエレメントフラグメントを取得します。
|
|
744
|
+
* 存在しない場合はnullを返します。
|
|
745
|
+
*
|
|
746
|
+
* @return 前のエレメントフラグメントまたはnull
|
|
747
|
+
*/
|
|
748
|
+
getPrevious() {
|
|
749
|
+
const parent = this.getParent();
|
|
750
|
+
if (parent === null) {
|
|
751
|
+
return null;
|
|
752
|
+
}
|
|
753
|
+
const siblings = parent.getChildElementFragments();
|
|
754
|
+
const index = siblings.indexOf(this);
|
|
755
|
+
if (index <= 0) {
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
return siblings[index - 1];
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* 次のエレメントフラグメントを取得します。
|
|
762
|
+
* 存在しない場合はnullを返します。
|
|
763
|
+
*
|
|
764
|
+
* @return 次のエレメントフラグメントまたはnull
|
|
765
|
+
*/
|
|
766
|
+
getNext() {
|
|
767
|
+
const parent = this.getParent();
|
|
768
|
+
if (parent === null) {
|
|
769
|
+
return null;
|
|
770
|
+
}
|
|
771
|
+
const siblings = parent.getChildElementFragments();
|
|
772
|
+
const index = siblings.indexOf(this);
|
|
773
|
+
if (index < 0 || index + 1 >= siblings.length) {
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
return siblings[index + 1];
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* 表示状態を返します。
|
|
780
|
+
*
|
|
781
|
+
* @returns 表示状態
|
|
782
|
+
*/
|
|
783
|
+
isVisible() {
|
|
784
|
+
return this.visible;
|
|
785
|
+
}
|
|
786
|
+
/**
|
|
787
|
+
* エレメントを非表示にします。
|
|
788
|
+
*
|
|
789
|
+
* @returns エレメントの非表示のPromise
|
|
790
|
+
*/
|
|
791
|
+
hide() {
|
|
792
|
+
this.visible = false;
|
|
793
|
+
this.display = this.getTarget().style.display;
|
|
794
|
+
this.getTarget().style.display = 'none';
|
|
795
|
+
this.getTarget().setAttribute(`${Env.prefix}if-false`, '');
|
|
796
|
+
return Promise.resolve();
|
|
797
|
+
}
|
|
798
|
+
/**
|
|
799
|
+
* エレメントを表示します。
|
|
800
|
+
*
|
|
801
|
+
* @return エレメントの表示のPromise
|
|
802
|
+
*/
|
|
803
|
+
show() {
|
|
804
|
+
this.getTarget().style.display = this.display ?? '';
|
|
805
|
+
this.getTarget().removeAttribute(`${Env.prefix}if-false`);
|
|
806
|
+
this.visible = true;
|
|
807
|
+
return Promise.resolve();
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* 指定した属性名を持つ最も近い親要素を返します。
|
|
811
|
+
* 見つからない場合はnullを返します。
|
|
812
|
+
*
|
|
813
|
+
* @param name 属性名
|
|
814
|
+
* @returns 最も近い親要素またはnull
|
|
815
|
+
*/
|
|
816
|
+
closestByAttribute(name) {
|
|
817
|
+
if (this.hasAttribute(name)) {
|
|
818
|
+
return this;
|
|
819
|
+
}
|
|
820
|
+
const parent = this.getParent();
|
|
821
|
+
if (parent === null) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
return parent.closestByAttribute(name);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* テキストフラグメント。
|
|
829
|
+
* テキストノードを表現します。
|
|
830
|
+
*/
|
|
831
|
+
export class TextFragment extends Fragment {
|
|
832
|
+
/**
|
|
833
|
+
* テキストフラグメントのコンストラクタ。
|
|
834
|
+
* 対象テキストノードの内容を初期化します。
|
|
835
|
+
*
|
|
836
|
+
* @param target 対象テキストノード
|
|
837
|
+
*/
|
|
838
|
+
constructor(target) {
|
|
839
|
+
super(target);
|
|
840
|
+
/** 更新スキップフラグ(オブザーバーによる無限ループ対応) */
|
|
841
|
+
this.skipMutation = false;
|
|
842
|
+
this.text = target.textContent || '';
|
|
843
|
+
this.contents = new TextContents(this.text);
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* フラグメントをクローンします。
|
|
847
|
+
*
|
|
848
|
+
* @returns クローンされたフラグメント
|
|
849
|
+
*/
|
|
850
|
+
clone() {
|
|
851
|
+
const clone = new TextFragment(this.target.cloneNode(true));
|
|
852
|
+
clone.mounted = false;
|
|
853
|
+
clone.text = this.text;
|
|
854
|
+
clone.contents = this.contents;
|
|
855
|
+
return clone;
|
|
856
|
+
}
|
|
857
|
+
/**
|
|
858
|
+
* フラグメントの対象ノードを取得します。
|
|
859
|
+
*
|
|
860
|
+
* @returns フラグメントの対象ノード
|
|
861
|
+
*/
|
|
862
|
+
getTarget() {
|
|
863
|
+
return this.target;
|
|
864
|
+
}
|
|
865
|
+
/**
|
|
866
|
+
* コンテンツを更新します。
|
|
867
|
+
*
|
|
868
|
+
* @param text テキスト
|
|
869
|
+
* @returns 更新のPromise
|
|
870
|
+
*/
|
|
871
|
+
setContent(text) {
|
|
872
|
+
if (this.skipMutation || this.text === text) {
|
|
873
|
+
return Promise.resolve();
|
|
874
|
+
}
|
|
875
|
+
this.text = text;
|
|
876
|
+
this.contents = new TextContents(text);
|
|
877
|
+
return this.evaluate();
|
|
878
|
+
}
|
|
879
|
+
/**
|
|
880
|
+
* フラグメントを評価します。
|
|
881
|
+
*
|
|
882
|
+
* @returns 評価結果のPromise
|
|
883
|
+
*/
|
|
884
|
+
evaluate() {
|
|
885
|
+
if (this.contents.isRawEvaluate && this.parent === null) {
|
|
886
|
+
return Promise.reject(new Error('Parent fragment is required for raw evaluation'));
|
|
887
|
+
}
|
|
888
|
+
return Queue.enqueue(() => {
|
|
889
|
+
this.skipMutation = true;
|
|
890
|
+
if (this.contents.isRawEvaluate) {
|
|
891
|
+
this.parent.getTarget().innerHTML = this.contents.evaluate(this.parent.getBindingData())[0];
|
|
892
|
+
}
|
|
893
|
+
else if (this.contents.isEvaluate) {
|
|
894
|
+
this.target.textContent = TextContents.joinEvaluateResults(this.contents.evaluate(this.parent.getBindingData()));
|
|
895
|
+
}
|
|
896
|
+
else {
|
|
897
|
+
this.target.textContent = this.text;
|
|
898
|
+
}
|
|
899
|
+
}).finally(() => {
|
|
900
|
+
this.skipMutation = false;
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
/**
|
|
905
|
+
* コメントフラグメント。
|
|
906
|
+
* コメントノードを表現します。
|
|
907
|
+
*/
|
|
908
|
+
export class CommentFragment extends Fragment {
|
|
909
|
+
/**
|
|
910
|
+
* コメントフラグメントのコンストラクタ。
|
|
911
|
+
* 対象コメントノードの内容を初期化します。
|
|
912
|
+
*
|
|
913
|
+
* @param target 対象コメントノード
|
|
914
|
+
*/
|
|
915
|
+
constructor(target) {
|
|
916
|
+
super(target);
|
|
917
|
+
/** 更新スキップフラグ(オブザーバーによる無限ループ対応) */
|
|
918
|
+
this.skipMutation = false;
|
|
919
|
+
this.text = target.textContent || '';
|
|
920
|
+
}
|
|
921
|
+
/**
|
|
922
|
+
* フラグメントをクローンします。
|
|
923
|
+
*
|
|
924
|
+
* @returns クローンされたフラグメント
|
|
925
|
+
*/
|
|
926
|
+
clone() {
|
|
927
|
+
const clone = new CommentFragment(this.target.cloneNode(true));
|
|
928
|
+
clone.mounted = false;
|
|
929
|
+
clone.text = this.text;
|
|
930
|
+
return clone;
|
|
931
|
+
}
|
|
932
|
+
/**
|
|
933
|
+
* フラグメントの対象ノードを取得します。
|
|
934
|
+
*
|
|
935
|
+
* @returns フラグメントの対象ノード
|
|
936
|
+
*/
|
|
937
|
+
getTarget() {
|
|
938
|
+
return this.target;
|
|
939
|
+
}
|
|
940
|
+
/**
|
|
941
|
+
* コンテンツを更新します。
|
|
942
|
+
*
|
|
943
|
+
* @param text テキスト
|
|
944
|
+
* @return 更新のPromise
|
|
945
|
+
*/
|
|
946
|
+
setContent(text) {
|
|
947
|
+
if (this.skipMutation || this.text === text) {
|
|
948
|
+
return Promise.resolve();
|
|
949
|
+
}
|
|
950
|
+
this.text = text;
|
|
951
|
+
return Queue.enqueue(() => {
|
|
952
|
+
this.skipMutation = true;
|
|
953
|
+
this.target.textContent = this.text;
|
|
954
|
+
}).finally(() => {
|
|
955
|
+
this.skipMutation = false;
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
/**
|
|
960
|
+
* 値の種別。
|
|
961
|
+
*/
|
|
962
|
+
var ExpressionType;
|
|
963
|
+
(function (ExpressionType) {
|
|
964
|
+
/** テキスト */
|
|
965
|
+
ExpressionType[ExpressionType["TEXT"] = 0] = "TEXT";
|
|
966
|
+
/** 評価式 */
|
|
967
|
+
ExpressionType[ExpressionType["EXPRESSION"] = 1] = "EXPRESSION";
|
|
968
|
+
/** 生の評価式 */
|
|
969
|
+
ExpressionType[ExpressionType["RAW_EXPRESSION"] = 2] = "RAW_EXPRESSION";
|
|
970
|
+
})(ExpressionType || (ExpressionType = {}));
|
|
971
|
+
/**
|
|
972
|
+
* テキストコンテンツを管理するクラスです。
|
|
973
|
+
* 一度生成されると内部は変更しません。
|
|
974
|
+
*/
|
|
975
|
+
class TextContents {
|
|
976
|
+
/**
|
|
977
|
+
* 評価結果を結合して文字列にします。
|
|
978
|
+
*
|
|
979
|
+
* @param contents 評価結果の配列
|
|
980
|
+
* @returns 結合された文字列
|
|
981
|
+
*/
|
|
982
|
+
static joinEvaluateResults(contents) {
|
|
983
|
+
if (contents === null || contents.length === 0) {
|
|
984
|
+
return '';
|
|
985
|
+
}
|
|
986
|
+
return contents
|
|
987
|
+
.map(c => {
|
|
988
|
+
if (c === null || c === undefined || c === false || Number.isNaN(c)) {
|
|
989
|
+
return '';
|
|
990
|
+
}
|
|
991
|
+
else if (typeof c !== 'string') {
|
|
992
|
+
return String(c);
|
|
993
|
+
}
|
|
994
|
+
else {
|
|
995
|
+
return c;
|
|
996
|
+
}
|
|
997
|
+
})
|
|
998
|
+
.join('');
|
|
999
|
+
}
|
|
1000
|
+
/**
|
|
1001
|
+
* コンストラクタ。
|
|
1002
|
+
*
|
|
1003
|
+
* @param text テキスト
|
|
1004
|
+
*/
|
|
1005
|
+
constructor(text) {
|
|
1006
|
+
/** コンテンツのリスト */
|
|
1007
|
+
this.contents = [];
|
|
1008
|
+
/** 評価式が含まれるかどうか */
|
|
1009
|
+
this.isEvaluate = false;
|
|
1010
|
+
/** 生の評価式が含まれるかどうか */
|
|
1011
|
+
this.isRawEvaluate = false;
|
|
1012
|
+
this.value = text;
|
|
1013
|
+
const matches = [...text.matchAll(TextContents.PLACEHOLDER_REGEX)];
|
|
1014
|
+
let lastIndex = 0;
|
|
1015
|
+
let hasEvaluate = false;
|
|
1016
|
+
let hasRawEvaluate = false;
|
|
1017
|
+
for (const match of matches) {
|
|
1018
|
+
// プレースホルダ前の通常テキスト
|
|
1019
|
+
if (match.index > lastIndex) {
|
|
1020
|
+
this.contents.push({
|
|
1021
|
+
text: text.slice(lastIndex, match.index),
|
|
1022
|
+
type: ExpressionType.TEXT,
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
// プレースホルダ本体
|
|
1026
|
+
const content = {
|
|
1027
|
+
text: match[1] ?? match[2],
|
|
1028
|
+
type: match[1]
|
|
1029
|
+
? ExpressionType.RAW_EXPRESSION
|
|
1030
|
+
: ExpressionType.EXPRESSION,
|
|
1031
|
+
};
|
|
1032
|
+
hasEvaluate = true;
|
|
1033
|
+
hasRawEvaluate =
|
|
1034
|
+
hasRawEvaluate || content.type === ExpressionType.RAW_EXPRESSION;
|
|
1035
|
+
this.contents.push(content);
|
|
1036
|
+
lastIndex = match.index + match[0].length;
|
|
1037
|
+
}
|
|
1038
|
+
// 最後のプレースホルダ以降の通常テキスト
|
|
1039
|
+
if (lastIndex < text.length) {
|
|
1040
|
+
this.contents.push({
|
|
1041
|
+
text: text.slice(lastIndex),
|
|
1042
|
+
type: ExpressionType.TEXT,
|
|
1043
|
+
});
|
|
1044
|
+
}
|
|
1045
|
+
this.isEvaluate = hasEvaluate;
|
|
1046
|
+
this.isRawEvaluate = hasRawEvaluate;
|
|
1047
|
+
this.checkRawExpressions();
|
|
1048
|
+
}
|
|
1049
|
+
/**
|
|
1050
|
+
* 評価前の値を取得します。
|
|
1051
|
+
*
|
|
1052
|
+
* @returns 評価前の値
|
|
1053
|
+
*/
|
|
1054
|
+
getValue() {
|
|
1055
|
+
return this.value;
|
|
1056
|
+
}
|
|
1057
|
+
/**
|
|
1058
|
+
* RAW_EXPRESSION のチェックを行います。
|
|
1059
|
+
*/
|
|
1060
|
+
checkRawExpressions() {
|
|
1061
|
+
for (let i = 0; i < this.contents.length; i++) {
|
|
1062
|
+
const content = this.contents[i];
|
|
1063
|
+
if (content.type === ExpressionType.RAW_EXPRESSION &&
|
|
1064
|
+
this.contents.length > 1) {
|
|
1065
|
+
Log.error('[Haori]', 'Raw expressions are not allowed in multi-content expressions.');
|
|
1066
|
+
this.contents[i].type = ExpressionType.EXPRESSION;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
/**
|
|
1071
|
+
* 式評価を行い、結果を返します。
|
|
1072
|
+
*
|
|
1073
|
+
* @param bindingValues バインディングされた値のオブジェクト
|
|
1074
|
+
* @returns 評価結果のリスト
|
|
1075
|
+
*/
|
|
1076
|
+
evaluate(bindingValues) {
|
|
1077
|
+
if (!this.isEvaluate && !this.isRawEvaluate) {
|
|
1078
|
+
return this.contents.map(c => c.text);
|
|
1079
|
+
}
|
|
1080
|
+
const results = [];
|
|
1081
|
+
this.contents.forEach(c => {
|
|
1082
|
+
try {
|
|
1083
|
+
if (c.type === ExpressionType.EXPRESSION ||
|
|
1084
|
+
c.type === ExpressionType.RAW_EXPRESSION) {
|
|
1085
|
+
const result = Expression.evaluate(c.text, bindingValues);
|
|
1086
|
+
results.push(result);
|
|
1087
|
+
}
|
|
1088
|
+
else {
|
|
1089
|
+
results.push(c.text);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
catch (error) {
|
|
1093
|
+
Log.error('[Haori]', `Error evaluating text expression: ${c.text}`, error);
|
|
1094
|
+
results.push('');
|
|
1095
|
+
}
|
|
1096
|
+
});
|
|
1097
|
+
return results;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
/** プレースホルダ検出用の正規表現 */
|
|
1101
|
+
TextContents.PLACEHOLDER_REGEX = /\{\{\{([\s\S]+?)\}\}\}|\{\{([\s\S]+?)\}\}/g;
|
|
1102
|
+
/**
|
|
1103
|
+
* 属性のコンテンツを管理するクラスです。
|
|
1104
|
+
* 一度生成されると内部は変更しません。
|
|
1105
|
+
*/
|
|
1106
|
+
class AttributeContents extends TextContents {
|
|
1107
|
+
/**
|
|
1108
|
+
* コンストラクタ。
|
|
1109
|
+
*
|
|
1110
|
+
* @param name 属性名
|
|
1111
|
+
* @param text 属性値
|
|
1112
|
+
*/
|
|
1113
|
+
constructor(name, value) {
|
|
1114
|
+
super(value);
|
|
1115
|
+
this.forceEvaluation =
|
|
1116
|
+
AttributeContents.FORCE_EVALUATION_ATTRIBUTES.includes(name);
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* 強制評価フラグを取得します。
|
|
1120
|
+
*
|
|
1121
|
+
* @returns 強制評価フラグ
|
|
1122
|
+
*/
|
|
1123
|
+
isForceEvaluation() {
|
|
1124
|
+
return this.forceEvaluation;
|
|
1125
|
+
}
|
|
1126
|
+
/**
|
|
1127
|
+
* 式評価を行い、結果を返します。
|
|
1128
|
+
*
|
|
1129
|
+
* @param bindingValues バインディングされた値のオブジェクト
|
|
1130
|
+
* @returns 評価結果のリスト
|
|
1131
|
+
*/
|
|
1132
|
+
evaluate(bindingValues) {
|
|
1133
|
+
if (!this.isEvaluate && !this.forceEvaluation) {
|
|
1134
|
+
return this.contents.map(c => c.text);
|
|
1135
|
+
}
|
|
1136
|
+
const results = [];
|
|
1137
|
+
this.contents.forEach(c => {
|
|
1138
|
+
try {
|
|
1139
|
+
if ((this.forceEvaluation && c.type === ExpressionType.TEXT) ||
|
|
1140
|
+
c.type === ExpressionType.EXPRESSION ||
|
|
1141
|
+
c.type === ExpressionType.RAW_EXPRESSION) {
|
|
1142
|
+
const result = Expression.evaluate(c.text, bindingValues);
|
|
1143
|
+
results.push(result);
|
|
1144
|
+
}
|
|
1145
|
+
else {
|
|
1146
|
+
results.push(c.text);
|
|
1147
|
+
}
|
|
1148
|
+
}
|
|
1149
|
+
catch (error) {
|
|
1150
|
+
Log.error('[Haori]', `Error evaluating attribute expression: ${c.text}`, error);
|
|
1151
|
+
results.push('');
|
|
1152
|
+
}
|
|
1153
|
+
});
|
|
1154
|
+
if (this.forceEvaluation && results.length > 1) {
|
|
1155
|
+
Log.error('[Haori]', 'each or if expressions must have a single content.', results);
|
|
1156
|
+
return [results[0]];
|
|
1157
|
+
}
|
|
1158
|
+
return results;
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
/** 強制評価する属性名 */
|
|
1162
|
+
AttributeContents.FORCE_EVALUATION_ATTRIBUTES = [
|
|
1163
|
+
'data-if',
|
|
1164
|
+
'hor-if',
|
|
1165
|
+
'data-each',
|
|
1166
|
+
'hor-each',
|
|
1167
|
+
];
|
|
1168
|
+
//# sourceMappingURL=fragment.js.map
|