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