-
-
-
- @if (isFolder(node)) {
- @if (folderTpl(); as tpl) {
-
- } @else {
-
- {{ node.expanded ? '▾' : '▸' }}
-
- {{ node.label }}
- }
- } @else {
- @if (itemTpl(); as tpl) {
-
- } @else {
- {{ node.label }}
- }
+ @if (folder) {
+ @if (folderTpl(); as tpl) {
+
}
-
-
-
+ } @else {
+ @if (itemTpl(); as tpl) {
+
+ }
+ }
- @if (node.children?.length) {
+ @if (folder) {
+ @let childLevel = level + 1;
- @for (child of node.children; track child.id) {
+ @for (child of node.children ?? []; track child.id) {
}
diff --git a/src/app/ui/tree-dnd/tree.component.scss b/src/app/ui/tree-dnd/tree.component.scss
index be5caa3f00..ecf99b2d6d 100644
--- a/src/app/ui/tree-dnd/tree.component.scss
+++ b/src/app/ui/tree-dnd/tree.component.scss
@@ -5,32 +5,39 @@
.tree {
display: block;
position: relative;
+
+ &.is-dragging {
+ cursor: grabbing;
+
+ * {
+ cursor: grabbing !important;
+ }
+
+ &.is-drag-invalid {
+ cursor: not-allowed;
+
+ * {
+ cursor: not-allowed !important;
+ }
+ }
+ }
}
.item {
position: relative;
+ padding-left: var(--tree-indent, 0);
&.is-dragging {
opacity: 0.4;
+ pointer-events: none;
}
&.is-over-inside {
- background: greenyellow;
+ background: rgba(138, 43, 226, 0.2); /* Purple for "inside" folder */
}
}
-.row {
- min-height: 28px;
- user-select: none;
- -webkit-user-select: none;
- cursor: grab;
-
- &.is-over {
- background: greenyellow !important;
- }
-}
-
-.item.just-dropped > .row {
+.item.was-just-dropped {
background: gold;
}
@@ -40,52 +47,20 @@
user-select: none;
}
-.drop {
- height: 12px;
- pointer-events: none;
- width: 100%;
- background: transparent;
- position: absolute;
- z-index: 1;
-
- .is-dragging & {
- pointer-events: all;
- background: rgba(0, 123, 255, 0.1);
- }
-
- &:hover {
- background: rgba(0, 123, 255, 0.15);
- }
-
- .item:not(.is-folder) & {
- height: 16px;
- }
-}
-
-.drop--before,
-.drop--after {
- border-top: 2px solid transparent;
-}
-
-.drop--before {
- top: 0;
-}
-
-.drop--after {
- bottom: 0;
-}
-
-.group {
+.folder {
margin-left: 0;
}
.root-drop {
- height: 10px;
+ height: 20px;
border-top: 2px dashed transparent;
+ position: relative;
+ z-index: 5;
- &.is-over {
- border-top-color: #007bff;
- background: rgba(0, 123, 255, 0.1);
+ &.is-root-over {
+ border-top-color: #dc3545;
+ background: rgba(220, 53, 69, 0.15); /* Red for root drop (end of list) */
+ z-index: 15;
}
&:hover {
@@ -93,11 +68,22 @@
}
}
+.drag-preview {
+ background: var(--tree-preview-bg, #ffffff);
+ border: 1px solid var(--tree-preview-border, #ddd);
+ border-radius: 4px;
+ padding: 6px 10px;
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
+ white-space: nowrap;
+ pointer-events: none;
+}
+
/* indicator */
.indicator {
position: absolute;
height: 0;
pointer-events: none;
+ display: none;
}
.indicator-dot {
diff --git a/src/app/ui/tree-dnd/tree.component.ts b/src/app/ui/tree-dnd/tree.component.ts
index 70ed62c57a..b59e03d2a9 100644
--- a/src/app/ui/tree-dnd/tree.component.ts
+++ b/src/app/ui/tree-dnd/tree.component.ts
@@ -1,38 +1,49 @@
import {
- AfterViewInit,
ChangeDetectionStrategy,
Component,
contentChild,
DestroyRef,
- effect,
ElementRef,
inject,
input,
+ model,
output,
signal,
TemplateRef,
} from '@angular/core';
-import { trigger, state, style, transition, animate } from '@angular/animations';
-import { NgTemplateOutlet } from '@angular/common';
-import { monitorForElements } from '@atlaskit/pragmatic-drag-and-drop/element/adapter';
+import { animate, state, style, transition, trigger } from '@angular/animations';
+import { NgClass, NgStyle, NgTemplateOutlet } from '@angular/common';
import {
- DragData,
- DropData,
+ CdkDrag,
+ CdkDragDrop,
+ CdkDragEnd,
+ CdkDragMove,
+ CdkDropList,
+ DragDropModule,
+} from '@angular/cdk/drag-drop';
+import {
+ CanDropPredicate,
+ DropListContext,
DropWhere,
FolderTplContext,
+ HoverTarget,
ItemTplContext,
MoveInstruction,
+ PointerPosition,
+ TreeId,
TreeNode,
} from './tree.types';
import { moveNode } from './tree.utils';
-import { DndDraggableDirective } from './dnd-draggable.directive';
-import { DndDropTargetDirective } from './dnd-drop-target.directive';
-import { asDragData, asDropData } from './dnd.helpers';
+import { TreeDragService } from './tree-drag.service';
+import { TreeIndicatorService } from './tree-indicator.service';
+import { TREE_CONSTANTS } from './tree-constants';
+import { assertTreeId } from './tree-guards';
@Component({
selector: 'tree-dnd',
standalone: true,
- imports: [NgTemplateOutlet, DndDraggableDirective, DndDropTargetDirective],
+ imports: [NgTemplateOutlet, NgStyle, NgClass, DragDropModule],
+ providers: [TreeIndicatorService, TreeDragService],
templateUrl: './tree.component.html',
styleUrls: ['./tree.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -56,57 +67,68 @@ import { asDragData, asDropData } from './dnd.helpers';
),
transition('collapsed <=> expanded', [
style({ overflow: 'hidden' }),
- animate('300ms ease-in-out'),
+ animate(`${TREE_CONSTANTS.ANIMATION_DURATION}ms ease-in-out`),
]),
]),
],
})
-export class TreeDndComponent implements AfterViewInit {
- private host = inject(ElementRef