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