UNPKG

@progress/kendo-angular-utils

Version:

Kendo UI Angular utils component

592 lines (591 loc) 25.3 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, EventEmitter, Output, ElementRef, Renderer2, NgZone, Input, ContentChildren, QueryList, ViewContainerRef, isDevMode, HostBinding } from "@angular/core"; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { DragHandleDirective } from "./draghandle.directive"; import { getScrollableParent } from "@progress/kendo-draggable-common"; import { DragStateService } from "./drag-state.service"; import { getAction, isPresent, setElementStyles, dragTargetTransition, noop } from './util'; import { contains, isDocumentAvailable, parseCSSClassNames } from "@progress/kendo-angular-common"; import { HintComponent } from "./hint.component"; import { DragTargetDragEndEvent, DragTargetDragEvent, DragTargetDragStartEvent, DragTargetPressEvent } from "./events/drag-target"; import { DragTargetReleaseEvent } from "./events/drag-target/release-event"; import { DragTargetDragReadyEvent } from "./events/drag-target/dragready-event"; import * as i0 from "@angular/core"; import * as i1 from "./drag-state.service"; let isDragStartPrevented = false; let isDragPrevented = false; /** * Represents the Kendo UI DragTarget directive for Angular. */ export class DragTargetDirective { element; renderer; ngZone; service; viewContainer; get touchActionStyle() { return this.dragHandles.length > 0 ? null : 'none'; } /** * Defines whether a hint will be used for dragging. By default, the hint is a copy of the drag target. ([see example]({% slug drag_hint %})). * * @default false */ hint = false; /** * The number of pixels the pointer moves in any direction before the dragging starts ([see example]({% slug minimum_distance %})). Applicable when `manualDrag` is set to `false`. * * @default 0 */ threshold = 0; /** * Defines the automatic container scrolling behavior when close to the edge ([see example]({% slug auto_scroll %})). * * @default true */ autoScroll = true; /** * Defines a unique identifier for the dragTarget. */ dragTargetId; /** * Defines the delay in milliseconds after which the drag will begin ([see example]({% slug drag_delay %})). * * @default 0 */ dragDelay = 0; /** * Restricts the element to be dragged horizontally or vertically only ([see example]({% slug axis_lock %})). Applicable when `mode` is set to `auto`. */ restrictByAxis; /** * Specifies whether the default dragging behavior will be performed or the developer will manually handle the drag action. * * @default 'auto' */ mode = 'auto'; /** * Defines a callback function used for attaching custom data to the dragTarget. * The data will be available in the events of the respective [`DropTarget`]({% slug api_utils_droptargetdirective %}) or [`DropTargetContainer`]({% slug api_utils_droptargetcontainerdirective %}) directives. * The current DragTarget HTML element and its `dragTargetId` will be available as arguments. */ set dragData(fn) { if (isDevMode && typeof fn !== 'function') { throw new Error(`dragData must be a function, but received ${JSON.stringify(fn)}.`); } this._dragData = fn; } get dragData() { return this._dragData; } /** * Specifies the cursor style of the drag target. Accepts same values as the [CSS `cursor` property](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values). * * @default 'move' */ cursorStyle = 'move'; /** * Fires when the user presses the DragTarget element. */ onPress = new EventEmitter(); /** * Fires when the dragging of the DragTarget element begins. */ onDragStart = new EventEmitter(); /** * Fires while the user drags the DragTarget element. */ onDrag = new EventEmitter(); /** * Fires when the DragTarget's `dragDelay` has passed and the user is able to drag the element. */ onDragReady = new EventEmitter(); /** * Fires when the user releases the DragTarget element after being pressed. */ onRelease = new EventEmitter(); /** * Fires when the dragging of the DragTarget ends and the element is released. */ onDragEnd = new EventEmitter(); dragTarget = null; hintComponent = null; dragStarted = false; pressed = false; dragReady = false; dragTimeout = null; initialPosition = { x: 0, y: 0 }; position = { x: 0, y: 0 }; scrollableParent = null; defaultHint = null; _dragData = () => null; prevUserSelect; get hintTemplate() { return isPresent(this.hint) && typeof this.hint === 'object' ? this.hint.hintTemplate : null; } get nativeElement() { return this.element.nativeElement; } get hintElem() { return this.hintTemplate && isPresent(this.hintComponent) ? this.hintComponent.instance.element.nativeElement : this.defaultHint; } onPointerDown(event) { if (this.dragHandles.length && !this.isDragHandle(event.target)) { return; } const action = getAction(event, this.dragTarget); this.service.handleDragAndDrop(action); this.service.autoScroll = typeof this.autoScroll === 'object' ? this.autoScroll.enabled !== false : this.autoScroll; this.service.scrollableParent = this.getAutoScrollContainer(); this.service.autoScrollDirection = typeof this.autoScroll === 'object' ? this.autoScroll.direction : { horizontal: true, vertical: true }; this.attachDomHandlers(); } onTouchStart(event) { if (this.dragHandles.length && !this.isDragHandle(event.target)) { return; } event.preventDefault(); const action = getAction(event, this.dragTarget); this.service.handleDragAndDrop(action); this.service.autoScroll = typeof this.autoScroll === 'object' ? this.autoScroll.enabled !== false : this.autoScroll; this.service.scrollableParent = this.getAutoScrollContainer(); this.service.autoScrollDirection = typeof this.autoScroll === 'object' ? this.autoScroll.direction : { horizontal: true, vertical: true }; this.attachDomHandlers(); } onPointerMove(event) { const action = getAction(event, this.dragTarget); this.service.handleDragAndDrop(action); } onTouchMove(event) { event.preventDefault(); const action = getAction(event, this.dragTarget); this.service.handleDragAndDrop(action); } onPointerUp(event) { const action = getAction(event, this.dragTarget); this.service.handleDragAndDrop(action); this.attachDomHandlers(); } onContextMenu(event) { event.preventDefault(); const action = getAction(event, this.dragTarget); this.service.handleDragAndDrop(action); this.attachDomHandlers(); } dragHandles; constructor(element, renderer, ngZone, service, viewContainer) { this.element = element; this.renderer = renderer; this.ngZone = ngZone; this.service = service; this.viewContainer = viewContainer; validatePackage(packageMetadata); } ngOnInit() { this.initializeDragTarget(); } ngAfterContentInit() { if (isPresent(this.element) || isPresent(this.dragTarget)) { this.attachDomHandlers(); if (!this.dragHandles.length) { this.renderer.setStyle(this.nativeElement, 'cursor', this.cursorStyle); } } this.service.dragTargets.push(this.dragTarget); } ngOnDestroy() { this.removeListeners(); const currentDragTargetIndex = this.service.dragTargets.indexOf(this.dragTarget); this.service.dragTargets.splice(currentDragTargetIndex, 1); } handlePress(event) { this.pressed = true; if (this.dragDelay > 0) { this.dragTimeout = window.setTimeout(() => { this.dragReady = true; this.emitZoneAwareEvent('onDragReady', event); }, this.dragDelay); } else { this.dragReady = true; } this.scrollableParent = this.dragTarget.element ? getScrollableParent(this.dragTarget.element) : null; this.prevUserSelect = this.dragTarget.element.style.userSelect; this.renderer.setStyle(this.dragTarget.element, 'user-select', 'none'); this.emitZoneAwareEvent('onPress', event); } handleDragStart(event) { if (!this.pressed) { if (this.dragTimeout) { window.clearTimeout(this.dragTimeout); this.dragTimeout = null; } return; } if (!this.dragReady) { return; } isDragStartPrevented = this.emitZoneAwareEvent('onDragStart', event).isDefaultPrevented(); if (isDragStartPrevented) { return; } if (this.hint) { this.createHint(); if (this.mode === 'auto') { this.renderer.setStyle(this.nativeElement, 'opacity', '0.7'); } this.initialPosition = { x: event.offsetX, y: event.offsetY }; } else { this.initialPosition = { x: event.clientX - this.position.x, y: event.clientY - this.position.y }; } this.dragStarted = this.threshold === 0; this.service.dragTarget = this.dragTarget; this.service.dragTargetDirective = this; this.service.dragData = this.dragData({ dragTarget: this.dragTarget.element, dragTargetId: this.dragTargetIdResult, dragTargetIndex: null }); } handleDrag(event) { if (!this.pressed || !this.dragReady || isDragStartPrevented) { return; } const elem = this.hint ? this.hintElem : this.nativeElement; this.position = this.calculatePosition(elem, event); const thresholdNotReached = Math.abs(this.position.x) < this.threshold && Math.abs(this.position.y) < this.threshold; if (!this.dragStarted && thresholdNotReached) { return; } if (!this.dragStarted && this.threshold > 0) { this.dragStarted = true; } isDragPrevented = this.emitZoneAwareEvent('onDrag', event).isDefaultPrevented(); if (isDragPrevented) { return; } if (this.mode === 'auto') { this.performDrag(); } else { this.dragStarted = true; } } handleRelease(event) { if (this.dragTimeout) { clearTimeout(this.dragTimeout); this.dragTimeout = null; } this.pressed = false; this.dragReady = false; this.prevUserSelect ? this.renderer.setStyle(this.dragTarget.element, 'user-select', this.prevUserSelect) : this.renderer.removeStyle(this.dragTarget.element, 'user-select'); this.prevUserSelect = null; this.emitZoneAwareEvent('onRelease', event); } handleDragEnd(event) { if (this.mode === 'auto') { const isDroppedOverParentTarget = isPresent(this.service.dropTarget) && !contains(this.service.dropTarget?.element, this.service.dragTarget?.element, true); const elem = this.hint ? this.hintElem : this.nativeElement; if (isDroppedOverParentTarget || this.service.dropTargets.length > 0 && isPresent(elem)) { this.renderer.removeStyle(elem, 'transform'); setElementStyles(this.renderer, elem, { transition: dragTargetTransition }); this.position = { x: 0, y: 0 }; } } if (this.hint && isPresent(this.hintElem)) { this.destroyHint(); if (this.mode === 'auto') { this.renderer.removeStyle(this.nativeElement, 'opacity'); } } this.service.dragTarget = null; this.service.dragTargetDirective = null; if (!this.dragStarted || isDragStartPrevented || isDragPrevented) { return; } this.emitZoneAwareEvent('onDragEnd', event); this.dragStarted = false; } initializeDragTarget() { this.dragTarget = { element: this.nativeElement, hint: null, onPress: this.handlePress.bind(this), onRelease: this.handleRelease.bind(this), onDragStart: this.handleDragStart.bind(this), onDrag: this.handleDrag.bind(this), onDragEnd: this.handleDragEnd.bind(this) }; } get supportPointerEvent() { return Boolean(typeof window !== 'undefined' && window.PointerEvent); } removeListeners() { if (isPresent(this.scrollableParent)) { this.scrollableParent.removeEventListener('scroll', this.onPointerMove); } const element = this.nativeElement; if (!isDocumentAvailable()) { return; } document.removeEventListener('pointermove', this.onPointerMove); document.removeEventListener('pointerup', this.onPointerUp, true); document.removeEventListener('contextmenu', this.onContextMenu); document.removeEventListener('pointercancel', this.onPointerUp); window.removeEventListener('touchmove', noop); element.removeEventListener('touchmove', this.onTouchMove); element.removeEventListener('touchend', this.onPointerUp); document.removeEventListener('mousemove', this.onPointerMove); document.removeEventListener('mouseup', this.onPointerUp); document.removeEventListener('touchcancel', this.onPointerUp); element.removeEventListener('pointerdown', this.onPointerDown); element.removeEventListener('mousedown', this.onPointerDown); element.removeEventListener('touchstart', this.onTouchStart); } attachDomHandlers() { this.ngZone.runOutsideAngular(() => { this.removeListeners(); if (!(isDocumentAvailable() && isPresent(this.element))) { return; } this.onPointerMove = this.onPointerMove.bind(this); this.onPointerUp = this.onPointerUp.bind(this); this.onTouchMove = this.onTouchMove.bind(this); this.onContextMenu = this.onContextMenu.bind(this); this.onPointerDown = this.onPointerDown.bind(this); this.onTouchStart = this.onTouchStart.bind(this); const element = this.nativeElement; if (this.supportPointerEvent) { if (isPresent(this.scrollableParent)) { if (this.scrollableParent === document.getElementsByTagName('html')[0]) { this.scrollableParent = window; } this.scrollableParent.addEventListener('scroll', this.onPointerMove, { passive: true }); } element.addEventListener('pointerdown', this.onPointerDown, { passive: true }); if (this.pressed) { document.addEventListener('pointermove', this.onPointerMove); document.addEventListener('pointerup', this.onPointerUp, true); document.addEventListener('contextmenu', this.onContextMenu); document.addEventListener('pointercancel', this.onPointerUp, { passive: true }); } } else { window.addEventListener('touchmove', noop, { capture: false, passive: false }); element.addEventListener('mousedown', this.onPointerDown, { passive: true }); element.addEventListener('touchstart', this.onTouchStart, { passive: true }); if (this.pressed) { document.addEventListener('mousemove', this.onPointerMove, { passive: true }); document.addEventListener('mouseup', this.onPointerUp, { passive: true }); element.addEventListener('touchmove', this.onTouchMove, { passive: true }); element.addEventListener('touchend', this.onPointerUp, { passive: true }); } } }); } isDragHandle(el) { return this.dragHandles.toArray().some(dh => contains(dh.element.nativeElement, el, true)); } getAutoScrollContainer() { return typeof this.autoScroll === 'object' && this.autoScroll.boundaryElementRef && this.autoScroll.boundaryElementRef.nativeElement ? this.autoScroll.boundaryElementRef.nativeElement : null; } createHint() { if (!(isDocumentAvailable() && isPresent(this.element))) { return; } if (isPresent(this.hint) && typeof this.hint === 'object') { if (isPresent(this.hint.hintTemplate)) { this.createCustomHint(); } else { this.createDefaultHint(); } } else { this.createDefaultHint(); } this.dragTarget.hint = this.hintElem; if (typeof this.hint === 'object' && isPresent(this.hint.appendTo)) { this.hint.appendTo.element.nativeElement.appendChild(this.hintElem); } else { document.body.appendChild(this.hintElem); } } createDefaultHint() { this.defaultHint = this.nativeElement.cloneNode(true); if (typeof this.hint === 'object') { if (isPresent(this.hint.hintClass)) { const hintClasses = parseCSSClassNames(this.hint.hintClass); hintClasses.forEach(className => this.renderer.addClass(this.defaultHint, className)); } } } createCustomHint() { if (isPresent(this.hint.appendTo)) { this.hintComponent = this.hint.appendTo.createComponent(HintComponent); } else { this.hintComponent = this.viewContainer.createComponent(HintComponent); } this.hintComponent.instance.template = this.hintTemplate; this.hintComponent.instance.directive = this; this.hintComponent.changeDetectorRef.detectChanges(); } destroyHint() { if (isPresent(this.hintTemplate)) { this.hintComponent.destroy(); this.hintComponent.changeDetectorRef.detectChanges(); this.hintComponent = null; } else { if (typeof this.hint === 'object' && isPresent(this.hint.appendTo)) { this.hint.appendTo.element.nativeElement.removeChild(this.defaultHint); } else { document.body.removeChild(this.defaultHint); } this.defaultHint = null; } this.dragTarget.hint = null; } emitZoneAwareEvent(event, normalizedEvent) { const eventProps = { dragTarget: this.nativeElement, dragEvent: normalizedEvent }; if (this.hint && isPresent(this.hintElem)) { eventProps.hintElement = this.hintElem; } if (this.dragTargetId && this.dragTargetId !== '') { eventProps.dragTargetId = this.dragTargetIdResult; } let eventArgs; switch (event) { case 'onDragReady': eventArgs = new DragTargetDragReadyEvent(eventProps); break; case 'onPress': eventArgs = new DragTargetPressEvent(eventProps); break; case 'onDragStart': eventArgs = new DragTargetDragStartEvent(eventProps); break; case 'onDrag': eventArgs = new DragTargetDragEvent(eventProps); break; case 'onRelease': eventArgs = new DragTargetReleaseEvent(eventProps); break; case 'onDragEnd': eventArgs = new DragTargetDragEndEvent(eventProps); break; default: break; } this.ngZone.run(() => { this[event].emit(eventArgs); }); return eventArgs; } get dragTargetIdResult() { if (this.dragTargetId && this.dragTargetId !== '') { return typeof this.dragTargetId === 'string' ? this.dragTargetId : this.dragTargetId({ dragTarget: this.dragTarget.element, dragTargetIndex: null }); } } performDrag() { const elem = this.hint ? this.hintElem : this.nativeElement; if (elem) { const styles = this.getStylesPerElement(elem); setElementStyles(this.renderer, elem, styles); } } calculatePosition(element, event) { let position = null; if (element === this.hintElem) { position = { x: event.clientX + window.scrollX, y: event.clientY + window.scrollY }; } else { position = { x: event.clientX - this.initialPosition.x + event.scrollX, y: event.clientY - this.initialPosition.y + event.scrollY }; } if (this.restrictByAxis === 'horizontal') { position.y = 0; } else if (this.restrictByAxis === 'vertical') { position.x = 0; } return position; } getStylesPerElement(element) { if (element === this.hintElem) { const hintCoordinates = { x: this.position.x - this.initialPosition.x, y: this.position.y - this.initialPosition.y }; return { top: `${hintCoordinates.y}px`, left: `${hintCoordinates.x}px`, transition: 'none', position: 'absolute', zIndex: 1999 }; } else { const transform = `translate(${this.position.x}px, ${this.position.y}px)`; return { transform: transform, transition: 'none' }; } } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DragTargetDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i0.NgZone }, { token: i1.DragStateService }, { token: i0.ViewContainerRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: DragTargetDirective, isStandalone: true, selector: "[kendoDragTarget]", inputs: { hint: "hint", threshold: "threshold", autoScroll: "autoScroll", dragTargetId: "dragTargetId", dragDelay: "dragDelay", restrictByAxis: "restrictByAxis", mode: "mode", dragData: "dragData", cursorStyle: "cursorStyle" }, outputs: { onPress: "onPress", onDragStart: "onDragStart", onDrag: "onDrag", onDragReady: "onDragReady", onRelease: "onRelease", onDragEnd: "onDragEnd" }, host: { properties: { "style.touch-action": "this.touchActionStyle" } }, queries: [{ propertyName: "dragHandles", predicate: DragHandleDirective, descendants: true }], exportAs: ["kendoDragTarget"], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DragTargetDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoDragTarget]', exportAs: 'kendoDragTarget', standalone: true }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i0.NgZone }, { type: i1.DragStateService }, { type: i0.ViewContainerRef }]; }, propDecorators: { touchActionStyle: [{ type: HostBinding, args: ['style.touch-action'] }], hint: [{ type: Input }], threshold: [{ type: Input }], autoScroll: [{ type: Input }], dragTargetId: [{ type: Input }], dragDelay: [{ type: Input }], restrictByAxis: [{ type: Input }], mode: [{ type: Input }], dragData: [{ type: Input }], cursorStyle: [{ type: Input }], onPress: [{ type: Output }], onDragStart: [{ type: Output }], onDrag: [{ type: Output }], onDragReady: [{ type: Output }], onRelease: [{ type: Output }], onDragEnd: [{ type: Output }], dragHandles: [{ type: ContentChildren, args: [DragHandleDirective, { descendants: true }] }] } });