@reidelsaltres/pureper 0.1.174 → 0.1.176
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 +3 -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 -4
- package/out/foundation/component_api/UniHtml.d.ts.map +1 -1
- package/out/foundation/component_api/UniHtml.js +4 -11
- package/out/foundation/component_api/UniHtml.js.map +1 -1
- package/out/foundation/engine/Expression.d.ts.map +1 -1
- package/out/foundation/engine/Expression.js.map +1 -1
- package/out/foundation/engine/TemplateEngine.d.ts +45 -3
- package/out/foundation/engine/TemplateEngine.d.ts.map +1 -1
- package/out/foundation/engine/TemplateEngine.js +385 -49
- package/out/foundation/engine/TemplateEngine.js.map +1 -1
- package/out/foundation/worker/Router.d.ts.map +1 -1
- package/out/foundation/worker/Router.js +4 -2
- package/out/foundation/worker/Router.js.map +1 -1
- package/package.json +1 -1
- package/src/foundation/Triplet.ts +5 -6
- package/src/foundation/component_api/Component.ts +2 -1
- package/src/foundation/component_api/UniHtml.ts +14 -21
- package/src/foundation/engine/Expression.ts +1 -2
- package/src/foundation/engine/TemplateEngine.ts +414 -53
- package/src/foundation/worker/Router.ts +5 -3
- package/src/foundation/engine/BalancedParser.ts +0 -353
- package/src/foundation/engine/EscapeHandler.ts +0 -54
- package/src/foundation/engine/Rule.ts +0 -138
- package/src/foundation/engine/TemplateEngine.old.ts +0 -318
- package/src/foundation/engine/TemplateInstance.md +0 -110
- package/src/foundation/engine/TemplateInstance.old.ts +0 -673
- package/src/foundation/engine/TemplateInstance.ts +0 -843
- package/src/foundation/engine/exceptions/TemplateExceptions.ts +0 -27
- package/src/foundation/engine/rules/attribute/EventRule.ts +0 -171
- package/src/foundation/engine/rules/attribute/InjectionRule.ts +0 -140
- package/src/foundation/engine/rules/attribute/RefRule.ts +0 -126
- package/src/foundation/engine/rules/syntax/ExpressionRule.ts +0 -102
- package/src/foundation/engine/rules/syntax/ForRule.ts +0 -267
- package/src/foundation/engine/rules/syntax/IfRule.ts +0 -261
- package/src/foundation/hmle/Context.ts +0 -90
|
@@ -1,843 +0,0 @@
|
|
|
1
|
-
import Scope from './Scope.js';
|
|
2
|
-
import Observable, { MutationObserver } from '../api/Observer.js';
|
|
3
|
-
import Rule, { RuleMatch, RuleResult } from './Rule.js';
|
|
4
|
-
import Expression from './Expression.js';
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* FragmentChangeEvent - событие изменения конкретного фрагмента
|
|
8
|
-
*/
|
|
9
|
-
export interface FragmentChangeEvent {
|
|
10
|
-
fragmentId: string;
|
|
11
|
-
oldNodes: Node[];
|
|
12
|
-
newNodes: Node[];
|
|
13
|
-
affectedObservables: Observable<any>[];
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* TemplateSection - секция шаблона, связанная с Rule
|
|
18
|
-
*/
|
|
19
|
-
export interface TemplateSection {
|
|
20
|
-
/** Rule который создал эту секцию */
|
|
21
|
-
rule: Rule;
|
|
22
|
-
/** Оригинальный match */
|
|
23
|
-
match: RuleMatch;
|
|
24
|
-
/** Текущий результат */
|
|
25
|
-
result: RuleResult;
|
|
26
|
-
/** Исходный шаблон секции (для пересоздания) */
|
|
27
|
-
sourceTemplate: string;
|
|
28
|
-
/** Дочерние секции */
|
|
29
|
-
children: TemplateSection[];
|
|
30
|
-
/** Observable подписки для отслеживания */
|
|
31
|
-
subscriptions: Array<{ observable: Observable<any>; unsubscribe: () => void }>;
|
|
32
|
-
/** ID фрагмента, к которому принадлежит секция */
|
|
33
|
-
fragmentId?: string;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/**
|
|
37
|
-
* FragmentBinding - привязка фрагмента
|
|
38
|
-
*/
|
|
39
|
-
export interface FragmentBinding {
|
|
40
|
-
/** Уникальный ID фрагмента */
|
|
41
|
-
id: string;
|
|
42
|
-
/** Текущий HTML контент фрагмента (с placeholder-ами для дочерних) */
|
|
43
|
-
html: string;
|
|
44
|
-
/** Исходный шаблон (до обработки) */
|
|
45
|
-
sourceTemplate: string;
|
|
46
|
-
/** Секции, входящие в этот фрагмент */
|
|
47
|
-
sections: TemplateSection[];
|
|
48
|
-
/** Observable, от которых зависит фрагмент */
|
|
49
|
-
observables: Set<Observable<any>>;
|
|
50
|
-
/** ID родительского фрагмента (null для корневого) */
|
|
51
|
-
parentId: string | null;
|
|
52
|
-
/** ID дочерних фрагментов (placeholder-ы) */
|
|
53
|
-
childIds: string[];
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* ContainerBinding - привязка к DOM-контейнеру
|
|
58
|
-
*/
|
|
59
|
-
export interface ContainerBinding {
|
|
60
|
-
/** DOM-контейнер */
|
|
61
|
-
container: Element;
|
|
62
|
-
/** Маркеры фрагментов в этом контейнере: fragmentId -> { start, end, nodes } */
|
|
63
|
-
markers: Map<string, {
|
|
64
|
-
startMarker: Comment;
|
|
65
|
-
endMarker: Comment;
|
|
66
|
-
nodes: Node[];
|
|
67
|
-
}>;
|
|
68
|
-
/** Функции отписки событий для этого контейнера */
|
|
69
|
-
eventUnbinders: Array<() => void>;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* ObservableTracking - отслеживание Observable и связанных секций
|
|
74
|
-
*/
|
|
75
|
-
interface ObservableTracking {
|
|
76
|
-
observable: Observable<any>;
|
|
77
|
-
sections: Array<{
|
|
78
|
-
section: TemplateSection;
|
|
79
|
-
rebuild: (section: TemplateSection) => RuleResult;
|
|
80
|
-
}>;
|
|
81
|
-
/** ID фрагментов, зависящих от этого Observable */
|
|
82
|
-
fragmentIds: Set<string>;
|
|
83
|
-
unsubscribe: () => void;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* TemplateInstance - динамический шаблон страницы.
|
|
88
|
-
*
|
|
89
|
-
* Поддерживает:
|
|
90
|
-
* - Множество мелких фрагментов, каждый обновляется независимо
|
|
91
|
-
* - Множество container bindings
|
|
92
|
-
* - Автоматическое обновление DOM при изменении Observable
|
|
93
|
-
* - bind/unbind для refs и events
|
|
94
|
-
*/
|
|
95
|
-
export default class TemplateInstance {
|
|
96
|
-
private scope: Scope;
|
|
97
|
-
private sections: TemplateSection[] = [];
|
|
98
|
-
|
|
99
|
-
/** Все фрагменты шаблона */
|
|
100
|
-
private fragments = new Map<string, FragmentBinding>();
|
|
101
|
-
|
|
102
|
-
/** ID корневого фрагмента */
|
|
103
|
-
private rootFragmentId: string | null = null;
|
|
104
|
-
|
|
105
|
-
/** Счётчик для генерации ID фрагментов */
|
|
106
|
-
private fragmentIdCounter = 0;
|
|
107
|
-
|
|
108
|
-
/** Observers for fragment changes */
|
|
109
|
-
private fragmentChangeObserver = new MutationObserver<FragmentChangeEvent>();
|
|
110
|
-
|
|
111
|
-
/** Группировка секций по Observable */
|
|
112
|
-
private observableTrackings = new Map<Observable<any>, ObservableTracking>();
|
|
113
|
-
|
|
114
|
-
/** Привязки к контейнерам */
|
|
115
|
-
private containerBindings = new Map<Element, ContainerBinding>();
|
|
116
|
-
|
|
117
|
-
constructor(scope: Scope) {
|
|
118
|
-
this.scope = scope;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ========================================
|
|
122
|
-
// Public API - Getters
|
|
123
|
-
// ========================================
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Получить Scope
|
|
127
|
-
*/
|
|
128
|
-
public getScope(): Scope {
|
|
129
|
-
return this.scope;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* Получить все секции
|
|
134
|
-
*/
|
|
135
|
-
public getSections(): TemplateSection[] {
|
|
136
|
-
return this.sections;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Получить все фрагменты
|
|
141
|
-
*/
|
|
142
|
-
public getAllFragments(): Map<string, FragmentBinding> {
|
|
143
|
-
return this.fragments;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
/**
|
|
147
|
-
* Получить фрагмент по ID
|
|
148
|
-
*/
|
|
149
|
-
public getFragmentBinding(id: string): FragmentBinding | undefined {
|
|
150
|
-
return this.fragments.get(id);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Получить финальный HTML (собранный из всех фрагментов)
|
|
155
|
-
*/
|
|
156
|
-
public getTemplate(): string {
|
|
157
|
-
if (!this.rootFragmentId) return '';
|
|
158
|
-
return this.buildHtmlFromFragment(this.rootFragmentId);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// ========================================
|
|
162
|
-
// Public API - Fragment Management
|
|
163
|
-
// ========================================
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Создать новый фрагмент и вернуть его ID
|
|
167
|
-
*/
|
|
168
|
-
public createFragment(
|
|
169
|
-
html: string,
|
|
170
|
-
sourceTemplate: string,
|
|
171
|
-
sections: TemplateSection[] = [],
|
|
172
|
-
parentId: string | null = null
|
|
173
|
-
): string {
|
|
174
|
-
const id = `f${this.fragmentIdCounter++}`;
|
|
175
|
-
|
|
176
|
-
const binding: FragmentBinding = {
|
|
177
|
-
id,
|
|
178
|
-
html,
|
|
179
|
-
sourceTemplate,
|
|
180
|
-
sections,
|
|
181
|
-
observables: new Set(),
|
|
182
|
-
parentId,
|
|
183
|
-
childIds: []
|
|
184
|
-
};
|
|
185
|
-
|
|
186
|
-
// Привязываем секции к фрагменту
|
|
187
|
-
for (const section of sections) {
|
|
188
|
-
section.fragmentId = id;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
this.fragments.set(id, binding);
|
|
192
|
-
|
|
193
|
-
// Если нет корневого фрагмента, это первый
|
|
194
|
-
if (this.rootFragmentId === null) {
|
|
195
|
-
this.rootFragmentId = id;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
// Добавляем в дочерние родителя
|
|
199
|
-
if (parentId) {
|
|
200
|
-
const parent = this.fragments.get(parentId);
|
|
201
|
-
if (parent) {
|
|
202
|
-
parent.childIds.push(id);
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return id;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Установить корневой фрагмент
|
|
211
|
-
*/
|
|
212
|
-
public setRootFragment(id: string): void {
|
|
213
|
-
this.rootFragmentId = id;
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
/**
|
|
217
|
-
* Добавить секцию шаблона
|
|
218
|
-
*/
|
|
219
|
-
public addSection(section: TemplateSection): void {
|
|
220
|
-
this.sections.push(section);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
/**
|
|
224
|
-
* Вставить добавленный фрагмент во все привязанные контейнеры.
|
|
225
|
-
* Вызывается после appendTemplate.
|
|
226
|
-
*/
|
|
227
|
-
public insertAppendedFragment(fragmentId: string): void {
|
|
228
|
-
const fragment = this.fragments.get(fragmentId);
|
|
229
|
-
if (!fragment) return;
|
|
230
|
-
|
|
231
|
-
for (const [container, binding] of this.containerBindings) {
|
|
232
|
-
// Вставляем фрагмент в конец контейнера
|
|
233
|
-
this.insertFragmentRecursive(fragmentId, container, binding);
|
|
234
|
-
|
|
235
|
-
// Привязываем refs, events и injections для новых элементов
|
|
236
|
-
this.bindRefsForContainer(container);
|
|
237
|
-
this.processInjectionsForContainer(container);
|
|
238
|
-
this.bindEventsForNewFragment(fragmentId, binding);
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
/**
|
|
243
|
-
* Привязать события только для нового фрагмента
|
|
244
|
-
*/
|
|
245
|
-
private bindEventsForNewFragment(fragmentId: string, binding: ContainerBinding): void {
|
|
246
|
-
const markerInfo = binding.markers.get(fragmentId);
|
|
247
|
-
if (!markerInfo) return;
|
|
248
|
-
|
|
249
|
-
for (const node of markerInfo.nodes) {
|
|
250
|
-
if (node instanceof Element) {
|
|
251
|
-
const unbinders = this.bindEventsToElement(node);
|
|
252
|
-
binding.eventUnbinders.push(...unbinders);
|
|
253
|
-
|
|
254
|
-
const children = node.querySelectorAll('*');
|
|
255
|
-
for (const child of Array.from(children)) {
|
|
256
|
-
const childUnbinders = this.bindEventsToElement(child);
|
|
257
|
-
binding.eventUnbinders.push(...childUnbinders);
|
|
258
|
-
}
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
// ========================================
|
|
264
|
-
// Public API - Events
|
|
265
|
-
// ========================================
|
|
266
|
-
|
|
267
|
-
/**
|
|
268
|
-
* Подписаться на изменения фрагментов
|
|
269
|
-
*/
|
|
270
|
-
public onFragmentChange(listener: (event: FragmentChangeEvent) => void): () => void {
|
|
271
|
-
const wrapper = (_oldEvent: FragmentChangeEvent, newEvent: FragmentChangeEvent) => {
|
|
272
|
-
listener(newEvent);
|
|
273
|
-
};
|
|
274
|
-
this.fragmentChangeObserver.subscribe(wrapper);
|
|
275
|
-
return () => this.fragmentChangeObserver.unsubscribe(wrapper);
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
// ========================================
|
|
279
|
-
// Public API - Observable Tracking
|
|
280
|
-
// ========================================
|
|
281
|
-
|
|
282
|
-
/**
|
|
283
|
-
* Подписаться на Observable и автоматически пересоздавать секцию
|
|
284
|
-
*/
|
|
285
|
-
public trackObservable(
|
|
286
|
-
observable: Observable<any>,
|
|
287
|
-
section: TemplateSection,
|
|
288
|
-
rebuild: (section: TemplateSection) => RuleResult
|
|
289
|
-
): () => void {
|
|
290
|
-
let tracking = this.observableTrackings.get(observable);
|
|
291
|
-
|
|
292
|
-
if (!tracking) {
|
|
293
|
-
const listener = (_newValue: any) => {
|
|
294
|
-
this.rebuildFragmentsForObservable(observable);
|
|
295
|
-
};
|
|
296
|
-
|
|
297
|
-
observable.subscribe(listener);
|
|
298
|
-
|
|
299
|
-
tracking = {
|
|
300
|
-
observable,
|
|
301
|
-
sections: [],
|
|
302
|
-
fragmentIds: new Set(),
|
|
303
|
-
unsubscribe: () => observable.unsubscribe(listener)
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
this.observableTrackings.set(observable, tracking);
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
tracking.sections.push({ section, rebuild });
|
|
310
|
-
|
|
311
|
-
if (section.fragmentId) {
|
|
312
|
-
tracking.fragmentIds.add(section.fragmentId);
|
|
313
|
-
const binding = this.fragments.get(section.fragmentId);
|
|
314
|
-
if (binding) {
|
|
315
|
-
binding.observables.add(observable);
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
return () => {
|
|
320
|
-
if (tracking) {
|
|
321
|
-
tracking.sections = tracking.sections.filter(s => s.section !== section);
|
|
322
|
-
if (tracking.sections.length === 0) {
|
|
323
|
-
tracking.unsubscribe();
|
|
324
|
-
this.observableTrackings.delete(observable);
|
|
325
|
-
}
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// ========================================
|
|
331
|
-
// Public API - Container Binding
|
|
332
|
-
// ========================================
|
|
333
|
-
|
|
334
|
-
/**
|
|
335
|
-
* Привязать к контейнеру.
|
|
336
|
-
* Вставляет DOM, вызывает bindRefs, processInjections и bindEvents.
|
|
337
|
-
*/
|
|
338
|
-
public bind(container: Element): void {
|
|
339
|
-
if (this.containerBindings.has(container)) {
|
|
340
|
-
console.warn('[TemplateInstance] Container already bound');
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
const binding: ContainerBinding = {
|
|
345
|
-
container,
|
|
346
|
-
markers: new Map(),
|
|
347
|
-
eventUnbinders: []
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
this.containerBindings.set(container, binding);
|
|
351
|
-
|
|
352
|
-
// Вставляем DOM с маркерами
|
|
353
|
-
this.insertFragmentsIntoContainer(container, binding);
|
|
354
|
-
|
|
355
|
-
// Привязываем refs, обрабатываем injections, привязываем events
|
|
356
|
-
this.bindRefsForContainer(container);
|
|
357
|
-
this.processInjectionsForContainer(container);
|
|
358
|
-
this.bindEventsForContainer(container, binding);
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* Отвязать от контейнера.
|
|
363
|
-
* Отвязывает refs и events, но оставляет DOM.
|
|
364
|
-
*/
|
|
365
|
-
public unbind(container: Element): void {
|
|
366
|
-
const binding = this.containerBindings.get(container);
|
|
367
|
-
if (!binding) {
|
|
368
|
-
console.warn('[TemplateInstance] Container not bound');
|
|
369
|
-
return;
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
// Отвязываем события для этого контейнера
|
|
373
|
-
for (const unbind of binding.eventUnbinders) {
|
|
374
|
-
unbind();
|
|
375
|
-
}
|
|
376
|
-
binding.eventUnbinders = [];
|
|
377
|
-
|
|
378
|
-
// Отвязываем refs
|
|
379
|
-
this.unbindRefsForContainer(container);
|
|
380
|
-
|
|
381
|
-
// Удаляем binding
|
|
382
|
-
this.containerBindings.delete(container);
|
|
383
|
-
}
|
|
384
|
-
|
|
385
|
-
/**
|
|
386
|
-
* Привязать refs к элементам.
|
|
387
|
-
* Если есть контейнеры - привязывает для них.
|
|
388
|
-
* Если нет - создаёт временный DocumentFragment и привязывает refs из него.
|
|
389
|
-
*/
|
|
390
|
-
public bindRefs(): void {
|
|
391
|
-
if (this.containerBindings.size > 0) {
|
|
392
|
-
for (const [container] of this.containerBindings) {
|
|
393
|
-
this.bindRefsForContainer(container);
|
|
394
|
-
}
|
|
395
|
-
} else {
|
|
396
|
-
// Нет контейнеров - создаём временный fragment
|
|
397
|
-
const fragment = this.createDOMFragment();
|
|
398
|
-
this.bindRefsForFragment(fragment);
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* Привязать refs из DocumentFragment (без контейнера)
|
|
404
|
-
*/
|
|
405
|
-
private bindRefsForFragment(fragment: DocumentFragment): void {
|
|
406
|
-
const refElements = fragment.querySelectorAll('[data-ref]');
|
|
407
|
-
|
|
408
|
-
for (const element of Array.from(refElements)) {
|
|
409
|
-
const refName = element.getAttribute('data-ref');
|
|
410
|
-
if (refName) {
|
|
411
|
-
this.scope.set(refName, element);
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
}
|
|
415
|
-
|
|
416
|
-
/**
|
|
417
|
-
* Отвязать refs (для всех контейнеров)
|
|
418
|
-
*/
|
|
419
|
-
public unbindRefs(): void {
|
|
420
|
-
for (const section of this.sections) {
|
|
421
|
-
if (section.rule.name === 'ref' && section.result.data?.refName) {
|
|
422
|
-
const refName = section.result.data.refName;
|
|
423
|
-
if (this.scope.has(refName)) {
|
|
424
|
-
this.scope.set(refName, null);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* Привязать события (для всех контейнеров)
|
|
432
|
-
*/
|
|
433
|
-
public bindEvents(): void {
|
|
434
|
-
for (const [container, binding] of this.containerBindings) {
|
|
435
|
-
this.bindEventsForContainer(container, binding);
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
/**
|
|
440
|
-
* Отвязать события (для всех контейнеров)
|
|
441
|
-
*/
|
|
442
|
-
public unbindEvents(): void {
|
|
443
|
-
for (const [_, binding] of this.containerBindings) {
|
|
444
|
-
for (const unbind of binding.eventUnbinders) {
|
|
445
|
-
unbind();
|
|
446
|
-
}
|
|
447
|
-
binding.eventUnbinders = [];
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
|
|
451
|
-
/**
|
|
452
|
-
* Очистить все подписки и bindings
|
|
453
|
-
*/
|
|
454
|
-
public dispose(): void {
|
|
455
|
-
// Отвязываем все контейнеры
|
|
456
|
-
for (const [container] of this.containerBindings) {
|
|
457
|
-
this.unbind(container);
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
// Отписываемся от всех Observable
|
|
461
|
-
for (const tracking of this.observableTrackings.values()) {
|
|
462
|
-
tracking.unsubscribe();
|
|
463
|
-
}
|
|
464
|
-
this.observableTrackings.clear();
|
|
465
|
-
|
|
466
|
-
// Очищаем секции
|
|
467
|
-
for (const section of this.sections) {
|
|
468
|
-
this.unsubscribeSection(section);
|
|
469
|
-
}
|
|
470
|
-
this.sections = [];
|
|
471
|
-
|
|
472
|
-
// Очищаем фрагменты
|
|
473
|
-
this.fragments.clear();
|
|
474
|
-
this.rootFragmentId = null;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
// ========================================
|
|
478
|
-
// Private - DOM Operations
|
|
479
|
-
// ========================================
|
|
480
|
-
|
|
481
|
-
/**
|
|
482
|
-
* Вставить все фрагменты в контейнер с маркерами
|
|
483
|
-
*/
|
|
484
|
-
private insertFragmentsIntoContainer(container: Element, binding: ContainerBinding): void {
|
|
485
|
-
if (!this.rootFragmentId) return;
|
|
486
|
-
|
|
487
|
-
// Рекурсивно вставляем фрагменты
|
|
488
|
-
this.insertFragmentRecursive(this.rootFragmentId, container, binding);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* Рекурсивно вставить фрагмент и его дочерние
|
|
493
|
-
*/
|
|
494
|
-
private insertFragmentRecursive(
|
|
495
|
-
fragmentId: string,
|
|
496
|
-
parent: Element | DocumentFragment,
|
|
497
|
-
containerBinding: ContainerBinding
|
|
498
|
-
): void {
|
|
499
|
-
const fragment = this.fragments.get(fragmentId);
|
|
500
|
-
if (!fragment) return;
|
|
501
|
-
|
|
502
|
-
// Создаём маркеры
|
|
503
|
-
const startMarker = document.createComment(`fragment:${fragmentId}`);
|
|
504
|
-
const endMarker = document.createComment(`/fragment:${fragmentId}`);
|
|
505
|
-
|
|
506
|
-
parent.appendChild(startMarker);
|
|
507
|
-
|
|
508
|
-
// Парсим HTML и вставляем, заменяя placeholder-ы на дочерние фрагменты
|
|
509
|
-
const nodes = this.createNodesFromHtml(fragment.html);
|
|
510
|
-
const insertedNodes: Node[] = [];
|
|
511
|
-
|
|
512
|
-
// Node.COMMENT_NODE = 8
|
|
513
|
-
const COMMENT_NODE = 8;
|
|
514
|
-
|
|
515
|
-
for (const node of nodes) {
|
|
516
|
-
// Проверяем на placeholder комментарии
|
|
517
|
-
if (node.nodeType === COMMENT_NODE) {
|
|
518
|
-
const comment = node as Comment;
|
|
519
|
-
const match = comment.data.match(/^placeholder:(.+)$/);
|
|
520
|
-
if (match) {
|
|
521
|
-
const childId = match[1];
|
|
522
|
-
// Рекурсивно вставляем дочерний фрагмент
|
|
523
|
-
this.insertFragmentRecursive(childId, parent as Element, containerBinding);
|
|
524
|
-
continue;
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
parent.appendChild(node);
|
|
529
|
-
insertedNodes.push(node);
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
parent.appendChild(endMarker);
|
|
533
|
-
|
|
534
|
-
// Сохраняем маркеры
|
|
535
|
-
containerBinding.markers.set(fragmentId, {
|
|
536
|
-
startMarker,
|
|
537
|
-
endMarker,
|
|
538
|
-
nodes: insertedNodes
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Создать ноды из HTML строки
|
|
544
|
-
*/
|
|
545
|
-
private createNodesFromHtml(html: string): Node[] {
|
|
546
|
-
const template = document.createElement('template');
|
|
547
|
-
template.innerHTML = html;
|
|
548
|
-
return Array.from(template.content.childNodes);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
/**
|
|
552
|
-
* Собрать HTML из фрагмента (рекурсивно заменяя placeholder-ы)
|
|
553
|
-
*/
|
|
554
|
-
private buildHtmlFromFragment(fragmentId: string): string {
|
|
555
|
-
const fragment = this.fragments.get(fragmentId);
|
|
556
|
-
if (!fragment) return '';
|
|
557
|
-
|
|
558
|
-
let html = fragment.html;
|
|
559
|
-
|
|
560
|
-
// Заменяем placeholder-ы на контент дочерних фрагментов
|
|
561
|
-
for (const childId of fragment.childIds) {
|
|
562
|
-
const placeholder = `<!--placeholder:${childId}-->`;
|
|
563
|
-
const childHtml = this.buildHtmlFromFragment(childId);
|
|
564
|
-
html = html.replace(placeholder, childHtml);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
return html;
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
/**
|
|
571
|
-
* Привязать refs для конкретного контейнера
|
|
572
|
-
*/
|
|
573
|
-
private bindRefsForContainer(container: Element): void {
|
|
574
|
-
const refElements = container.querySelectorAll('[data-ref]');
|
|
575
|
-
|
|
576
|
-
for (const element of Array.from(refElements)) {
|
|
577
|
-
const refName = element.getAttribute('data-ref');
|
|
578
|
-
if (refName) {
|
|
579
|
-
this.scope.set(refName, element);
|
|
580
|
-
}
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
/**
|
|
585
|
-
* Обработать инжекции для конкретного контейнера.
|
|
586
|
-
* Находит элементы с data-injection-* атрибутами и перемещает их в целевые элементы.
|
|
587
|
-
*/
|
|
588
|
-
private processInjectionsForContainer(container: Element): void {
|
|
589
|
-
const injectElements = container.querySelectorAll('[data-injection-type][data-injection-target]');
|
|
590
|
-
|
|
591
|
-
for (const element of Array.from(injectElements)) {
|
|
592
|
-
const type = element.getAttribute('data-injection-type') as 'head' | 'tail';
|
|
593
|
-
const targetRefName = decodeURIComponent(element.getAttribute('data-injection-target') || '');
|
|
594
|
-
|
|
595
|
-
if (!targetRefName) {
|
|
596
|
-
// Нет target - удаляем элемент из DOM
|
|
597
|
-
element.remove();
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
// Получаем целевой элемент из scope
|
|
602
|
-
const targetElement = this.scope.get(targetRefName);
|
|
603
|
-
|
|
604
|
-
if (!targetElement || !(targetElement instanceof Element)) {
|
|
605
|
-
// Target не найден - удаляем элемент из DOM
|
|
606
|
-
element.remove();
|
|
607
|
-
continue;
|
|
608
|
-
}
|
|
609
|
-
|
|
610
|
-
// Удаляем атрибуты инжекции
|
|
611
|
-
element.removeAttribute('data-injection-type');
|
|
612
|
-
element.removeAttribute('data-injection-target');
|
|
613
|
-
|
|
614
|
-
// Выполняем инжекцию
|
|
615
|
-
if (type === 'head') {
|
|
616
|
-
targetElement.prepend(element);
|
|
617
|
-
} else {
|
|
618
|
-
targetElement.append(element);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Отвязать refs для конкретного контейнера
|
|
625
|
-
*/
|
|
626
|
-
private unbindRefsForContainer(_container: Element): void {
|
|
627
|
-
// Устанавливаем все refs в null
|
|
628
|
-
for (const section of this.sections) {
|
|
629
|
-
if (section.rule.name === 'ref' && section.result.data?.refName) {
|
|
630
|
-
const refName = section.result.data.refName;
|
|
631
|
-
if (this.scope.has(refName)) {
|
|
632
|
-
this.scope.set(refName, null);
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
|
|
638
|
-
/**
|
|
639
|
-
* Привязать события для конкретного контейнера
|
|
640
|
-
*/
|
|
641
|
-
private bindEventsForContainer(container: Element, binding: ContainerBinding): void {
|
|
642
|
-
const allElements = container.querySelectorAll('*');
|
|
643
|
-
|
|
644
|
-
for (const element of Array.from(allElements)) {
|
|
645
|
-
const unbinders = this.bindEventsToElement(element);
|
|
646
|
-
binding.eventUnbinders.push(...unbinders);
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
/**
|
|
651
|
-
* Привязать события к одному элементу
|
|
652
|
-
*/
|
|
653
|
-
private bindEventsToElement(element: Element): Array<() => void> {
|
|
654
|
-
const unbinders: Array<() => void> = [];
|
|
655
|
-
const attributes = Array.from(element.attributes);
|
|
656
|
-
|
|
657
|
-
for (const attr of attributes) {
|
|
658
|
-
if (attr.name.startsWith('data-event-')) {
|
|
659
|
-
const eventName = attr.name.slice('data-event-'.length);
|
|
660
|
-
const exprCode = decodeURIComponent(attr.value);
|
|
661
|
-
|
|
662
|
-
const handler = (event: Event) => {
|
|
663
|
-
const localScope = this.scope.createChild({ event });
|
|
664
|
-
const expr = new Expression(exprCode);
|
|
665
|
-
|
|
666
|
-
try {
|
|
667
|
-
expr.execute(localScope);
|
|
668
|
-
} catch (error) {
|
|
669
|
-
console.error(`[TemplateInstance] Error executing handler for ${eventName}:`, error);
|
|
670
|
-
}
|
|
671
|
-
};
|
|
672
|
-
|
|
673
|
-
element.addEventListener(eventName, handler);
|
|
674
|
-
unbinders.push(() => element.removeEventListener(eventName, handler));
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
return unbinders;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// ========================================
|
|
682
|
-
// Private - Observable Rebuild
|
|
683
|
-
// ========================================
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
* Перестроить фрагменты при изменении Observable
|
|
687
|
-
*/
|
|
688
|
-
private rebuildFragmentsForObservable(observable: Observable<any>): void {
|
|
689
|
-
const tracking = this.observableTrackings.get(observable);
|
|
690
|
-
if (!tracking || tracking.sections.length === 0) return;
|
|
691
|
-
|
|
692
|
-
// Собираем затронутые фрагменты
|
|
693
|
-
const affectedFragmentIds = new Set<string>();
|
|
694
|
-
|
|
695
|
-
// Перестраиваем секции и обновляем результаты
|
|
696
|
-
for (const { section, rebuild } of tracking.sections) {
|
|
697
|
-
this.unsubscribeSectionNested(section);
|
|
698
|
-
|
|
699
|
-
const newResult = rebuild(section);
|
|
700
|
-
section.result = newResult;
|
|
701
|
-
|
|
702
|
-
if (section.fragmentId) {
|
|
703
|
-
affectedFragmentIds.add(section.fragmentId);
|
|
704
|
-
}
|
|
705
|
-
}
|
|
706
|
-
|
|
707
|
-
// Перестраиваем HTML затронутых фрагментов
|
|
708
|
-
for (const fragmentId of affectedFragmentIds) {
|
|
709
|
-
const fragment = this.fragments.get(fragmentId);
|
|
710
|
-
if (!fragment) continue;
|
|
711
|
-
|
|
712
|
-
// Собираем новый HTML из результатов всех секций фрагмента
|
|
713
|
-
let newHtml = fragment.sourceTemplate;
|
|
714
|
-
for (const section of fragment.sections) {
|
|
715
|
-
// Заменяем исходный match на новый output
|
|
716
|
-
newHtml = newHtml.replace(section.match.fullMatch, section.result.output);
|
|
717
|
-
}
|
|
718
|
-
fragment.html = newHtml;
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
// Обновляем DOM во всех контейнерах
|
|
722
|
-
for (const fragmentId of affectedFragmentIds) {
|
|
723
|
-
this.updateFragmentInAllContainers(fragmentId, observable);
|
|
724
|
-
}
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
/**
|
|
728
|
-
* Обновить фрагмент во всех контейнерах
|
|
729
|
-
*/
|
|
730
|
-
private updateFragmentInAllContainers(fragmentId: string, observable: Observable<any>): void {
|
|
731
|
-
const fragment = this.fragments.get(fragmentId);
|
|
732
|
-
if (!fragment) return;
|
|
733
|
-
|
|
734
|
-
for (const [container, binding] of this.containerBindings) {
|
|
735
|
-
const markerInfo = binding.markers.get(fragmentId);
|
|
736
|
-
if (!markerInfo) continue;
|
|
737
|
-
|
|
738
|
-
const { startMarker, endMarker, nodes: oldNodes } = markerInfo;
|
|
739
|
-
const parent = startMarker.parentNode;
|
|
740
|
-
if (!parent) continue;
|
|
741
|
-
|
|
742
|
-
// Отвязываем события от старых элементов
|
|
743
|
-
this.unbindEventsFromNodes(oldNodes, binding);
|
|
744
|
-
|
|
745
|
-
// Удаляем старые ноды (проверяем что node всё ещё child, т.к. injection мог переместить)
|
|
746
|
-
for (const node of oldNodes) {
|
|
747
|
-
if (node.parentNode === parent) {
|
|
748
|
-
parent.removeChild(node);
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
// Создаём новые ноды
|
|
753
|
-
const newNodes = this.createNodesFromHtml(fragment.html);
|
|
754
|
-
const insertedNodes: Node[] = [];
|
|
755
|
-
|
|
756
|
-
for (const node of newNodes) {
|
|
757
|
-
parent.insertBefore(node, endMarker);
|
|
758
|
-
insertedNodes.push(node);
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
// Сохраняем новые ноды
|
|
762
|
-
markerInfo.nodes = insertedNodes;
|
|
763
|
-
|
|
764
|
-
// Привязываем refs, обрабатываем injections и events к новым элементам
|
|
765
|
-
this.bindRefsForContainer(container);
|
|
766
|
-
this.processInjectionsForContainer(container);
|
|
767
|
-
for (const node of insertedNodes) {
|
|
768
|
-
if (node instanceof Element) {
|
|
769
|
-
const unbinders = this.bindEventsToElement(node);
|
|
770
|
-
binding.eventUnbinders.push(...unbinders);
|
|
771
|
-
|
|
772
|
-
// И для всех дочерних
|
|
773
|
-
const children = node.querySelectorAll('*');
|
|
774
|
-
for (const child of Array.from(children)) {
|
|
775
|
-
const childUnbinders = this.bindEventsToElement(child);
|
|
776
|
-
binding.eventUnbinders.push(...childUnbinders);
|
|
777
|
-
}
|
|
778
|
-
}
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
// Уведомляем об изменении
|
|
782
|
-
this.fragmentChangeObserver.notify(
|
|
783
|
-
{ fragmentId, oldNodes, newNodes: insertedNodes, affectedObservables: [observable] },
|
|
784
|
-
{ fragmentId, oldNodes, newNodes: insertedNodes, affectedObservables: [observable] }
|
|
785
|
-
);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
|
|
789
|
-
/**
|
|
790
|
-
* Отвязать события от нод
|
|
791
|
-
*/
|
|
792
|
-
private unbindEventsFromNodes(nodes: Node[], binding: ContainerBinding): void {
|
|
793
|
-
// Простая реализация: очищаем все unbinders
|
|
794
|
-
// В идеале нужно отслеживать какие unbinders к каким элементам относятся
|
|
795
|
-
// Но для простоты пока так
|
|
796
|
-
for (const unbind of binding.eventUnbinders) {
|
|
797
|
-
unbind();
|
|
798
|
-
}
|
|
799
|
-
binding.eventUnbinders = [];
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
/**
|
|
803
|
-
* Отписаться от вложенных Observable в секции
|
|
804
|
-
*/
|
|
805
|
-
private unsubscribeSectionNested(section: TemplateSection): void {
|
|
806
|
-
for (const sub of section.subscriptions) {
|
|
807
|
-
sub.unsubscribe();
|
|
808
|
-
}
|
|
809
|
-
section.subscriptions = [];
|
|
810
|
-
|
|
811
|
-
for (const child of section.children) {
|
|
812
|
-
this.unsubscribeSectionNested(child);
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
|
|
816
|
-
/**
|
|
817
|
-
* Отписаться от всех Observable в секции
|
|
818
|
-
*/
|
|
819
|
-
private unsubscribeSection(section: TemplateSection): void {
|
|
820
|
-
for (const sub of section.subscriptions) {
|
|
821
|
-
sub.unsubscribe();
|
|
822
|
-
}
|
|
823
|
-
section.subscriptions = [];
|
|
824
|
-
|
|
825
|
-
for (const child of section.children) {
|
|
826
|
-
this.unsubscribeSection(child);
|
|
827
|
-
}
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
// ========================================
|
|
831
|
-
// Legacy API (для обратной совместимости)
|
|
832
|
-
// ========================================
|
|
833
|
-
|
|
834
|
-
/**
|
|
835
|
-
* @deprecated Используйте bind(container)
|
|
836
|
-
*/
|
|
837
|
-
public createDOMFragment(): DocumentFragment {
|
|
838
|
-
const html = this.getTemplate();
|
|
839
|
-
const template = document.createElement('template');
|
|
840
|
-
template.innerHTML = html;
|
|
841
|
-
return template.content;
|
|
842
|
-
}
|
|
843
|
-
}
|