@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,15 +1,16 @@
|
|
|
1
1
|
import Scope from './Scope.js';
|
|
2
2
|
import Observable, { MutationObserver } from '../api/Observer.js';
|
|
3
3
|
import Rule, { RuleMatch, RuleResult } from './Rule.js';
|
|
4
|
+
import Expression from './Expression.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
*
|
|
7
|
+
* FragmentChangeEvent - событие изменения конкретного фрагмента
|
|
7
8
|
*/
|
|
8
|
-
export interface
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
export interface FragmentChangeEvent {
|
|
10
|
+
fragmentId: string;
|
|
11
|
+
oldNodes: Node[];
|
|
12
|
+
newNodes: Node[];
|
|
13
|
+
affectedObservables: Observable<any>[];
|
|
13
14
|
}
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -28,6 +29,44 @@ export interface TemplateSection {
|
|
|
28
29
|
children: TemplateSection[];
|
|
29
30
|
/** Observable подписки для отслеживания */
|
|
30
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>;
|
|
31
70
|
}
|
|
32
71
|
|
|
33
72
|
/**
|
|
@@ -39,71 +78,139 @@ interface ObservableTracking {
|
|
|
39
78
|
section: TemplateSection;
|
|
40
79
|
rebuild: (section: TemplateSection) => RuleResult;
|
|
41
80
|
}>;
|
|
81
|
+
/** ID фрагментов, зависящих от этого Observable */
|
|
82
|
+
fragmentIds: Set<string>;
|
|
42
83
|
unsubscribe: () => void;
|
|
43
84
|
}
|
|
44
85
|
|
|
45
86
|
/**
|
|
46
|
-
*
|
|
47
|
-
* Хранит обработанные Rule и поддерживает реактивное обновление.
|
|
87
|
+
* TemplateInstance - динамический шаблон страницы.
|
|
48
88
|
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
89
|
+
* Поддерживает:
|
|
90
|
+
* - Множество мелких фрагментов, каждый обновляется независимо
|
|
91
|
+
* - Множество container bindings
|
|
92
|
+
* - Автоматическое обновление DOM при изменении Observable
|
|
93
|
+
* - bind/unbind для refs и events
|
|
51
94
|
*/
|
|
52
95
|
export default class TemplateInstance {
|
|
53
|
-
private template: string;
|
|
54
96
|
private scope: Scope;
|
|
55
97
|
private sections: TemplateSection[] = [];
|
|
56
|
-
private fragment: DocumentFragment | null = null;
|
|
57
98
|
|
|
58
|
-
/**
|
|
59
|
-
private
|
|
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>();
|
|
60
110
|
|
|
61
111
|
/** Группировка секций по Observable */
|
|
62
112
|
private observableTrackings = new Map<Observable<any>, ObservableTracking>();
|
|
113
|
+
|
|
114
|
+
/** Привязки к контейнерам */
|
|
115
|
+
private containerBindings = new Map<Element, ContainerBinding>();
|
|
63
116
|
|
|
64
|
-
constructor(
|
|
65
|
-
this.template = template;
|
|
117
|
+
constructor(scope: Scope) {
|
|
66
118
|
this.scope = scope;
|
|
67
119
|
}
|
|
68
120
|
|
|
121
|
+
// ========================================
|
|
122
|
+
// Public API - Getters
|
|
123
|
+
// ========================================
|
|
124
|
+
|
|
69
125
|
/**
|
|
70
|
-
* Получить
|
|
126
|
+
* Получить Scope
|
|
71
127
|
*/
|
|
72
|
-
public
|
|
73
|
-
return this.
|
|
128
|
+
public getScope(): Scope {
|
|
129
|
+
return this.scope;
|
|
74
130
|
}
|
|
75
131
|
|
|
76
132
|
/**
|
|
77
|
-
*
|
|
133
|
+
* Получить все секции
|
|
78
134
|
*/
|
|
79
|
-
public
|
|
80
|
-
|
|
81
|
-
this.template = newTemplate;
|
|
82
|
-
|
|
83
|
-
this.changeObserver.notify(
|
|
84
|
-
{ oldValue: null, newValue: null, oldTemplate, newTemplate },
|
|
85
|
-
{ oldValue: null, newValue: null, oldTemplate, newTemplate }
|
|
86
|
-
);
|
|
135
|
+
public getSections(): TemplateSection[] {
|
|
136
|
+
return this.sections;
|
|
87
137
|
}
|
|
88
138
|
|
|
89
139
|
/**
|
|
90
|
-
* Получить
|
|
140
|
+
* Получить все фрагменты
|
|
91
141
|
*/
|
|
92
|
-
public
|
|
93
|
-
return this.
|
|
142
|
+
public getAllFragments(): Map<string, FragmentBinding> {
|
|
143
|
+
return this.fragments;
|
|
94
144
|
}
|
|
95
145
|
|
|
96
146
|
/**
|
|
97
|
-
*
|
|
147
|
+
* Получить фрагмент по ID
|
|
98
148
|
*/
|
|
99
|
-
public
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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: []
|
|
104
184
|
};
|
|
105
|
-
|
|
106
|
-
|
|
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;
|
|
107
214
|
}
|
|
108
215
|
|
|
109
216
|
/**
|
|
@@ -114,28 +221,77 @@ export default class TemplateInstance {
|
|
|
114
221
|
}
|
|
115
222
|
|
|
116
223
|
/**
|
|
117
|
-
*
|
|
224
|
+
* Вставить добавленный фрагмент во все привязанные контейнеры.
|
|
225
|
+
* Вызывается после appendTemplate.
|
|
118
226
|
*/
|
|
119
|
-
public
|
|
120
|
-
|
|
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);
|
|
121
276
|
}
|
|
122
277
|
|
|
278
|
+
// ========================================
|
|
279
|
+
// Public API - Observable Tracking
|
|
280
|
+
// ========================================
|
|
281
|
+
|
|
123
282
|
/**
|
|
124
|
-
* Подписаться на Observable и автоматически пересоздавать
|
|
125
|
-
* Все секции, зависящие от одного Observable, обновляются разом.
|
|
283
|
+
* Подписаться на Observable и автоматически пересоздавать секцию
|
|
126
284
|
*/
|
|
127
285
|
public trackObservable(
|
|
128
286
|
observable: Observable<any>,
|
|
129
287
|
section: TemplateSection,
|
|
130
288
|
rebuild: (section: TemplateSection) => RuleResult
|
|
131
289
|
): () => void {
|
|
132
|
-
// Проверяем, есть ли уже отслеживание для этого Observable
|
|
133
290
|
let tracking = this.observableTrackings.get(observable);
|
|
134
291
|
|
|
135
292
|
if (!tracking) {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
this.rebuildAllSectionsForObservable(observable, newValue);
|
|
293
|
+
const listener = (_newValue: any) => {
|
|
294
|
+
this.rebuildFragmentsForObservable(observable);
|
|
139
295
|
};
|
|
140
296
|
|
|
141
297
|
observable.subscribe(listener);
|
|
@@ -143,21 +299,26 @@ export default class TemplateInstance {
|
|
|
143
299
|
tracking = {
|
|
144
300
|
observable,
|
|
145
301
|
sections: [],
|
|
302
|
+
fragmentIds: new Set(),
|
|
146
303
|
unsubscribe: () => observable.unsubscribe(listener)
|
|
147
304
|
};
|
|
148
305
|
|
|
149
306
|
this.observableTrackings.set(observable, tracking);
|
|
150
307
|
}
|
|
151
308
|
|
|
152
|
-
// Добавляем секцию в отслеживание
|
|
153
309
|
tracking.sections.push({ section, rebuild });
|
|
154
310
|
|
|
155
|
-
|
|
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
|
+
|
|
156
319
|
return () => {
|
|
157
320
|
if (tracking) {
|
|
158
321
|
tracking.sections = tracking.sections.filter(s => s.section !== section);
|
|
159
|
-
|
|
160
|
-
// Если больше нет секций, отписываемся от Observable
|
|
161
322
|
if (tracking.sections.length === 0) {
|
|
162
323
|
tracking.unsubscribe();
|
|
163
324
|
this.observableTrackings.delete(observable);
|
|
@@ -166,116 +327,136 @@ export default class TemplateInstance {
|
|
|
166
327
|
};
|
|
167
328
|
}
|
|
168
329
|
|
|
330
|
+
// ========================================
|
|
331
|
+
// Public API - Container Binding
|
|
332
|
+
// ========================================
|
|
333
|
+
|
|
169
334
|
/**
|
|
170
|
-
*
|
|
335
|
+
* Привязать к контейнеру.
|
|
336
|
+
* Вставляет DOM, вызывает bindRefs, processInjections и bindEvents.
|
|
171
337
|
*/
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const oldTemplate = this.template;
|
|
177
|
-
let currentTemplate = this.template;
|
|
178
|
-
|
|
179
|
-
// Собираем все замены: старый output -> новый output
|
|
180
|
-
// Сортируем по позиции в шаблоне (с конца), чтобы замены не сбивали индексы
|
|
181
|
-
const replacements: Array<{ oldOutput: string; newOutput: string; section: TemplateSection }> = [];
|
|
182
|
-
|
|
183
|
-
for (const { section, rebuild } of tracking.sections) {
|
|
184
|
-
// Unsubscribe old nested observables
|
|
185
|
-
this.unsubscribeSectionNested(section);
|
|
186
|
-
|
|
187
|
-
// Rebuild section
|
|
188
|
-
const newResult = rebuild(section);
|
|
189
|
-
const oldOutput = section.result.output;
|
|
190
|
-
|
|
191
|
-
replacements.push({
|
|
192
|
-
oldOutput,
|
|
193
|
-
newOutput: newResult.output,
|
|
194
|
-
section
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
section.result = newResult;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Применяем все замены
|
|
201
|
-
for (const { oldOutput, newOutput } of replacements) {
|
|
202
|
-
currentTemplate = currentTemplate.replace(oldOutput, newOutput);
|
|
338
|
+
public bind(container: Element): void {
|
|
339
|
+
if (this.containerBindings.has(container)) {
|
|
340
|
+
console.warn('[TemplateInstance] Container already bound');
|
|
341
|
+
return;
|
|
203
342
|
}
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
);
|
|
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);
|
|
212
359
|
}
|
|
213
360
|
|
|
214
361
|
/**
|
|
215
|
-
*
|
|
362
|
+
* Отвязать от контейнера.
|
|
363
|
+
* Отвязывает refs и events, но оставляет DOM.
|
|
216
364
|
*/
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
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;
|
|
221
370
|
}
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
this.unsubscribeSectionNested(child);
|
|
371
|
+
|
|
372
|
+
// Отвязываем события для этого контейнера
|
|
373
|
+
for (const unbind of binding.eventUnbinders) {
|
|
374
|
+
unbind();
|
|
227
375
|
}
|
|
376
|
+
binding.eventUnbinders = [];
|
|
377
|
+
|
|
378
|
+
// Отвязываем refs
|
|
379
|
+
this.unbindRefsForContainer(container);
|
|
380
|
+
|
|
381
|
+
// Удаляем binding
|
|
382
|
+
this.containerBindings.delete(container);
|
|
228
383
|
}
|
|
229
384
|
|
|
230
385
|
/**
|
|
231
|
-
*
|
|
386
|
+
* Привязать refs к элементам.
|
|
387
|
+
* Если есть контейнеры - привязывает для них.
|
|
388
|
+
* Если нет - создаёт временный DocumentFragment и привязывает refs из него.
|
|
232
389
|
*/
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
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);
|
|
236
399
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
+
}
|
|
242
413
|
}
|
|
243
414
|
}
|
|
244
415
|
|
|
245
416
|
/**
|
|
246
|
-
*
|
|
417
|
+
* Отвязать refs (для всех контейнеров)
|
|
247
418
|
*/
|
|
248
|
-
public
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
}
|
|
251
427
|
}
|
|
252
|
-
|
|
253
|
-
const template = document.createElement('template');
|
|
254
|
-
template.innerHTML = this.template;
|
|
255
|
-
this.fragment = template.content.cloneNode(true) as DocumentFragment;
|
|
256
|
-
|
|
257
|
-
return this.fragment;
|
|
258
428
|
}
|
|
259
429
|
|
|
260
430
|
/**
|
|
261
|
-
*
|
|
431
|
+
* Привязать события (для всех контейнеров)
|
|
262
432
|
*/
|
|
263
|
-
public
|
|
264
|
-
|
|
433
|
+
public bindEvents(): void {
|
|
434
|
+
for (const [container, binding] of this.containerBindings) {
|
|
435
|
+
this.bindEventsForContainer(container, binding);
|
|
436
|
+
}
|
|
265
437
|
}
|
|
266
438
|
|
|
267
439
|
/**
|
|
268
|
-
*
|
|
440
|
+
* Отвязать события (для всех контейнеров)
|
|
269
441
|
*/
|
|
270
|
-
public
|
|
271
|
-
this.
|
|
272
|
-
|
|
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
|
+
}
|
|
273
449
|
}
|
|
274
450
|
|
|
275
451
|
/**
|
|
276
|
-
* Очистить все подписки
|
|
452
|
+
* Очистить все подписки и bindings
|
|
277
453
|
*/
|
|
278
454
|
public dispose(): void {
|
|
455
|
+
// Отвязываем все контейнеры
|
|
456
|
+
for (const [container] of this.containerBindings) {
|
|
457
|
+
this.unbind(container);
|
|
458
|
+
}
|
|
459
|
+
|
|
279
460
|
// Отписываемся от всех Observable
|
|
280
461
|
for (const tracking of this.observableTrackings.values()) {
|
|
281
462
|
tracking.unsubscribe();
|
|
@@ -287,14 +468,110 @@ export default class TemplateInstance {
|
|
|
287
468
|
this.unsubscribeSection(section);
|
|
288
469
|
}
|
|
289
470
|
this.sections = [];
|
|
290
|
-
|
|
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);
|
|
291
489
|
}
|
|
292
490
|
|
|
293
491
|
/**
|
|
294
|
-
*
|
|
492
|
+
* Рекурсивно вставить фрагмент и его дочерние
|
|
295
493
|
*/
|
|
296
|
-
|
|
297
|
-
|
|
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]');
|
|
298
575
|
|
|
299
576
|
for (const element of Array.from(refElements)) {
|
|
300
577
|
const refName = element.getAttribute('data-ref');
|
|
@@ -305,32 +582,36 @@ export default class TemplateInstance {
|
|
|
305
582
|
}
|
|
306
583
|
|
|
307
584
|
/**
|
|
308
|
-
* Обработать инжекции
|
|
309
|
-
*
|
|
585
|
+
* Обработать инжекции для конкретного контейнера.
|
|
586
|
+
* Находит элементы с data-injection-* атрибутами и перемещает их в целевые элементы.
|
|
310
587
|
*/
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const injectElements = root.querySelectorAll('[data-injection-type][data-injection-target]');
|
|
588
|
+
private processInjectionsForContainer(container: Element): void {
|
|
589
|
+
const injectElements = container.querySelectorAll('[data-injection-type][data-injection-target]');
|
|
314
590
|
|
|
315
591
|
for (const element of Array.from(injectElements)) {
|
|
316
592
|
const type = element.getAttribute('data-injection-type') as 'head' | 'tail';
|
|
317
593
|
const targetRefName = decodeURIComponent(element.getAttribute('data-injection-target') || '');
|
|
318
594
|
|
|
319
|
-
if (!targetRefName)
|
|
595
|
+
if (!targetRefName) {
|
|
596
|
+
// Нет target - удаляем элемент из DOM
|
|
597
|
+
element.remove();
|
|
598
|
+
continue;
|
|
599
|
+
}
|
|
320
600
|
|
|
321
|
-
//
|
|
601
|
+
// Получаем целевой элемент из scope
|
|
322
602
|
const targetElement = this.scope.get(targetRefName);
|
|
323
603
|
|
|
324
604
|
if (!targetElement || !(targetElement instanceof Element)) {
|
|
325
|
-
|
|
605
|
+
// Target не найден - удаляем элемент из DOM
|
|
606
|
+
element.remove();
|
|
326
607
|
continue;
|
|
327
608
|
}
|
|
328
609
|
|
|
329
|
-
//
|
|
610
|
+
// Удаляем атрибуты инжекции
|
|
330
611
|
element.removeAttribute('data-injection-type');
|
|
331
612
|
element.removeAttribute('data-injection-target');
|
|
332
613
|
|
|
333
|
-
//
|
|
614
|
+
// Выполняем инжекцию
|
|
334
615
|
if (type === 'head') {
|
|
335
616
|
targetElement.prepend(element);
|
|
336
617
|
} else {
|
|
@@ -340,16 +621,223 @@ export default class TemplateInstance {
|
|
|
340
621
|
}
|
|
341
622
|
|
|
342
623
|
/**
|
|
343
|
-
* Отвязать refs
|
|
624
|
+
* Отвязать refs для конкретного контейнера
|
|
344
625
|
*/
|
|
345
|
-
|
|
626
|
+
private unbindRefsForContainer(_container: Element): void {
|
|
627
|
+
// Устанавливаем все refs в null
|
|
346
628
|
for (const section of this.sections) {
|
|
347
|
-
if (section.rule.name === 'ref' && section.
|
|
348
|
-
const refName = section.
|
|
629
|
+
if (section.rule.name === 'ref' && section.result.data?.refName) {
|
|
630
|
+
const refName = section.result.data.refName;
|
|
349
631
|
if (this.scope.has(refName)) {
|
|
350
632
|
this.scope.set(refName, null);
|
|
351
633
|
}
|
|
352
634
|
}
|
|
353
635
|
}
|
|
354
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
|
+
}
|
|
355
843
|
}
|