@neuravision/ng-construct 0.5.1 → 0.8.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.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, inject, input, output, signal, computed, ChangeDetectionStrategy, Component, Injectable, forwardRef, model, booleanAttribute, viewChild, contentChildren, effect, DOCUMENT as DOCUMENT$1, isDevMode, contentChild, ElementRef, TemplateRef, Directive, viewChildren, Renderer2, numberAttribute, Pipe } from '@angular/core';
|
|
2
|
+
import { InjectionToken, inject, input, output, signal, computed, ChangeDetectionStrategy, Component, Injectable, forwardRef, model, booleanAttribute, viewChild, contentChildren, effect, DOCUMENT as DOCUMENT$1, isDevMode, contentChild, ElementRef, TemplateRef, Directive, viewChildren, Renderer2, numberAttribute, Injector, Pipe } from '@angular/core';
|
|
3
3
|
import { DOCUMENT, NgTemplateOutlet } from '@angular/common';
|
|
4
4
|
import { NG_VALUE_ACCESSOR, NG_VALIDATORS } from '@angular/forms';
|
|
5
5
|
import { RouterLink, RouterLinkActive } from '@angular/router';
|
|
@@ -10354,6 +10354,955 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
10354
10354
|
`, styles: [":host{display:contents}\n"] }]
|
|
10355
10355
|
}], propDecorators: { type: [{ type: i0.Input, args: [{ isSignal: true, alias: "type", required: false }] }] } });
|
|
10356
10356
|
|
|
10357
|
+
/**
|
|
10358
|
+
* Injection token to override tree screen-reader announcements
|
|
10359
|
+
* and visible labels for i18n.
|
|
10360
|
+
*
|
|
10361
|
+
* @example
|
|
10362
|
+
* providers: [{
|
|
10363
|
+
* provide: AF_TREE_I18N,
|
|
10364
|
+
* useValue: {
|
|
10365
|
+
* expanded: '{label} ausgeklappt',
|
|
10366
|
+
* collapsed: '{label} eingeklappt',
|
|
10367
|
+
* selected: '{label} ausgewählt',
|
|
10368
|
+
* toggleLabel: 'Aufklappen / Zuklappen',
|
|
10369
|
+
* loadingLabel: 'Lädt …',
|
|
10370
|
+
* emptyMessage: 'Keine Einträge',
|
|
10371
|
+
* orphanLabel: 'Übergeordneter Eintrag fehlt',
|
|
10372
|
+
* },
|
|
10373
|
+
* }]
|
|
10374
|
+
*/
|
|
10375
|
+
const AF_TREE_I18N = new InjectionToken('AfTreeI18n', {
|
|
10376
|
+
factory: () => ({
|
|
10377
|
+
expanded: '{label} expanded',
|
|
10378
|
+
collapsed: '{label} collapsed',
|
|
10379
|
+
selected: '{label} selected',
|
|
10380
|
+
toggleLabel: 'Toggle',
|
|
10381
|
+
loadingLabel: 'Loading…',
|
|
10382
|
+
emptyMessage: 'No entries',
|
|
10383
|
+
orphanLabel: 'Parent missing',
|
|
10384
|
+
}),
|
|
10385
|
+
});
|
|
10386
|
+
|
|
10387
|
+
/** Reset window for incremental type-ahead matching. */
|
|
10388
|
+
const TYPEAHEAD_RESET_MS = 500;
|
|
10389
|
+
/** Auto-incremented suffix used for the live-region id when no `aria-label` is set. */
|
|
10390
|
+
let nextTreeUid = 0;
|
|
10391
|
+
/**
|
|
10392
|
+
* Recursive internal component that renders a single `<li role="treeitem">`
|
|
10393
|
+
* and any visible children. Not part of the public API — consumers always
|
|
10394
|
+
* use {@link AfTreeComponent}.
|
|
10395
|
+
*
|
|
10396
|
+
* @docs-private
|
|
10397
|
+
*/
|
|
10398
|
+
class AfTreeNodeComponent {
|
|
10399
|
+
/** Backreference to the host tree — provides shared state, templates, and event hooks. */
|
|
10400
|
+
tree = inject(forwardRef(() => AfTreeComponent));
|
|
10401
|
+
host = inject(ElementRef);
|
|
10402
|
+
/** Node descriptor for this row. */
|
|
10403
|
+
node = input.required(...(ngDevMode ? [{ debugName: "node" }] : []));
|
|
10404
|
+
/** 1-based depth — sets `aria-level` and the `--ct-level` CSS custom property. */
|
|
10405
|
+
level = input.required(...(ngDevMode ? [{ debugName: "level" }] : []));
|
|
10406
|
+
/** Total siblings on this level — sets `aria-setsize`. */
|
|
10407
|
+
setSize = input.required(...(ngDevMode ? [{ debugName: "setSize" }] : []));
|
|
10408
|
+
/** 1-based position among siblings — sets `aria-posinset`. */
|
|
10409
|
+
posInSet = input.required(...(ngDevMode ? [{ debugName: "posInSet" }] : []));
|
|
10410
|
+
expandable = computed(() => this.tree.isExpandable(this.node()), ...(ngDevMode ? [{ debugName: "expandable" }] : []));
|
|
10411
|
+
expanded = computed(() => this.tree.isExpanded(this.node()), ...(ngDevMode ? [{ debugName: "expanded" }] : []));
|
|
10412
|
+
selected = computed(() => this.tree.isSelected(this.node()), ...(ngDevMode ? [{ debugName: "selected" }] : []));
|
|
10413
|
+
focused = computed(() => this.tree.focusedId() === this.node().id, ...(ngDevMode ? [{ debugName: "focused" }] : []));
|
|
10414
|
+
orphan = computed(() => this.node().meta?.['orphan'] === true, ...(ngDevMode ? [{ debugName: "orphan" }] : []));
|
|
10415
|
+
ariaSelected = computed(() => {
|
|
10416
|
+
if (this.tree.selection() === 'none')
|
|
10417
|
+
return null;
|
|
10418
|
+
return this.selected() ? 'true' : 'false';
|
|
10419
|
+
}, ...(ngDevMode ? [{ debugName: "ariaSelected" }] : []));
|
|
10420
|
+
visibleChildren = computed(() => {
|
|
10421
|
+
const children = this.node().children ?? [];
|
|
10422
|
+
return this.tree.filterChildren(children);
|
|
10423
|
+
}, ...(ngDevMode ? [{ debugName: "visibleChildren" }] : []));
|
|
10424
|
+
defaultLabelHtml = computed(() => this.tree.highlight(this.node().label), ...(ngDevMode ? [{ debugName: "defaultLabelHtml" }] : []));
|
|
10425
|
+
templateContext = computed(() => ({
|
|
10426
|
+
$implicit: this.node(),
|
|
10427
|
+
node: this.node(),
|
|
10428
|
+
level: this.level(),
|
|
10429
|
+
filter: this.tree.filter(),
|
|
10430
|
+
expanded: this.expanded(),
|
|
10431
|
+
selected: this.selected(),
|
|
10432
|
+
focused: this.focused(),
|
|
10433
|
+
}), ...(ngDevMode ? [{ debugName: "templateContext" }] : []));
|
|
10434
|
+
/** Returns the underlying `<li>` element so the host can move DOM focus here. */
|
|
10435
|
+
getLiElement() {
|
|
10436
|
+
return this.host.nativeElement.querySelector(':scope > li');
|
|
10437
|
+
}
|
|
10438
|
+
onFocus() {
|
|
10439
|
+
this.tree.focusedId.set(this.node().id);
|
|
10440
|
+
this.tree.nodeFocus.emit(this.node());
|
|
10441
|
+
}
|
|
10442
|
+
onToggleClick(event) {
|
|
10443
|
+
event.stopPropagation();
|
|
10444
|
+
if (this.node().disabled)
|
|
10445
|
+
return;
|
|
10446
|
+
this.tree.toggle(this.node());
|
|
10447
|
+
}
|
|
10448
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfTreeNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
10449
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfTreeNodeComponent, isStandalone: true, selector: "af-tree-node", inputs: { node: { classPropertyName: "node", publicName: "node", isSignal: true, isRequired: true, transformFunction: null }, level: { classPropertyName: "level", publicName: "level", isSignal: true, isRequired: true, transformFunction: null }, setSize: { classPropertyName: "setSize", publicName: "setSize", isSignal: true, isRequired: true, transformFunction: null }, posInSet: { classPropertyName: "posInSet", publicName: "posInSet", isSignal: true, isRequired: true, transformFunction: null } }, ngImport: i0, template: `
|
|
10450
|
+
<li
|
|
10451
|
+
#li
|
|
10452
|
+
class="ct-tree__node"
|
|
10453
|
+
[class.ct-tree__node--selected]="selected()"
|
|
10454
|
+
[class.ct-tree__node--orphan]="orphan()"
|
|
10455
|
+
role="treeitem"
|
|
10456
|
+
[attr.aria-level]="level()"
|
|
10457
|
+
[attr.aria-setsize]="setSize()"
|
|
10458
|
+
[attr.aria-posinset]="posInSet()"
|
|
10459
|
+
[attr.aria-expanded]="expandable() ? expanded() : null"
|
|
10460
|
+
[attr.aria-selected]="ariaSelected()"
|
|
10461
|
+
[attr.aria-disabled]="node().disabled ? 'true' : null"
|
|
10462
|
+
[attr.aria-busy]="node().loading ? 'true' : null"
|
|
10463
|
+
[attr.tabindex]="focused() ? 0 : -1"
|
|
10464
|
+
[attr.data-tree-id]="node().id"
|
|
10465
|
+
(focus)="onFocus()">
|
|
10466
|
+
<div class="ct-tree__row" [style.--ct-level]="level()">
|
|
10467
|
+
@if (expandable()) {
|
|
10468
|
+
<button
|
|
10469
|
+
type="button"
|
|
10470
|
+
class="ct-tree__toggle"
|
|
10471
|
+
tabindex="-1"
|
|
10472
|
+
[attr.aria-hidden]="true"
|
|
10473
|
+
[attr.aria-label]="tree.i18n.toggleLabel"
|
|
10474
|
+
(click)="onToggleClick($event)">
|
|
10475
|
+
<svg
|
|
10476
|
+
class="ct-tree__chevron"
|
|
10477
|
+
viewBox="0 0 24 24"
|
|
10478
|
+
fill="none"
|
|
10479
|
+
stroke="currentColor"
|
|
10480
|
+
stroke-width="2"
|
|
10481
|
+
stroke-linecap="round"
|
|
10482
|
+
stroke-linejoin="round"
|
|
10483
|
+
aria-hidden="true"
|
|
10484
|
+
focusable="false">
|
|
10485
|
+
<polyline points="9 6 15 12 9 18" />
|
|
10486
|
+
</svg>
|
|
10487
|
+
</button>
|
|
10488
|
+
} @else {
|
|
10489
|
+
<span class="ct-tree__spacer" aria-hidden="true"></span>
|
|
10490
|
+
}
|
|
10491
|
+
|
|
10492
|
+
<div class="ct-tree__content">
|
|
10493
|
+
@if (tree.nodeContent(); as tpl) {
|
|
10494
|
+
<ng-container
|
|
10495
|
+
[ngTemplateOutlet]="tpl"
|
|
10496
|
+
[ngTemplateOutletContext]="templateContext()"></ng-container>
|
|
10497
|
+
} @else {
|
|
10498
|
+
<span class="ct-tree__label" [innerHTML]="defaultLabelHtml()"></span>
|
|
10499
|
+
}
|
|
10500
|
+
@if (tree.nodeWarning(); as warnTpl) {
|
|
10501
|
+
<ng-container
|
|
10502
|
+
[ngTemplateOutlet]="warnTpl"
|
|
10503
|
+
[ngTemplateOutletContext]="templateContext()"></ng-container>
|
|
10504
|
+
}
|
|
10505
|
+
@if (node().loading) {
|
|
10506
|
+
<span class="ct-tree__sr-only">{{ tree.i18n.loadingLabel }}</span>
|
|
10507
|
+
}
|
|
10508
|
+
</div>
|
|
10509
|
+
|
|
10510
|
+
@if (tree.nodeActions(); as actTpl) {
|
|
10511
|
+
<div class="ct-tree__actions">
|
|
10512
|
+
<ng-container
|
|
10513
|
+
[ngTemplateOutlet]="actTpl"
|
|
10514
|
+
[ngTemplateOutletContext]="templateContext()"></ng-container>
|
|
10515
|
+
</div>
|
|
10516
|
+
}
|
|
10517
|
+
</div>
|
|
10518
|
+
|
|
10519
|
+
@if (expandable() && expanded() && !node().loading) {
|
|
10520
|
+
<ul
|
|
10521
|
+
class="ct-tree__group"
|
|
10522
|
+
role="group"
|
|
10523
|
+
[style.--ct-parent-level]="level() - 1">
|
|
10524
|
+
@for (
|
|
10525
|
+
child of visibleChildren();
|
|
10526
|
+
track tree.trackBy()(child);
|
|
10527
|
+
let i = $index, count = $count
|
|
10528
|
+
) {
|
|
10529
|
+
<af-tree-node
|
|
10530
|
+
[node]="child"
|
|
10531
|
+
[level]="level() + 1"
|
|
10532
|
+
[setSize]="count"
|
|
10533
|
+
[posInSet]="i + 1"></af-tree-node>
|
|
10534
|
+
}
|
|
10535
|
+
</ul>
|
|
10536
|
+
}
|
|
10537
|
+
</li>
|
|
10538
|
+
`, isInline: true, styles: [":host{display:contents}.ct-tree__sr-only{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"], dependencies: [{ kind: "component", type: i0.forwardRef(() => AfTreeNodeComponent), selector: "af-tree-node", inputs: ["node", "level", "setSize", "posInSet"] }, { kind: "directive", type: i0.forwardRef(() => NgTemplateOutlet), selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
10539
|
+
}
|
|
10540
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfTreeNodeComponent, decorators: [{
|
|
10541
|
+
type: Component,
|
|
10542
|
+
args: [{ selector: 'af-tree-node', standalone: true, imports: [NgTemplateOutlet, forwardRef(() => AfTreeNodeComponent)], changeDetection: ChangeDetectionStrategy.OnPush, template: `
|
|
10543
|
+
<li
|
|
10544
|
+
#li
|
|
10545
|
+
class="ct-tree__node"
|
|
10546
|
+
[class.ct-tree__node--selected]="selected()"
|
|
10547
|
+
[class.ct-tree__node--orphan]="orphan()"
|
|
10548
|
+
role="treeitem"
|
|
10549
|
+
[attr.aria-level]="level()"
|
|
10550
|
+
[attr.aria-setsize]="setSize()"
|
|
10551
|
+
[attr.aria-posinset]="posInSet()"
|
|
10552
|
+
[attr.aria-expanded]="expandable() ? expanded() : null"
|
|
10553
|
+
[attr.aria-selected]="ariaSelected()"
|
|
10554
|
+
[attr.aria-disabled]="node().disabled ? 'true' : null"
|
|
10555
|
+
[attr.aria-busy]="node().loading ? 'true' : null"
|
|
10556
|
+
[attr.tabindex]="focused() ? 0 : -1"
|
|
10557
|
+
[attr.data-tree-id]="node().id"
|
|
10558
|
+
(focus)="onFocus()">
|
|
10559
|
+
<div class="ct-tree__row" [style.--ct-level]="level()">
|
|
10560
|
+
@if (expandable()) {
|
|
10561
|
+
<button
|
|
10562
|
+
type="button"
|
|
10563
|
+
class="ct-tree__toggle"
|
|
10564
|
+
tabindex="-1"
|
|
10565
|
+
[attr.aria-hidden]="true"
|
|
10566
|
+
[attr.aria-label]="tree.i18n.toggleLabel"
|
|
10567
|
+
(click)="onToggleClick($event)">
|
|
10568
|
+
<svg
|
|
10569
|
+
class="ct-tree__chevron"
|
|
10570
|
+
viewBox="0 0 24 24"
|
|
10571
|
+
fill="none"
|
|
10572
|
+
stroke="currentColor"
|
|
10573
|
+
stroke-width="2"
|
|
10574
|
+
stroke-linecap="round"
|
|
10575
|
+
stroke-linejoin="round"
|
|
10576
|
+
aria-hidden="true"
|
|
10577
|
+
focusable="false">
|
|
10578
|
+
<polyline points="9 6 15 12 9 18" />
|
|
10579
|
+
</svg>
|
|
10580
|
+
</button>
|
|
10581
|
+
} @else {
|
|
10582
|
+
<span class="ct-tree__spacer" aria-hidden="true"></span>
|
|
10583
|
+
}
|
|
10584
|
+
|
|
10585
|
+
<div class="ct-tree__content">
|
|
10586
|
+
@if (tree.nodeContent(); as tpl) {
|
|
10587
|
+
<ng-container
|
|
10588
|
+
[ngTemplateOutlet]="tpl"
|
|
10589
|
+
[ngTemplateOutletContext]="templateContext()"></ng-container>
|
|
10590
|
+
} @else {
|
|
10591
|
+
<span class="ct-tree__label" [innerHTML]="defaultLabelHtml()"></span>
|
|
10592
|
+
}
|
|
10593
|
+
@if (tree.nodeWarning(); as warnTpl) {
|
|
10594
|
+
<ng-container
|
|
10595
|
+
[ngTemplateOutlet]="warnTpl"
|
|
10596
|
+
[ngTemplateOutletContext]="templateContext()"></ng-container>
|
|
10597
|
+
}
|
|
10598
|
+
@if (node().loading) {
|
|
10599
|
+
<span class="ct-tree__sr-only">{{ tree.i18n.loadingLabel }}</span>
|
|
10600
|
+
}
|
|
10601
|
+
</div>
|
|
10602
|
+
|
|
10603
|
+
@if (tree.nodeActions(); as actTpl) {
|
|
10604
|
+
<div class="ct-tree__actions">
|
|
10605
|
+
<ng-container
|
|
10606
|
+
[ngTemplateOutlet]="actTpl"
|
|
10607
|
+
[ngTemplateOutletContext]="templateContext()"></ng-container>
|
|
10608
|
+
</div>
|
|
10609
|
+
}
|
|
10610
|
+
</div>
|
|
10611
|
+
|
|
10612
|
+
@if (expandable() && expanded() && !node().loading) {
|
|
10613
|
+
<ul
|
|
10614
|
+
class="ct-tree__group"
|
|
10615
|
+
role="group"
|
|
10616
|
+
[style.--ct-parent-level]="level() - 1">
|
|
10617
|
+
@for (
|
|
10618
|
+
child of visibleChildren();
|
|
10619
|
+
track tree.trackBy()(child);
|
|
10620
|
+
let i = $index, count = $count
|
|
10621
|
+
) {
|
|
10622
|
+
<af-tree-node
|
|
10623
|
+
[node]="child"
|
|
10624
|
+
[level]="level() + 1"
|
|
10625
|
+
[setSize]="count"
|
|
10626
|
+
[posInSet]="i + 1"></af-tree-node>
|
|
10627
|
+
}
|
|
10628
|
+
</ul>
|
|
10629
|
+
}
|
|
10630
|
+
</li>
|
|
10631
|
+
`, styles: [":host{display:contents}.ct-tree__sr-only{position:absolute;inline-size:1px;block-size:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}\n"] }]
|
|
10632
|
+
}], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: true }] }], level: [{ type: i0.Input, args: [{ isSignal: true, alias: "level", required: true }] }], setSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "setSize", required: true }] }], posInSet: [{ type: i0.Input, args: [{ isSignal: true, alias: "posInSet", required: true }] }] } });
|
|
10633
|
+
/**
|
|
10634
|
+
* Accessible Tree component implementing the WAI-ARIA Tree View pattern.
|
|
10635
|
+
*
|
|
10636
|
+
* Wraps the Construct `ct-tree` CSS component into a signal-based, OnPush
|
|
10637
|
+
* Angular component with full keyboard navigation, type-ahead, single /
|
|
10638
|
+
* multi-selection, async-load support, and client-side filtering.
|
|
10639
|
+
*
|
|
10640
|
+
* @example Static org tree
|
|
10641
|
+
* <af-tree
|
|
10642
|
+
* [nodes]="organizations()"
|
|
10643
|
+
* ariaLabel="Organizations"
|
|
10644
|
+
* [showIndentGuides]="true"
|
|
10645
|
+
* selection="single"
|
|
10646
|
+
* [(selectedIds)]="selected"
|
|
10647
|
+
* (nodeActivate)="open($event)">
|
|
10648
|
+
* <ng-template #nodeContent let-node>
|
|
10649
|
+
* <af-icon name="folder" />
|
|
10650
|
+
* <span>{{ node.label }}</span>
|
|
10651
|
+
* <af-badge>{{ node.data.customerType }}</af-badge>
|
|
10652
|
+
* </ng-template>
|
|
10653
|
+
* </af-tree>
|
|
10654
|
+
*
|
|
10655
|
+
* @example Async lazy-load
|
|
10656
|
+
* <af-tree
|
|
10657
|
+
* [nodes]="nodes()"
|
|
10658
|
+
* ariaLabel="File system"
|
|
10659
|
+
* (loadChildren)="onLoad($event)" />
|
|
10660
|
+
*
|
|
10661
|
+
* @accessibility
|
|
10662
|
+
* - Container exposes `role="tree"` and the required `aria-label`.
|
|
10663
|
+
* - Each node is a `<li role="treeitem">` carrying `aria-level`,
|
|
10664
|
+
* `aria-setsize`, `aria-posinset`, and (when expandable) `aria-expanded`.
|
|
10665
|
+
* - Roving tabindex on the `<li>` so screen readers announce treeitem role,
|
|
10666
|
+
* level and selection state when focus lands.
|
|
10667
|
+
* - Keyboard: `↑`/`↓` move focus, `→` expands or steps into children,
|
|
10668
|
+
* `←` collapses or steps to the parent, `Home`/`End` jump, `Enter`
|
|
10669
|
+
* activates, `Space` toggles selection (multi) or activates (single),
|
|
10670
|
+
* `*` expands all sibling branches, A–Z performs incremental type-ahead.
|
|
10671
|
+
* - Selection state is mirrored via `aria-selected` only when
|
|
10672
|
+
* `selection !== 'none'` — leaves implicit selection off in static trees.
|
|
10673
|
+
* - `aria-busy="true"` is rendered on rows whose `node.loading` is `true`.
|
|
10674
|
+
* - Custom slot templates receive the active filter so they can highlight
|
|
10675
|
+
* matches consistently with the default renderer.
|
|
10676
|
+
*/
|
|
10677
|
+
class AfTreeComponent {
|
|
10678
|
+
/** Internal id used to scope live-region announcements (debug aid). */
|
|
10679
|
+
uid = ++nextTreeUid;
|
|
10680
|
+
/** I18n bundle resolved via {@link AF_TREE_I18N}. Public so the recursive child component can render labels. */
|
|
10681
|
+
i18n = inject(AF_TREE_I18N);
|
|
10682
|
+
announcer = inject(AriaLiveAnnouncer);
|
|
10683
|
+
host = inject(ElementRef);
|
|
10684
|
+
injector = inject(Injector);
|
|
10685
|
+
// ─── Inputs ─────────────────────────────────────────────────────────────
|
|
10686
|
+
/** Hierarchical node list. */
|
|
10687
|
+
nodes = input.required(...(ngDevMode ? [{ debugName: "nodes" }] : []));
|
|
10688
|
+
/** Container `aria-label` — required by the WAI-ARIA Tree pattern. */
|
|
10689
|
+
ariaLabel = input.required(...(ngDevMode ? [{ debugName: "ariaLabel" }] : []));
|
|
10690
|
+
/** Selection mode. Defaults to `'none'`. */
|
|
10691
|
+
selection = input('none', ...(ngDevMode ? [{ debugName: "selection" }] : []));
|
|
10692
|
+
/** Two-way bound set of expanded node ids. */
|
|
10693
|
+
expandedIds = model(new Set(), ...(ngDevMode ? [{ debugName: "expandedIds" }] : []));
|
|
10694
|
+
/** Two-way bound set of selected node ids. */
|
|
10695
|
+
selectedIds = model(new Set(), ...(ngDevMode ? [{ debugName: "selectedIds" }] : []));
|
|
10696
|
+
/** Case-insensitive substring filter; auto-expands ancestors of matches. */
|
|
10697
|
+
filter = input('', ...(ngDevMode ? [{ debugName: "filter" }] : []));
|
|
10698
|
+
/** Render `.ct-tree--guides` (vertical indent lines). */
|
|
10699
|
+
showIndentGuides = input(false, { ...(ngDevMode ? { debugName: "showIndentGuides" } : {}), transform: booleanAttribute });
|
|
10700
|
+
/** Render `.ct-tree--dense` modifier. */
|
|
10701
|
+
dense = input(false, { ...(ngDevMode ? { debugName: "dense" } : {}), transform: booleanAttribute });
|
|
10702
|
+
/** Render `.ct-tree--bordered` (surface variant). */
|
|
10703
|
+
bordered = input(false, { ...(ngDevMode ? { debugName: "bordered" } : {}), transform: booleanAttribute });
|
|
10704
|
+
/** TrackBy override — defaults to `node.id`. */
|
|
10705
|
+
trackBy = input((n) => n.id, ...(ngDevMode ? [{ debugName: "trackBy" }] : []));
|
|
10706
|
+
// ─── Outputs ────────────────────────────────────────────────────────────
|
|
10707
|
+
/** Emits when a node is activated (Enter or row click). */
|
|
10708
|
+
nodeActivate = output();
|
|
10709
|
+
/** Emits when a node is expanded or collapsed. */
|
|
10710
|
+
nodeToggle = output();
|
|
10711
|
+
/** Emits when focus moves to a node. */
|
|
10712
|
+
nodeFocus = output();
|
|
10713
|
+
/** Emits the first time a node with `children === undefined` is expanded (lazy-load hook). */
|
|
10714
|
+
loadChildren = output();
|
|
10715
|
+
// ─── Slots (ContentChild templates) ─────────────────────────────────────
|
|
10716
|
+
/** Template for custom node content; falls back to `node.label` with highlight. */
|
|
10717
|
+
nodeContent = contentChild('nodeContent', ...(ngDevMode ? [{ debugName: "nodeContent" }] : []));
|
|
10718
|
+
/** Template for action buttons rendered visible-on-hover/focus. */
|
|
10719
|
+
nodeActions = contentChild('nodeActions', ...(ngDevMode ? [{ debugName: "nodeActions" }] : []));
|
|
10720
|
+
/** Template for warning slot (e.g. orphan indicators). */
|
|
10721
|
+
nodeWarning = contentChild('nodeWarning', ...(ngDevMode ? [{ debugName: "nodeWarning" }] : []));
|
|
10722
|
+
/** Template shown when the (filtered) tree is empty. */
|
|
10723
|
+
emptySlot = contentChild('empty', ...(ngDevMode ? [{ debugName: "emptySlot" }] : []));
|
|
10724
|
+
// ─── Internal state ─────────────────────────────────────────────────────
|
|
10725
|
+
/** Currently focused node id (drives the roving tabindex). */
|
|
10726
|
+
focusedId = signal(null, ...(ngDevMode ? [{ debugName: "focusedId" }] : []));
|
|
10727
|
+
/** Tracks nodes whose `loadChildren` has already fired so we don't refire on re-expand. */
|
|
10728
|
+
loadedIds = new Set();
|
|
10729
|
+
/** Type-ahead buffer + reset timer. */
|
|
10730
|
+
typeBuffer = '';
|
|
10731
|
+
typeTimer = null;
|
|
10732
|
+
// ─── Derived state ──────────────────────────────────────────────────────
|
|
10733
|
+
/** Flat list of visible (expanded-path) nodes. Used for keyboard nav. */
|
|
10734
|
+
visibleNodeOrder = computed(() => {
|
|
10735
|
+
const out = [];
|
|
10736
|
+
const walk = (list) => {
|
|
10737
|
+
for (const n of this.filterChildren(list)) {
|
|
10738
|
+
out.push(n);
|
|
10739
|
+
if (this.isExpanded(n) && !n.loading) {
|
|
10740
|
+
walk(n.children ?? []);
|
|
10741
|
+
}
|
|
10742
|
+
}
|
|
10743
|
+
};
|
|
10744
|
+
walk(this.nodes());
|
|
10745
|
+
return out;
|
|
10746
|
+
}, ...(ngDevMode ? [{ debugName: "visibleNodeOrder" }] : []));
|
|
10747
|
+
/** Top-level filtered nodes — used by the template. */
|
|
10748
|
+
visibleNodes = computed(() => this.filterChildren(this.nodes()), ...(ngDevMode ? [{ debugName: "visibleNodes" }] : []));
|
|
10749
|
+
treeClasses = computed(() => {
|
|
10750
|
+
const classes = ['ct-tree'];
|
|
10751
|
+
if (this.showIndentGuides())
|
|
10752
|
+
classes.push('ct-tree--guides');
|
|
10753
|
+
if (this.dense())
|
|
10754
|
+
classes.push('ct-tree--dense');
|
|
10755
|
+
if (this.bordered())
|
|
10756
|
+
classes.push('ct-tree--bordered');
|
|
10757
|
+
return classes.join(' ');
|
|
10758
|
+
}, ...(ngDevMode ? [{ debugName: "treeClasses" }] : []));
|
|
10759
|
+
/**
|
|
10760
|
+
* Set of node ids matching the current filter. Empty set means no filter active.
|
|
10761
|
+
* Filtering is case-insensitive substring on `node.label`.
|
|
10762
|
+
*/
|
|
10763
|
+
filterMatches = computed(() => {
|
|
10764
|
+
const q = this.filter().trim().toLowerCase();
|
|
10765
|
+
if (!q)
|
|
10766
|
+
return null;
|
|
10767
|
+
const matches = new Set();
|
|
10768
|
+
const walk = (list) => {
|
|
10769
|
+
let any = false;
|
|
10770
|
+
for (const n of list) {
|
|
10771
|
+
const self = n.label.toLowerCase().includes(q);
|
|
10772
|
+
const childHit = walk(n.children ?? []);
|
|
10773
|
+
if (self || childHit) {
|
|
10774
|
+
matches.add(n.id);
|
|
10775
|
+
any = true;
|
|
10776
|
+
}
|
|
10777
|
+
}
|
|
10778
|
+
return any;
|
|
10779
|
+
};
|
|
10780
|
+
walk(this.nodes());
|
|
10781
|
+
return matches;
|
|
10782
|
+
}, ...(ngDevMode ? [{ debugName: "filterMatches" }] : []));
|
|
10783
|
+
/** Auto-expanded ids derived from active filter (ancestors of matches). */
|
|
10784
|
+
autoExpandedIds = computed(() => {
|
|
10785
|
+
const matches = this.filterMatches();
|
|
10786
|
+
if (!matches)
|
|
10787
|
+
return null;
|
|
10788
|
+
const expanded = new Set();
|
|
10789
|
+
const walk = (list) => {
|
|
10790
|
+
for (const n of list) {
|
|
10791
|
+
if (matches.has(n.id) && (n.children?.length ?? 0) > 0) {
|
|
10792
|
+
expanded.add(n.id);
|
|
10793
|
+
}
|
|
10794
|
+
walk(n.children ?? []);
|
|
10795
|
+
}
|
|
10796
|
+
};
|
|
10797
|
+
walk(this.nodes());
|
|
10798
|
+
return expanded;
|
|
10799
|
+
}, ...(ngDevMode ? [{ debugName: "autoExpandedIds" }] : []));
|
|
10800
|
+
constructor() {
|
|
10801
|
+
/** Seed the focused id when nodes first become available so Tab lands on a row. */
|
|
10802
|
+
effect(() => {
|
|
10803
|
+
const first = this.visibleNodeOrder()[0];
|
|
10804
|
+
const current = this.focusedId();
|
|
10805
|
+
if (!first) {
|
|
10806
|
+
if (current)
|
|
10807
|
+
this.focusedId.set(null);
|
|
10808
|
+
return;
|
|
10809
|
+
}
|
|
10810
|
+
if (!current || !this.findNode(current)) {
|
|
10811
|
+
this.focusedId.set(first.id);
|
|
10812
|
+
}
|
|
10813
|
+
});
|
|
10814
|
+
}
|
|
10815
|
+
// ─── State queries (called from the recursive child) ────────────────────
|
|
10816
|
+
/** A node is expandable when it has unloaded children, real children, or is loading. */
|
|
10817
|
+
isExpandable(node) {
|
|
10818
|
+
if (node.isLeaf)
|
|
10819
|
+
return false;
|
|
10820
|
+
if (node.loading)
|
|
10821
|
+
return true;
|
|
10822
|
+
if (node.children === undefined)
|
|
10823
|
+
return true;
|
|
10824
|
+
return node.children.length > 0;
|
|
10825
|
+
}
|
|
10826
|
+
isExpanded(node) {
|
|
10827
|
+
if (this.autoExpandedIds()?.has(node.id))
|
|
10828
|
+
return true;
|
|
10829
|
+
return this.expandedIds().has(node.id);
|
|
10830
|
+
}
|
|
10831
|
+
isSelected(node) {
|
|
10832
|
+
return this.selectedIds().has(node.id);
|
|
10833
|
+
}
|
|
10834
|
+
/** Returns the children list filtered by the active filter (or all if none). */
|
|
10835
|
+
filterChildren(children) {
|
|
10836
|
+
const matches = this.filterMatches();
|
|
10837
|
+
if (!matches)
|
|
10838
|
+
return children;
|
|
10839
|
+
return children.filter((c) => matches.has(c.id));
|
|
10840
|
+
}
|
|
10841
|
+
/** Wraps the first case-insensitive match of the active filter in `<mark>` tags. */
|
|
10842
|
+
highlight(label) {
|
|
10843
|
+
const q = this.filter().trim();
|
|
10844
|
+
const safe = escapeHtml(label);
|
|
10845
|
+
if (!q)
|
|
10846
|
+
return safe;
|
|
10847
|
+
const idx = safe.toLowerCase().indexOf(q.toLowerCase());
|
|
10848
|
+
if (idx < 0)
|
|
10849
|
+
return safe;
|
|
10850
|
+
const end = idx + q.length;
|
|
10851
|
+
return `${safe.slice(0, idx)}<mark>${safe.slice(idx, end)}</mark>${safe.slice(end)}`;
|
|
10852
|
+
}
|
|
10853
|
+
// ─── Mutation API ───────────────────────────────────────────────────────
|
|
10854
|
+
/** Toggle the expanded state of `node`; fires `loadChildren` on first lazy-expand. */
|
|
10855
|
+
toggle(node) {
|
|
10856
|
+
if (node.disabled || !this.isExpandable(node))
|
|
10857
|
+
return;
|
|
10858
|
+
const wasExpanded = this.expandedIds().has(node.id);
|
|
10859
|
+
const next = new Set(this.expandedIds());
|
|
10860
|
+
if (wasExpanded)
|
|
10861
|
+
next.delete(node.id);
|
|
10862
|
+
else
|
|
10863
|
+
next.add(node.id);
|
|
10864
|
+
this.expandedIds.set(next);
|
|
10865
|
+
const expanded = !wasExpanded;
|
|
10866
|
+
this.nodeToggle.emit({ node, expanded });
|
|
10867
|
+
this.announcer.announce((expanded ? this.i18n.expanded : this.i18n.collapsed).replace('{label}', node.label));
|
|
10868
|
+
if (expanded && node.children === undefined && !this.loadedIds.has(node.id)) {
|
|
10869
|
+
this.loadedIds.add(node.id);
|
|
10870
|
+
this.loadChildren.emit(node);
|
|
10871
|
+
}
|
|
10872
|
+
}
|
|
10873
|
+
/**
|
|
10874
|
+
* Activate `node` — always emits `nodeActivate` and updates selection per
|
|
10875
|
+
* mode. In `multiple` mode `Enter` activates without toggling selection so
|
|
10876
|
+
* keyboard users can drive a primary action without disturbing checkboxes.
|
|
10877
|
+
*/
|
|
10878
|
+
activate(node, source) {
|
|
10879
|
+
if (node.disabled)
|
|
10880
|
+
return;
|
|
10881
|
+
const mode = this.selection();
|
|
10882
|
+
if (mode === 'single') {
|
|
10883
|
+
this.applySingleSelection(node);
|
|
10884
|
+
}
|
|
10885
|
+
else if (mode === 'multiple' && source !== 'enter') {
|
|
10886
|
+
this.toggleMultiSelection(node);
|
|
10887
|
+
}
|
|
10888
|
+
this.nodeActivate.emit(node);
|
|
10889
|
+
}
|
|
10890
|
+
applySingleSelection(node) {
|
|
10891
|
+
const current = this.selectedIds();
|
|
10892
|
+
if (current.size === 1 && current.has(node.id))
|
|
10893
|
+
return;
|
|
10894
|
+
this.selectedIds.set(new Set([node.id]));
|
|
10895
|
+
this.announcer.announce(this.i18n.selected.replace('{label}', node.label));
|
|
10896
|
+
}
|
|
10897
|
+
toggleMultiSelection(node) {
|
|
10898
|
+
const next = new Set(this.selectedIds());
|
|
10899
|
+
if (next.has(node.id))
|
|
10900
|
+
next.delete(node.id);
|
|
10901
|
+
else {
|
|
10902
|
+
next.add(node.id);
|
|
10903
|
+
this.announcer.announce(this.i18n.selected.replace('{label}', node.label));
|
|
10904
|
+
}
|
|
10905
|
+
this.selectedIds.set(next);
|
|
10906
|
+
}
|
|
10907
|
+
// ─── Keyboard handling ──────────────────────────────────────────────────
|
|
10908
|
+
handleKeydown(event) {
|
|
10909
|
+
const target = event.target;
|
|
10910
|
+
const li = target?.closest('li.ct-tree__node');
|
|
10911
|
+
if (!li || !this.host.nativeElement.contains(li))
|
|
10912
|
+
return;
|
|
10913
|
+
const id = li.getAttribute('data-tree-id');
|
|
10914
|
+
if (!id)
|
|
10915
|
+
return;
|
|
10916
|
+
const node = this.findNode(id);
|
|
10917
|
+
if (!node)
|
|
10918
|
+
return;
|
|
10919
|
+
const order = this.visibleNodeOrder();
|
|
10920
|
+
const idx = order.findIndex((n) => n.id === id);
|
|
10921
|
+
if (idx < 0)
|
|
10922
|
+
return;
|
|
10923
|
+
switch (event.key) {
|
|
10924
|
+
case 'ArrowDown': {
|
|
10925
|
+
event.preventDefault();
|
|
10926
|
+
const next = order[Math.min(order.length - 1, idx + 1)];
|
|
10927
|
+
if (next)
|
|
10928
|
+
this.moveFocus(next);
|
|
10929
|
+
break;
|
|
10930
|
+
}
|
|
10931
|
+
case 'ArrowUp': {
|
|
10932
|
+
event.preventDefault();
|
|
10933
|
+
const prev = order[Math.max(0, idx - 1)];
|
|
10934
|
+
if (prev)
|
|
10935
|
+
this.moveFocus(prev);
|
|
10936
|
+
break;
|
|
10937
|
+
}
|
|
10938
|
+
case 'ArrowRight': {
|
|
10939
|
+
event.preventDefault();
|
|
10940
|
+
if (!this.isExpandable(node))
|
|
10941
|
+
break;
|
|
10942
|
+
if (!this.expandedIds().has(node.id)) {
|
|
10943
|
+
this.toggle(node);
|
|
10944
|
+
}
|
|
10945
|
+
else {
|
|
10946
|
+
const firstChild = (node.children ?? [])[0];
|
|
10947
|
+
if (firstChild)
|
|
10948
|
+
this.moveFocus(firstChild);
|
|
10949
|
+
}
|
|
10950
|
+
break;
|
|
10951
|
+
}
|
|
10952
|
+
case 'ArrowLeft': {
|
|
10953
|
+
event.preventDefault();
|
|
10954
|
+
if (this.isExpandable(node) && this.expandedIds().has(node.id)) {
|
|
10955
|
+
this.toggle(node);
|
|
10956
|
+
}
|
|
10957
|
+
else {
|
|
10958
|
+
const parent = this.findParent(node.id);
|
|
10959
|
+
if (parent)
|
|
10960
|
+
this.moveFocus(parent);
|
|
10961
|
+
}
|
|
10962
|
+
break;
|
|
10963
|
+
}
|
|
10964
|
+
case 'Home': {
|
|
10965
|
+
event.preventDefault();
|
|
10966
|
+
if (order[0])
|
|
10967
|
+
this.moveFocus(order[0]);
|
|
10968
|
+
break;
|
|
10969
|
+
}
|
|
10970
|
+
case 'End': {
|
|
10971
|
+
event.preventDefault();
|
|
10972
|
+
const last = order[order.length - 1];
|
|
10973
|
+
if (last)
|
|
10974
|
+
this.moveFocus(last);
|
|
10975
|
+
break;
|
|
10976
|
+
}
|
|
10977
|
+
case 'Enter': {
|
|
10978
|
+
event.preventDefault();
|
|
10979
|
+
if (node.disabled)
|
|
10980
|
+
break;
|
|
10981
|
+
this.activate(node, 'enter');
|
|
10982
|
+
break;
|
|
10983
|
+
}
|
|
10984
|
+
case ' ': {
|
|
10985
|
+
event.preventDefault();
|
|
10986
|
+
if (node.disabled)
|
|
10987
|
+
break;
|
|
10988
|
+
if (this.selection() === 'none')
|
|
10989
|
+
this.activate(node, 'enter');
|
|
10990
|
+
else
|
|
10991
|
+
this.activate(node, 'space');
|
|
10992
|
+
break;
|
|
10993
|
+
}
|
|
10994
|
+
case '*': {
|
|
10995
|
+
event.preventDefault();
|
|
10996
|
+
this.expandClosedSiblings(node);
|
|
10997
|
+
break;
|
|
10998
|
+
}
|
|
10999
|
+
default: {
|
|
11000
|
+
if (event.key.length === 1 &&
|
|
11001
|
+
/\S/.test(event.key) &&
|
|
11002
|
+
!event.ctrlKey &&
|
|
11003
|
+
!event.metaKey &&
|
|
11004
|
+
!event.altKey) {
|
|
11005
|
+
this.typeahead(event.key);
|
|
11006
|
+
}
|
|
11007
|
+
}
|
|
11008
|
+
}
|
|
11009
|
+
}
|
|
11010
|
+
handleClick(event) {
|
|
11011
|
+
const target = event.target;
|
|
11012
|
+
/** Toggle button has its own handler — it stops propagation, but guard anyway. */
|
|
11013
|
+
if (target.closest('.ct-tree__toggle'))
|
|
11014
|
+
return;
|
|
11015
|
+
/** Clicks inside the actions slot belong to the consumer's buttons. */
|
|
11016
|
+
if (target.closest('.ct-tree__actions'))
|
|
11017
|
+
return;
|
|
11018
|
+
const li = target.closest('li.ct-tree__node');
|
|
11019
|
+
if (!li || !this.host.nativeElement.contains(li))
|
|
11020
|
+
return;
|
|
11021
|
+
const id = li.getAttribute('data-tree-id');
|
|
11022
|
+
if (!id)
|
|
11023
|
+
return;
|
|
11024
|
+
const node = this.findNode(id);
|
|
11025
|
+
if (!node || node.disabled)
|
|
11026
|
+
return;
|
|
11027
|
+
this.moveFocus(node);
|
|
11028
|
+
this.activate(node, 'click');
|
|
11029
|
+
}
|
|
11030
|
+
expandClosedSiblings(node) {
|
|
11031
|
+
const parent = this.findParent(node.id);
|
|
11032
|
+
const siblings = parent ? (parent.children ?? []) : this.nodes();
|
|
11033
|
+
for (const sib of siblings) {
|
|
11034
|
+
if (this.isExpandable(sib) && !sib.disabled && !this.expandedIds().has(sib.id)) {
|
|
11035
|
+
this.toggle(sib);
|
|
11036
|
+
}
|
|
11037
|
+
}
|
|
11038
|
+
}
|
|
11039
|
+
typeahead(char) {
|
|
11040
|
+
this.typeBuffer += char.toLowerCase();
|
|
11041
|
+
if (this.typeTimer)
|
|
11042
|
+
clearTimeout(this.typeTimer);
|
|
11043
|
+
this.typeTimer = setTimeout(() => {
|
|
11044
|
+
this.typeBuffer = '';
|
|
11045
|
+
this.typeTimer = null;
|
|
11046
|
+
}, TYPEAHEAD_RESET_MS);
|
|
11047
|
+
const order = this.visibleNodeOrder();
|
|
11048
|
+
if (order.length === 0)
|
|
11049
|
+
return;
|
|
11050
|
+
const currentId = this.focusedId();
|
|
11051
|
+
const startIdx = Math.max(0, order.findIndex((n) => n.id === currentId));
|
|
11052
|
+
const ordered = order.slice(startIdx + 1).concat(order.slice(0, startIdx + 1));
|
|
11053
|
+
const buf = this.typeBuffer;
|
|
11054
|
+
const match = ordered.find((n) => n.label.toLowerCase().startsWith(buf));
|
|
11055
|
+
if (match)
|
|
11056
|
+
this.moveFocus(match);
|
|
11057
|
+
}
|
|
11058
|
+
// ─── Focus / lookup helpers ─────────────────────────────────────────────
|
|
11059
|
+
/** Move focus to `node` — updates the roving tabindex and pulls DOM focus into the tree. */
|
|
11060
|
+
moveFocus(node) {
|
|
11061
|
+
if (node.disabled)
|
|
11062
|
+
return;
|
|
11063
|
+
this.focusedId.set(node.id);
|
|
11064
|
+
queueMicrotask(() => {
|
|
11065
|
+
const el = this.host.nativeElement.querySelector(`li.ct-tree__node[data-tree-id="${cssEscape(node.id)}"]`);
|
|
11066
|
+
el?.focus();
|
|
11067
|
+
});
|
|
11068
|
+
}
|
|
11069
|
+
/** Programmatically focus the tree row matching `id`. */
|
|
11070
|
+
focusNode(id) {
|
|
11071
|
+
const node = this.findNode(id);
|
|
11072
|
+
if (node)
|
|
11073
|
+
this.moveFocus(node);
|
|
11074
|
+
}
|
|
11075
|
+
/** Walk the tree until a node with the given id is found. */
|
|
11076
|
+
findNode(id, list = this.nodes()) {
|
|
11077
|
+
for (const n of list) {
|
|
11078
|
+
if (n.id === id)
|
|
11079
|
+
return n;
|
|
11080
|
+
const hit = n.children ? this.findNode(id, n.children) : null;
|
|
11081
|
+
if (hit)
|
|
11082
|
+
return hit;
|
|
11083
|
+
}
|
|
11084
|
+
return null;
|
|
11085
|
+
}
|
|
11086
|
+
/** Locate the parent of `id` — returns null when the node is at root level. */
|
|
11087
|
+
findParent(id, list = this.nodes()) {
|
|
11088
|
+
for (const n of list) {
|
|
11089
|
+
if (n.children?.some((c) => c.id === id))
|
|
11090
|
+
return n;
|
|
11091
|
+
if (n.children) {
|
|
11092
|
+
const hit = this.findParent(id, n.children);
|
|
11093
|
+
if (hit)
|
|
11094
|
+
return hit;
|
|
11095
|
+
}
|
|
11096
|
+
}
|
|
11097
|
+
return null;
|
|
11098
|
+
}
|
|
11099
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
11100
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.1.2", type: AfTreeComponent, isStandalone: true, selector: "af-tree", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: true, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: true, transformFunction: null }, selection: { classPropertyName: "selection", publicName: "selection", isSignal: true, isRequired: false, transformFunction: null }, expandedIds: { classPropertyName: "expandedIds", publicName: "expandedIds", isSignal: true, isRequired: false, transformFunction: null }, selectedIds: { classPropertyName: "selectedIds", publicName: "selectedIds", isSignal: true, isRequired: false, transformFunction: null }, filter: { classPropertyName: "filter", publicName: "filter", isSignal: true, isRequired: false, transformFunction: null }, showIndentGuides: { classPropertyName: "showIndentGuides", publicName: "showIndentGuides", isSignal: true, isRequired: false, transformFunction: null }, dense: { classPropertyName: "dense", publicName: "dense", isSignal: true, isRequired: false, transformFunction: null }, bordered: { classPropertyName: "bordered", publicName: "bordered", isSignal: true, isRequired: false, transformFunction: null }, trackBy: { classPropertyName: "trackBy", publicName: "trackBy", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { expandedIds: "expandedIdsChange", selectedIds: "selectedIdsChange", nodeActivate: "nodeActivate", nodeToggle: "nodeToggle", nodeFocus: "nodeFocus", loadChildren: "loadChildren" }, host: { listeners: { "keydown": "handleKeydown($event)", "click": "handleClick($event)" }, properties: { "class.af-tree": "true" } }, queries: [{ propertyName: "nodeContent", first: true, predicate: ["nodeContent"], descendants: true, isSignal: true }, { propertyName: "nodeActions", first: true, predicate: ["nodeActions"], descendants: true, isSignal: true }, { propertyName: "nodeWarning", first: true, predicate: ["nodeWarning"], descendants: true, isSignal: true }, { propertyName: "emptySlot", first: true, predicate: ["empty"], descendants: true, isSignal: true }], ngImport: i0, template: `
|
|
11101
|
+
@if (visibleNodes().length > 0) {
|
|
11102
|
+
<ul
|
|
11103
|
+
[class]="treeClasses()"
|
|
11104
|
+
role="tree"
|
|
11105
|
+
[attr.aria-label]="ariaLabel()"
|
|
11106
|
+
[attr.aria-multiselectable]="selection() === 'multiple' ? 'true' : null">
|
|
11107
|
+
@for (
|
|
11108
|
+
node of visibleNodes();
|
|
11109
|
+
track trackBy()(node);
|
|
11110
|
+
let i = $index, count = $count
|
|
11111
|
+
) {
|
|
11112
|
+
<af-tree-node
|
|
11113
|
+
[node]="node"
|
|
11114
|
+
[level]="1"
|
|
11115
|
+
[setSize]="count"
|
|
11116
|
+
[posInSet]="i + 1"></af-tree-node>
|
|
11117
|
+
}
|
|
11118
|
+
</ul>
|
|
11119
|
+
} @else {
|
|
11120
|
+
<div class="af-tree__empty" role="status">
|
|
11121
|
+
@if (emptySlot(); as tpl) {
|
|
11122
|
+
<ng-container [ngTemplateOutlet]="tpl"></ng-container>
|
|
11123
|
+
} @else {
|
|
11124
|
+
<span>{{ i18n.emptyMessage }}</span>
|
|
11125
|
+
}
|
|
11126
|
+
</div>
|
|
11127
|
+
}
|
|
11128
|
+
`, isInline: true, styles: [":host{display:block}.af-tree__empty{padding:var(--space-4, 1rem);color:var(--color-text-muted, currentColor);font-size:var(--font-size-sm, .875rem)}\n"], dependencies: [{ kind: "component", type: AfTreeNodeComponent, selector: "af-tree-node", inputs: ["node", "level", "setSize", "posInSet"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
11129
|
+
}
|
|
11130
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImport: i0, type: AfTreeComponent, decorators: [{
|
|
11131
|
+
type: Component,
|
|
11132
|
+
args: [{ selector: 'af-tree', standalone: true, imports: [AfTreeNodeComponent, NgTemplateOutlet], changeDetection: ChangeDetectionStrategy.OnPush, host: {
|
|
11133
|
+
'[class.af-tree]': 'true',
|
|
11134
|
+
'(keydown)': 'handleKeydown($event)',
|
|
11135
|
+
'(click)': 'handleClick($event)',
|
|
11136
|
+
}, template: `
|
|
11137
|
+
@if (visibleNodes().length > 0) {
|
|
11138
|
+
<ul
|
|
11139
|
+
[class]="treeClasses()"
|
|
11140
|
+
role="tree"
|
|
11141
|
+
[attr.aria-label]="ariaLabel()"
|
|
11142
|
+
[attr.aria-multiselectable]="selection() === 'multiple' ? 'true' : null">
|
|
11143
|
+
@for (
|
|
11144
|
+
node of visibleNodes();
|
|
11145
|
+
track trackBy()(node);
|
|
11146
|
+
let i = $index, count = $count
|
|
11147
|
+
) {
|
|
11148
|
+
<af-tree-node
|
|
11149
|
+
[node]="node"
|
|
11150
|
+
[level]="1"
|
|
11151
|
+
[setSize]="count"
|
|
11152
|
+
[posInSet]="i + 1"></af-tree-node>
|
|
11153
|
+
}
|
|
11154
|
+
</ul>
|
|
11155
|
+
} @else {
|
|
11156
|
+
<div class="af-tree__empty" role="status">
|
|
11157
|
+
@if (emptySlot(); as tpl) {
|
|
11158
|
+
<ng-container [ngTemplateOutlet]="tpl"></ng-container>
|
|
11159
|
+
} @else {
|
|
11160
|
+
<span>{{ i18n.emptyMessage }}</span>
|
|
11161
|
+
}
|
|
11162
|
+
</div>
|
|
11163
|
+
}
|
|
11164
|
+
`, styles: [":host{display:block}.af-tree__empty{padding:var(--space-4, 1rem);color:var(--color-text-muted, currentColor);font-size:var(--font-size-sm, .875rem)}\n"] }]
|
|
11165
|
+
}], ctorParameters: () => [], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: true }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: true }] }], selection: [{ type: i0.Input, args: [{ isSignal: true, alias: "selection", required: false }] }], expandedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedIds", required: false }] }, { type: i0.Output, args: ["expandedIdsChange"] }], selectedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "selectedIds", required: false }] }, { type: i0.Output, args: ["selectedIdsChange"] }], filter: [{ type: i0.Input, args: [{ isSignal: true, alias: "filter", required: false }] }], showIndentGuides: [{ type: i0.Input, args: [{ isSignal: true, alias: "showIndentGuides", required: false }] }], dense: [{ type: i0.Input, args: [{ isSignal: true, alias: "dense", required: false }] }], bordered: [{ type: i0.Input, args: [{ isSignal: true, alias: "bordered", required: false }] }], trackBy: [{ type: i0.Input, args: [{ isSignal: true, alias: "trackBy", required: false }] }], nodeActivate: [{ type: i0.Output, args: ["nodeActivate"] }], nodeToggle: [{ type: i0.Output, args: ["nodeToggle"] }], nodeFocus: [{ type: i0.Output, args: ["nodeFocus"] }], loadChildren: [{ type: i0.Output, args: ["loadChildren"] }], nodeContent: [{ type: i0.ContentChild, args: ['nodeContent', { isSignal: true }] }], nodeActions: [{ type: i0.ContentChild, args: ['nodeActions', { isSignal: true }] }], nodeWarning: [{ type: i0.ContentChild, args: ['nodeWarning', { isSignal: true }] }], emptySlot: [{ type: i0.ContentChild, args: ['empty', { isSignal: true }] }] } });
|
|
11166
|
+
/** Minimal HTML escaper for the default label renderer. */
|
|
11167
|
+
function escapeHtml(value) {
|
|
11168
|
+
return value
|
|
11169
|
+
.replace(/&/g, '&')
|
|
11170
|
+
.replace(/</g, '<')
|
|
11171
|
+
.replace(/>/g, '>')
|
|
11172
|
+
.replace(/"/g, '"')
|
|
11173
|
+
.replace(/'/g, ''');
|
|
11174
|
+
}
|
|
11175
|
+
/** Escape an arbitrary id for safe use inside a `[data-tree-id="…"]` attribute selector. */
|
|
11176
|
+
function cssEscape(value) {
|
|
11177
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
11178
|
+
return CSS.escape(value);
|
|
11179
|
+
}
|
|
11180
|
+
return value.replace(/(["\\])/g, '\\$1');
|
|
11181
|
+
}
|
|
11182
|
+
|
|
11183
|
+
/**
|
|
11184
|
+
* Test harness for {@link AfTreeComponent}.
|
|
11185
|
+
*
|
|
11186
|
+
* Wraps the rendered DOM behind a semantic API so specs and host apps can
|
|
11187
|
+
* navigate the tree without coupling to internal CSS class names.
|
|
11188
|
+
*
|
|
11189
|
+
* @example
|
|
11190
|
+
* const harness = new AfTreeHarness(fixture.nativeElement);
|
|
11191
|
+
* harness.getNode('root').focus();
|
|
11192
|
+
* harness.pressKey('ArrowDown');
|
|
11193
|
+
* expect(harness.focusedId()).toBe('child-1');
|
|
11194
|
+
*/
|
|
11195
|
+
class AfTreeHarness {
|
|
11196
|
+
hostEl;
|
|
11197
|
+
constructor(container) {
|
|
11198
|
+
const el = container.querySelector('af-tree');
|
|
11199
|
+
if (!el) {
|
|
11200
|
+
throw new Error('AfTreeHarness: af-tree element not found in container.');
|
|
11201
|
+
}
|
|
11202
|
+
this.hostEl = el;
|
|
11203
|
+
}
|
|
11204
|
+
/** Container `<ul role="tree">` element. */
|
|
11205
|
+
getRootElement() {
|
|
11206
|
+
return this.hostEl.querySelector(':scope > ul.ct-tree');
|
|
11207
|
+
}
|
|
11208
|
+
/** Returns harnesses for every visible (rendered) treeitem in document order. */
|
|
11209
|
+
getVisibleNodes() {
|
|
11210
|
+
const lis = Array.from(this.hostEl.querySelectorAll('li.ct-tree__node'));
|
|
11211
|
+
return lis.map((li) => new AfTreeNodeHarness(li));
|
|
11212
|
+
}
|
|
11213
|
+
/** Returns the harness for the node with the given id, or null when not rendered. */
|
|
11214
|
+
getNode(id) {
|
|
11215
|
+
const li = this.hostEl.querySelector(`li.ct-tree__node[data-tree-id="${cssAttrEscape(id)}"]`);
|
|
11216
|
+
return li ? new AfTreeNodeHarness(li) : null;
|
|
11217
|
+
}
|
|
11218
|
+
/** Id of the currently focused node (the one whose `<li>` carries `tabindex=0`). */
|
|
11219
|
+
focusedId() {
|
|
11220
|
+
const li = this.hostEl.querySelector('li.ct-tree__node[tabindex="0"]');
|
|
11221
|
+
return li?.getAttribute('data-tree-id') ?? null;
|
|
11222
|
+
}
|
|
11223
|
+
/** Dispatches a keydown on the focused row (or first row if none focused). */
|
|
11224
|
+
pressKey(key) {
|
|
11225
|
+
const target = this.hostEl.querySelector('li.ct-tree__node[tabindex="0"]') ??
|
|
11226
|
+
this.hostEl.querySelector('li.ct-tree__node');
|
|
11227
|
+
target?.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
|
|
11228
|
+
}
|
|
11229
|
+
/** Returns whether `aria-multiselectable` is set on the root list. */
|
|
11230
|
+
isMultiselectable() {
|
|
11231
|
+
return this.getRootElement()?.getAttribute('aria-multiselectable') === 'true';
|
|
11232
|
+
}
|
|
11233
|
+
/** Returns the `aria-label` of the root list. */
|
|
11234
|
+
getAriaLabel() {
|
|
11235
|
+
return this.getRootElement()?.getAttribute('aria-label') ?? null;
|
|
11236
|
+
}
|
|
11237
|
+
/** True when the empty-state is rendered (no rows visible). */
|
|
11238
|
+
isEmpty() {
|
|
11239
|
+
return !this.getRootElement();
|
|
11240
|
+
}
|
|
11241
|
+
}
|
|
11242
|
+
/** Test harness for a single `<li role="treeitem">` rendered by `af-tree`. */
|
|
11243
|
+
class AfTreeNodeHarness {
|
|
11244
|
+
liEl;
|
|
11245
|
+
constructor(liEl) {
|
|
11246
|
+
this.liEl = liEl;
|
|
11247
|
+
}
|
|
11248
|
+
/** Stable id of the node (mirrors `TreeNode.id`). */
|
|
11249
|
+
getId() {
|
|
11250
|
+
return this.liEl.getAttribute('data-tree-id') ?? '';
|
|
11251
|
+
}
|
|
11252
|
+
/** Trimmed label text of the node. */
|
|
11253
|
+
getLabel() {
|
|
11254
|
+
const content = this.liEl.querySelector(':scope > .ct-tree__row .ct-tree__content');
|
|
11255
|
+
return (content?.textContent ?? '').trim();
|
|
11256
|
+
}
|
|
11257
|
+
/** 1-based depth of the node. */
|
|
11258
|
+
getLevel() {
|
|
11259
|
+
return Number(this.liEl.getAttribute('aria-level') ?? '0');
|
|
11260
|
+
}
|
|
11261
|
+
isExpanded() {
|
|
11262
|
+
return this.liEl.getAttribute('aria-expanded') === 'true';
|
|
11263
|
+
}
|
|
11264
|
+
isExpandable() {
|
|
11265
|
+
return this.liEl.hasAttribute('aria-expanded');
|
|
11266
|
+
}
|
|
11267
|
+
isSelected() {
|
|
11268
|
+
return this.liEl.getAttribute('aria-selected') === 'true';
|
|
11269
|
+
}
|
|
11270
|
+
isDisabled() {
|
|
11271
|
+
return this.liEl.getAttribute('aria-disabled') === 'true';
|
|
11272
|
+
}
|
|
11273
|
+
isBusy() {
|
|
11274
|
+
return this.liEl.getAttribute('aria-busy') === 'true';
|
|
11275
|
+
}
|
|
11276
|
+
isFocused() {
|
|
11277
|
+
return this.liEl.getAttribute('tabindex') === '0';
|
|
11278
|
+
}
|
|
11279
|
+
/** Programmatically focus the row's `<li>` element. */
|
|
11280
|
+
focus() {
|
|
11281
|
+
this.liEl.focus();
|
|
11282
|
+
}
|
|
11283
|
+
/** Click the row (centre area, not the toggle / actions). */
|
|
11284
|
+
clickRow() {
|
|
11285
|
+
const row = this.liEl.querySelector(':scope > .ct-tree__row .ct-tree__content');
|
|
11286
|
+
row?.click();
|
|
11287
|
+
}
|
|
11288
|
+
/** Click the chevron toggle button. */
|
|
11289
|
+
clickToggle() {
|
|
11290
|
+
const toggle = this.liEl.querySelector(':scope > .ct-tree__row .ct-tree__toggle');
|
|
11291
|
+
toggle?.click();
|
|
11292
|
+
}
|
|
11293
|
+
/** Return the underlying `<li>` element for advanced assertions. */
|
|
11294
|
+
getElement() {
|
|
11295
|
+
return this.liEl;
|
|
11296
|
+
}
|
|
11297
|
+
}
|
|
11298
|
+
/** Escape characters that would break a `[attr="…"]` selector. */
|
|
11299
|
+
function cssAttrEscape(value) {
|
|
11300
|
+
if (typeof CSS !== 'undefined' && typeof CSS.escape === 'function') {
|
|
11301
|
+
return CSS.escape(value);
|
|
11302
|
+
}
|
|
11303
|
+
return value.replace(/(["\\])/g, '\\$1');
|
|
11304
|
+
}
|
|
11305
|
+
|
|
10357
11306
|
// Components
|
|
10358
11307
|
|
|
10359
11308
|
/**
|
|
@@ -10392,5 +11341,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.1.2", ngImpor
|
|
|
10392
11341
|
* Generated bundle index. Do not edit.
|
|
10393
11342
|
*/
|
|
10394
11343
|
|
|
10395
|
-
export { AF_ACCORDION_I18N, AF_ALERT_I18N, AF_INPUT_I18N, AF_SELECT_I18N, AF_SELECT_MENU_I18N, AVATAR_SEED_PALETTE_SIZE, AfAccordionComponent, AfAccordionHarness, AfAccordionItemComponent, AfAccordionItemHarness, AfAlertComponent, AfAlertHarness, AfAppShellComponent, AfAppShellPageHeaderComponent, AfAppShellV2Component, AfAppShellV2ToolbarComponent, AfAvatarComponent, AfBadgeComponent, AfBadgeHarness, AfBannerComponent, AfBreadcrumbsComponent, AfButtonComponent, AfButtonHarness, AfCardComponent, AfCellDefDirective, AfCheckboxComponent, AfChipComponent, AfChipInputComponent, AfComboboxComponent, AfDataTableComponent, AfDatepickerComponent, AfDividerComponent, AfDrawerComponent, AfDropdownComponent, AfEmptyStateComponent, AfFieldComponent, AfFileUploadComponent, AfFormatLabelPipe, AfIconComponent, AfInputComponent, AfInputHarness, AfModalComponent, AfNavItemComponent, AfNavTabsComponent, AfNavbarComponent, AfPaginationComponent, AfPopoverComponent, AfPopoverTriggerDirective, AfProgressBarComponent, AfRadioComponent, AfRadioGroupComponent, AfSelectComponent, AfSelectHarness, AfSelectMenuComponent, AfSelectMenuHarness, AfSidebarComponent, AfSkeletonComponent, AfSkipLinkComponent, AfSliderComponent, AfSpinnerComponent, AfSwitchComponent, AfTabPanelComponent, AfTableBodyComponent, AfTableCellComponent, AfTableComponent, AfTableHeaderCellComponent, AfTableHeaderComponent, AfTableRowComponent, AfTabsComponent, AfTextareaComponent, AfToastContainerComponent, AfToastService, AfToggleGroupComponent, AfToolbarComponent, AfTooltipDirective };
|
|
11344
|
+
export { AF_ACCORDION_I18N, AF_ALERT_I18N, AF_INPUT_I18N, AF_SELECT_I18N, AF_SELECT_MENU_I18N, AF_TREE_I18N, AVATAR_SEED_PALETTE_SIZE, AfAccordionComponent, AfAccordionHarness, AfAccordionItemComponent, AfAccordionItemHarness, AfAlertComponent, AfAlertHarness, AfAppShellComponent, AfAppShellPageHeaderComponent, AfAppShellV2Component, AfAppShellV2ToolbarComponent, AfAvatarComponent, AfBadgeComponent, AfBadgeHarness, AfBannerComponent, AfBreadcrumbsComponent, AfButtonComponent, AfButtonHarness, AfCardComponent, AfCellDefDirective, AfCheckboxComponent, AfChipComponent, AfChipInputComponent, AfComboboxComponent, AfDataTableComponent, AfDatepickerComponent, AfDividerComponent, AfDrawerComponent, AfDropdownComponent, AfEmptyStateComponent, AfFieldComponent, AfFileUploadComponent, AfFormatLabelPipe, AfIconComponent, AfInputComponent, AfInputHarness, AfModalComponent, AfNavItemComponent, AfNavTabsComponent, AfNavbarComponent, AfPaginationComponent, AfPopoverComponent, AfPopoverTriggerDirective, AfProgressBarComponent, AfRadioComponent, AfRadioGroupComponent, AfSelectComponent, AfSelectHarness, AfSelectMenuComponent, AfSelectMenuHarness, AfSidebarComponent, AfSkeletonComponent, AfSkipLinkComponent, AfSliderComponent, AfSpinnerComponent, AfSwitchComponent, AfTabPanelComponent, AfTableBodyComponent, AfTableCellComponent, AfTableComponent, AfTableHeaderCellComponent, AfTableHeaderComponent, AfTableRowComponent, AfTabsComponent, AfTextareaComponent, AfToastContainerComponent, AfToastService, AfToggleGroupComponent, AfToolbarComponent, AfTooltipDirective, AfTreeComponent, AfTreeHarness, AfTreeNodeHarness };
|
|
10396
11345
|
//# sourceMappingURL=neuravision-ng-construct.mjs.map
|