@akcelik/strct 4.3.0 → 4.4.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 { inject, DOCUMENT, signal, computed, Injectable, input, isDevMode, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, booleanAttribute, afterNextRender, Directive, model, output, HostListener, contentChildren, effect, ApplicationRef, EnvironmentInjector, createComponent, forwardRef, DestroyRef, TemplateRef, contentChild, viewChild, InjectionToken, untracked, Injector, viewChildren, Renderer2, Pipe, PLATFORM_ID, afterRenderEffect } from '@angular/core';
2
+ import { inject, DOCUMENT, signal, computed, Injectable, input, isDevMode, ViewEncapsulation, ChangeDetectionStrategy, Component, ElementRef, NgZone, booleanAttribute, afterNextRender, Directive, model, output, HostListener, contentChildren, effect, ApplicationRef, EnvironmentInjector, createComponent, PLATFORM_ID, forwardRef, DestroyRef, TemplateRef, contentChild, viewChild, InjectionToken, untracked, Injector, viewChildren, Renderer2, Pipe, afterRenderEffect } from '@angular/core';
3
3
  import { DomSanitizer } from '@angular/platform-browser';
4
4
  import { NgTemplateOutlet, DOCUMENT as DOCUMENT$1, isPlatformBrowser } from '@angular/common';
5
5
  import { RouterLink, RouterLinkActive } from '@angular/router';
@@ -2791,6 +2791,61 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
2791
2791
  args: ['contextmenu', ['$event']]
2792
2792
  }] } });
2793
2793
 
2794
+ /**
2795
+ * Screen-reader announcements for state changes that have no visible text —
2796
+ * "12 rows loaded", "Snapshot created" (Material's LiveAnnouncer, strct-sized):
2797
+ *
2798
+ * private announcer = inject(StrctAnnouncer);
2799
+ * this.announcer.announce('12 rows loaded');
2800
+ *
2801
+ * Maintains one visually-hidden live region per politeness level; repeated
2802
+ * identical messages are re-announced (the region is cleared first).
2803
+ */
2804
+ class StrctAnnouncer {
2805
+ regions = new Map();
2806
+ clearTimers = new Map();
2807
+ isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
2808
+ announce(message, politeness = 'polite') {
2809
+ if (!this.isBrowser)
2810
+ return;
2811
+ const region = this.regionFor(politeness);
2812
+ // Clear first so identical consecutive messages still fire.
2813
+ region.textContent = '';
2814
+ setTimeout(() => (region.textContent = message));
2815
+ clearTimeout(this.clearTimers.get(politeness));
2816
+ // Stale announcements should not linger for the next SR user to stumble on.
2817
+ this.clearTimers.set(politeness, setTimeout(() => (region.textContent = ''), 10_000));
2818
+ }
2819
+ regionFor(politeness) {
2820
+ let region = this.regions.get(politeness);
2821
+ if (!region) {
2822
+ region = document.createElement('div');
2823
+ region.setAttribute('aria-live', politeness);
2824
+ region.setAttribute('aria-atomic', 'true');
2825
+ region.className = 'strct-announcer';
2826
+ region.style.cssText =
2827
+ 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
2828
+ document.body.appendChild(region);
2829
+ this.regions.set(politeness, region);
2830
+ }
2831
+ return region;
2832
+ }
2833
+ ngOnDestroy() {
2834
+ for (const timer of this.clearTimers.values())
2835
+ clearTimeout(timer);
2836
+ this.clearTimers.clear();
2837
+ for (const region of this.regions.values())
2838
+ region.remove();
2839
+ this.regions.clear();
2840
+ }
2841
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
2842
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, providedIn: 'root' });
2843
+ }
2844
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, decorators: [{
2845
+ type: Injectable,
2846
+ args: [{ providedIn: 'root' }]
2847
+ }] });
2848
+
2794
2849
  /** Default chevron aria-label factory — mirrors the `chevronAriaLabel` input default. */
2795
2850
  const defaultChevronAriaLabel = (expanded, label) => `${expanded ? 'Collapse' : 'Expand'} ${label}`;
2796
2851
  let treeNodeCounter = 0;
@@ -2831,6 +2886,67 @@ class StrctTreeNode {
2831
2886
  nodeActivated = output();
2832
2887
  /** Data-mode right-click menu selection (bubbles to the tree). */
2833
2888
  nodeMenuSelect = output();
2889
+ /** Whether this row can be picked up — the tree asks the consumer's `canDrag`.
2890
+ * Reads `nodes()` too, so a refresh that makes a node movable is honoured. */
2891
+ isDraggable = computed(() => {
2892
+ const n = this.node();
2893
+ this.tree?.nodes();
2894
+ return !!n && !!this.tree?.canDragNode(n);
2895
+ }, /* @ts-ignore */
2896
+ ...(ngDevMode ? [{ debugName: "isDraggable" }] : /* istanbul ignore next */ []));
2897
+ isDragSource = computed(() => {
2898
+ const n = this.node();
2899
+ return !!n && !!this.tree?.isDragSource(n);
2900
+ }, /* @ts-ignore */
2901
+ ...(ngDevMode ? [{ debugName: "isDragSource" }] : /* istanbul ignore next */ []));
2902
+ isDropTarget = computed(() => {
2903
+ const n = this.node();
2904
+ return !!n && !!this.tree?.isDropTarget(n);
2905
+ }, /* @ts-ignore */
2906
+ ...(ngDevMode ? [{ debugName: "isDropTarget" }] : /* istanbul ignore next */ []));
2907
+ onDragStart(event) {
2908
+ const n = this.node();
2909
+ if (!n || !this.tree || !this.isDraggable())
2910
+ return;
2911
+ event.stopPropagation();
2912
+ // Firefox starts no drag without data on the transfer.
2913
+ event.dataTransfer?.setData('text/plain', n.label);
2914
+ if (event.dataTransfer)
2915
+ event.dataTransfer.effectAllowed = 'move';
2916
+ this.tree.beginDrag(n);
2917
+ }
2918
+ onDragEnd() {
2919
+ this.tree?.endDrag();
2920
+ }
2921
+ onDragOver(event) {
2922
+ const n = this.node();
2923
+ if (!n || !this.tree)
2924
+ return;
2925
+ event.stopPropagation();
2926
+ this.tree.dragOver(n, event.clientY);
2927
+ if (!this.tree.acceptsDrop(n)) {
2928
+ // No preventDefault: the pointer keeps the browser's "not allowed" cursor.
2929
+ if (event.dataTransfer)
2930
+ event.dataTransfer.dropEffect = 'none';
2931
+ return;
2932
+ }
2933
+ event.preventDefault();
2934
+ if (event.dataTransfer)
2935
+ event.dataTransfer.dropEffect = 'move';
2936
+ }
2937
+ onDragLeave() {
2938
+ const n = this.node();
2939
+ if (n)
2940
+ this.tree?.dragLeave(n);
2941
+ }
2942
+ onDrop(event) {
2943
+ const n = this.node();
2944
+ if (!n || !this.tree)
2945
+ return;
2946
+ event.preventDefault();
2947
+ event.stopPropagation();
2948
+ this.tree.dropOn(n);
2949
+ }
2834
2950
  /** Right-click menu items for this node ([] when no resolver / not data mode). */
2835
2951
  menuItems = computed(() => {
2836
2952
  const fn = this.nodeMenu();
@@ -2992,9 +3108,17 @@ class StrctTreeNode {
2992
3108
  [attr.aria-selected]="displayActive()"
2993
3109
  [attr.aria-expanded]="hasChildren() ? isOpen() : null"
2994
3110
  [attr.aria-owns]="hasChildren() && isOpen() ? groupId : null"
3111
+ [attr.draggable]="isDraggable() ? 'true' : null"
3112
+ [class.strct-tnode__row--dragging]="isDragSource()"
3113
+ [class.strct-tnode__row--droptarget]="isDropTarget()"
2995
3114
  [strctContextMenu]="menuItems()"
2996
3115
  [strctContextMenuData]="node()"
2997
3116
  (menuSelect)="onMenuSelect($event)"
3117
+ (dragstart)="onDragStart($event)"
3118
+ (dragend)="onDragEnd()"
3119
+ (dragover)="onDragOver($event)"
3120
+ (dragleave)="onDragLeave()"
3121
+ (drop)="onDrop($event)"
2998
3122
  (click)="onActivate()"
2999
3123
  (focus)="onRowFocus()"
3000
3124
  (keydown)="onRowKeydown($event)"
@@ -3045,7 +3169,7 @@ class StrctTreeNode {
3045
3169
  }
3046
3170
  </div>
3047
3171
  }
3048
- `, isInline: true, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:6px;padding:4px 9px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;align-items:center;justify-content:center;color:var(--t3);transition:transform .15s ease;width:24px;height:24px;margin:-5px;margin-inline-end:-10px;flex-shrink:0}.strct-tnode__chevron--open{transform:rotate(90deg)}[dir=rtl] .strct-tnode__chevron:not(.strct-tnode__chevron--open){transform:rotate(180deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}@media(prefers-reduced-motion:reduce){.strct-tnode__chevron{transition:none}}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-inline-start:16px}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded", "nodeMenu"], outputs: ["expandedChange", "activated", "nodeActivated", "nodeMenuSelect"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctContextMenuTrigger, selector: "[strctContextMenu]", inputs: ["strctContextMenu", "strctContextMenuData"], outputs: ["menuSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3172
+ `, isInline: true, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:6px;padding:4px 9px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row--dragging{opacity:.45}.strct-tnode__row--droptarget,.strct-tnode__row--droptarget:hover{background:var(--acc-s);outline:1px solid var(--acc);outline-offset:-1px}.strct-tnode__row:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;align-items:center;justify-content:center;color:var(--t3);transition:transform .15s ease;width:24px;height:24px;margin:-5px;margin-inline-end:-10px;flex-shrink:0}.strct-tnode__chevron--open{transform:rotate(90deg)}[dir=rtl] .strct-tnode__chevron:not(.strct-tnode__chevron--open){transform:rotate(180deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}@media(prefers-reduced-motion:reduce){.strct-tnode__chevron{transition:none}}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-inline-start:16px}\n"], dependencies: [{ kind: "component", type: StrctTreeNode, selector: "strct-tree-node", inputs: ["node", "label", "icon", "badge", "active", "expanded", "nodeMenu"], outputs: ["expandedChange", "activated", "nodeActivated", "nodeMenuSelect"] }, { kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctContextMenuTrigger, selector: "[strctContextMenu]", inputs: ["strctContextMenu", "strctContextMenuData"], outputs: ["menuSelect"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
3049
3173
  }
3050
3174
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTreeNode, decorators: [{
3051
3175
  type: Component,
@@ -3061,9 +3185,17 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
3061
3185
  [attr.aria-selected]="displayActive()"
3062
3186
  [attr.aria-expanded]="hasChildren() ? isOpen() : null"
3063
3187
  [attr.aria-owns]="hasChildren() && isOpen() ? groupId : null"
3188
+ [attr.draggable]="isDraggable() ? 'true' : null"
3189
+ [class.strct-tnode__row--dragging]="isDragSource()"
3190
+ [class.strct-tnode__row--droptarget]="isDropTarget()"
3064
3191
  [strctContextMenu]="menuItems()"
3065
3192
  [strctContextMenuData]="node()"
3066
3193
  (menuSelect)="onMenuSelect($event)"
3194
+ (dragstart)="onDragStart($event)"
3195
+ (dragend)="onDragEnd()"
3196
+ (dragover)="onDragOver($event)"
3197
+ (dragleave)="onDragLeave()"
3198
+ (drop)="onDrop($event)"
3067
3199
  (click)="onActivate()"
3068
3200
  (focus)="onRowFocus()"
3069
3201
  (keydown)="onRowKeydown($event)"
@@ -3120,7 +3252,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
3120
3252
  // siblings in the DOM, so the host must not appear in the a11y tree.
3121
3253
  role: 'none',
3122
3254
  '[attr.data-node-id]': 'node()?.id ?? null',
3123
- }, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:6px;padding:4px 9px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;align-items:center;justify-content:center;color:var(--t3);transition:transform .15s ease;width:24px;height:24px;margin:-5px;margin-inline-end:-10px;flex-shrink:0}.strct-tnode__chevron--open{transform:rotate(90deg)}[dir=rtl] .strct-tnode__chevron:not(.strct-tnode__chevron--open){transform:rotate(180deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}@media(prefers-reduced-motion:reduce){.strct-tnode__chevron{transition:none}}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-inline-start:16px}\n"] }]
3255
+ }, styles: [".strct-tnode{display:block}.strct-tnode__row{display:flex;align-items:center;gap:6px;padding:4px 9px;border-radius:5px;cursor:pointer;font-size:13px;color:var(--t1);-webkit-user-select:none;user-select:none}.strct-tnode__row:hover{background:var(--bg-3)}.strct-tnode__row--dragging{opacity:.45}.strct-tnode__row--droptarget,.strct-tnode__row--droptarget:hover{background:var(--acc-s);outline:1px solid var(--acc);outline-offset:-1px}.strct-tnode__row:focus-visible{outline:none;box-shadow:inset 0 0 0 2px var(--acc50)}.strct-tnode__row--active{background:var(--acc-m);color:var(--acc);font-weight:500}.strct-tnode__row--active .strct-tnode__icon,.strct-tnode__row--active .strct-tnode__chevron{color:var(--acc)}.strct-tnode__chevron{display:inline-flex;align-items:center;justify-content:center;color:var(--t3);transition:transform .15s ease;width:24px;height:24px;margin:-5px;margin-inline-end:-10px;flex-shrink:0}.strct-tnode__chevron--open{transform:rotate(90deg)}[dir=rtl] .strct-tnode__chevron:not(.strct-tnode__chevron--open){transform:rotate(180deg)}.strct-tnode__spacer{width:14px;flex-shrink:0}@media(prefers-reduced-motion:reduce){.strct-tnode__chevron{transition:none}}.strct-tnode__icon{color:var(--t2);flex-shrink:0}.strct-tnode__label{flex:1;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tnode__children{margin-inline-start:16px}\n"] }]
3124
3256
  }], ctorParameters: () => [], propDecorators: { node: [{ type: i0.Input, args: [{ isSignal: true, alias: "node", required: false }] }], label: [{ type: i0.Input, args: [{ isSignal: true, alias: "label", required: false }] }], icon: [{ type: i0.Input, args: [{ isSignal: true, alias: "icon", required: false }] }], badge: [{ type: i0.Input, args: [{ isSignal: true, alias: "badge", required: false }] }], active: [{ type: i0.Input, args: [{ isSignal: true, alias: "active", required: false }] }], expanded: [{ type: i0.Input, args: [{ isSignal: true, alias: "expanded", required: false }] }, { type: i0.Output, args: ["expandedChange"] }], nodeMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeMenu", required: false }] }], activated: [{ type: i0.Output, args: ["activated"] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], nodeMenuSelect: [{ type: i0.Output, args: ["nodeMenuSelect"] }], childNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { isSignal: true }] }], directChildNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { ...{ descendants: false }, isSignal: true }] }] } });
3125
3257
  /**
3126
3258
  * Root container for a tree. Either project `<strct-tree-node>` children, or
@@ -3141,6 +3273,29 @@ class StrctTree {
3141
3273
  /** Per-node right-click menu resolver. */
3142
3274
  nodeMenu = input(null, /* @ts-ignore */
3143
3275
  ...(ngDevMode ? [{ debugName: "nodeMenu" }] : /* istanbul ignore next */ []));
3276
+ /**
3277
+ * Which nodes can be picked up. Default: none, so a tree without this input
3278
+ * behaves exactly as before. Asked again whenever `nodes` changes, because
3279
+ * a refresh can make a node movable.
3280
+ */
3281
+ canDrag = input(null, /* @ts-ignore */
3282
+ ...(ngDevMode ? [{ debugName: "canDrag" }] : /* istanbul ignore next */ []));
3283
+ /**
3284
+ * Where the dragged node may land, asked during `dragover`. A browser does
3285
+ * not let `dragover` read the drag's data, so the tree keeps the source node
3286
+ * and hands it to you here. Without this input nothing accepts a drop — the
3287
+ * gesture belongs to the tree, the rule belongs to you.
3288
+ *
3289
+ * Two rules are built in whatever this returns: a node is never dropped on
3290
+ * itself, and never into its own subtree (that would be a cycle).
3291
+ */
3292
+ canDrop = input(null, /* @ts-ignore */
3293
+ ...(ngDevMode ? [{ debugName: "canDrop" }] : /* istanbul ignore next */ []));
3294
+ /** How long a collapsed node must be hovered mid-drag before it expands (ms). */
3295
+ dragExpandDelay = input(700, /* @ts-ignore */
3296
+ ...(ngDevMode ? [{ debugName: "dragExpandDelay" }] : /* istanbul ignore next */ []));
3297
+ /** Emitted for an accepted drop only. */
3298
+ nodeDrop = output();
3144
3299
  /** Accessible label of each node's chevron toggle; receives the expanded state and the node label. */
3145
3300
  chevronAriaLabel = input(defaultChevronAriaLabel, /* @ts-ignore */
3146
3301
  ...(ngDevMode ? [{ debugName: "chevronAriaLabel" }] : /* istanbul ignore next */ []));
@@ -3324,6 +3479,144 @@ class StrctTree {
3324
3479
  }
3325
3480
  }
3326
3481
  }
3482
+ // ── Drag and drop ──────────────────────────────────────────────
3483
+ announcer = inject(StrctAnnouncer);
3484
+ hostEl = inject(ElementRef);
3485
+ dragSourceNode = signal(null, /* @ts-ignore */
3486
+ ...(ngDevMode ? [{ debugName: "dragSourceNode" }] : /* istanbul ignore next */ []));
3487
+ dropTargetNode = signal(null, /* @ts-ignore */
3488
+ ...(ngDevMode ? [{ debugName: "dropTargetNode" }] : /* istanbul ignore next */ []));
3489
+ /** Pending hover-expand for a collapsed target. */
3490
+ expandTimer = null;
3491
+ expandTimerKey = null;
3492
+ /** Edge auto-scroll while a drag is in flight. */
3493
+ scrollFrame = null;
3494
+ scrollStep = 0;
3495
+ canDragNode(node) {
3496
+ return this.canDrag()?.(node) ?? false;
3497
+ }
3498
+ isDragSource(node) {
3499
+ const src = this.dragSourceNode();
3500
+ return !!src && this.keyOf(src) === this.keyOf(node);
3501
+ }
3502
+ isDropTarget(node) {
3503
+ const t = this.dropTargetNode();
3504
+ return !!t && this.keyOf(t) === this.keyOf(node);
3505
+ }
3506
+ /** Whether the current drag may land on `target`: the built-in guards first,
3507
+ * then the consumer's rule. */
3508
+ acceptsDrop(target) {
3509
+ const source = this.dragSourceNode();
3510
+ if (!source)
3511
+ return false;
3512
+ if (this.keyOf(source) === this.keyOf(target))
3513
+ return false;
3514
+ if (this.containsNode(source, target))
3515
+ return false;
3516
+ return this.canDrop()?.(source, target) ?? false;
3517
+ }
3518
+ /** Whether `node` is somewhere inside `root`'s subtree. */
3519
+ containsNode(root, node) {
3520
+ const key = this.keyOf(node);
3521
+ const walk = (list) => (list ?? []).some((n) => this.keyOf(n) === key || walk(n.children));
3522
+ return walk(root.children);
3523
+ }
3524
+ beginDrag(node) {
3525
+ this.dragSourceNode.set(node);
3526
+ this.announcer.announce(`Dragging ${node.label}`);
3527
+ }
3528
+ /** Called from every `dragover` on a row: highlight, hover-expand, scroll. */
3529
+ dragOver(target, clientY) {
3530
+ if (!this.dragSourceNode())
3531
+ return;
3532
+ const accepts = this.acceptsDrop(target);
3533
+ this.dropTargetNode.set(accepts ? target : null);
3534
+ this.scheduleHoverExpand(target);
3535
+ this.edgeScroll(clientY);
3536
+ }
3537
+ dragLeave(target) {
3538
+ if (this.isDropTarget(target))
3539
+ this.dropTargetNode.set(null);
3540
+ if (this.expandTimerKey === this.keyOf(target))
3541
+ this.clearExpandTimer();
3542
+ this.stopEdgeScroll();
3543
+ }
3544
+ dropOn(target) {
3545
+ const source = this.dragSourceNode();
3546
+ const accepted = !!source && this.acceptsDrop(target);
3547
+ if (source && accepted) {
3548
+ this.nodeDrop.emit({ source, target, position: 'into' });
3549
+ this.announcer.announce(`Dropped ${source.label} on ${target.label}`);
3550
+ }
3551
+ this.endDrag();
3552
+ }
3553
+ /** Always runs, including when the drop lands outside the tree (dragend). */
3554
+ endDrag() {
3555
+ this.dragSourceNode.set(null);
3556
+ this.dropTargetNode.set(null);
3557
+ this.clearExpandTimer();
3558
+ this.stopEdgeScroll();
3559
+ }
3560
+ /** A collapsed node hovered long enough opens, so a target that is not yet
3561
+ * visible can be reached at all — the normal case in a big inventory. */
3562
+ scheduleHoverExpand(target) {
3563
+ const key = this.keyOf(target);
3564
+ if (this.expandTimerKey === key)
3565
+ return;
3566
+ this.clearExpandTimer();
3567
+ if (!target.children?.length || this.currentSet().has(key))
3568
+ return;
3569
+ this.expandTimerKey = key;
3570
+ this.expandTimer = setTimeout(() => {
3571
+ this.expandTimer = null;
3572
+ this.expandTimerKey = null;
3573
+ if (this.dragSourceNode() && !this.currentSet().has(key))
3574
+ this.toggleNode(target);
3575
+ }, this.dragExpandDelay());
3576
+ }
3577
+ clearExpandTimer() {
3578
+ if (this.expandTimer != null)
3579
+ clearTimeout(this.expandTimer);
3580
+ this.expandTimer = null;
3581
+ this.expandTimerKey = null;
3582
+ }
3583
+ /** Nearest scrolling ancestor, so a long tree can be traversed mid-drag. */
3584
+ scrollParent() {
3585
+ let el = this.hostEl.nativeElement;
3586
+ while (el) {
3587
+ const canScroll = el.scrollHeight > el.clientHeight + 1;
3588
+ if (canScroll && /auto|scroll|overlay/.test(getComputedStyle(el).overflowY))
3589
+ return el;
3590
+ el = el.parentElement;
3591
+ }
3592
+ return null;
3593
+ }
3594
+ edgeScroll(clientY) {
3595
+ const box = this.scrollParent();
3596
+ if (!box)
3597
+ return;
3598
+ const r = box.getBoundingClientRect();
3599
+ const zone = 36;
3600
+ const step = clientY < r.top + zone ? -12 : clientY > r.bottom - zone ? 12 : 0;
3601
+ this.scrollStep = step;
3602
+ if (!step)
3603
+ return this.stopEdgeScroll();
3604
+ if (this.scrollFrame != null)
3605
+ return;
3606
+ const tick = () => {
3607
+ if (!this.scrollStep || !this.dragSourceNode())
3608
+ return this.stopEdgeScroll();
3609
+ box.scrollTop += this.scrollStep;
3610
+ this.scrollFrame = requestAnimationFrame(tick);
3611
+ };
3612
+ this.scrollFrame = requestAnimationFrame(tick);
3613
+ }
3614
+ stopEdgeScroll() {
3615
+ if (this.scrollFrame != null)
3616
+ cancelAnimationFrame(this.scrollFrame);
3617
+ this.scrollFrame = null;
3618
+ this.scrollStep = 0;
3619
+ }
3327
3620
  /** Toggle a node's expansion, updating state and emitting outputs. */
3328
3621
  toggleNode(node) {
3329
3622
  const key = this.keyOf(node);
@@ -3345,7 +3638,7 @@ class StrctTree {
3345
3638
  this.expandedChange.emit([...this.currentSet()]);
3346
3639
  }
3347
3640
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTree, deps: [], target: i0.ɵɵFactoryTarget.Component });
3348
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctTree, isStandalone: true, selector: "strct-tree", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, nodeMenu: { classPropertyName: "nodeMenu", publicName: "nodeMenu", isSignal: true, isRequired: false, transformFunction: null }, chevronAriaLabel: { classPropertyName: "chevronAriaLabel", publicName: "chevronAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, expandedIds: { classPropertyName: "expandedIds", publicName: "expandedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeActivated: "nodeActivated", nodeMenuSelect: "nodeMenuSelect", expandedIds: "expandedIdsChange", expandedChange: "expandedChange", nodeToggled: "nodeToggled" }, host: { attributes: { "role": "tree" }, properties: { "class.strct-tree--comfortable": "density() === 'comfortable'" }, classAttribute: "strct-tree" }, queries: [{ propertyName: "projectedNodes", predicate: StrctTreeNode, isSignal: true }], ngImport: i0, template: `
3641
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.1.4", type: StrctTree, isStandalone: true, selector: "strct-tree", inputs: { nodes: { classPropertyName: "nodes", publicName: "nodes", isSignal: true, isRequired: false, transformFunction: null }, density: { classPropertyName: "density", publicName: "density", isSignal: true, isRequired: false, transformFunction: null }, nodeMenu: { classPropertyName: "nodeMenu", publicName: "nodeMenu", isSignal: true, isRequired: false, transformFunction: null }, canDrag: { classPropertyName: "canDrag", publicName: "canDrag", isSignal: true, isRequired: false, transformFunction: null }, canDrop: { classPropertyName: "canDrop", publicName: "canDrop", isSignal: true, isRequired: false, transformFunction: null }, dragExpandDelay: { classPropertyName: "dragExpandDelay", publicName: "dragExpandDelay", isSignal: true, isRequired: false, transformFunction: null }, chevronAriaLabel: { classPropertyName: "chevronAriaLabel", publicName: "chevronAriaLabel", isSignal: true, isRequired: false, transformFunction: null }, expandedIds: { classPropertyName: "expandedIds", publicName: "expandedIds", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { nodeDrop: "nodeDrop", nodeActivated: "nodeActivated", nodeMenuSelect: "nodeMenuSelect", expandedIds: "expandedIdsChange", expandedChange: "expandedChange", nodeToggled: "nodeToggled" }, host: { attributes: { "role": "tree" }, properties: { "class.strct-tree--comfortable": "density() === 'comfortable'" }, classAttribute: "strct-tree" }, queries: [{ propertyName: "projectedNodes", predicate: StrctTreeNode, isSignal: true }], ngImport: i0, template: `
3349
3642
  @if (nodes(); as ns) {
3350
3643
  @for (n of ns; track n.id ?? n.label) {
3351
3644
  <strct-tree-node
@@ -3380,7 +3673,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
3380
3673
  role: 'tree',
3381
3674
  '[class.strct-tree--comfortable]': "density() === 'comfortable'",
3382
3675
  }, styles: [".strct-tree{display:block}.strct-tree--comfortable .strct-tnode__row{gap:7px;padding:6px 10px;border-radius:6px;font-size:14px}.strct-tree--comfortable .strct-tnode__chevron{margin:-4px;margin-inline-end:-9px}.strct-tree--comfortable .strct-tnode__spacer{width:16px}.strct-tree--comfortable .strct-tnode__children{margin-inline-start:19px}\n"] }]
3383
- }], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], nodeMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeMenu", required: false }] }], chevronAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "chevronAriaLabel", required: false }] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], nodeMenuSelect: [{ type: i0.Output, args: ["nodeMenuSelect"] }], expandedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedIds", required: false }] }, { type: i0.Output, args: ["expandedIdsChange"] }], expandedChange: [{ type: i0.Output, args: ["expandedChange"] }], nodeToggled: [{ type: i0.Output, args: ["nodeToggled"] }], projectedNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { ...{ descendants: false }, isSignal: true }] }] } });
3676
+ }], propDecorators: { nodes: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodes", required: false }] }], density: [{ type: i0.Input, args: [{ isSignal: true, alias: "density", required: false }] }], nodeMenu: [{ type: i0.Input, args: [{ isSignal: true, alias: "nodeMenu", required: false }] }], canDrag: [{ type: i0.Input, args: [{ isSignal: true, alias: "canDrag", required: false }] }], canDrop: [{ type: i0.Input, args: [{ isSignal: true, alias: "canDrop", required: false }] }], dragExpandDelay: [{ type: i0.Input, args: [{ isSignal: true, alias: "dragExpandDelay", required: false }] }], nodeDrop: [{ type: i0.Output, args: ["nodeDrop"] }], chevronAriaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "chevronAriaLabel", required: false }] }], nodeActivated: [{ type: i0.Output, args: ["nodeActivated"] }], nodeMenuSelect: [{ type: i0.Output, args: ["nodeMenuSelect"] }], expandedIds: [{ type: i0.Input, args: [{ isSignal: true, alias: "expandedIds", required: false }] }, { type: i0.Output, args: ["expandedIdsChange"] }], expandedChange: [{ type: i0.Output, args: ["expandedChange"] }], nodeToggled: [{ type: i0.Output, args: ["nodeToggled"] }], projectedNodes: [{ type: i0.ContentChildren, args: [i0.forwardRef(() => StrctTreeNode), { ...{ descendants: false }, isSignal: true }] }] } });
3384
3677
 
3385
3678
  /**
3386
3679
  * Dev-mode diagnostics for combinations a component accepts but cannot honour.
@@ -19254,61 +19547,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImpor
19254
19547
  args: ['document:keydown.escape', ['$event']]
19255
19548
  }] } });
19256
19549
 
19257
- /**
19258
- * Screen-reader announcements for state changes that have no visible text —
19259
- * "12 rows loaded", "Snapshot created" (Material's LiveAnnouncer, strct-sized):
19260
- *
19261
- * private announcer = inject(StrctAnnouncer);
19262
- * this.announcer.announce('12 rows loaded');
19263
- *
19264
- * Maintains one visually-hidden live region per politeness level; repeated
19265
- * identical messages are re-announced (the region is cleared first).
19266
- */
19267
- class StrctAnnouncer {
19268
- regions = new Map();
19269
- clearTimers = new Map();
19270
- isBrowser = isPlatformBrowser(inject(PLATFORM_ID));
19271
- announce(message, politeness = 'polite') {
19272
- if (!this.isBrowser)
19273
- return;
19274
- const region = this.regionFor(politeness);
19275
- // Clear first so identical consecutive messages still fire.
19276
- region.textContent = '';
19277
- setTimeout(() => (region.textContent = message));
19278
- clearTimeout(this.clearTimers.get(politeness));
19279
- // Stale announcements should not linger for the next SR user to stumble on.
19280
- this.clearTimers.set(politeness, setTimeout(() => (region.textContent = ''), 10_000));
19281
- }
19282
- regionFor(politeness) {
19283
- let region = this.regions.get(politeness);
19284
- if (!region) {
19285
- region = document.createElement('div');
19286
- region.setAttribute('aria-live', politeness);
19287
- region.setAttribute('aria-atomic', 'true');
19288
- region.className = 'strct-announcer';
19289
- region.style.cssText =
19290
- 'position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap;';
19291
- document.body.appendChild(region);
19292
- this.regions.set(politeness, region);
19293
- }
19294
- return region;
19295
- }
19296
- ngOnDestroy() {
19297
- for (const timer of this.clearTimers.values())
19298
- clearTimeout(timer);
19299
- this.clearTimers.clear();
19300
- for (const region of this.regions.values())
19301
- region.remove();
19302
- this.regions.clear();
19303
- }
19304
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
19305
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, providedIn: 'root' });
19306
- }
19307
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctAnnouncer, decorators: [{
19308
- type: Injectable,
19309
- args: [{ providedIn: 'root' }]
19310
- }] });
19311
-
19312
19550
  function normalize(combo) {
19313
19551
  return combo
19314
19552
  .toLowerCase()
@@ -20678,7 +20916,7 @@ class StrctTreeSelect {
20678
20916
  }
20679
20917
  </div>
20680
20918
  }
20681
- `, isInline: true, styles: [".strct-tsel{position:relative;display:block;width:100%}.strct-tsel__field{position:relative}.strct-tsel__trigger{display:flex;align-items:center;gap:8px;padding-inline-end:30px;text-align:start;cursor:pointer}.strct-tsel__trigger--clearable{padding-inline-end:48px}.strct-tsel__trigger:disabled{cursor:not-allowed}.strct-tsel__value{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tsel__value--empty{color:var(--t3)}.strct-tsel__caret{position:absolute;inset-inline-end:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-tsel__clear{position:absolute;inset-inline-end:26px;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;padding:2px;color:var(--t3);background:none;border:none;border-radius:var(--radius-sm);cursor:pointer}.strct-tsel__clear:hover{color:var(--t1)}.strct-tsel__panel{z-index:var(--z-dropdown);max-height:260px;overflow-y:auto;padding:var(--space-1);background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-lg);box-shadow:var(--shh)}.strct-tsel__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }, { kind: "component", type: StrctTree, selector: "strct-tree", inputs: ["nodes", "density", "nodeMenu", "chevronAriaLabel", "expandedIds"], outputs: ["nodeActivated", "nodeMenuSelect", "expandedIdsChange", "expandedChange", "nodeToggled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
20919
+ `, isInline: true, styles: [".strct-tsel{position:relative;display:block;width:100%}.strct-tsel__field{position:relative}.strct-tsel__trigger{display:flex;align-items:center;gap:8px;padding-inline-end:30px;text-align:start;cursor:pointer}.strct-tsel__trigger--clearable{padding-inline-end:48px}.strct-tsel__trigger:disabled{cursor:not-allowed}.strct-tsel__value{flex:1;min-width:0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.strct-tsel__value--empty{color:var(--t3)}.strct-tsel__caret{position:absolute;inset-inline-end:9px;top:50%;transform:translateY(-50%);color:var(--t3);pointer-events:none}.strct-tsel__clear{position:absolute;inset-inline-end:26px;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;padding:2px;color:var(--t3);background:none;border:none;border-radius:var(--radius-sm);cursor:pointer}.strct-tsel__clear:hover{color:var(--t1)}.strct-tsel__panel{z-index:var(--z-dropdown);max-height:260px;overflow-y:auto;padding:var(--space-1);background:var(--bg-1);border:1px solid var(--b2);border-radius:var(--radius-lg);box-shadow:var(--shh)}.strct-tsel__empty{padding:9px 10px;font-size:13px;color:var(--t3)}\n"], dependencies: [{ kind: "component", type: StrctIcon, selector: "strct-icon", inputs: ["name", "strictName", "size", "strokeWidth", "badge", "ariaLabel"] }, { kind: "directive", type: StrctOverlay, selector: "[strctOverlay]", inputs: ["strctOverlay", "strctOverlayPlacement", "strctOverlayMatchWidth", "strctOverlayGap"] }, { kind: "component", type: StrctTree, selector: "strct-tree", inputs: ["nodes", "density", "nodeMenu", "canDrag", "canDrop", "dragExpandDelay", "chevronAriaLabel", "expandedIds"], outputs: ["nodeDrop", "nodeActivated", "nodeMenuSelect", "expandedIdsChange", "expandedChange", "nodeToggled"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush, encapsulation: i0.ViewEncapsulation.None });
20682
20920
  }
20683
20921
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.1.4", ngImport: i0, type: StrctTreeSelect, decorators: [{
20684
20922
  type: Component,