UNPKG

@progress/kendo-angular-treeview

Version:
337 lines (336 loc) 17.4 kB
/**----------------------------------------------------------------------------------------- * Copyright © 2025 Progress Software Corporation. All rights reserved. * Licensed under commercial license. See LICENSE.md in the project root for more information *-------------------------------------------------------------------------------------------*/ import { Directive, ElementRef, NgZone, ContentChild, Input, HostBinding } from '@angular/core'; import { hasObservers } from '@progress/kendo-angular-common'; import { Draggable } from '@progress/kendo-draggable'; import { DragClueService } from './drag-clue/drag-clue.service'; import { DropHintService } from './drop-hint/drop-hint.service'; import { DragClueTemplateDirective } from './drag-clue/drag-clue-template.directive'; import { DropHintTemplateDirective } from './drop-hint/drop-hint-template.directive'; import { getDropAction, getDropPosition, treeItemFromEventTarget, getContainerOffset, getDropTarget } from './drag-and-drop-utils'; import { closestWithMatch, isPresent, isContent } from '../utils'; import { TreeViewComponent } from '../treeview.component'; import { TreeItemDropEvent, DropPosition, TreeItemDragStartEvent } from './models'; import * as i0 from "@angular/core"; import * as i1 from "../treeview.component"; import * as i2 from "./drag-clue/drag-clue.service"; import * as i3 from "./drop-hint/drop-hint.service"; const DEFAULT_SCROLL_SETTINGS = { enabled: true, step: 1, interval: 1 }; /** * A directive which enables the dragging and dropping items inside the current TreeView or between multiple linked TreeView component instances * ([see example]({% slug draganddrop_treeview %})). * * Triggers the [`nodeDragStart`]({% slug api_treeview_treeviewcomponent %}#toc-nodedragstart), * [`nodeDrag`]({% slug api_treeview_treeviewcomponent %}#toc-nodedrag), * [`nodeDrop`]({% slug api_treeview_treeviewcomponent %}#toc-nodedrop), * [`nodeDragEnd`]({% slug api_treeview_treeviewcomponent %}#toc-nodedragend), * [`addItem`]({% slug api_treeview_treeviewcomponent %}#toc-additem) and * [`removeItem`]({% slug api_treeview_treeviewcomponent %}#toc-removeitem) * events when the corresponding actions occur on the respective TreeView instance. */ export class DragAndDropDirective { element; zone; treeview; dragClueService; dropHintService; /** * Specifies whether the `removeItem` event will be fired after an item is dropped when the `ctrl` key is pressed. * If enabled, the `removeItem` event will not be fired on the source TreeView * ([see example]({% slug draganddrop_treeview %}#toc-multiple-treeviews)). * * @default false */ allowCopy = false; /** * Specifes the TreeViewComponent instances into which dragged items from the current TreeViewComponent can be dropped * ([see example]({% slug draganddrop_treeview %}#toc-multiple-treeviews)). */ dropZoneTreeViews = []; /** * Specifies the distance in pixels from the initial item pointerdown event, before the dragging is initiated. * The `nodeDragStart` and all consequent TreeView drag events will not be fired until the actual dragging begins. * * @default 5 */ startDragAfter = 5; /** * Controlls the auto-scrolling behavior during drag-and-drop ([see example]({% slug draganddrop_treeview %}#toc-auto-scrolling)). * Enbaled by default. To turn the auto-scrolling off, set this prop to `false`. * * By default, the scrolling will be performed by 1 pixel at every 1 millisecond, when the dragged item reaches the top or the bottom of the scrollable container. * The `step` and `interval` can be overridden by providing a `DragAndDropScrollSettings` object to this prop. * * @default true */ autoScroll = true; /** * @hidden */ dragClueTemplate; /** * @hidden */ dropHintTemplate; /** * @hidden */ userSelectStyle = 'none'; draggable; draggedItem; /** * The pointer event of the last successful item pointerdown event (the draggable `press` event). * Used for determining whether the `startDragAfter` distance is covered and for the `nodeDragStart` event args. * Used also as a flag for whether a drag attempt is pending. Should be set to `null` once the dragging begins. */ pendingDragStartEvent; get scrollSettings() { const userProvidedSettings = typeof this.autoScroll === 'boolean' ? { enabled: this.autoScroll } : this.autoScroll; return Object.assign({}, DEFAULT_SCROLL_SETTINGS, userProvidedSettings); } /** * Describes the offset of the parent element if the latter has the `transform` CSS prop applied. * Transformed parents create new stacking context and the fixed children must be position based on the transformed parent. * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context */ containerOffset = { top: 0, left: 0 }; constructor(element, zone, treeview, dragClueService, dropHintService) { this.element = element; this.zone = zone; this.treeview = treeview; this.dragClueService = dragClueService; this.dropHintService = dropHintService; this.treeview.touchActions = false; } ngAfterContentInit() { this.initalizeDraggable(); this.dragClueService.initialize(this.treeview.assetsContainer, this.dragClueTemplate && this.dragClueTemplate.templateRef); this.dropHintService.initialize(this.treeview.assetsContainer, this.dropHintTemplate && this.dropHintTemplate.templateRef); } ngOnDestroy() { this.draggable.destroy(); } /** * @hidden */ handlePress({ originalEvent }) { if (!isContent(originalEvent.target)) { return; } // store the drag target on press, show it only when it's actually dragged this.draggedItem = closestWithMatch(originalEvent.target, '.k-treeview-leaf'); // record the current pointer down coords - copared to the `startDragAfter` value to calculate whether to initiate dragging this.pendingDragStartEvent = originalEvent; } /** * @hidden */ handleDrag({ originalEvent, clientX, clientY }) { if (this.shouldInitiateDragStart({ clientX, clientY })) { this.initiateDragStart(); } if (!isPresent(this.draggedItem) || isPresent(this.pendingDragStartEvent)) { return; } const dropTarget = getDropTarget(originalEvent); if (hasObservers(this.treeview.nodeDrag)) { this.zone.run(() => this.notifyDrag(originalEvent, dropTarget)); } const targetTreeView = this.getTargetTreeView(dropTarget); const dropPosition = getDropPosition(this.draggedItem, dropTarget, clientY, targetTreeView, this.containerOffset); const dropHintAnchor = closestWithMatch(dropTarget, '.k-treeview-top, .k-treeview-mid, .k-treeview-bot'); const dropAction = getDropAction(dropPosition, dropTarget); const sourceItem = treeItemFromEventTarget(this.treeview, this.draggedItem); const destinationItem = treeItemFromEventTarget(targetTreeView, dropTarget); this.updateDropHintState(dropPosition, dropHintAnchor, dropAction, sourceItem, destinationItem); this.updateDragClueState(dropAction, clientX, clientY, sourceItem, destinationItem); if (this.scrollSettings.enabled) { this.dragClueService.scrollIntoView(this.scrollSettings); } } /** * @hidden */ handleRelease({ originalEvent, clientY }) { if (this.scrollSettings.enabled) { this.dragClueService.cancelScroll(); } if (!isPresent(this.draggedItem) || isPresent(this.pendingDragStartEvent)) { this.pendingDragStartEvent = null; this.draggedItem = null; return; } const dropTarget = getDropTarget(originalEvent); const sourceTree = this.treeview; const destinationTree = this.getTargetTreeView(dropTarget); const dropPosition = getDropPosition(this.draggedItem, dropTarget, clientY, this.getTargetTreeView(dropTarget), this.containerOffset); const sourceItem = treeItemFromEventTarget(sourceTree, this.draggedItem); const destinationItem = treeItemFromEventTarget(destinationTree, dropTarget); if (isPresent(destinationItem) && isPresent(dropPosition)) { this.zone.run(() => this.notifyDrop({ sourceItem, destinationItem, dropPosition, sourceTree, destinationTree }, originalEvent)); } else { this.dragClueService.animateDragClueToElementPosition(this.draggedItem); } if (hasObservers(this.treeview.nodeDragEnd)) { this.zone.run(() => this.notifyDragEnd({ sourceItem, destinationItem, originalEvent })); } this.dropHintService.hide(); this.draggedItem = null; } updateDropHintState(dropPosition, dropHintAnchor, dropAction, sourceItem, destinationItem) { if (!isPresent(dropHintAnchor) || dropPosition === DropPosition.Over || !isPresent(dropPosition)) { this.dropHintService.hide(); return; } const anchorViewPortCoords = dropHintAnchor.getBoundingClientRect(); const insertBefore = dropPosition === DropPosition.Before; const top = insertBefore ? anchorViewPortCoords.top : (anchorViewPortCoords.top + anchorViewPortCoords.height); this.dropHintService.updateDropHintData(dropAction, sourceItem, destinationItem); // clear any possible container offset created by parent elements with `transform` css property set this.dropHintService.move(anchorViewPortCoords.left - this.containerOffset.left, top - this.containerOffset.top); this.dropHintService.show(); } updateDragClueState(dropAction, clientX, clientY, sourceItem, destinationItem) { // clear any possible container offset created by parent elements with `transform` css property set this.dragClueService.move(clientX - this.containerOffset.left, clientY - this.containerOffset.top); this.dragClueService.updateDragClueData(dropAction, sourceItem, destinationItem); this.dragClueService.show(); } initalizeDraggable() { this.draggable = new Draggable({ press: this.handlePress.bind(this), drag: this.handleDrag.bind(this), release: this.handleRelease.bind(this) }); this.zone.runOutsideAngular(() => this.draggable.bindTo(this.element.nativeElement)); } notifyDragStart(originalEvent, dropTarget) { const sourceItem = treeItemFromEventTarget(this.treeview, dropTarget); const event = new TreeItemDragStartEvent({ sourceItem, originalEvent }); this.treeview.nodeDragStart.emit(event); return event; } notifyDrag(originalEvent, dropTarget) { const dragEvent = { sourceItem: treeItemFromEventTarget(this.treeview, this.draggedItem), destinationItem: treeItemFromEventTarget(this.getTargetTreeView(dropTarget), dropTarget), originalEvent }; this.treeview.nodeDrag.emit(dragEvent); } notifyDrop(args, originalEvent) { const event = new TreeItemDropEvent(args, originalEvent); args.destinationTree.nodeDrop.emit(event); // disable the animations on drop and restore them afterwards (if they were initially turned on) this.disableAnimationsForNextTick(args.destinationTree); if (args.sourceTree !== args.destinationTree) { this.disableAnimationsForNextTick(args.sourceTree); } if (!event.isDefaultPrevented() && event.isValid) { this.dragClueService.hide(); // order matters in a flat data binding scenario (first add, then remove) args.destinationTree.addItem.emit(args); if (!(originalEvent.ctrlKey && this.allowCopy)) { args.sourceTree.removeItem.emit(args); } } else if (event.isDefaultPrevented()) { // directly hide the clue if the default is prevented this.dragClueService.hide(); } else if (!event.isValid) { // animate the clue back to the source item position if marked as invalid this.dragClueService.animateDragClueToElementPosition(this.draggedItem); } } notifyDragEnd(dragEndEvent) { this.treeview.nodeDragEnd.emit(dragEndEvent); } getTargetTreeView(dropTarget) { const treeViewTagName = this.treeview.element.nativeElement.tagName; const targetTreeView = closestWithMatch(dropTarget, treeViewTagName); return [this.treeview, ...this.dropZoneTreeViews].find(treeView => isPresent(treeView) && treeView.element.nativeElement === targetTreeView); } disableAnimationsForNextTick(treeView) { // the treeView.animate getter returns `true` when the animations are turned off // confusing, but seems on purpose (the `animate` prop sets the value of the @.disabled host-bound attribute) if (treeView.animate) { return; } treeView.animate = false; this.zone.runOutsideAngular(() => setTimeout(() => treeView.animate = true)); } shouldInitiateDragStart(currentPointerCoords) { if (!isPresent(this.pendingDragStartEvent)) { return false; } const distanceFromPointerDown = Math.sqrt(Math.pow((this.pendingDragStartEvent.clientX - currentPointerCoords.clientX), 2) + Math.pow((this.pendingDragStartEvent.clientY - currentPointerCoords.clientY), 2)); return distanceFromPointerDown >= this.startDragAfter; } initiateDragStart() { if (hasObservers(this.treeview.nodeDragStart)) { const dragStartEvent = this.zone.run(() => this.notifyDragStart(this.pendingDragStartEvent, getDropTarget(this.pendingDragStartEvent))); if (dragStartEvent.isDefaultPrevented()) { this.pendingDragStartEvent = null; this.draggedItem = null; return; } } this.dragClueService.cancelReturnAnimation(); this.dragClueService.updateText(this.draggedItem.innerText); this.containerOffset = getContainerOffset(this.draggedItem); this.pendingDragStartEvent = null; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DragAndDropDirective, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: i1.TreeViewComponent }, { token: i2.DragClueService }, { token: i3.DropHintService }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: DragAndDropDirective, isStandalone: true, selector: "[kendoTreeViewDragAndDrop]", inputs: { allowCopy: "allowCopy", dropZoneTreeViews: "dropZoneTreeViews", startDragAfter: "startDragAfter", autoScroll: "autoScroll" }, host: { properties: { "style.user-select": "this.userSelectStyle", "style.-ms-user-select": "this.userSelectStyle", "style.-moz-user-select": "this.userSelectStyle", "style.-webkit-user-select": "this.userSelectStyle" } }, providers: [ DragClueService, DropHintService ], queries: [{ propertyName: "dragClueTemplate", first: true, predicate: DragClueTemplateDirective, descendants: true }, { propertyName: "dropHintTemplate", first: true, predicate: DropHintTemplateDirective, descendants: true }], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DragAndDropDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoTreeViewDragAndDrop]', providers: [ DragClueService, DropHintService ], standalone: true }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: i1.TreeViewComponent }, { type: i2.DragClueService }, { type: i3.DropHintService }]; }, propDecorators: { allowCopy: [{ type: Input }], dropZoneTreeViews: [{ type: Input }], startDragAfter: [{ type: Input }], autoScroll: [{ type: Input }], dragClueTemplate: [{ type: ContentChild, args: [DragClueTemplateDirective, { static: false }] }], dropHintTemplate: [{ type: ContentChild, args: [DropHintTemplateDirective, { static: false }] }], userSelectStyle: [{ type: HostBinding, args: ['style.user-select'] }, { type: HostBinding, args: ['style.-ms-user-select'] }, { type: HostBinding, args: ['style.-moz-user-select'] }, { type: HostBinding, args: ['style.-webkit-user-select'] }] } });