@reidelsaltres/pureper 0.1.160 → 0.1.162
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/out/foundation/Triplet.d.ts.map +1 -1
- package/out/foundation/Triplet.js +4 -5
- package/out/foundation/Triplet.js.map +1 -1
- package/out/foundation/component_api/Component.d.ts +2 -2
- package/out/foundation/component_api/Component.d.ts.map +1 -1
- package/out/foundation/component_api/Component.js.map +1 -1
- package/out/foundation/component_api/UniHtml.d.ts +4 -10
- package/out/foundation/component_api/UniHtml.d.ts.map +1 -1
- package/out/foundation/component_api/UniHtml.js +7 -15
- package/out/foundation/component_api/UniHtml.js.map +1 -1
- package/out/foundation/engine/Rule.d.ts +2 -0
- package/out/foundation/engine/Rule.d.ts.map +1 -1
- package/out/foundation/engine/Rule.js.map +1 -1
- package/out/foundation/engine/Scope.d.ts +5 -1
- package/out/foundation/engine/Scope.d.ts.map +1 -1
- package/out/foundation/engine/Scope.js +12 -3
- package/out/foundation/engine/Scope.js.map +1 -1
- package/out/foundation/engine/TemplateEngine.d.ts +17 -0
- package/out/foundation/engine/TemplateEngine.d.ts.map +1 -1
- package/out/foundation/engine/TemplateEngine.js +51 -3
- package/out/foundation/engine/TemplateEngine.js.map +1 -1
- package/out/foundation/engine/TemplateInstance.d.ts +167 -47
- package/out/foundation/engine/TemplateInstance.d.ts.map +1 -1
- package/out/foundation/engine/TemplateInstance.js +500 -118
- package/out/foundation/engine/TemplateInstance.js.map +1 -1
- package/out/foundation/engine/TemplateInstance.old.d.ts +219 -0
- package/out/foundation/engine/TemplateInstance.old.d.ts.map +1 -0
- package/out/foundation/engine/TemplateInstance.old.js +487 -0
- package/out/foundation/engine/TemplateInstance.old.js.map +1 -0
- package/out/foundation/engine/rules/attribute/RefRule.d.ts.map +1 -1
- package/out/foundation/engine/rules/attribute/RefRule.js +7 -1
- package/out/foundation/engine/rules/attribute/RefRule.js.map +1 -1
- package/out/index.d.ts +0 -1
- package/out/index.d.ts.map +1 -1
- package/out/index.js +0 -1
- package/out/index.js.map +1 -1
- package/package.json +1 -1
- package/src/foundation/Triplet.ts +6 -6
- package/src/foundation/component_api/Component.ts +2 -1
- package/src/foundation/component_api/UniHtml.ts +12 -22
- package/src/foundation/engine/Rule.ts +2 -0
- package/src/foundation/engine/Scope.ts +13 -3
- package/src/foundation/engine/TemplateEngine.ts +79 -4
- package/src/foundation/engine/TemplateInstance.md +110 -0
- package/src/foundation/engine/TemplateInstance.old.ts +673 -0
- package/src/foundation/engine/TemplateInstance.ts +635 -147
- package/src/foundation/engine/rules/attribute/RefRule.ts +8 -1
- package/src/index.ts +0 -1
|
@@ -1,53 +1,107 @@
|
|
|
1
1
|
import { MutationObserver } from '../api/Observer.js';
|
|
2
|
+
import Expression from './Expression.js';
|
|
2
3
|
/**
|
|
3
|
-
*
|
|
4
|
-
* Хранит обработанные Rule и поддерживает реактивное обновление.
|
|
4
|
+
* TemplateInstance - динамический шаблон страницы.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
6
|
+
* Поддерживает:
|
|
7
|
+
* - Множество мелких фрагментов, каждый обновляется независимо
|
|
8
|
+
* - Множество container bindings
|
|
9
|
+
* - Автоматическое обновление DOM при изменении Observable
|
|
10
|
+
* - bind/unbind для refs и events
|
|
8
11
|
*/
|
|
9
12
|
export default class TemplateInstance {
|
|
10
|
-
template;
|
|
11
13
|
scope;
|
|
12
14
|
sections = [];
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
15
|
+
/** Все фрагменты шаблона */
|
|
16
|
+
fragments = new Map();
|
|
17
|
+
/** ID корневого фрагмента */
|
|
18
|
+
rootFragmentId = null;
|
|
19
|
+
/** Счётчик для генерации ID фрагментов */
|
|
20
|
+
fragmentIdCounter = 0;
|
|
21
|
+
/** Observers for fragment changes */
|
|
22
|
+
fragmentChangeObserver = new MutationObserver();
|
|
16
23
|
/** Группировка секций по Observable */
|
|
17
24
|
observableTrackings = new Map();
|
|
18
|
-
|
|
19
|
-
|
|
25
|
+
/** Привязки к контейнерам */
|
|
26
|
+
containerBindings = new Map();
|
|
27
|
+
constructor(scope) {
|
|
20
28
|
this.scope = scope;
|
|
21
29
|
}
|
|
30
|
+
// ========================================
|
|
31
|
+
// Public API - Getters
|
|
32
|
+
// ========================================
|
|
22
33
|
/**
|
|
23
|
-
* Получить
|
|
34
|
+
* Получить Scope
|
|
24
35
|
*/
|
|
25
|
-
|
|
26
|
-
return this.
|
|
36
|
+
getScope() {
|
|
37
|
+
return this.scope;
|
|
27
38
|
}
|
|
28
39
|
/**
|
|
29
|
-
*
|
|
40
|
+
* Получить все секции
|
|
30
41
|
*/
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
this.template = newTemplate;
|
|
34
|
-
this.changeObserver.notify({ oldValue: null, newValue: null, oldTemplate, newTemplate }, { oldValue: null, newValue: null, oldTemplate, newTemplate });
|
|
42
|
+
getSections() {
|
|
43
|
+
return this.sections;
|
|
35
44
|
}
|
|
36
45
|
/**
|
|
37
|
-
* Получить
|
|
46
|
+
* Получить все фрагменты
|
|
38
47
|
*/
|
|
39
|
-
|
|
40
|
-
return this.
|
|
48
|
+
getAllFragments() {
|
|
49
|
+
return this.fragments;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Получить фрагмент по ID
|
|
53
|
+
*/
|
|
54
|
+
getFragmentBinding(id) {
|
|
55
|
+
return this.fragments.get(id);
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Получить финальный HTML (собранный из всех фрагментов)
|
|
59
|
+
*/
|
|
60
|
+
getTemplate() {
|
|
61
|
+
if (!this.rootFragmentId)
|
|
62
|
+
return '';
|
|
63
|
+
return this.buildHtmlFromFragment(this.rootFragmentId);
|
|
41
64
|
}
|
|
65
|
+
// ========================================
|
|
66
|
+
// Public API - Fragment Management
|
|
67
|
+
// ========================================
|
|
42
68
|
/**
|
|
43
|
-
*
|
|
69
|
+
* Создать новый фрагмент и вернуть его ID
|
|
44
70
|
*/
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
71
|
+
createFragment(html, sourceTemplate, sections = [], parentId = null) {
|
|
72
|
+
const id = `f${this.fragmentIdCounter++}`;
|
|
73
|
+
const binding = {
|
|
74
|
+
id,
|
|
75
|
+
html,
|
|
76
|
+
sourceTemplate,
|
|
77
|
+
sections,
|
|
78
|
+
observables: new Set(),
|
|
79
|
+
parentId,
|
|
80
|
+
childIds: []
|
|
48
81
|
};
|
|
49
|
-
|
|
50
|
-
|
|
82
|
+
// Привязываем секции к фрагменту
|
|
83
|
+
for (const section of sections) {
|
|
84
|
+
section.fragmentId = id;
|
|
85
|
+
}
|
|
86
|
+
this.fragments.set(id, binding);
|
|
87
|
+
// Если нет корневого фрагмента, это первый
|
|
88
|
+
if (this.rootFragmentId === null) {
|
|
89
|
+
this.rootFragmentId = id;
|
|
90
|
+
}
|
|
91
|
+
// Добавляем в дочерние родителя
|
|
92
|
+
if (parentId) {
|
|
93
|
+
const parent = this.fragments.get(parentId);
|
|
94
|
+
if (parent) {
|
|
95
|
+
parent.childIds.push(id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return id;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Установить корневой фрагмент
|
|
102
|
+
*/
|
|
103
|
+
setRootFragment(id) {
|
|
104
|
+
this.rootFragmentId = id;
|
|
51
105
|
}
|
|
52
106
|
/**
|
|
53
107
|
* Добавить секцию шаблона
|
|
@@ -56,38 +110,86 @@ export default class TemplateInstance {
|
|
|
56
110
|
this.sections.push(section);
|
|
57
111
|
}
|
|
58
112
|
/**
|
|
59
|
-
*
|
|
113
|
+
* Вставить добавленный фрагмент во все привязанные контейнеры.
|
|
114
|
+
* Вызывается после appendTemplate.
|
|
60
115
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
116
|
+
insertAppendedFragment(fragmentId) {
|
|
117
|
+
const fragment = this.fragments.get(fragmentId);
|
|
118
|
+
if (!fragment)
|
|
119
|
+
return;
|
|
120
|
+
for (const [container, binding] of this.containerBindings) {
|
|
121
|
+
// Вставляем фрагмент в конец контейнера
|
|
122
|
+
this.insertFragmentRecursive(fragmentId, container, binding);
|
|
123
|
+
// Привязываем refs, events и injections для новых элементов
|
|
124
|
+
this.bindRefsForContainer(container);
|
|
125
|
+
this.processInjectionsForContainer(container);
|
|
126
|
+
this.bindEventsForNewFragment(fragmentId, binding);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Привязать события только для нового фрагмента
|
|
131
|
+
*/
|
|
132
|
+
bindEventsForNewFragment(fragmentId, binding) {
|
|
133
|
+
const markerInfo = binding.markers.get(fragmentId);
|
|
134
|
+
if (!markerInfo)
|
|
135
|
+
return;
|
|
136
|
+
for (const node of markerInfo.nodes) {
|
|
137
|
+
if (node instanceof Element) {
|
|
138
|
+
const unbinders = this.bindEventsToElement(node);
|
|
139
|
+
binding.eventUnbinders.push(...unbinders);
|
|
140
|
+
const children = node.querySelectorAll('*');
|
|
141
|
+
for (const child of Array.from(children)) {
|
|
142
|
+
const childUnbinders = this.bindEventsToElement(child);
|
|
143
|
+
binding.eventUnbinders.push(...childUnbinders);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
63
147
|
}
|
|
148
|
+
// ========================================
|
|
149
|
+
// Public API - Events
|
|
150
|
+
// ========================================
|
|
64
151
|
/**
|
|
65
|
-
* Подписаться на
|
|
66
|
-
|
|
152
|
+
* Подписаться на изменения фрагментов
|
|
153
|
+
*/
|
|
154
|
+
onFragmentChange(listener) {
|
|
155
|
+
const wrapper = (_oldEvent, newEvent) => {
|
|
156
|
+
listener(newEvent);
|
|
157
|
+
};
|
|
158
|
+
this.fragmentChangeObserver.subscribe(wrapper);
|
|
159
|
+
return () => this.fragmentChangeObserver.unsubscribe(wrapper);
|
|
160
|
+
}
|
|
161
|
+
// ========================================
|
|
162
|
+
// Public API - Observable Tracking
|
|
163
|
+
// ========================================
|
|
164
|
+
/**
|
|
165
|
+
* Подписаться на Observable и автоматически пересоздавать секцию
|
|
67
166
|
*/
|
|
68
167
|
trackObservable(observable, section, rebuild) {
|
|
69
|
-
// Проверяем, есть ли уже отслеживание для этого Observable
|
|
70
168
|
let tracking = this.observableTrackings.get(observable);
|
|
71
169
|
if (!tracking) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.rebuildAllSectionsForObservable(observable, newValue);
|
|
170
|
+
const listener = (_newValue) => {
|
|
171
|
+
this.rebuildFragmentsForObservable(observable);
|
|
75
172
|
};
|
|
76
173
|
observable.subscribe(listener);
|
|
77
174
|
tracking = {
|
|
78
175
|
observable,
|
|
79
176
|
sections: [],
|
|
177
|
+
fragmentIds: new Set(),
|
|
80
178
|
unsubscribe: () => observable.unsubscribe(listener)
|
|
81
179
|
};
|
|
82
180
|
this.observableTrackings.set(observable, tracking);
|
|
83
181
|
}
|
|
84
|
-
// Добавляем секцию в отслеживание
|
|
85
182
|
tracking.sections.push({ section, rebuild });
|
|
86
|
-
|
|
183
|
+
if (section.fragmentId) {
|
|
184
|
+
tracking.fragmentIds.add(section.fragmentId);
|
|
185
|
+
const binding = this.fragments.get(section.fragmentId);
|
|
186
|
+
if (binding) {
|
|
187
|
+
binding.observables.add(observable);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
87
190
|
return () => {
|
|
88
191
|
if (tracking) {
|
|
89
192
|
tracking.sections = tracking.sections.filter(s => s.section !== section);
|
|
90
|
-
// Если больше нет секций, отписываемся от Observable
|
|
91
193
|
if (tracking.sections.length === 0) {
|
|
92
194
|
tracking.unsubscribe();
|
|
93
195
|
this.observableTrackings.delete(observable);
|
|
@@ -95,95 +197,120 @@ export default class TemplateInstance {
|
|
|
95
197
|
}
|
|
96
198
|
};
|
|
97
199
|
}
|
|
200
|
+
// ========================================
|
|
201
|
+
// Public API - Container Binding
|
|
202
|
+
// ========================================
|
|
98
203
|
/**
|
|
99
|
-
*
|
|
204
|
+
* Привязать к контейнеру.
|
|
205
|
+
* Вставляет DOM, вызывает bindRefs, processInjections и bindEvents.
|
|
100
206
|
*/
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
207
|
+
bind(container) {
|
|
208
|
+
if (this.containerBindings.has(container)) {
|
|
209
|
+
console.warn('[TemplateInstance] Container already bound');
|
|
104
210
|
return;
|
|
105
|
-
const oldTemplate = this.template;
|
|
106
|
-
let currentTemplate = this.template;
|
|
107
|
-
// Собираем все замены: старый output -> новый output
|
|
108
|
-
// Сортируем по позиции в шаблоне (с конца), чтобы замены не сбивали индексы
|
|
109
|
-
const replacements = [];
|
|
110
|
-
for (const { section, rebuild } of tracking.sections) {
|
|
111
|
-
// Unsubscribe old nested observables
|
|
112
|
-
this.unsubscribeSectionNested(section);
|
|
113
|
-
// Rebuild section
|
|
114
|
-
const newResult = rebuild(section);
|
|
115
|
-
const oldOutput = section.result.output;
|
|
116
|
-
replacements.push({
|
|
117
|
-
oldOutput,
|
|
118
|
-
newOutput: newResult.output,
|
|
119
|
-
section
|
|
120
|
-
});
|
|
121
|
-
section.result = newResult;
|
|
122
|
-
}
|
|
123
|
-
// Применяем все замены
|
|
124
|
-
for (const { oldOutput, newOutput } of replacements) {
|
|
125
|
-
currentTemplate = currentTemplate.replace(oldOutput, newOutput);
|
|
126
211
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
212
|
+
const binding = {
|
|
213
|
+
container,
|
|
214
|
+
markers: new Map(),
|
|
215
|
+
eventUnbinders: []
|
|
216
|
+
};
|
|
217
|
+
this.containerBindings.set(container, binding);
|
|
218
|
+
// Вставляем DOM с маркерами
|
|
219
|
+
this.insertFragmentsIntoContainer(container, binding);
|
|
220
|
+
// Привязываем refs, обрабатываем injections, привязываем events
|
|
221
|
+
this.bindRefsForContainer(container);
|
|
222
|
+
this.processInjectionsForContainer(container);
|
|
223
|
+
this.bindEventsForContainer(container, binding);
|
|
130
224
|
}
|
|
131
225
|
/**
|
|
132
|
-
*
|
|
226
|
+
* Отвязать от контейнера.
|
|
227
|
+
* Отвязывает refs и events, но оставляет DOM.
|
|
133
228
|
*/
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
229
|
+
unbind(container) {
|
|
230
|
+
const binding = this.containerBindings.get(container);
|
|
231
|
+
if (!binding) {
|
|
232
|
+
console.warn('[TemplateInstance] Container not bound');
|
|
233
|
+
return;
|
|
138
234
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
this.unsubscribeSectionNested(child);
|
|
235
|
+
// Отвязываем события для этого контейнера
|
|
236
|
+
for (const unbind of binding.eventUnbinders) {
|
|
237
|
+
unbind();
|
|
143
238
|
}
|
|
239
|
+
binding.eventUnbinders = [];
|
|
240
|
+
// Отвязываем refs
|
|
241
|
+
this.unbindRefsForContainer(container);
|
|
242
|
+
// Удаляем binding
|
|
243
|
+
this.containerBindings.delete(container);
|
|
144
244
|
}
|
|
145
245
|
/**
|
|
146
|
-
*
|
|
246
|
+
* Привязать refs к элементам.
|
|
247
|
+
* Если есть контейнеры - привязывает для них.
|
|
248
|
+
* Если нет - создаёт временный DocumentFragment и привязывает refs из него.
|
|
147
249
|
*/
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
250
|
+
bindRefs() {
|
|
251
|
+
if (this.containerBindings.size > 0) {
|
|
252
|
+
for (const [container] of this.containerBindings) {
|
|
253
|
+
this.bindRefsForContainer(container);
|
|
254
|
+
}
|
|
151
255
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
this.
|
|
256
|
+
else {
|
|
257
|
+
// Нет контейнеров - создаём временный fragment
|
|
258
|
+
const fragment = this.createDOMFragment();
|
|
259
|
+
this.bindRefsForFragment(fragment);
|
|
156
260
|
}
|
|
157
261
|
}
|
|
158
262
|
/**
|
|
159
|
-
*
|
|
263
|
+
* Привязать refs из DocumentFragment (без контейнера)
|
|
160
264
|
*/
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
265
|
+
bindRefsForFragment(fragment) {
|
|
266
|
+
const refElements = fragment.querySelectorAll('[data-ref]');
|
|
267
|
+
for (const element of Array.from(refElements)) {
|
|
268
|
+
const refName = element.getAttribute('data-ref');
|
|
269
|
+
if (refName) {
|
|
270
|
+
this.scope.set(refName, element);
|
|
271
|
+
}
|
|
164
272
|
}
|
|
165
|
-
const template = document.createElement('template');
|
|
166
|
-
template.innerHTML = this.template;
|
|
167
|
-
this.fragment = template.content.cloneNode(true);
|
|
168
|
-
return this.fragment;
|
|
169
273
|
}
|
|
170
274
|
/**
|
|
171
|
-
*
|
|
275
|
+
* Отвязать refs (для всех контейнеров)
|
|
172
276
|
*/
|
|
173
|
-
|
|
174
|
-
|
|
277
|
+
unbindRefs() {
|
|
278
|
+
for (const section of this.sections) {
|
|
279
|
+
if (section.rule.name === 'ref' && section.result.data?.refName) {
|
|
280
|
+
const refName = section.result.data.refName;
|
|
281
|
+
if (this.scope.has(refName)) {
|
|
282
|
+
this.scope.set(refName, null);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Привязать события (для всех контейнеров)
|
|
289
|
+
*/
|
|
290
|
+
bindEvents() {
|
|
291
|
+
for (const [container, binding] of this.containerBindings) {
|
|
292
|
+
this.bindEventsForContainer(container, binding);
|
|
293
|
+
}
|
|
175
294
|
}
|
|
176
295
|
/**
|
|
177
|
-
*
|
|
296
|
+
* Отвязать события (для всех контейнеров)
|
|
178
297
|
*/
|
|
179
|
-
|
|
180
|
-
this.
|
|
181
|
-
|
|
298
|
+
unbindEvents() {
|
|
299
|
+
for (const [_, binding] of this.containerBindings) {
|
|
300
|
+
for (const unbind of binding.eventUnbinders) {
|
|
301
|
+
unbind();
|
|
302
|
+
}
|
|
303
|
+
binding.eventUnbinders = [];
|
|
304
|
+
}
|
|
182
305
|
}
|
|
183
306
|
/**
|
|
184
|
-
* Очистить все подписки
|
|
307
|
+
* Очистить все подписки и bindings
|
|
185
308
|
*/
|
|
186
309
|
dispose() {
|
|
310
|
+
// Отвязываем все контейнеры
|
|
311
|
+
for (const [container] of this.containerBindings) {
|
|
312
|
+
this.unbind(container);
|
|
313
|
+
}
|
|
187
314
|
// Отписываемся от всех Observable
|
|
188
315
|
for (const tracking of this.observableTrackings.values()) {
|
|
189
316
|
tracking.unsubscribe();
|
|
@@ -194,13 +321,90 @@ export default class TemplateInstance {
|
|
|
194
321
|
this.unsubscribeSection(section);
|
|
195
322
|
}
|
|
196
323
|
this.sections = [];
|
|
197
|
-
|
|
324
|
+
// Очищаем фрагменты
|
|
325
|
+
this.fragments.clear();
|
|
326
|
+
this.rootFragmentId = null;
|
|
327
|
+
}
|
|
328
|
+
// ========================================
|
|
329
|
+
// Private - DOM Operations
|
|
330
|
+
// ========================================
|
|
331
|
+
/**
|
|
332
|
+
* Вставить все фрагменты в контейнер с маркерами
|
|
333
|
+
*/
|
|
334
|
+
insertFragmentsIntoContainer(container, binding) {
|
|
335
|
+
if (!this.rootFragmentId)
|
|
336
|
+
return;
|
|
337
|
+
// Рекурсивно вставляем фрагменты
|
|
338
|
+
this.insertFragmentRecursive(this.rootFragmentId, container, binding);
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Рекурсивно вставить фрагмент и его дочерние
|
|
342
|
+
*/
|
|
343
|
+
insertFragmentRecursive(fragmentId, parent, containerBinding) {
|
|
344
|
+
const fragment = this.fragments.get(fragmentId);
|
|
345
|
+
if (!fragment)
|
|
346
|
+
return;
|
|
347
|
+
// Создаём маркеры
|
|
348
|
+
const startMarker = document.createComment(`fragment:${fragmentId}`);
|
|
349
|
+
const endMarker = document.createComment(`/fragment:${fragmentId}`);
|
|
350
|
+
parent.appendChild(startMarker);
|
|
351
|
+
// Парсим HTML и вставляем, заменяя placeholder-ы на дочерние фрагменты
|
|
352
|
+
const nodes = this.createNodesFromHtml(fragment.html);
|
|
353
|
+
const insertedNodes = [];
|
|
354
|
+
// Node.COMMENT_NODE = 8
|
|
355
|
+
const COMMENT_NODE = 8;
|
|
356
|
+
for (const node of nodes) {
|
|
357
|
+
// Проверяем на placeholder комментарии
|
|
358
|
+
if (node.nodeType === COMMENT_NODE) {
|
|
359
|
+
const comment = node;
|
|
360
|
+
const match = comment.data.match(/^placeholder:(.+)$/);
|
|
361
|
+
if (match) {
|
|
362
|
+
const childId = match[1];
|
|
363
|
+
// Рекурсивно вставляем дочерний фрагмент
|
|
364
|
+
this.insertFragmentRecursive(childId, parent, containerBinding);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
parent.appendChild(node);
|
|
369
|
+
insertedNodes.push(node);
|
|
370
|
+
}
|
|
371
|
+
parent.appendChild(endMarker);
|
|
372
|
+
// Сохраняем маркеры
|
|
373
|
+
containerBinding.markers.set(fragmentId, {
|
|
374
|
+
startMarker,
|
|
375
|
+
endMarker,
|
|
376
|
+
nodes: insertedNodes
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Создать ноды из HTML строки
|
|
381
|
+
*/
|
|
382
|
+
createNodesFromHtml(html) {
|
|
383
|
+
const template = document.createElement('template');
|
|
384
|
+
template.innerHTML = html;
|
|
385
|
+
return Array.from(template.content.childNodes);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Собрать HTML из фрагмента (рекурсивно заменяя placeholder-ы)
|
|
389
|
+
*/
|
|
390
|
+
buildHtmlFromFragment(fragmentId) {
|
|
391
|
+
const fragment = this.fragments.get(fragmentId);
|
|
392
|
+
if (!fragment)
|
|
393
|
+
return '';
|
|
394
|
+
let html = fragment.html;
|
|
395
|
+
// Заменяем placeholder-ы на контент дочерних фрагментов
|
|
396
|
+
for (const childId of fragment.childIds) {
|
|
397
|
+
const placeholder = `<!--placeholder:${childId}-->`;
|
|
398
|
+
const childHtml = this.buildHtmlFromFragment(childId);
|
|
399
|
+
html = html.replace(placeholder, childHtml);
|
|
400
|
+
}
|
|
401
|
+
return html;
|
|
198
402
|
}
|
|
199
403
|
/**
|
|
200
|
-
* Привязать refs
|
|
404
|
+
* Привязать refs для конкретного контейнера
|
|
201
405
|
*/
|
|
202
|
-
|
|
203
|
-
const refElements =
|
|
406
|
+
bindRefsForContainer(container) {
|
|
407
|
+
const refElements = container.querySelectorAll('[data-ref]');
|
|
204
408
|
for (const element of Array.from(refElements)) {
|
|
205
409
|
const refName = element.getAttribute('data-ref');
|
|
206
410
|
if (refName) {
|
|
@@ -209,27 +413,30 @@ export default class TemplateInstance {
|
|
|
209
413
|
}
|
|
210
414
|
}
|
|
211
415
|
/**
|
|
212
|
-
* Обработать инжекции
|
|
213
|
-
*
|
|
416
|
+
* Обработать инжекции для конкретного контейнера.
|
|
417
|
+
* Находит элементы с data-injection-* атрибутами и перемещает их в целевые элементы.
|
|
214
418
|
*/
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const injectElements = root.querySelectorAll('[data-injection-type][data-injection-target]');
|
|
419
|
+
processInjectionsForContainer(container) {
|
|
420
|
+
const injectElements = container.querySelectorAll('[data-injection-type][data-injection-target]');
|
|
218
421
|
for (const element of Array.from(injectElements)) {
|
|
219
422
|
const type = element.getAttribute('data-injection-type');
|
|
220
423
|
const targetRefName = decodeURIComponent(element.getAttribute('data-injection-target') || '');
|
|
221
|
-
if (!targetRefName)
|
|
424
|
+
if (!targetRefName) {
|
|
425
|
+
// Нет target - удаляем элемент из DOM
|
|
426
|
+
element.remove();
|
|
222
427
|
continue;
|
|
223
|
-
|
|
428
|
+
}
|
|
429
|
+
// Получаем целевой элемент из scope
|
|
224
430
|
const targetElement = this.scope.get(targetRefName);
|
|
225
431
|
if (!targetElement || !(targetElement instanceof Element)) {
|
|
226
|
-
|
|
432
|
+
// Target не найден - удаляем элемент из DOM
|
|
433
|
+
element.remove();
|
|
227
434
|
continue;
|
|
228
435
|
}
|
|
229
|
-
//
|
|
436
|
+
// Удаляем атрибуты инжекции
|
|
230
437
|
element.removeAttribute('data-injection-type');
|
|
231
438
|
element.removeAttribute('data-injection-target');
|
|
232
|
-
//
|
|
439
|
+
// Выполняем инжекцию
|
|
233
440
|
if (type === 'head') {
|
|
234
441
|
targetElement.prepend(element);
|
|
235
442
|
}
|
|
@@ -239,17 +446,192 @@ export default class TemplateInstance {
|
|
|
239
446
|
}
|
|
240
447
|
}
|
|
241
448
|
/**
|
|
242
|
-
* Отвязать refs
|
|
449
|
+
* Отвязать refs для конкретного контейнера
|
|
243
450
|
*/
|
|
244
|
-
|
|
451
|
+
unbindRefsForContainer(_container) {
|
|
452
|
+
// Устанавливаем все refs в null
|
|
245
453
|
for (const section of this.sections) {
|
|
246
|
-
if (section.rule.name === 'ref' && section.
|
|
247
|
-
const refName = section.
|
|
454
|
+
if (section.rule.name === 'ref' && section.result.data?.refName) {
|
|
455
|
+
const refName = section.result.data.refName;
|
|
248
456
|
if (this.scope.has(refName)) {
|
|
249
457
|
this.scope.set(refName, null);
|
|
250
458
|
}
|
|
251
459
|
}
|
|
252
460
|
}
|
|
253
461
|
}
|
|
462
|
+
/**
|
|
463
|
+
* Привязать события для конкретного контейнера
|
|
464
|
+
*/
|
|
465
|
+
bindEventsForContainer(container, binding) {
|
|
466
|
+
const allElements = container.querySelectorAll('*');
|
|
467
|
+
for (const element of Array.from(allElements)) {
|
|
468
|
+
const unbinders = this.bindEventsToElement(element);
|
|
469
|
+
binding.eventUnbinders.push(...unbinders);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
/**
|
|
473
|
+
* Привязать события к одному элементу
|
|
474
|
+
*/
|
|
475
|
+
bindEventsToElement(element) {
|
|
476
|
+
const unbinders = [];
|
|
477
|
+
const attributes = Array.from(element.attributes);
|
|
478
|
+
for (const attr of attributes) {
|
|
479
|
+
if (attr.name.startsWith('data-event-')) {
|
|
480
|
+
const eventName = attr.name.slice('data-event-'.length);
|
|
481
|
+
const exprCode = decodeURIComponent(attr.value);
|
|
482
|
+
const handler = (event) => {
|
|
483
|
+
const localScope = this.scope.createChild({ event });
|
|
484
|
+
const expr = new Expression(exprCode);
|
|
485
|
+
try {
|
|
486
|
+
expr.execute(localScope);
|
|
487
|
+
}
|
|
488
|
+
catch (error) {
|
|
489
|
+
console.error(`[TemplateInstance] Error executing handler for ${eventName}:`, error);
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
element.addEventListener(eventName, handler);
|
|
493
|
+
unbinders.push(() => element.removeEventListener(eventName, handler));
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
return unbinders;
|
|
497
|
+
}
|
|
498
|
+
// ========================================
|
|
499
|
+
// Private - Observable Rebuild
|
|
500
|
+
// ========================================
|
|
501
|
+
/**
|
|
502
|
+
* Перестроить фрагменты при изменении Observable
|
|
503
|
+
*/
|
|
504
|
+
rebuildFragmentsForObservable(observable) {
|
|
505
|
+
const tracking = this.observableTrackings.get(observable);
|
|
506
|
+
if (!tracking || tracking.sections.length === 0)
|
|
507
|
+
return;
|
|
508
|
+
// Собираем затронутые фрагменты
|
|
509
|
+
const affectedFragmentIds = new Set();
|
|
510
|
+
// Перестраиваем секции и обновляем результаты
|
|
511
|
+
for (const { section, rebuild } of tracking.sections) {
|
|
512
|
+
this.unsubscribeSectionNested(section);
|
|
513
|
+
const newResult = rebuild(section);
|
|
514
|
+
section.result = newResult;
|
|
515
|
+
if (section.fragmentId) {
|
|
516
|
+
affectedFragmentIds.add(section.fragmentId);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
// Перестраиваем HTML затронутых фрагментов
|
|
520
|
+
for (const fragmentId of affectedFragmentIds) {
|
|
521
|
+
const fragment = this.fragments.get(fragmentId);
|
|
522
|
+
if (!fragment)
|
|
523
|
+
continue;
|
|
524
|
+
// Собираем новый HTML из результатов всех секций фрагмента
|
|
525
|
+
let newHtml = fragment.sourceTemplate;
|
|
526
|
+
for (const section of fragment.sections) {
|
|
527
|
+
// Заменяем исходный match на новый output
|
|
528
|
+
newHtml = newHtml.replace(section.match.fullMatch, section.result.output);
|
|
529
|
+
}
|
|
530
|
+
fragment.html = newHtml;
|
|
531
|
+
}
|
|
532
|
+
// Обновляем DOM во всех контейнерах
|
|
533
|
+
for (const fragmentId of affectedFragmentIds) {
|
|
534
|
+
this.updateFragmentInAllContainers(fragmentId, observable);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Обновить фрагмент во всех контейнерах
|
|
539
|
+
*/
|
|
540
|
+
updateFragmentInAllContainers(fragmentId, observable) {
|
|
541
|
+
const fragment = this.fragments.get(fragmentId);
|
|
542
|
+
if (!fragment)
|
|
543
|
+
return;
|
|
544
|
+
for (const [container, binding] of this.containerBindings) {
|
|
545
|
+
const markerInfo = binding.markers.get(fragmentId);
|
|
546
|
+
if (!markerInfo)
|
|
547
|
+
continue;
|
|
548
|
+
const { startMarker, endMarker, nodes: oldNodes } = markerInfo;
|
|
549
|
+
const parent = startMarker.parentNode;
|
|
550
|
+
if (!parent)
|
|
551
|
+
continue;
|
|
552
|
+
// Отвязываем события от старых элементов
|
|
553
|
+
this.unbindEventsFromNodes(oldNodes, binding);
|
|
554
|
+
// Удаляем старые ноды (проверяем что node всё ещё child, т.к. injection мог переместить)
|
|
555
|
+
for (const node of oldNodes) {
|
|
556
|
+
if (node.parentNode === parent) {
|
|
557
|
+
parent.removeChild(node);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
// Создаём новые ноды
|
|
561
|
+
const newNodes = this.createNodesFromHtml(fragment.html);
|
|
562
|
+
const insertedNodes = [];
|
|
563
|
+
for (const node of newNodes) {
|
|
564
|
+
parent.insertBefore(node, endMarker);
|
|
565
|
+
insertedNodes.push(node);
|
|
566
|
+
}
|
|
567
|
+
// Сохраняем новые ноды
|
|
568
|
+
markerInfo.nodes = insertedNodes;
|
|
569
|
+
// Привязываем refs, обрабатываем injections и events к новым элементам
|
|
570
|
+
this.bindRefsForContainer(container);
|
|
571
|
+
this.processInjectionsForContainer(container);
|
|
572
|
+
for (const node of insertedNodes) {
|
|
573
|
+
if (node instanceof Element) {
|
|
574
|
+
const unbinders = this.bindEventsToElement(node);
|
|
575
|
+
binding.eventUnbinders.push(...unbinders);
|
|
576
|
+
// И для всех дочерних
|
|
577
|
+
const children = node.querySelectorAll('*');
|
|
578
|
+
for (const child of Array.from(children)) {
|
|
579
|
+
const childUnbinders = this.bindEventsToElement(child);
|
|
580
|
+
binding.eventUnbinders.push(...childUnbinders);
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
// Уведомляем об изменении
|
|
585
|
+
this.fragmentChangeObserver.notify({ fragmentId, oldNodes, newNodes: insertedNodes, affectedObservables: [observable] }, { fragmentId, oldNodes, newNodes: insertedNodes, affectedObservables: [observable] });
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
/**
|
|
589
|
+
* Отвязать события от нод
|
|
590
|
+
*/
|
|
591
|
+
unbindEventsFromNodes(nodes, binding) {
|
|
592
|
+
// Простая реализация: очищаем все unbinders
|
|
593
|
+
// В идеале нужно отслеживать какие unbinders к каким элементам относятся
|
|
594
|
+
// Но для простоты пока так
|
|
595
|
+
for (const unbind of binding.eventUnbinders) {
|
|
596
|
+
unbind();
|
|
597
|
+
}
|
|
598
|
+
binding.eventUnbinders = [];
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Отписаться от вложенных Observable в секции
|
|
602
|
+
*/
|
|
603
|
+
unsubscribeSectionNested(section) {
|
|
604
|
+
for (const sub of section.subscriptions) {
|
|
605
|
+
sub.unsubscribe();
|
|
606
|
+
}
|
|
607
|
+
section.subscriptions = [];
|
|
608
|
+
for (const child of section.children) {
|
|
609
|
+
this.unsubscribeSectionNested(child);
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Отписаться от всех Observable в секции
|
|
614
|
+
*/
|
|
615
|
+
unsubscribeSection(section) {
|
|
616
|
+
for (const sub of section.subscriptions) {
|
|
617
|
+
sub.unsubscribe();
|
|
618
|
+
}
|
|
619
|
+
section.subscriptions = [];
|
|
620
|
+
for (const child of section.children) {
|
|
621
|
+
this.unsubscribeSection(child);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
// ========================================
|
|
625
|
+
// Legacy API (для обратной совместимости)
|
|
626
|
+
// ========================================
|
|
627
|
+
/**
|
|
628
|
+
* @deprecated Используйте bind(container)
|
|
629
|
+
*/
|
|
630
|
+
createDOMFragment() {
|
|
631
|
+
const html = this.getTemplate();
|
|
632
|
+
const template = document.createElement('template');
|
|
633
|
+
template.innerHTML = html;
|
|
634
|
+
return template.content;
|
|
635
|
+
}
|
|
254
636
|
}
|
|
255
637
|
//# sourceMappingURL=TemplateInstance.js.map
|