@framesquared/component 0.1.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.
package/dist/index.js ADDED
@@ -0,0 +1,1644 @@
1
+ // src/Component.ts
2
+ import { Base, Observable, generateId } from "@framesquared/core";
3
+ var ObservableMixin = Observable;
4
+ function ensureObservable(instance) {
5
+ const proto = ObservableMixin.prototype;
6
+ for (const name of Object.getOwnPropertyNames(proto)) {
7
+ if (name === "constructor") continue;
8
+ if (name in instance) continue;
9
+ const desc = Object.getOwnPropertyDescriptor(proto, name);
10
+ if (desc && typeof desc.value === "function") {
11
+ instance[name] = desc.value.bind(instance);
12
+ }
13
+ }
14
+ }
15
+ function toCssValue(v) {
16
+ return typeof v === "number" ? `${v}px` : v;
17
+ }
18
+ function matchesCQSimple(cmp, selector) {
19
+ selector = selector.trim();
20
+ if (selector.startsWith("#")) {
21
+ return cmp.componentId === selector.slice(1);
22
+ }
23
+ if (selector.startsWith(".")) {
24
+ return cmp.el?.classList?.contains(selector.slice(1)) ?? false;
25
+ }
26
+ return cmp._config?.xtype === selector;
27
+ }
28
+ var Component = class extends Base {
29
+ static $className = "Ext.Component";
30
+ // -- Public state --
31
+ el = null;
32
+ rendered = false;
33
+ /** The Container that owns this Component (set by Container.add). */
34
+ ownerCt = null;
35
+ // -- Internal state --
36
+ _hidden = false;
37
+ _disabled = false;
38
+ _tpl = null;
39
+ _data = null;
40
+ _resizeObserver = null;
41
+ _lastWidth = 0;
42
+ _lastHeight = 0;
43
+ _config;
44
+ /** Unique component id */
45
+ componentId;
46
+ $destroyHooks = [];
47
+ // -----------------------------------------------------------------------
48
+ // Construction
49
+ // -----------------------------------------------------------------------
50
+ constructor(config = {}) {
51
+ super();
52
+ this._config = config;
53
+ this.componentId = generateId("ext-cmp");
54
+ ensureObservable(this);
55
+ if (config.listeners) {
56
+ for (const [event, handler] of Object.entries(config.listeners)) {
57
+ this.on(event, handler);
58
+ }
59
+ }
60
+ this._tpl = config.tpl ?? null;
61
+ this._data = config.data ?? null;
62
+ this._hidden = config.hidden ?? false;
63
+ this._disabled = config.disabled ?? false;
64
+ this.beforeInitialize();
65
+ this.initialize();
66
+ this.afterInitialize();
67
+ if (config.renderTo) {
68
+ this.render(config.renderTo);
69
+ }
70
+ }
71
+ // -----------------------------------------------------------------------
72
+ // Lifecycle hooks (override in subclasses)
73
+ // -----------------------------------------------------------------------
74
+ /** Called before initialize(). Override in subclasses. */
75
+ beforeInitialize() {
76
+ }
77
+ /** Main initialization. Override in subclasses. */
78
+ initialize() {
79
+ }
80
+ /** Called after initialize(). Override in subclasses. */
81
+ afterInitialize() {
82
+ }
83
+ /** Called before render(). Override in subclasses. */
84
+ beforeRender() {
85
+ }
86
+ /** Called after render(). Override in subclasses. */
87
+ afterRender() {
88
+ }
89
+ /** Called before destroy(). Override in subclasses. */
90
+ beforeDestroy() {
91
+ }
92
+ /** Called during destroy(). Override in subclasses. */
93
+ onDestroy() {
94
+ }
95
+ /** Called after destroy(). Override in subclasses. */
96
+ afterDestroy() {
97
+ }
98
+ // -----------------------------------------------------------------------
99
+ // Render
100
+ // -----------------------------------------------------------------------
101
+ render(container, position) {
102
+ if (this.rendered || this.isDestroyed) return;
103
+ this.fire("beforerender", this);
104
+ this.beforeRender();
105
+ this.el = document.createElement("div");
106
+ this.el.id = this.componentId;
107
+ this.el.classList.add("x-component");
108
+ this.applyConfigs();
109
+ const target = this.resolveElement(container);
110
+ if (target) {
111
+ if (typeof position === "number") {
112
+ const ref = target.children[position];
113
+ if (ref) {
114
+ target.insertBefore(this.el, ref);
115
+ } else {
116
+ target.appendChild(this.el);
117
+ }
118
+ } else if (position instanceof Element) {
119
+ target.insertBefore(this.el, position);
120
+ } else {
121
+ target.appendChild(this.el);
122
+ }
123
+ }
124
+ this.setupResizeObserver();
125
+ this.rendered = true;
126
+ this.fire("render", this);
127
+ this.afterRender();
128
+ this.fire("afterrender", this);
129
+ }
130
+ resolveElement(target) {
131
+ if (!target) return null;
132
+ if (typeof target === "string") {
133
+ return document.querySelector(target);
134
+ }
135
+ return target;
136
+ }
137
+ // -----------------------------------------------------------------------
138
+ // Config application to DOM
139
+ // -----------------------------------------------------------------------
140
+ applyConfigs() {
141
+ const el = this.el;
142
+ const cfg = this._config;
143
+ if (cfg.cls) {
144
+ const classes = Array.isArray(cfg.cls) ? cfg.cls : cfg.cls.split(/\s+/);
145
+ for (const c of classes) {
146
+ if (c) el.classList.add(c);
147
+ }
148
+ }
149
+ if (cfg.style) {
150
+ if (typeof cfg.style === "string") {
151
+ el.style.cssText += cfg.style;
152
+ } else {
153
+ const styleObj = cfg.style;
154
+ for (const [prop, value] of Object.entries(styleObj)) {
155
+ el.style[prop] = value;
156
+ }
157
+ }
158
+ }
159
+ if (cfg.width !== void 0) el.style.width = toCssValue(cfg.width);
160
+ if (cfg.height !== void 0) el.style.height = toCssValue(cfg.height);
161
+ if (cfg.minWidth !== void 0) el.style.minWidth = toCssValue(cfg.minWidth);
162
+ if (cfg.maxWidth !== void 0) el.style.maxWidth = toCssValue(cfg.maxWidth);
163
+ if (cfg.minHeight !== void 0) el.style.minHeight = toCssValue(cfg.minHeight);
164
+ if (cfg.maxHeight !== void 0) el.style.maxHeight = toCssValue(cfg.maxHeight);
165
+ if (cfg.margin !== void 0) el.style.margin = toCssValue(cfg.margin);
166
+ if (cfg.padding !== void 0) el.style.padding = toCssValue(cfg.padding);
167
+ if (this._tpl && this._data) {
168
+ el.innerHTML = this._tpl.apply(this._data);
169
+ } else if (cfg.html !== void 0) {
170
+ el.innerHTML = cfg.html;
171
+ }
172
+ if (this._hidden) {
173
+ el.style.display = "none";
174
+ }
175
+ if (this._disabled) {
176
+ el.classList.add("x-disabled");
177
+ el.setAttribute("aria-disabled", "true");
178
+ }
179
+ if (cfg.floating) {
180
+ el.style.position = "absolute";
181
+ el.classList.add("x-floating");
182
+ }
183
+ }
184
+ // -----------------------------------------------------------------------
185
+ // Visibility
186
+ // -----------------------------------------------------------------------
187
+ isVisible() {
188
+ return !this._hidden;
189
+ }
190
+ setVisible(visible) {
191
+ if (visible) this.show();
192
+ else this.hide();
193
+ }
194
+ show() {
195
+ if (this.isDestroyed) return;
196
+ this._hidden = false;
197
+ if (this.el) {
198
+ this.el.style.display = "";
199
+ }
200
+ this.fire("show", this);
201
+ }
202
+ hide() {
203
+ if (this.isDestroyed) return;
204
+ this._hidden = true;
205
+ if (this.el) {
206
+ this.el.style.display = "none";
207
+ }
208
+ this.fire("hide", this);
209
+ }
210
+ // -----------------------------------------------------------------------
211
+ // Enable / Disable
212
+ // -----------------------------------------------------------------------
213
+ isDisabled() {
214
+ return this._disabled;
215
+ }
216
+ enable() {
217
+ if (this.isDestroyed) return;
218
+ this._disabled = false;
219
+ if (this.el) {
220
+ this.el.classList.remove("x-disabled");
221
+ this.el.removeAttribute("aria-disabled");
222
+ }
223
+ this.fire("enable", this);
224
+ }
225
+ disable() {
226
+ if (this.isDestroyed) return;
227
+ this._disabled = true;
228
+ if (this.el) {
229
+ this.el.classList.add("x-disabled");
230
+ this.el.setAttribute("aria-disabled", "true");
231
+ }
232
+ this.fire("disable", this);
233
+ }
234
+ // -----------------------------------------------------------------------
235
+ // Size & Position
236
+ // -----------------------------------------------------------------------
237
+ getWidth() {
238
+ return this.el?.offsetWidth ?? 0;
239
+ }
240
+ getHeight() {
241
+ return this.el?.offsetHeight ?? 0;
242
+ }
243
+ getSize() {
244
+ return { width: this.getWidth(), height: this.getHeight() };
245
+ }
246
+ setWidth(width) {
247
+ if (this.el) this.el.style.width = toCssValue(width);
248
+ }
249
+ setHeight(height) {
250
+ if (this.el) this.el.style.height = toCssValue(height);
251
+ }
252
+ setSize(width, height) {
253
+ this.setWidth(width);
254
+ this.setHeight(height);
255
+ }
256
+ getX() {
257
+ return this.el?.offsetLeft ?? 0;
258
+ }
259
+ getY() {
260
+ return this.el?.offsetTop ?? 0;
261
+ }
262
+ setX(x) {
263
+ if (this.el) this.el.style.left = `${x}px`;
264
+ }
265
+ setY(y) {
266
+ if (this.el) this.el.style.top = `${y}px`;
267
+ }
268
+ setPosition(x, y) {
269
+ this.setX(x);
270
+ this.setY(y);
271
+ }
272
+ getBox() {
273
+ return {
274
+ x: this.getX(),
275
+ y: this.getY(),
276
+ width: this.getWidth(),
277
+ height: this.getHeight()
278
+ };
279
+ }
280
+ // -----------------------------------------------------------------------
281
+ // ResizeObserver
282
+ // -----------------------------------------------------------------------
283
+ setupResizeObserver() {
284
+ if (!this.el || typeof ResizeObserver === "undefined") return;
285
+ this._resizeObserver = new ResizeObserver((entries) => {
286
+ for (const entry of entries) {
287
+ const { width, height } = entry.contentRect;
288
+ const oldWidth = this._lastWidth;
289
+ const oldHeight = this._lastHeight;
290
+ this._lastWidth = width;
291
+ this._lastHeight = height;
292
+ this.fire("resize", this, width, height, oldWidth, oldHeight);
293
+ }
294
+ });
295
+ this._resizeObserver.observe(this.el);
296
+ }
297
+ // -----------------------------------------------------------------------
298
+ // CSS operations
299
+ // -----------------------------------------------------------------------
300
+ addCls(...classes) {
301
+ if (!this.el) return;
302
+ for (const c of classes) {
303
+ if (c) this.el.classList.add(c);
304
+ }
305
+ }
306
+ removeCls(...classes) {
307
+ if (!this.el) return;
308
+ for (const c of classes) {
309
+ if (c) this.el.classList.remove(c);
310
+ }
311
+ }
312
+ toggleCls(cls, state) {
313
+ if (!this.el) return;
314
+ if (state !== void 0) {
315
+ this.el.classList.toggle(cls, state);
316
+ } else {
317
+ this.el.classList.toggle(cls);
318
+ }
319
+ }
320
+ hasCls(cls) {
321
+ return this.el?.classList.contains(cls) ?? false;
322
+ }
323
+ setStyle(propOrObj, value) {
324
+ if (!this.el) return;
325
+ if (typeof propOrObj === "string") {
326
+ this.el.style[propOrObj] = value;
327
+ } else {
328
+ for (const [prop, val] of Object.entries(propOrObj)) {
329
+ this.el.style[prop] = val;
330
+ }
331
+ }
332
+ }
333
+ getStyle(prop) {
334
+ if (!this.el) return "";
335
+ return this.el.style[prop] ?? "";
336
+ }
337
+ // -----------------------------------------------------------------------
338
+ // Content update
339
+ // -----------------------------------------------------------------------
340
+ update(htmlOrData) {
341
+ if (typeof htmlOrData === "string") {
342
+ this.setHtml(htmlOrData);
343
+ } else {
344
+ this.setData(htmlOrData);
345
+ }
346
+ }
347
+ setHtml(html) {
348
+ if (this.el) {
349
+ this.el.innerHTML = html;
350
+ }
351
+ }
352
+ setData(data) {
353
+ this._data = data;
354
+ if (this.el && this._tpl) {
355
+ this.el.innerHTML = this._tpl.apply(data);
356
+ }
357
+ }
358
+ // -----------------------------------------------------------------------
359
+ // Element access
360
+ // -----------------------------------------------------------------------
361
+ getEl() {
362
+ return this.el;
363
+ }
364
+ getElement() {
365
+ return this.el;
366
+ }
367
+ // -----------------------------------------------------------------------
368
+ // Focus management
369
+ // -----------------------------------------------------------------------
370
+ focus() {
371
+ const target = this.getFocusEl();
372
+ if (target) target.focus();
373
+ }
374
+ blur() {
375
+ const target = this.getFocusEl();
376
+ if (target) target.blur();
377
+ }
378
+ isFocusable() {
379
+ if (this._disabled || this.isDestroyed || !this.rendered) return false;
380
+ const el = this.getFocusEl();
381
+ if (!el) return false;
382
+ return el.tabIndex >= 0;
383
+ }
384
+ /** Returns the element that receives focus. Override for inner focus targets. */
385
+ getFocusEl() {
386
+ return this.el;
387
+ }
388
+ // -----------------------------------------------------------------------
389
+ // Owner lifecycle hooks
390
+ // -----------------------------------------------------------------------
391
+ /** Called when added to a Container. */
392
+ onAdded(_owner, _index) {
393
+ this.fire("added", this, _owner, _index);
394
+ }
395
+ /** Called when removed from a Container. */
396
+ onRemoved(_destroying) {
397
+ this.fire("removed", this, _destroying);
398
+ }
399
+ // -----------------------------------------------------------------------
400
+ // Ancestor traversal
401
+ // -----------------------------------------------------------------------
402
+ /**
403
+ * Walks up the ownerCt chain and returns the first Component whose
404
+ * xtype matches the given selector string.
405
+ */
406
+ up(selector) {
407
+ let current = this.ownerCt;
408
+ while (current) {
409
+ if (matchesCQSimple(current, selector)) return current;
410
+ current = current.ownerCt;
411
+ }
412
+ return void 0;
413
+ }
414
+ // -----------------------------------------------------------------------
415
+ // Destroy
416
+ // -----------------------------------------------------------------------
417
+ destroy() {
418
+ if (this.isDestroyed) return;
419
+ this.fire("beforedestroy", this);
420
+ this.beforeDestroy();
421
+ if (this._resizeObserver) {
422
+ this._resizeObserver.disconnect();
423
+ this._resizeObserver = null;
424
+ }
425
+ if (this.el && this.el.parentNode) {
426
+ this.el.parentNode.removeChild(this.el);
427
+ }
428
+ this.onDestroy();
429
+ this.el = null;
430
+ this.isDestroyed = true;
431
+ this.afterDestroy();
432
+ this.fire("destroy", this);
433
+ for (const hook of this.$destroyHooks) {
434
+ hook();
435
+ }
436
+ if (typeof this.clearListeners === "function") {
437
+ this.clearListeners();
438
+ }
439
+ }
440
+ // -----------------------------------------------------------------------
441
+ // Event helper
442
+ // -----------------------------------------------------------------------
443
+ fire(eventName, ...args) {
444
+ if (typeof this.fireEvent === "function") {
445
+ return this.fireEvent(eventName, ...args) !== false;
446
+ }
447
+ return true;
448
+ }
449
+ };
450
+
451
+ // src/Template.ts
452
+ var Template = class {
453
+ source;
454
+ constructor(source) {
455
+ this.source = source;
456
+ }
457
+ /**
458
+ * Applies data to the template and returns the resulting HTML string.
459
+ * Replaces `{key}` tokens with values from the data object.
460
+ * Supports nested keys via dot notation: `{user.name}`.
461
+ */
462
+ apply(data) {
463
+ return this.source.replace(/\{([^}]+)\}/g, (_match, key) => {
464
+ const value = this.resolve(data, key.trim());
465
+ return value !== void 0 && value !== null ? String(value) : "";
466
+ });
467
+ }
468
+ /**
469
+ * Alias for apply().
470
+ */
471
+ render(data) {
472
+ return this.apply(data);
473
+ }
474
+ resolve(data, path) {
475
+ const parts = path.split(".");
476
+ let current = data;
477
+ for (const part of parts) {
478
+ if (current === null || current === void 0 || typeof current !== "object") {
479
+ return void 0;
480
+ }
481
+ current = current[part];
482
+ }
483
+ return current;
484
+ }
485
+ };
486
+
487
+ // src/query/CQParser.ts
488
+ function tokenize(input) {
489
+ const tokens = [];
490
+ let i = 0;
491
+ while (i < input.length) {
492
+ const ch = input[i];
493
+ if (ch === " " || ch === " " || ch === "\n") {
494
+ while (i < input.length && /\s/.test(input[i])) i++;
495
+ if (tokens.length > 0 && i < input.length) {
496
+ tokens.push({ type: "whitespace", value: " " });
497
+ }
498
+ continue;
499
+ }
500
+ if (ch === ",") {
501
+ if (tokens.length > 0 && tokens[tokens.length - 1].type === "whitespace") {
502
+ tokens.pop();
503
+ }
504
+ tokens.push({ type: "comma", value: "," });
505
+ i++;
506
+ continue;
507
+ }
508
+ if (ch === ">" || ch === "~" || ch === "+") {
509
+ if (tokens.length > 0 && tokens[tokens.length - 1].type === "whitespace") {
510
+ tokens.pop();
511
+ }
512
+ tokens.push({ type: "combinator", value: ch });
513
+ i++;
514
+ while (i < input.length && /\s/.test(input[i])) i++;
515
+ continue;
516
+ }
517
+ if (ch === "#") {
518
+ i++;
519
+ let id = "";
520
+ while (i < input.length && /[\w-]/.test(input[i])) {
521
+ id += input[i++];
522
+ }
523
+ tokens.push({ type: "hash", value: id });
524
+ continue;
525
+ }
526
+ if (ch === ".") {
527
+ i++;
528
+ let cls = "";
529
+ while (i < input.length && /[\w-]/.test(input[i])) {
530
+ cls += input[i++];
531
+ }
532
+ tokens.push({ type: "dot", value: cls });
533
+ continue;
534
+ }
535
+ if (ch === "[") {
536
+ i++;
537
+ let content = "";
538
+ let depth = 1;
539
+ while (i < input.length && depth > 0) {
540
+ if (input[i] === "[") depth++;
541
+ else if (input[i] === "]") depth--;
542
+ if (depth > 0) content += input[i];
543
+ i++;
544
+ }
545
+ tokens.push({ type: "bracket", value: content });
546
+ continue;
547
+ }
548
+ if (ch === ":") {
549
+ i++;
550
+ let name = "";
551
+ while (i < input.length && /[\w-]/.test(input[i])) {
552
+ name += input[i++];
553
+ }
554
+ let arg = "";
555
+ if (i < input.length && input[i] === "(") {
556
+ i++;
557
+ let depth = 1;
558
+ while (i < input.length && depth > 0) {
559
+ if (input[i] === "(") depth++;
560
+ else if (input[i] === ")") depth--;
561
+ if (depth > 0) arg += input[i];
562
+ i++;
563
+ }
564
+ }
565
+ tokens.push({ type: "colon", value: name + (arg ? `(${arg})` : "") });
566
+ continue;
567
+ }
568
+ if (ch === "{") {
569
+ i++;
570
+ let name = "";
571
+ while (i < input.length && input[i] !== "}") {
572
+ name += input[i++];
573
+ }
574
+ if (i < input.length) i++;
575
+ tokens.push({ type: "brace", value: name });
576
+ continue;
577
+ }
578
+ if (/[\w-]/.test(ch)) {
579
+ let ident = "";
580
+ while (i < input.length && /[\w-]/.test(input[i])) {
581
+ ident += input[i++];
582
+ }
583
+ tokens.push({ type: "ident", value: ident });
584
+ continue;
585
+ }
586
+ i++;
587
+ }
588
+ if (tokens.length > 0 && tokens[tokens.length - 1].type === "whitespace") {
589
+ tokens.pop();
590
+ }
591
+ return tokens;
592
+ }
593
+ var CQParser = class _CQParser {
594
+ tokens;
595
+ pos;
596
+ constructor(tokens) {
597
+ this.tokens = tokens;
598
+ this.pos = 0;
599
+ }
600
+ static parse(selector) {
601
+ selector = selector.trim();
602
+ if (!selector) throw new Error("CQ: empty selector");
603
+ const tokens = tokenize(selector);
604
+ if (tokens.length === 0) throw new Error(`CQ: invalid selector "${selector}"`);
605
+ const parser = new _CQParser(tokens);
606
+ return parser.parseSelectorList();
607
+ }
608
+ peek() {
609
+ return this.tokens[this.pos];
610
+ }
611
+ advance() {
612
+ return this.tokens[this.pos++];
613
+ }
614
+ // selectorList = combinatorExpr ( ',' combinatorExpr )*
615
+ parseSelectorList() {
616
+ const first = this.parseCombinatorExpr();
617
+ if (!this.peek() || this.peek().type !== "comma") return first;
618
+ const items = [first];
619
+ while (this.peek()?.type === "comma") {
620
+ this.advance();
621
+ while (this.peek()?.type === "whitespace") this.advance();
622
+ items.push(this.parseCombinatorExpr());
623
+ }
624
+ return { type: "list", items };
625
+ }
626
+ // combinatorExpr = compound ( (combinator | whitespace) compound )*
627
+ parseCombinatorExpr() {
628
+ let left = this.parseCompound();
629
+ while (this.peek()) {
630
+ const tok = this.peek();
631
+ if (tok.type === "combinator") {
632
+ const combinator = this.advance().value;
633
+ const right = this.parseCompound();
634
+ left = { type: "combinator", combinator, left, right };
635
+ } else if (tok.type === "whitespace") {
636
+ this.advance();
637
+ const next = this.peek();
638
+ if (!next || next.type === "comma") break;
639
+ if (next.type === "combinator") continue;
640
+ const right = this.parseCompound();
641
+ left = { type: "combinator", combinator: " ", left, right };
642
+ } else {
643
+ break;
644
+ }
645
+ }
646
+ return left;
647
+ }
648
+ // compound = simpleSelector+
649
+ parseCompound() {
650
+ const selectors = [];
651
+ while (this.peek()) {
652
+ const tok = this.peek();
653
+ if (tok.type === "ident") {
654
+ selectors.push({ type: "type", value: this.advance().value });
655
+ } else if (tok.type === "hash") {
656
+ selectors.push({ type: "id", value: this.advance().value });
657
+ } else if (tok.type === "dot") {
658
+ selectors.push({ type: "class", value: this.advance().value });
659
+ } else if (tok.type === "bracket") {
660
+ selectors.push(this.parseAttribute(this.advance().value));
661
+ } else if (tok.type === "colon") {
662
+ selectors.push(this.parsePseudo(this.advance().value));
663
+ } else if (tok.type === "brace") {
664
+ selectors.push({ type: "method", value: this.advance().value });
665
+ } else {
666
+ break;
667
+ }
668
+ }
669
+ if (selectors.length === 0) {
670
+ throw new Error("CQ: expected selector");
671
+ }
672
+ return { type: "compound", selectors };
673
+ }
674
+ // [name], [name=value], [name>5], [name>=5], [name<5], [name<=5], [name!=v]
675
+ parseAttribute(content) {
676
+ const opMatch = content.match(/^([\w-]+)\s*(>=|<=|!=|>|<|=)\s*(.*)$/);
677
+ if (opMatch) {
678
+ let val = opMatch[3].trim();
679
+ if (val.startsWith('"') && val.endsWith('"') || val.startsWith("'") && val.endsWith("'")) {
680
+ val = val.slice(1, -1);
681
+ }
682
+ return { type: "attribute", name: opMatch[1], operator: opMatch[2], value: val };
683
+ }
684
+ return { type: "attribute", name: content.trim() };
685
+ }
686
+ // :name or :name(arg)
687
+ parsePseudo(raw) {
688
+ const parenIdx = raw.indexOf("(");
689
+ if (parenIdx === -1) {
690
+ return { type: "pseudo", name: raw };
691
+ }
692
+ const name = raw.slice(0, parenIdx);
693
+ const argStr = raw.slice(parenIdx + 1, -1);
694
+ if (name === "not") {
695
+ const inner = _CQParser.parse(argStr);
696
+ return { type: "pseudo", name: "not", argument: inner };
697
+ }
698
+ return { type: "pseudo", name, argument: argStr };
699
+ }
700
+ };
701
+
702
+ // src/query/CQMatcher.ts
703
+ var CQMatcher = class {
704
+ /**
705
+ * Returns true if `component` matches the selector string.
706
+ */
707
+ static matches(component, selector) {
708
+ const ast = CQParser.parse(selector);
709
+ return matchesNode(component, ast);
710
+ }
711
+ /**
712
+ * Returns all descendants of `root` matching the selector string.
713
+ */
714
+ static query(root, selector) {
715
+ const ast = CQParser.parse(selector);
716
+ const all = collectDescendants(root);
717
+ return all.filter((c) => matchesInContext(c, ast, root));
718
+ }
719
+ };
720
+ function collectDescendants(container) {
721
+ const results = [];
722
+ const items = container.getItems?.() ?? [];
723
+ for (const child of items) {
724
+ results.push(child);
725
+ if (typeof child.getItems === "function") {
726
+ results.push(...collectDescendants(child));
727
+ }
728
+ }
729
+ return results;
730
+ }
731
+ function matchesInContext(component, ast, _root) {
732
+ if (ast.type === "list") {
733
+ return ast.items.some(
734
+ (item) => matchesInContext(component, item, _root)
735
+ );
736
+ }
737
+ if (ast.type === "compound") {
738
+ return matchesCompound(component, ast);
739
+ }
740
+ if (ast.type === "combinator") {
741
+ return matchesCombinator(component, ast);
742
+ }
743
+ return false;
744
+ }
745
+ function matchesCombinator(component, node) {
746
+ const { combinator, left, right } = node;
747
+ if (!matchesNode(component, right)) return false;
748
+ switch (combinator) {
749
+ case " ":
750
+ return hasAncestorMatching(component, left);
751
+ case ">":
752
+ return hasParentMatching(component, left);
753
+ case "~":
754
+ return hasPrecedingSiblingMatching(component, left);
755
+ case "+":
756
+ return hasAdjacentSiblingMatching(component, left);
757
+ default:
758
+ return false;
759
+ }
760
+ }
761
+ function hasAncestorMatching(component, ast) {
762
+ let current = component.ownerCt;
763
+ while (current) {
764
+ if (matchesNode(current, ast)) return true;
765
+ current = current.ownerCt;
766
+ }
767
+ return false;
768
+ }
769
+ function hasParentMatching(component, ast) {
770
+ const parent = component.ownerCt;
771
+ if (!parent) return false;
772
+ return matchesNode(parent, ast);
773
+ }
774
+ function hasPrecedingSiblingMatching(component, ast) {
775
+ const siblings = getSiblings(component);
776
+ if (!siblings) return false;
777
+ const myIndex = siblings.indexOf(component);
778
+ for (let i = 0; i < myIndex; i++) {
779
+ if (matchesNode(siblings[i], ast)) return true;
780
+ }
781
+ return false;
782
+ }
783
+ function hasAdjacentSiblingMatching(component, ast) {
784
+ const siblings = getSiblings(component);
785
+ if (!siblings) return false;
786
+ const myIndex = siblings.indexOf(component);
787
+ if (myIndex <= 0) return false;
788
+ return matchesNode(siblings[myIndex - 1], ast);
789
+ }
790
+ function getSiblings(component) {
791
+ const parent = component.ownerCt;
792
+ if (!parent || typeof parent.getItems !== "function") return null;
793
+ return parent.getItems();
794
+ }
795
+ function matchesNode(component, ast) {
796
+ if (ast.type === "compound") {
797
+ return matchesCompound(component, ast);
798
+ }
799
+ if (ast.type === "combinator") {
800
+ return matchesCombinator(component, ast);
801
+ }
802
+ if (ast.type === "list") {
803
+ return ast.items.some(
804
+ (item) => matchesNode(component, item)
805
+ );
806
+ }
807
+ return false;
808
+ }
809
+ function matchesCompound(component, node) {
810
+ return node.selectors.every((sel) => matchesSimple(component, sel));
811
+ }
812
+ function matchesSimple(component, sel) {
813
+ switch (sel.type) {
814
+ case "type":
815
+ return component._config?.xtype === sel.value;
816
+ case "id":
817
+ return component.componentId === sel.value;
818
+ case "class":
819
+ return component.el?.classList?.contains(sel.value) ?? false;
820
+ case "attribute":
821
+ return matchesAttribute(component, sel);
822
+ case "pseudo":
823
+ return matchesPseudo(component, sel);
824
+ case "method":
825
+ return matchesMethod(component, sel);
826
+ default:
827
+ return false;
828
+ }
829
+ }
830
+ function matchesAttribute(component, sel) {
831
+ const config = component._config ?? {};
832
+ const actual = config[sel.name];
833
+ if (!sel.operator) {
834
+ return actual !== void 0 && actual !== null && actual !== false && actual !== "";
835
+ }
836
+ const expected = sel.value;
837
+ switch (sel.operator) {
838
+ case "=": {
839
+ const exp = expected;
840
+ if (exp === "true") return actual === true;
841
+ if (exp === "false") return actual === false;
842
+ return String(actual) === exp;
843
+ }
844
+ case "!=": {
845
+ const exp = expected;
846
+ if (exp === "true") return actual !== true;
847
+ if (exp === "false") return actual !== false;
848
+ return String(actual) !== exp;
849
+ }
850
+ case ">":
851
+ return Number(actual) > Number(expected);
852
+ case ">=":
853
+ return Number(actual) >= Number(expected);
854
+ case "<":
855
+ return Number(actual) < Number(expected);
856
+ case "<=":
857
+ return Number(actual) <= Number(expected);
858
+ default:
859
+ return false;
860
+ }
861
+ }
862
+ function matchesPseudo(component, sel) {
863
+ switch (sel.name) {
864
+ case "not": {
865
+ if (!sel.argument) return true;
866
+ return !matchesNode(component, sel.argument);
867
+ }
868
+ case "visible":
869
+ return typeof component.isVisible === "function" && component.isVisible();
870
+ case "disabled":
871
+ return typeof component.isDisabled === "function" && component.isDisabled();
872
+ case "focusable":
873
+ return typeof component.isFocusable === "function" && component.isFocusable();
874
+ case "first-child":
875
+ return isNthChild(component, 0);
876
+ case "last-child":
877
+ return isLastChild(component);
878
+ // Positional aliases (ExtJS compat)
879
+ case "first":
880
+ return isNthChild(component, 0);
881
+ case "last":
882
+ return isLastChild(component);
883
+ case "nth-child": {
884
+ const n = parseInt(sel.argument, 10);
885
+ if (Number.isNaN(n)) return false;
886
+ return isNthChild(component, n - 1);
887
+ }
888
+ default:
889
+ return false;
890
+ }
891
+ }
892
+ function isNthChild(component, index) {
893
+ const siblings = getSiblings(component);
894
+ if (!siblings) return false;
895
+ return siblings[index] === component;
896
+ }
897
+ function isLastChild(component) {
898
+ const siblings = getSiblings(component);
899
+ if (!siblings || siblings.length === 0) return false;
900
+ return siblings[siblings.length - 1] === component;
901
+ }
902
+ function matchesMethod(component, sel) {
903
+ const methodName = sel.value;
904
+ const fn = component[methodName];
905
+ if (typeof fn !== "function") return false;
906
+ return !!fn.call(component);
907
+ }
908
+
909
+ // src/ComponentQuery.ts
910
+ function matchesCQ(cmp, selector) {
911
+ try {
912
+ return CQMatcher.matches(cmp, selector);
913
+ } catch {
914
+ return false;
915
+ }
916
+ }
917
+ var ComponentQuery = class {
918
+ static query(root, selector) {
919
+ try {
920
+ selector = selector.trim();
921
+ if (selector.startsWith(">")) {
922
+ const innerSel = selector.slice(1).trim();
923
+ const items = root.getItems?.() ?? [];
924
+ return items.filter((child) => {
925
+ try {
926
+ return CQMatcher.matches(child, innerSel);
927
+ } catch {
928
+ return false;
929
+ }
930
+ });
931
+ }
932
+ return CQMatcher.query(root, selector);
933
+ } catch {
934
+ return [];
935
+ }
936
+ }
937
+ };
938
+
939
+ // src/Layout.ts
940
+ var Layout = class {
941
+ type;
942
+ constructor(config = {}) {
943
+ this.type = config.type ?? "auto";
944
+ }
945
+ /**
946
+ * Performs layout calculation on the owner container.
947
+ * Override in subclasses.
948
+ */
949
+ doLayout(_owner) {
950
+ }
951
+ };
952
+ function resolveLayout(config) {
953
+ if (!config) return new Layout();
954
+ if (typeof config === "string") return new Layout({ type: config });
955
+ return new Layout(config);
956
+ }
957
+
958
+ // src/Container.ts
959
+ import { ClassManager } from "@framesquared/core";
960
+ var Container = class _Container extends Component {
961
+ static $className = "Ext.Container";
962
+ constructor(config = {}) {
963
+ super(config);
964
+ }
965
+ // -----------------------------------------------------------------------
966
+ // Lifecycle overrides
967
+ // -----------------------------------------------------------------------
968
+ initialize() {
969
+ super.initialize();
970
+ this._items = [];
971
+ this._layout = null;
972
+ this._bodyEl = null;
973
+ }
974
+ afterRender() {
975
+ super.afterRender();
976
+ this._bodyEl = document.createElement("div");
977
+ this._bodyEl.classList.add("x-container-body");
978
+ this.el.appendChild(this._bodyEl);
979
+ for (const child of this._items) {
980
+ if (!child.rendered) {
981
+ child.render(this._bodyEl);
982
+ }
983
+ }
984
+ const config = this._config;
985
+ if (config.items && config.items.length > 0) {
986
+ this.add(...config.items);
987
+ }
988
+ }
989
+ onDestroy() {
990
+ const items = [...this._items];
991
+ for (const child of items) {
992
+ child.destroy();
993
+ }
994
+ this._items.length = 0;
995
+ super.onDestroy();
996
+ }
997
+ // -----------------------------------------------------------------------
998
+ // Body element
999
+ // -----------------------------------------------------------------------
1000
+ /**
1001
+ * Returns the inner element where children are rendered.
1002
+ * Falls back to el if body hasn't been created yet.
1003
+ */
1004
+ getBodyEl() {
1005
+ return this._bodyEl ?? this.el;
1006
+ }
1007
+ // -----------------------------------------------------------------------
1008
+ // Child management
1009
+ // -----------------------------------------------------------------------
1010
+ add(...items) {
1011
+ const added = [];
1012
+ for (const item of items) {
1013
+ const child = this.resolveItem(item);
1014
+ const index = this._items.length;
1015
+ if (this.fireReturning("beforeadd", this, child, index) === false) continue;
1016
+ this._items.push(child);
1017
+ child.ownerCt = this;
1018
+ if (this.rendered && this._bodyEl) {
1019
+ child.render(this._bodyEl);
1020
+ }
1021
+ child.onAdded(this, index);
1022
+ this.fire("add", this, child, index);
1023
+ added.push(child);
1024
+ }
1025
+ return added;
1026
+ }
1027
+ insert(index, item) {
1028
+ const child = this.resolveItem(item);
1029
+ if (this.fireReturning("beforeadd", this, child, index) === false) {
1030
+ return child;
1031
+ }
1032
+ const i = Math.max(0, Math.min(index, this._items.length));
1033
+ this._items.splice(i, 0, child);
1034
+ child.ownerCt = this;
1035
+ if (this.rendered && this._bodyEl) {
1036
+ const refEl = this._bodyEl.children[i];
1037
+ if (refEl) {
1038
+ child.render(this._bodyEl, refEl);
1039
+ } else {
1040
+ child.render(this._bodyEl);
1041
+ }
1042
+ }
1043
+ child.onAdded(this, i);
1044
+ this.fire("add", this, child, i);
1045
+ return child;
1046
+ }
1047
+ remove(item, destroy = true) {
1048
+ const index = this._items.indexOf(item);
1049
+ if (index === -1) return item;
1050
+ if (this.fireReturning("beforeremove", this, item, index) === false) {
1051
+ return item;
1052
+ }
1053
+ this._items.splice(index, 1);
1054
+ item.ownerCt = null;
1055
+ if (item.el && item.el.parentNode) {
1056
+ item.el.parentNode.removeChild(item.el);
1057
+ }
1058
+ item.onRemoved(destroy);
1059
+ this.fire("remove", this, item, index);
1060
+ if (destroy) {
1061
+ item.destroy();
1062
+ }
1063
+ return item;
1064
+ }
1065
+ removeAll(destroy = true) {
1066
+ const removed = [...this._items];
1067
+ for (let i = removed.length - 1; i >= 0; i--) {
1068
+ this.remove(removed[i], destroy);
1069
+ }
1070
+ return removed;
1071
+ }
1072
+ removeAt(index) {
1073
+ const item = this._items[index];
1074
+ if (!item) throw new Error(`No item at index ${index}`);
1075
+ return this.remove(item, true);
1076
+ }
1077
+ // -----------------------------------------------------------------------
1078
+ // Lookup
1079
+ // -----------------------------------------------------------------------
1080
+ getItems() {
1081
+ return [...this._items];
1082
+ }
1083
+ getAt(index) {
1084
+ return this._items[index];
1085
+ }
1086
+ getComponent(id) {
1087
+ return this._items.find((c) => c.componentId === id);
1088
+ }
1089
+ indexOf(item) {
1090
+ return this._items.indexOf(item);
1091
+ }
1092
+ contains(item, deep = false) {
1093
+ if (this._items.includes(item)) return true;
1094
+ if (deep) {
1095
+ for (const child of this._items) {
1096
+ if (child instanceof _Container && child.contains(item, true)) {
1097
+ return true;
1098
+ }
1099
+ }
1100
+ }
1101
+ return false;
1102
+ }
1103
+ getCount() {
1104
+ return this._items.length;
1105
+ }
1106
+ // -----------------------------------------------------------------------
1107
+ // Layout
1108
+ // -----------------------------------------------------------------------
1109
+ getLayout() {
1110
+ if (!this._layout) {
1111
+ const config = this._config;
1112
+ this._layout = resolveLayout(config.layout);
1113
+ }
1114
+ return this._layout;
1115
+ }
1116
+ doLayout() {
1117
+ this.getLayout().doLayout(this);
1118
+ }
1119
+ // -----------------------------------------------------------------------
1120
+ // Component Query
1121
+ // -----------------------------------------------------------------------
1122
+ query(selector) {
1123
+ return ComponentQuery.query(this, selector);
1124
+ }
1125
+ down(selector) {
1126
+ const results = this.query(selector);
1127
+ return results.length > 0 ? results[0] : void 0;
1128
+ }
1129
+ child(selector) {
1130
+ return this._items.find((c) => matchesCQ(c, selector));
1131
+ }
1132
+ queryById(id) {
1133
+ return this.query(`#${id}`)[0];
1134
+ }
1135
+ queryBy(fn) {
1136
+ const all = this.collectAllDescendants();
1137
+ return all.filter(fn);
1138
+ }
1139
+ // -----------------------------------------------------------------------
1140
+ // Reference
1141
+ // -----------------------------------------------------------------------
1142
+ lookupReference(ref) {
1143
+ return this.query(`[reference=${ref}]`)[0];
1144
+ }
1145
+ // -----------------------------------------------------------------------
1146
+ // Internal helpers
1147
+ // -----------------------------------------------------------------------
1148
+ resolveItem(item) {
1149
+ if (item instanceof Component) return item;
1150
+ const config = this._config;
1151
+ const merged = config.defaults ? { ...config.defaults, ...item } : { ...item };
1152
+ const xtype = merged.xtype;
1153
+ if (xtype) {
1154
+ const Ctor = ClassManager.getByAlias(`widget.${xtype}`);
1155
+ if (Ctor) return new Ctor(merged);
1156
+ }
1157
+ return new Component(merged);
1158
+ }
1159
+ collectAllDescendants() {
1160
+ const results = [];
1161
+ for (const child of this._items) {
1162
+ results.push(child);
1163
+ if (child instanceof _Container) {
1164
+ results.push(...child.collectAllDescendants());
1165
+ }
1166
+ }
1167
+ return results;
1168
+ }
1169
+ /**
1170
+ * Fires an event and returns false if any listener returned false.
1171
+ */
1172
+ fireReturning(eventName, ...args) {
1173
+ if (typeof this.fireEvent === "function") {
1174
+ return this.fireEvent(eventName, ...args);
1175
+ }
1176
+ return true;
1177
+ }
1178
+ };
1179
+
1180
+ // src/query/ComponentQuery.ts
1181
+ var CQ = class {
1182
+ /**
1183
+ * Finds all components matching the selector under the given root.
1184
+ */
1185
+ static query(selector, root) {
1186
+ if (!root) return [];
1187
+ return CQMatcher.query(root, selector);
1188
+ }
1189
+ /**
1190
+ * Returns true if the component matches the selector.
1191
+ */
1192
+ static is(component, selector) {
1193
+ return CQMatcher.matches(component, selector);
1194
+ }
1195
+ };
1196
+
1197
+ // src/template/XTemplate.ts
1198
+ var HTML_ENCODE_MAP = {
1199
+ "&": "&amp;",
1200
+ "<": "&lt;",
1201
+ ">": "&gt;",
1202
+ '"': "&quot;",
1203
+ "'": "&#39;"
1204
+ };
1205
+ var HTML_DECODE_MAP = {
1206
+ "&amp;": "&",
1207
+ "&lt;": "<",
1208
+ "&gt;": ">",
1209
+ "&quot;": '"',
1210
+ "&#39;": "'"
1211
+ };
1212
+ function htmlEncode(v) {
1213
+ return String(v ?? "").replace(/[&<>"']/g, (ch) => HTML_ENCODE_MAP[ch] ?? ch);
1214
+ }
1215
+ function htmlDecode(v) {
1216
+ return String(v ?? "").replace(/&amp;|&lt;|&gt;|&quot;|&#39;/g, (ent) => HTML_DECODE_MAP[ent] ?? ent);
1217
+ }
1218
+ var FORMAT_FNS = {
1219
+ htmlEncode: (v) => htmlEncode(v),
1220
+ htmlDecode: (v) => htmlDecode(v),
1221
+ trim: (v) => String(v ?? "").trim(),
1222
+ uppercase: (v) => String(v ?? "").toUpperCase(),
1223
+ lowercase: (v) => String(v ?? "").toLowerCase(),
1224
+ capitalize: (v) => {
1225
+ const s = String(v ?? "");
1226
+ return s.charAt(0).toUpperCase() + s.slice(1);
1227
+ },
1228
+ ellipsis: (v, arg) => {
1229
+ const s = String(v ?? "");
1230
+ const max = parseInt(arg ?? "0", 10);
1231
+ if (max <= 0 || s.length <= max) return s;
1232
+ return s.slice(0, max - 3) + "...";
1233
+ },
1234
+ round: (v, arg) => {
1235
+ const n = Number(v);
1236
+ const decimals = parseInt(arg ?? "0", 10);
1237
+ return n.toFixed(decimals);
1238
+ },
1239
+ number: (v) => String(Number(v)),
1240
+ currency: (v) => {
1241
+ const n = Number(v);
1242
+ return n.toFixed(2);
1243
+ },
1244
+ usMoney: (v) => {
1245
+ const n = Number(v);
1246
+ const formatted = n.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",");
1247
+ return n < 0 ? `-$${formatted.slice(1)}` : `$${formatted}`;
1248
+ },
1249
+ fileSize: (v) => {
1250
+ const n = Number(v);
1251
+ if (n >= 1073741824) return `${(n / 1073741824).toFixed(1)} GB`;
1252
+ if (n >= 1048576) return `${(n / 1048576).toFixed(0)} MB`;
1253
+ if (n >= 1024) return `${(n / 1024).toFixed(0)} KB`;
1254
+ return `${n} bytes`;
1255
+ },
1256
+ plural: (v, arg) => {
1257
+ const n = Number(v);
1258
+ const word = (arg ?? "").replace(/^["']|["']$/g, "");
1259
+ return n === 1 ? `${n} ${word}` : `${n} ${word}s`;
1260
+ },
1261
+ date: (v) => {
1262
+ const d = v instanceof Date ? v : new Date(String(v));
1263
+ return isNaN(d.getTime()) ? String(v) : d.toLocaleDateString("en-US");
1264
+ }
1265
+ };
1266
+ function parseTemplate(src) {
1267
+ const nodes = [];
1268
+ let pos = 0;
1269
+ while (pos < src.length) {
1270
+ const tplMatch = matchTplOpen(src, pos);
1271
+ if (tplMatch) {
1272
+ const { tag, end } = tplMatch;
1273
+ if (tag.startsWith("if=")) {
1274
+ const result = parseIf(src, tag, end);
1275
+ nodes.push(result.node);
1276
+ pos = result.pos;
1277
+ continue;
1278
+ }
1279
+ if (tag.startsWith("for=")) {
1280
+ const result = parseFor(src, tag, end);
1281
+ nodes.push(result.node);
1282
+ pos = result.pos;
1283
+ continue;
1284
+ }
1285
+ }
1286
+ if (src[pos] === "{" && pos + 1 < src.length && src[pos + 1] === "[") {
1287
+ const closeIdx = src.indexOf("]}", pos + 2);
1288
+ if (closeIdx !== -1) {
1289
+ nodes.push({ type: "expr", code: src.slice(pos + 2, closeIdx) });
1290
+ pos = closeIdx + 2;
1291
+ continue;
1292
+ }
1293
+ }
1294
+ if (src[pos] === "{") {
1295
+ const closeIdx = findClosingBrace(src, pos);
1296
+ if (closeIdx !== -1) {
1297
+ const inner = src.slice(pos + 1, closeIdx);
1298
+ nodes.push(parseInterp(inner));
1299
+ pos = closeIdx + 1;
1300
+ continue;
1301
+ }
1302
+ }
1303
+ let textEnd = pos + 1;
1304
+ while (textEnd < src.length) {
1305
+ if (src[textEnd] === "{") break;
1306
+ if (src[textEnd] === "<" && src.slice(textEnd, textEnd + 4) === "<tpl") break;
1307
+ textEnd++;
1308
+ }
1309
+ nodes.push({ type: "text", value: src.slice(pos, textEnd) });
1310
+ pos = textEnd;
1311
+ }
1312
+ return nodes;
1313
+ }
1314
+ function findClosingBrace(src, start) {
1315
+ for (let i = start + 1; i < src.length; i++) {
1316
+ if (src[i] === "}") return i;
1317
+ }
1318
+ return -1;
1319
+ }
1320
+ function parseInterp(inner) {
1321
+ const colonIdx = inner.indexOf(":");
1322
+ if (colonIdx === -1) {
1323
+ return { type: "interp", path: inner.trim() };
1324
+ }
1325
+ const path = inner.slice(0, colonIdx).trim();
1326
+ const rest = inner.slice(colonIdx + 1).trim();
1327
+ const parenIdx = rest.indexOf("(");
1328
+ if (parenIdx !== -1) {
1329
+ const format = rest.slice(0, parenIdx);
1330
+ const arg = rest.slice(parenIdx + 1, rest.lastIndexOf(")"));
1331
+ return { type: "interp", path, format, formatArg: arg };
1332
+ }
1333
+ return { type: "interp", path, format: rest };
1334
+ }
1335
+ function matchTplOpen(src, pos) {
1336
+ if (src.slice(pos, pos + 4) !== "<tpl") return null;
1337
+ let i = pos + 4;
1338
+ let inQuote = null;
1339
+ while (i < src.length) {
1340
+ const ch = src[i];
1341
+ if (inQuote) {
1342
+ if (ch === inQuote) inQuote = null;
1343
+ } else {
1344
+ if (ch === '"' || ch === "'") inQuote = ch;
1345
+ else if (ch === ">") {
1346
+ const tagContent = src.slice(pos + 5, i).trim();
1347
+ return { tag: tagContent, end: i + 1 };
1348
+ }
1349
+ }
1350
+ i++;
1351
+ }
1352
+ return null;
1353
+ }
1354
+ function findClosingTpl(src, pos) {
1355
+ let depth = 1;
1356
+ let i = pos;
1357
+ while (i < src.length && depth > 0) {
1358
+ if (src.slice(i, i + 6) === "</tpl>") {
1359
+ depth--;
1360
+ if (depth === 0) return i;
1361
+ i += 6;
1362
+ continue;
1363
+ }
1364
+ if (src.slice(i, i + 4) === "<tpl") {
1365
+ const tagEnd = findTplTagEnd(src, i + 4);
1366
+ if (tagEnd === -1) {
1367
+ i++;
1368
+ continue;
1369
+ }
1370
+ const tagContent = src.slice(i + 4, tagEnd).trim();
1371
+ if (depth === 1 && (tagContent === "else" || tagContent.startsWith("elseif"))) {
1372
+ i = tagEnd + 1;
1373
+ continue;
1374
+ }
1375
+ depth++;
1376
+ i = tagEnd + 1;
1377
+ continue;
1378
+ }
1379
+ i++;
1380
+ }
1381
+ return src.length;
1382
+ }
1383
+ function findTplTagEnd(src, start) {
1384
+ let i = start;
1385
+ let inQuote = null;
1386
+ while (i < src.length) {
1387
+ const ch = src[i];
1388
+ if (inQuote) {
1389
+ if (ch === inQuote) inQuote = null;
1390
+ } else {
1391
+ if (ch === '"' || ch === "'") inQuote = ch;
1392
+ else if (ch === ">") return i;
1393
+ }
1394
+ i++;
1395
+ }
1396
+ return -1;
1397
+ }
1398
+ function parseIf(src, tag, bodyStart) {
1399
+ const condition = extractQuoted(tag.slice(3));
1400
+ const closeTpl = findClosingTpl(src, bodyStart);
1401
+ const innerSrc = src.slice(bodyStart, closeTpl);
1402
+ const branches = splitBranches(innerSrc);
1403
+ const node = {
1404
+ type: "if",
1405
+ condition,
1406
+ body: parseTemplate(branches.ifBody),
1407
+ elseifs: branches.elseifs.map((ei) => ({
1408
+ condition: ei.condition,
1409
+ body: parseTemplate(ei.body)
1410
+ })),
1411
+ elseBody: branches.elseBody ? parseTemplate(branches.elseBody) : []
1412
+ };
1413
+ return { node, pos: closeTpl + 6 };
1414
+ }
1415
+ function extractQuoted(s) {
1416
+ s = s.trim();
1417
+ if (s.startsWith('"') && s.endsWith('"') || s.startsWith("'") && s.endsWith("'")) {
1418
+ return s.slice(1, -1);
1419
+ }
1420
+ return s;
1421
+ }
1422
+ function splitBranches(inner) {
1423
+ const result = { ifBody: "", elseifs: [], elseBody: null };
1424
+ const parts = [];
1425
+ let i = 0;
1426
+ let depth = 0;
1427
+ while (i < inner.length) {
1428
+ if (inner.slice(i, i + 6) === "</tpl>") {
1429
+ depth--;
1430
+ i += 6;
1431
+ continue;
1432
+ }
1433
+ if (inner.slice(i, i + 4) === "<tpl") {
1434
+ const tagEnd = findTplTagEnd(inner, i + 4);
1435
+ if (tagEnd === -1) {
1436
+ i++;
1437
+ continue;
1438
+ }
1439
+ const tagContent = inner.slice(i + 4, tagEnd).trim();
1440
+ if (depth === 0) {
1441
+ if (tagContent.startsWith("elseif=")) {
1442
+ parts.push({ type: "elseif", condition: extractQuoted(tagContent.slice(7)), start: tagEnd + 1 });
1443
+ i = tagEnd + 1;
1444
+ continue;
1445
+ }
1446
+ if (tagContent === "else") {
1447
+ parts.push({ type: "else", start: tagEnd + 1 });
1448
+ i = tagEnd + 1;
1449
+ continue;
1450
+ }
1451
+ }
1452
+ depth++;
1453
+ i = tagEnd + 1;
1454
+ continue;
1455
+ }
1456
+ i++;
1457
+ }
1458
+ if (parts.length === 0) {
1459
+ result.ifBody = inner;
1460
+ } else {
1461
+ result.ifBody = inner.slice(0, findTplTagBefore(inner, parts[0].start));
1462
+ for (let p = 0; p < parts.length; p++) {
1463
+ const part = parts[p];
1464
+ const bodyEnd = p + 1 < parts.length ? findTplTagBefore(inner, parts[p + 1].start) : inner.length;
1465
+ const body = inner.slice(part.start, bodyEnd);
1466
+ if (part.type === "elseif") {
1467
+ result.elseifs.push({ condition: part.condition, body });
1468
+ } else {
1469
+ result.elseBody = body;
1470
+ }
1471
+ }
1472
+ }
1473
+ return result;
1474
+ }
1475
+ function findTplTagBefore(src, contentStart) {
1476
+ let i = contentStart - 1;
1477
+ while (i >= 0 && src[i] !== "<") i--;
1478
+ return Math.max(0, i);
1479
+ }
1480
+ function parseFor(src, tag, bodyStart) {
1481
+ const field = extractQuoted(tag.slice(4));
1482
+ const closeTpl = findClosingTpl(src, bodyStart);
1483
+ const body = parseTemplate(src.slice(bodyStart, closeTpl));
1484
+ return { node: { type: "for", field, body }, pos: closeTpl + 6 };
1485
+ }
1486
+ function evaluate(nodes, data, parentData, xindex, xcount, memberFns) {
1487
+ let result = "";
1488
+ for (const node of nodes) {
1489
+ switch (node.type) {
1490
+ case "text":
1491
+ result += node.value;
1492
+ break;
1493
+ case "interp":
1494
+ result += evalInterp(node, data, parentData, xindex, xcount);
1495
+ break;
1496
+ case "expr":
1497
+ result += evalExpr(node.code, data, parentData, xindex, xcount, memberFns);
1498
+ break;
1499
+ case "if":
1500
+ result += evalIf(node, data, parentData, xindex, xcount, memberFns);
1501
+ break;
1502
+ case "for":
1503
+ result += evalFor(node, data, parentData, xindex, xcount, memberFns);
1504
+ break;
1505
+ }
1506
+ }
1507
+ return result;
1508
+ }
1509
+ function evalInterp(node, data, _parentData, xindex, _xcount) {
1510
+ let value;
1511
+ if (node.path === ".") {
1512
+ value = data instanceof PrimitiveScope ? data._value : data;
1513
+ } else if (node.path === "#") {
1514
+ value = xindex;
1515
+ } else {
1516
+ value = resolve(data, node.path);
1517
+ }
1518
+ if (value === void 0 || value === null) return "";
1519
+ let str = String(value);
1520
+ if (node.format && FORMAT_FNS[node.format]) {
1521
+ str = FORMAT_FNS[node.format](value, node.formatArg);
1522
+ }
1523
+ return str;
1524
+ }
1525
+ function resolve(data, path) {
1526
+ if (!data || !path) return void 0;
1527
+ const parts = path.split(".");
1528
+ let current = data;
1529
+ for (const part of parts) {
1530
+ if (current === null || current === void 0) return void 0;
1531
+ if (typeof current !== "object" && typeof current !== "function") return void 0;
1532
+ current = current[part];
1533
+ }
1534
+ return current;
1535
+ }
1536
+ function evalExpr(code, data, _parentData, xindex, xcount, memberFns) {
1537
+ try {
1538
+ const fn = new Function(
1539
+ "values",
1540
+ "xindex",
1541
+ "xcount",
1542
+ `"use strict"; return (${code});`
1543
+ );
1544
+ const result = fn.call(memberFns, data, xindex, xcount);
1545
+ return result === void 0 || result === null ? "" : String(result);
1546
+ } catch {
1547
+ return "";
1548
+ }
1549
+ }
1550
+ function evalIf(node, data, parentData, xindex, xcount, memberFns) {
1551
+ if (evalCondition(node.condition, data)) {
1552
+ return evaluate(node.body, data, parentData, xindex, xcount, memberFns);
1553
+ }
1554
+ for (const ei of node.elseifs) {
1555
+ if (evalCondition(ei.condition, data)) {
1556
+ return evaluate(ei.body, data, parentData, xindex, xcount, memberFns);
1557
+ }
1558
+ }
1559
+ return evaluate(node.elseBody, data, parentData, xindex, xcount, memberFns);
1560
+ }
1561
+ function evalCondition(condition, data) {
1562
+ try {
1563
+ const keys = Object.keys(data ?? {});
1564
+ const vals = keys.map((k) => data[k]);
1565
+ const fn = new Function(...keys, `"use strict"; return !!(${condition});`);
1566
+ return fn(...vals);
1567
+ } catch {
1568
+ return false;
1569
+ }
1570
+ }
1571
+ function evalFor(node, data, _parentData, _xindex, _xcount, memberFns) {
1572
+ let arr;
1573
+ if (node.field === ".") {
1574
+ arr = Array.isArray(data) ? data : [];
1575
+ } else {
1576
+ arr = resolve(data, node.field);
1577
+ if (!Array.isArray(arr)) return "";
1578
+ }
1579
+ let result = "";
1580
+ const count = arr.length;
1581
+ for (let i = 0; i < count; i++) {
1582
+ const item = arr[i];
1583
+ let scope;
1584
+ if (item !== null && typeof item === "object") {
1585
+ scope = { ...item, parent: data };
1586
+ } else {
1587
+ scope = new PrimitiveScope(item, data);
1588
+ }
1589
+ result += evaluate(node.body, scope, data, i + 1, count, memberFns);
1590
+ }
1591
+ return result;
1592
+ }
1593
+ var PrimitiveScope = class {
1594
+ _value;
1595
+ parent;
1596
+ constructor(value, parentData) {
1597
+ this._value = value;
1598
+ this.parent = parentData;
1599
+ }
1600
+ };
1601
+ var XTemplate = class {
1602
+ source;
1603
+ memberFns;
1604
+ ast = null;
1605
+ constructor(source, memberFns) {
1606
+ this.source = source;
1607
+ this.memberFns = memberFns ?? {};
1608
+ }
1609
+ /**
1610
+ * Pre-compiles the template. Called automatically on first apply().
1611
+ */
1612
+ compile() {
1613
+ if (!this.ast) {
1614
+ this.ast = parseTemplate(this.source);
1615
+ }
1616
+ }
1617
+ /**
1618
+ * Applies data to the template and returns the resulting HTML string.
1619
+ */
1620
+ apply(data) {
1621
+ this.compile();
1622
+ return evaluate(this.ast, data, null, 0, 0, this.memberFns);
1623
+ }
1624
+ /**
1625
+ * Applies data and appends the resulting HTML to an element.
1626
+ */
1627
+ append(el, data) {
1628
+ el.innerHTML += this.apply(data);
1629
+ }
1630
+ };
1631
+ export {
1632
+ CQ,
1633
+ CQMatcher,
1634
+ CQParser,
1635
+ Component,
1636
+ ComponentQuery,
1637
+ Container,
1638
+ Layout,
1639
+ Template,
1640
+ XTemplate,
1641
+ matchesCQ,
1642
+ resolveLayout
1643
+ };
1644
+ //# sourceMappingURL=index.js.map