@erickxavier/no-js 1.0.0

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.
@@ -0,0 +1,153 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVES: on:*, trigger
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { evaluate, _execStatement } from "../evaluate.js";
6
+ import { findContext } from "../dom.js";
7
+ import { registerDirective } from "../registry.js";
8
+
9
+ registerDirective("on:*", {
10
+ priority: 20,
11
+ init(el, name, expr) {
12
+ const ctx = findContext(el);
13
+ const parts = name.replace("on:", "").split(".");
14
+ const event = parts[0];
15
+ const modifiers = new Set(parts.slice(1));
16
+
17
+ // Lifecycle hooks
18
+ if (event === "mounted") {
19
+ requestAnimationFrame(() => _execStatement(expr, ctx, { $el: el }));
20
+ return;
21
+ }
22
+ if (event === "init") {
23
+ _execStatement(expr, ctx, { $el: el });
24
+ return;
25
+ }
26
+ if (event === "updated") {
27
+ const updatedObserver = new MutationObserver(() => {
28
+ _execStatement(expr, ctx, { $el: el });
29
+ });
30
+ updatedObserver.observe(el, { childList: true, subtree: true, characterData: true, attributes: true });
31
+ return;
32
+ }
33
+ if (event === "error") {
34
+ window.addEventListener("error", (e) => {
35
+ if (el.contains(e.target) || e.target === el) {
36
+ _execStatement(expr, ctx, { $el: el, $error: e.error || e.message });
37
+ }
38
+ });
39
+ return;
40
+ }
41
+ if (event === "unmounted") {
42
+ const observer = new MutationObserver((mutations) => {
43
+ for (const m of mutations) {
44
+ for (const node of m.removedNodes) {
45
+ if (node === el || node.contains?.(el)) {
46
+ _execStatement(expr, ctx, { $el: el });
47
+ observer.disconnect();
48
+ return;
49
+ }
50
+ }
51
+ }
52
+ });
53
+ if (el.parentElement)
54
+ observer.observe(el.parentElement, { childList: true });
55
+ return;
56
+ }
57
+
58
+ // Debounce / throttle
59
+ let debounceMs = 0;
60
+ let throttleMs = 0;
61
+ for (const mod of modifiers) {
62
+ if (/^\d+$/.test(mod)) {
63
+ if (modifiers.has("debounce")) debounceMs = parseInt(mod);
64
+ else if (modifiers.has("throttle")) throttleMs = parseInt(mod);
65
+ }
66
+ }
67
+
68
+ let handler = (e) => {
69
+ // Key modifiers
70
+ if (event === "keydown" || event === "keyup" || event === "keypress") {
71
+ const keyMods = [
72
+ "enter",
73
+ "escape",
74
+ "tab",
75
+ "space",
76
+ "delete",
77
+ "backspace",
78
+ "up",
79
+ "down",
80
+ "left",
81
+ "right",
82
+ "ctrl",
83
+ "alt",
84
+ "shift",
85
+ "meta",
86
+ ];
87
+ for (const mod of modifiers) {
88
+ if (!keyMods.includes(mod)) continue;
89
+ if (mod === "enter" && e.key !== "Enter") return;
90
+ if (mod === "escape" && e.key !== "Escape") return;
91
+ if (mod === "tab" && e.key !== "Tab") return;
92
+ if (mod === "space" && e.key !== " ") return;
93
+ if (mod === "delete" && e.key !== "Delete" && e.key !== "Backspace")
94
+ return;
95
+ if (mod === "up" && e.key !== "ArrowUp") return;
96
+ if (mod === "down" && e.key !== "ArrowDown") return;
97
+ if (mod === "left" && e.key !== "ArrowLeft") return;
98
+ if (mod === "right" && e.key !== "ArrowRight") return;
99
+ if (mod === "ctrl" && !e.ctrlKey) return;
100
+ if (mod === "alt" && !e.altKey) return;
101
+ if (mod === "shift" && !e.shiftKey) return;
102
+ if (mod === "meta" && !e.metaKey) return;
103
+ }
104
+ }
105
+
106
+ if (modifiers.has("prevent")) e.preventDefault();
107
+ if (modifiers.has("stop")) e.stopPropagation();
108
+ if (modifiers.has("self") && e.target !== el) return;
109
+
110
+ _execStatement(expr, ctx, { $event: e, $el: el });
111
+ };
112
+
113
+ // Wrap with debounce
114
+ if (debounceMs > 0) {
115
+ const original = handler;
116
+ let timer;
117
+ handler = (e) => {
118
+ clearTimeout(timer);
119
+ timer = setTimeout(() => original(e), debounceMs);
120
+ };
121
+ }
122
+
123
+ // Wrap with throttle
124
+ if (throttleMs > 0) {
125
+ const original = handler;
126
+ let last = 0;
127
+ handler = (e) => {
128
+ const now = Date.now();
129
+ if (now - last >= throttleMs) {
130
+ last = now;
131
+ original(e);
132
+ }
133
+ };
134
+ }
135
+
136
+ const opts = {};
137
+ if (modifiers.has("once")) opts.once = true;
138
+
139
+ el.addEventListener(event, handler, opts);
140
+ },
141
+ });
142
+
143
+ registerDirective("trigger", {
144
+ priority: 20,
145
+ init(el, name, eventName) {
146
+ const ctx = findContext(el);
147
+ const dataExpr = el.getAttribute("trigger-data");
148
+ el.addEventListener("click", () => {
149
+ const detail = dataExpr ? evaluate(dataExpr, ctx) : null;
150
+ el.dispatchEvent(new CustomEvent(eventName, { detail, bubbles: true }));
151
+ });
152
+ },
153
+ });
@@ -0,0 +1,288 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVES: get, post, put, patch, delete
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import {
6
+ _config,
7
+ _log,
8
+ _warn,
9
+ _stores,
10
+ _notifyStoreWatchers,
11
+ _emitEvent,
12
+ _routerInstance,
13
+ } from "../globals.js";
14
+ import { createContext } from "../context.js";
15
+ import { evaluate, _execStatement, _interpolate } from "../evaluate.js";
16
+ import { _doFetch, _cacheGet, _cacheSet } from "../fetch.js";
17
+ import { findContext, _clearDeclared, _cloneTemplate } from "../dom.js";
18
+ import { registerDirective, processTree } from "../registry.js";
19
+
20
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete"];
21
+
22
+ for (const method of HTTP_METHODS) {
23
+ registerDirective(method, {
24
+ priority: 1,
25
+ init(el, name, url) {
26
+ const asKey = el.getAttribute("as") || "data";
27
+ const loadingTpl = el.getAttribute("loading");
28
+ const errorTpl = el.getAttribute("error");
29
+ const emptyTpl = el.getAttribute("empty");
30
+ const successTpl = el.getAttribute("success");
31
+ const thenExpr = el.getAttribute("then");
32
+ const redirectPath = el.getAttribute("redirect");
33
+ const confirmMsg = el.getAttribute("confirm");
34
+ const refreshInterval = parseInt(el.getAttribute("refresh")) || 0;
35
+ const cachedRaw = el.getAttribute("cached");
36
+ const cacheStrategy = el.hasAttribute("cached")
37
+ ? cachedRaw || "memory"
38
+ : "none";
39
+ const bodyAttr = el.getAttribute("body");
40
+ const headersAttr = el.getAttribute("headers");
41
+ const varName = el.getAttribute("var");
42
+ const intoStore = el.getAttribute("into");
43
+ const retryCount =
44
+ parseInt(el.getAttribute("retry")) || _config.retries;
45
+ const retryDelay =
46
+ parseInt(el.getAttribute("retry-delay")) || _config.retryDelay || 1000;
47
+ const paramsAttr = el.getAttribute("params");
48
+
49
+ const parentCtx = el.parentElement
50
+ ? findContext(el.parentElement)
51
+ : createContext();
52
+ const ctx = el.__ctx || createContext({}, parentCtx);
53
+ el.__ctx = ctx;
54
+
55
+ const originalChildren = [...el.childNodes].map((n) =>
56
+ n.cloneNode(true),
57
+ );
58
+
59
+ let _activeAbort = null;
60
+
61
+ async function doRequest() {
62
+ // SwitchMap: abort previous in-flight request
63
+ if (_activeAbort) _activeAbort.abort();
64
+ _activeAbort = new AbortController();
65
+
66
+ // Confirmation
67
+ if (confirmMsg && !window.confirm(confirmMsg)) return;
68
+
69
+ let resolvedUrl = _interpolate(url, ctx);
70
+
71
+ // Append query params
72
+ if (paramsAttr) {
73
+ const paramsObj = evaluate(paramsAttr, ctx);
74
+ if (paramsObj && typeof paramsObj === "object") {
75
+ const sep = resolvedUrl.includes("?") ? "&" : "?";
76
+ const qs = new URLSearchParams(paramsObj).toString();
77
+ if (qs) resolvedUrl += sep + qs;
78
+ }
79
+ }
80
+
81
+ const cacheKey = method + ":" + resolvedUrl;
82
+
83
+ // Check cache
84
+ if (method === "get") {
85
+ const cached = _cacheGet(cacheKey, cacheStrategy);
86
+ if (cached != null) {
87
+ ctx.$set(asKey, cached);
88
+ _clearDeclared(el);
89
+ processTree(el);
90
+ return;
91
+ }
92
+ }
93
+
94
+ // Show loading
95
+ if (loadingTpl) {
96
+ const clone = _cloneTemplate(loadingTpl);
97
+ if (clone) {
98
+ el.innerHTML = "";
99
+ el.appendChild(clone);
100
+ processTree(el);
101
+ }
102
+ }
103
+
104
+ try {
105
+ let reqBody = null;
106
+ if (bodyAttr) {
107
+ const interpolated = _interpolate(bodyAttr, ctx);
108
+ try {
109
+ reqBody = JSON.parse(interpolated);
110
+ } catch {
111
+ reqBody = interpolated;
112
+ }
113
+ }
114
+
115
+ // For forms, collect form data
116
+ if (el.tagName === "FORM") {
117
+ const formData = new FormData(el);
118
+ reqBody = Object.fromEntries(formData.entries());
119
+ }
120
+
121
+ const extraHeaders = headersAttr ? JSON.parse(headersAttr) : {};
122
+ const savedRetries = _config.retries;
123
+ const savedRetryDelay = _config.retryDelay;
124
+ _config.retries = retryCount;
125
+ _config.retryDelay = retryDelay;
126
+ const data = await _doFetch(
127
+ resolvedUrl,
128
+ method,
129
+ reqBody,
130
+ extraHeaders,
131
+ el,
132
+ _activeAbort.signal,
133
+ );
134
+ _config.retries = savedRetries;
135
+ _config.retryDelay = savedRetryDelay;
136
+
137
+ // Cache response
138
+ if (method === "get") _cacheSet(cacheKey, data, cacheStrategy);
139
+
140
+ // Check empty
141
+ if (
142
+ emptyTpl &&
143
+ (data == null || (Array.isArray(data) && data.length === 0))
144
+ ) {
145
+ const clone = _cloneTemplate(emptyTpl);
146
+ if (clone) {
147
+ el.innerHTML = "";
148
+ el.appendChild(clone);
149
+ processTree(el);
150
+ }
151
+ return;
152
+ }
153
+
154
+ ctx.$set(asKey, data);
155
+
156
+ // Write to global store if into attribute is present
157
+ if (intoStore) {
158
+ if (!_stores[intoStore]) _stores[intoStore] = createContext({});
159
+ _stores[intoStore].$set(asKey, data);
160
+ _notifyStoreWatchers();
161
+ }
162
+
163
+ // Success template
164
+ if (successTpl) {
165
+ const clone = _cloneTemplate(successTpl);
166
+ if (clone) {
167
+ el.innerHTML = "";
168
+ // Inject var
169
+ const tplEl = document.getElementById(
170
+ successTpl.replace("#", ""),
171
+ );
172
+ const vn = tplEl?.getAttribute("var") || varName || "result";
173
+ const childCtx = createContext({ [vn]: data }, ctx);
174
+ const wrapper = document.createElement("div");
175
+ wrapper.style.display = "contents";
176
+ wrapper.__ctx = childCtx;
177
+ wrapper.appendChild(clone);
178
+ el.appendChild(wrapper);
179
+ processTree(wrapper);
180
+ }
181
+ } else {
182
+ // Restore original children and re-process
183
+ el.innerHTML = "";
184
+ for (const child of originalChildren)
185
+ el.appendChild(child.cloneNode(true));
186
+ _clearDeclared(el);
187
+ processTree(el);
188
+ }
189
+
190
+ // Then expression
191
+ if (thenExpr) _execStatement(thenExpr, ctx, { result: data });
192
+
193
+ // Redirect
194
+ if (redirectPath && _routerInstance)
195
+ _routerInstance.push(redirectPath);
196
+
197
+ _emitEvent("fetch:success", { url: resolvedUrl, data });
198
+ } catch (err) {
199
+ // SwitchMap: silently ignore aborted requests
200
+ if (err.name === "AbortError") return;
201
+
202
+ _warn(
203
+ `${method.toUpperCase()} ${resolvedUrl} failed:`,
204
+ err.message,
205
+ );
206
+ _emitEvent("fetch:error", { url: resolvedUrl, error: err });
207
+ _emitEvent("error", { url: resolvedUrl, error: err });
208
+
209
+ if (errorTpl) {
210
+ const clone = _cloneTemplate(errorTpl);
211
+ if (clone) {
212
+ el.innerHTML = "";
213
+ const tplEl = document.getElementById(
214
+ errorTpl.replace("#", ""),
215
+ );
216
+ const vn = tplEl?.getAttribute("var") || "err";
217
+ const childCtx = createContext(
218
+ {
219
+ [vn]: {
220
+ message: err.message,
221
+ status: err.status,
222
+ body: err.body,
223
+ },
224
+ },
225
+ ctx,
226
+ );
227
+ const wrapper = document.createElement("div");
228
+ wrapper.style.display = "contents";
229
+ wrapper.__ctx = childCtx;
230
+ wrapper.appendChild(clone);
231
+ el.appendChild(wrapper);
232
+ processTree(wrapper);
233
+ }
234
+ }
235
+ }
236
+ }
237
+
238
+ // For forms, intercept submit
239
+ if (el.tagName === "FORM" && method !== "get") {
240
+ el.addEventListener("submit", (e) => {
241
+ e.preventDefault();
242
+ doRequest();
243
+ });
244
+ } else if (method === "get") {
245
+ doRequest();
246
+ } else {
247
+ // Non-GET on non-FORM: attach click listener
248
+ el.addEventListener("click", (e) => {
249
+ e.preventDefault();
250
+ doRequest();
251
+ });
252
+ }
253
+
254
+ // Reactive URL watching: re-fetch when {expressions} in URL change
255
+ const hasInterpolation = /\{[^}]+\}/.test(url);
256
+ if (hasInterpolation) {
257
+ const debounceMs = parseInt(el.getAttribute("debounce")) || 0;
258
+ let _lastResolvedUrl = _interpolate(url, ctx);
259
+ let _debounceTimer = null;
260
+
261
+ function onAncestorChange() {
262
+ const newUrl = _interpolate(url, ctx);
263
+ if (newUrl !== _lastResolvedUrl) {
264
+ _lastResolvedUrl = newUrl;
265
+ if (_debounceTimer) clearTimeout(_debounceTimer);
266
+ if (debounceMs > 0) {
267
+ _debounceTimer = setTimeout(doRequest, debounceMs);
268
+ } else {
269
+ doRequest();
270
+ }
271
+ }
272
+ }
273
+
274
+ // Watch all ancestor contexts for changes
275
+ let ancestor = parentCtx;
276
+ while (ancestor && ancestor.__isProxy) {
277
+ ancestor.$watch(onAncestorChange);
278
+ ancestor = ancestor.$parent;
279
+ }
280
+ }
281
+
282
+ // Polling
283
+ if (refreshInterval > 0) {
284
+ setInterval(doRequest, refreshInterval);
285
+ }
286
+ },
287
+ });
288
+ }
@@ -0,0 +1,29 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVE: t (i18n translations)
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { _i18n } from "../i18n.js";
6
+ import { evaluate } from "../evaluate.js";
7
+ import { findContext } from "../dom.js";
8
+ import { registerDirective } from "../registry.js";
9
+
10
+ registerDirective("t", {
11
+ priority: 20,
12
+ init(el, name, key) {
13
+ const ctx = findContext(el);
14
+
15
+ function update() {
16
+ const params = {};
17
+ for (const attr of [...el.attributes]) {
18
+ if (attr.name.startsWith("t-") && attr.name !== "t") {
19
+ const paramName = attr.name.replace("t-", "");
20
+ params[paramName] = evaluate(attr.value, ctx) ?? attr.value;
21
+ }
22
+ }
23
+ el.textContent = _i18n.t(key, params);
24
+ }
25
+
26
+ ctx.$watch(update);
27
+ update();
28
+ },
29
+ });
@@ -0,0 +1,235 @@
1
+ // ═══════════════════════════════════════════════════════════════════════
2
+ // DIRECTIVES: each, foreach
3
+ // ═══════════════════════════════════════════════════════════════════════
4
+
5
+ import { createContext } from "../context.js";
6
+ import { evaluate, resolve } from "../evaluate.js";
7
+ import { findContext, _cloneTemplate } from "../dom.js";
8
+ import { registerDirective, processTree } from "../registry.js";
9
+ import { _animateOut } from "../animations.js";
10
+
11
+ registerDirective("each", {
12
+ priority: 10,
13
+ init(el, name, expr) {
14
+ const ctx = findContext(el);
15
+ const match = expr.match(/^(\w+)\s+in\s+(\S+)$/);
16
+ if (!match) return;
17
+ const [, itemName, listPath] = match;
18
+ const tplId = el.getAttribute("template");
19
+ const elseTpl = el.getAttribute("else");
20
+ const keyExpr = el.getAttribute("key");
21
+ const animEnter =
22
+ el.getAttribute("animate-enter") || el.getAttribute("animate");
23
+ const animLeave = el.getAttribute("animate-leave");
24
+ const stagger = parseInt(el.getAttribute("animate-stagger")) || 0;
25
+ const animDuration = parseInt(el.getAttribute("animate-duration")) || 0;
26
+
27
+ function update() {
28
+ let list = resolve(listPath, ctx);
29
+ if (!Array.isArray(list)) return;
30
+
31
+ // Empty state
32
+ if (list.length === 0 && elseTpl) {
33
+ const clone = _cloneTemplate(elseTpl);
34
+ if (clone) {
35
+ el.innerHTML = "";
36
+ el.appendChild(clone);
37
+ processTree(el);
38
+ }
39
+ return;
40
+ }
41
+
42
+ const tpl = tplId ? document.getElementById(tplId) : null;
43
+ if (!tpl) return;
44
+
45
+ // Animate out old items if animate-leave is set
46
+ if (animLeave && el.children.length > 0) {
47
+ const oldItems = [...el.children];
48
+ let remaining = oldItems.length;
49
+ oldItems.forEach((child) => {
50
+ const target = child.firstElementChild || child;
51
+ target.classList.add(animLeave);
52
+ const done = () => {
53
+ target.classList.remove(animLeave);
54
+ remaining--;
55
+ if (remaining <= 0) renderItems(tpl, list);
56
+ };
57
+ target.addEventListener("animationend", done, { once: true });
58
+ setTimeout(done, animDuration || 2000);
59
+ });
60
+ } else {
61
+ renderItems(tpl, list);
62
+ }
63
+ }
64
+
65
+ function renderItems(tpl, list) {
66
+ const count = list.length;
67
+ el.innerHTML = "";
68
+
69
+ list.forEach((item, i) => {
70
+ const childData = {
71
+ [itemName]: item,
72
+ $index: i,
73
+ $count: count,
74
+ $first: i === 0,
75
+ $last: i === count - 1,
76
+ $even: i % 2 === 0,
77
+ $odd: i % 2 !== 0,
78
+ };
79
+ const childCtx = createContext(childData, ctx);
80
+
81
+ const clone = tpl.content.cloneNode(true);
82
+ const wrapper = document.createElement("div");
83
+ wrapper.style.display = "contents";
84
+ wrapper.__ctx = childCtx;
85
+ wrapper.appendChild(clone);
86
+ el.appendChild(wrapper);
87
+ processTree(wrapper);
88
+
89
+ // Stagger animation
90
+ if (animEnter && stagger) {
91
+ wrapper.style.animationDelay = i * stagger + "ms";
92
+ }
93
+ if (animEnter) {
94
+ const firstChild = wrapper.firstElementChild;
95
+ if (firstChild) firstChild.classList.add(animEnter);
96
+ }
97
+ });
98
+ }
99
+
100
+ ctx.$watch(update);
101
+ update();
102
+ },
103
+ });
104
+
105
+ registerDirective("foreach", {
106
+ priority: 10,
107
+ init(el, name, itemName) {
108
+ const ctx = findContext(el);
109
+ const fromPath = el.getAttribute("from");
110
+ const indexName = el.getAttribute("index") || "$index";
111
+ const elseTpl = el.getAttribute("else");
112
+ const filterExpr = el.getAttribute("filter");
113
+ const sortProp = el.getAttribute("sort");
114
+ const limit = parseInt(el.getAttribute("limit")) || Infinity;
115
+ const offset = parseInt(el.getAttribute("offset")) || 0;
116
+ const tplId = el.getAttribute("template");
117
+ const animEnter = el.getAttribute("animate-enter") || el.getAttribute("animate");
118
+ const animLeave = el.getAttribute("animate-leave");
119
+ const stagger = parseInt(el.getAttribute("animate-stagger")) || 0;
120
+ const animDuration = parseInt(el.getAttribute("animate-duration")) || 0;
121
+
122
+ if (!fromPath || !itemName) return;
123
+
124
+ const templateContent = tplId
125
+ ? null // Will use external template
126
+ : el.cloneNode(true); // Use the element itself as template
127
+
128
+ function update() {
129
+ let list = resolve(fromPath, ctx);
130
+ if (!Array.isArray(list)) return;
131
+
132
+ // Filter
133
+ if (filterExpr) {
134
+ list = list.filter((item, i) => {
135
+ const tempCtx = createContext(
136
+ { [itemName]: item, [indexName]: i },
137
+ ctx,
138
+ );
139
+ return !!evaluate(filterExpr, tempCtx);
140
+ });
141
+ }
142
+
143
+ // Sort
144
+ if (sortProp) {
145
+ const desc = sortProp.startsWith("-");
146
+ const key = desc ? sortProp.slice(1) : sortProp;
147
+ list = [...list].sort((a, b) => {
148
+ const va = resolve(key, a) ?? a?.[key];
149
+ const vb = resolve(key, b) ?? b?.[key];
150
+ const r = va < vb ? -1 : va > vb ? 1 : 0;
151
+ return desc ? -r : r;
152
+ });
153
+ }
154
+
155
+ // Offset and limit
156
+ list = list.slice(offset, offset + limit);
157
+
158
+ // Empty
159
+ if (list.length === 0 && elseTpl) {
160
+ const clone = _cloneTemplate(elseTpl);
161
+ if (clone) {
162
+ el.innerHTML = "";
163
+ el.appendChild(clone);
164
+ processTree(el);
165
+ }
166
+ return;
167
+ }
168
+
169
+ const tpl = tplId ? document.getElementById(tplId) : null;
170
+ const count = list.length;
171
+
172
+ function renderForeachItems() {
173
+ el.innerHTML = "";
174
+ list.forEach((item, i) => {
175
+ const childData = {
176
+ [itemName]: item,
177
+ [indexName]: i,
178
+ $index: i,
179
+ $count: count,
180
+ $first: i === 0,
181
+ $last: i === count - 1,
182
+ $even: i % 2 === 0,
183
+ $odd: i % 2 !== 0,
184
+ };
185
+ const childCtx = createContext(childData, ctx);
186
+
187
+ let clone;
188
+ if (tpl) {
189
+ clone = tpl.content.cloneNode(true);
190
+ } else {
191
+ clone = templateContent.cloneNode(true);
192
+ }
193
+
194
+ const wrapper = document.createElement("div");
195
+ wrapper.style.display = "contents";
196
+ wrapper.__ctx = childCtx;
197
+ wrapper.appendChild(clone);
198
+ el.appendChild(wrapper);
199
+ processTree(wrapper);
200
+
201
+ // Stagger animation
202
+ if (animEnter && stagger) {
203
+ wrapper.style.animationDelay = (i * stagger) + "ms";
204
+ }
205
+ if (animEnter) {
206
+ const firstChild = wrapper.firstElementChild;
207
+ if (firstChild) firstChild.classList.add(animEnter);
208
+ }
209
+ });
210
+ }
211
+
212
+ // Animate out old items if animate-leave is set
213
+ if (animLeave && el.children.length > 0) {
214
+ const oldItems = [...el.children];
215
+ let remaining = oldItems.length;
216
+ oldItems.forEach((child) => {
217
+ const target = child.firstElementChild || child;
218
+ target.classList.add(animLeave);
219
+ const done = () => {
220
+ target.classList.remove(animLeave);
221
+ remaining--;
222
+ if (remaining <= 0) renderForeachItems();
223
+ };
224
+ target.addEventListener("animationend", done, { once: true });
225
+ setTimeout(done, animDuration || 2000);
226
+ });
227
+ } else {
228
+ renderForeachItems();
229
+ }
230
+ }
231
+
232
+ ctx.$watch(update);
233
+ update();
234
+ },
235
+ });