@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.
Files changed (48) hide show
  1. package/out/foundation/Triplet.d.ts.map +1 -1
  2. package/out/foundation/Triplet.js +4 -5
  3. package/out/foundation/Triplet.js.map +1 -1
  4. package/out/foundation/component_api/Component.d.ts +2 -2
  5. package/out/foundation/component_api/Component.d.ts.map +1 -1
  6. package/out/foundation/component_api/Component.js.map +1 -1
  7. package/out/foundation/component_api/UniHtml.d.ts +4 -10
  8. package/out/foundation/component_api/UniHtml.d.ts.map +1 -1
  9. package/out/foundation/component_api/UniHtml.js +7 -15
  10. package/out/foundation/component_api/UniHtml.js.map +1 -1
  11. package/out/foundation/engine/Rule.d.ts +2 -0
  12. package/out/foundation/engine/Rule.d.ts.map +1 -1
  13. package/out/foundation/engine/Rule.js.map +1 -1
  14. package/out/foundation/engine/Scope.d.ts +5 -1
  15. package/out/foundation/engine/Scope.d.ts.map +1 -1
  16. package/out/foundation/engine/Scope.js +12 -3
  17. package/out/foundation/engine/Scope.js.map +1 -1
  18. package/out/foundation/engine/TemplateEngine.d.ts +17 -0
  19. package/out/foundation/engine/TemplateEngine.d.ts.map +1 -1
  20. package/out/foundation/engine/TemplateEngine.js +51 -3
  21. package/out/foundation/engine/TemplateEngine.js.map +1 -1
  22. package/out/foundation/engine/TemplateInstance.d.ts +167 -47
  23. package/out/foundation/engine/TemplateInstance.d.ts.map +1 -1
  24. package/out/foundation/engine/TemplateInstance.js +500 -118
  25. package/out/foundation/engine/TemplateInstance.js.map +1 -1
  26. package/out/foundation/engine/TemplateInstance.old.d.ts +219 -0
  27. package/out/foundation/engine/TemplateInstance.old.d.ts.map +1 -0
  28. package/out/foundation/engine/TemplateInstance.old.js +487 -0
  29. package/out/foundation/engine/TemplateInstance.old.js.map +1 -0
  30. package/out/foundation/engine/rules/attribute/RefRule.d.ts.map +1 -1
  31. package/out/foundation/engine/rules/attribute/RefRule.js +7 -1
  32. package/out/foundation/engine/rules/attribute/RefRule.js.map +1 -1
  33. package/out/index.d.ts +0 -1
  34. package/out/index.d.ts.map +1 -1
  35. package/out/index.js +0 -1
  36. package/out/index.js.map +1 -1
  37. package/package.json +1 -1
  38. package/src/foundation/Triplet.ts +6 -6
  39. package/src/foundation/component_api/Component.ts +2 -1
  40. package/src/foundation/component_api/UniHtml.ts +12 -22
  41. package/src/foundation/engine/Rule.ts +2 -0
  42. package/src/foundation/engine/Scope.ts +13 -3
  43. package/src/foundation/engine/TemplateEngine.ts +79 -4
  44. package/src/foundation/engine/TemplateInstance.md +110 -0
  45. package/src/foundation/engine/TemplateInstance.old.ts +673 -0
  46. package/src/foundation/engine/TemplateInstance.ts +635 -147
  47. package/src/foundation/engine/rules/attribute/RefRule.ts +8 -1
  48. 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
- * TemplateChangeEvent - событие изменения шаблона
7
+ * FragmentChangeEvent - событие изменения конкретного фрагмента
7
8
  */
8
- export interface TemplateChangeEvent {
9
- oldValue: any;
10
- newValue: any;
11
- oldTemplate: string;
12
- newTemplate: string;
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
- * PageTemplate - динамический шаблон страницы.
47
- * Хранит обработанные Rule и поддерживает реактивное обновление.
87
+ * TemplateInstance - динамический шаблон страницы.
48
88
  *
49
- * При изменении Observable все зависимые секции обновляются
50
- * одновременно в одном событии onTemplateChange.
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
- /** Observers for template changes */
59
- private changeObserver = new MutationObserver<TemplateChangeEvent>();
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(template: string, scope: Scope) {
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 getTemplate(): string {
73
- return this.template;
128
+ public getScope(): Scope {
129
+ return this.scope;
74
130
  }
75
131
 
76
132
  /**
77
- * Установить новый шаблон (вызывает событие изменения)
133
+ * Получить все секции
78
134
  */
79
- public setTemplate(newTemplate: string): void {
80
- const oldTemplate = this.template;
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
- * Получить Scope
140
+ * Получить все фрагменты
91
141
  */
92
- public getScope(): Scope {
93
- return this.scope;
142
+ public getAllFragments(): Map<string, FragmentBinding> {
143
+ return this.fragments;
94
144
  }
95
145
 
96
146
  /**
97
- * Подписаться на изменения шаблона
147
+ * Получить фрагмент по ID
98
148
  */
99
- public onTemplateChange(
100
- listener: (oldValue: any, newValue: any, oldTemplate: string, newTemplate: string) => void
101
- ): () => void {
102
- const wrapper = (oldEvent: TemplateChangeEvent, newEvent: TemplateChangeEvent) => {
103
- listener(newEvent.oldValue, newEvent.newValue, newEvent.oldTemplate, newEvent.newTemplate);
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
- this.changeObserver.subscribe(wrapper);
106
- return () => this.changeObserver.unsubscribe(wrapper);
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 getSections(): TemplateSection[] {
120
- return this.sections;
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
- const listener = (newValue: any) => {
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
- * Перестроить все секции, зависящие от Observable, за один раз
335
+ * Привязать к контейнеру.
336
+ * Вставляет DOM, вызывает bindRefs, processInjections и bindEvents.
171
337
  */
172
- private rebuildAllSectionsForObservable(observable: Observable<any>, newValue: any): void {
173
- const tracking = this.observableTrackings.get(observable);
174
- if (!tracking || tracking.sections.length === 0) return;
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
- this.template = currentTemplate;
206
-
207
- // Одно событие для всех изменений
208
- this.changeObserver.notify(
209
- { oldValue: null, newValue, oldTemplate, newTemplate: this.template },
210
- { oldValue: null, newValue, oldTemplate, newTemplate: this.template }
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
- * Отписаться от вложенных Observable в секции (но не от главного)
362
+ * Отвязать от контейнера.
363
+ * Отвязывает refs и events, но оставляет DOM.
216
364
  */
217
- private unsubscribeSectionNested(section: TemplateSection): void {
218
- // Отписываемся только от подписок, сохранённых непосредственно в секции
219
- for (const sub of section.subscriptions) {
220
- sub.unsubscribe();
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
- section.subscriptions = [];
223
-
224
- // Recursively unsubscribe children
225
- for (const child of section.children) {
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
- * Отписаться от всех Observable в секции
386
+ * Привязать refs к элементам.
387
+ * Если есть контейнеры - привязывает для них.
388
+ * Если нет - создаёт временный DocumentFragment и привязывает refs из него.
232
389
  */
233
- private unsubscribeSection(section: TemplateSection): void {
234
- for (const sub of section.subscriptions) {
235
- sub.unsubscribe();
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
- section.subscriptions = [];
238
-
239
- // Recursively unsubscribe children
240
- for (const child of section.children) {
241
- this.unsubscribeSection(child);
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
- * Создать DocumentFragment из текущего шаблона
417
+ * Отвязать refs (для всех контейнеров)
247
418
  */
248
- public createFragment(): DocumentFragment {
249
- if (typeof document === 'undefined') {
250
- throw new Error('PageTemplate.createFragment() requires DOM environment');
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
- * Получить кэшированный DocumentFragment
431
+ * Привязать события (для всех контейнеров)
262
432
  */
263
- public getFragment(): DocumentFragment | null {
264
- return this.fragment;
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
- * Пересоздать fragment
440
+ * Отвязать события (для всех контейнеров)
269
441
  */
270
- public rebuildFragment(): DocumentFragment {
271
- this.fragment = null;
272
- return this.createFragment();
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
- this.fragment = null;
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
- * Привязать refs после вставки в DOM
492
+ * Рекурсивно вставить фрагмент и его дочерние
295
493
  */
296
- public bindRefs(root: Element | DocumentFragment): void {
297
- const refElements = root.querySelectorAll('[data-ref]');
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
- * Обработать инжекции (@injection[head/tail])
309
- * Должен вызываться после bindRefs
585
+ * Обработать инжекции для конкретного контейнера.
586
+ * Находит элементы с data-injection-* атрибутами и перемещает их в целевые элементы.
310
587
  */
311
- public processInjections(root: Element | DocumentFragment): void {
312
- // Find all elements with injection attributes
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) continue;
595
+ if (!targetRefName) {
596
+ // Нет target - удаляем элемент из DOM
597
+ element.remove();
598
+ continue;
599
+ }
320
600
 
321
- // Get target element from scope
601
+ // Получаем целевой элемент из scope
322
602
  const targetElement = this.scope.get(targetRefName);
323
603
 
324
604
  if (!targetElement || !(targetElement instanceof Element)) {
325
- console.warn(`[PageTemplate] Injection target "${targetRefName}" not found in scope or is not an Element`);
605
+ // Target не найден - удаляем элемент из DOM
606
+ element.remove();
326
607
  continue;
327
608
  }
328
609
 
329
- // Remove injection attributes
610
+ // Удаляем атрибуты инжекции
330
611
  element.removeAttribute('data-injection-type');
331
612
  element.removeAttribute('data-injection-target');
332
613
 
333
- // Perform injection
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 (установить null)
624
+ * Отвязать refs для конкретного контейнера
344
625
  */
345
- public unbindRefs(): void {
626
+ private unbindRefsForContainer(_container: Element): void {
627
+ // Устанавливаем все refs в null
346
628
  for (const section of this.sections) {
347
- if (section.rule.name === 'ref' && section.match.data?.expression) {
348
- const refName = section.match.data.expression;
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
  }