UNPKG

@progress/kendo-angular-utils

Version:

Kendo UI Angular utils component

709 lines (708 loc) 29.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 { ChangeDetectorRef, Directive, ElementRef, EventEmitter, Input, isDevMode, NgZone, Output, Renderer2, ViewContainerRef } from '@angular/core'; import { validatePackage } from '@progress/kendo-licensing'; import { packageMetadata } from '../package-metadata'; import { closestBySelector, getAction, isPresent, setElementStyles, dragTargetTransition, noop } from './util'; import { getScrollableParent } from '@progress/kendo-draggable-common'; import { DragStateService } from './drag-state.service'; import { contains, isDocumentAvailable, parseCSSClassNames } from '@progress/kendo-angular-common'; import { HintComponent } from './hint.component'; import { DragTargetDragEndEvent, DragTargetDragEvent, DragTargetDragReadyEvent, DragTargetDragStartEvent, DragTargetPressEvent, DragTargetReleaseEvent } from './events/drag-target'; import * as i0 from "@angular/core"; import * as i1 from "./drag-state.service"; let isDragStartPrevented = false; let isDragPrevented = false; /** * Represents the [Kendo UI DragTargetContainer directive for Angular]({% slug api_utils_dragtargetcontainerdirective %}). * Used to configure multiple elements as draggable. * * @example * ```ts-no-run * <ul kendoDragTargetContainer dragTargetFilter=".my-draggable"> * <li class="my-draggable">foo</li> * </ul> * ``` */ export class DragTargetContainerDirective { wrapper; ngZone; renderer; service; viewContainer; cdr; /** * Defines whether a hint will be used for dragging. By default, the hint is a copy of the current drag target. ([see example]({% slug drag_hint %})). * * @default false */ hint = false; /** * Specifies a selector for elements within a container which will be configured as draggable * ([see example]({% slug drag_target_container %})). The possible values include any * DOM `selector`. */ set dragTargetFilter(value) { this._dragTargetFilter = value; if (!this.dragDisabled) { this.initializeDragTargets(); } } get dragTargetFilter() { return this._dragTargetFilter; } /** * Specifies a selector for elements within each DragTarget which will be configured as drag handles. */ dragHandle; /** * Defines the delay in milliseconds after which the drag will begin ([see example](slug:drag_target_container#toc-events)). * * @default 0 */ dragDelay = 0; /** * The number of pixels the pointer moves in any direction before the dragging starts ([see example]({% slug minimum_distance %})). * * @default 0 */ threshold = 0; /** * Defines a unique identifier for each drag target. * It exposes the current DragTarget HTML element and its index in the collection of drag targets as arguments. */ set dragTargetId(fn) { if (isDevMode && typeof fn !== 'function') { throw new Error(`dragTargetId must be a function, but received ${JSON.stringify(fn)}.`); } this._dragTargetId = fn; } get dragTargetId() { return this._dragTargetId; } /** * Defines a callback function which returns custom data passed to the DropTarget events. * It exposes the current DragTarget HTML element, its `dragTargetId` and its index in the collection of drag targets 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; } /** * If set to true, the dragging of DragTargets within the container will be disabled. * * @default false */ set dragDisabled(value) { this._dragDisabled = value; if (value) { this.clearPreviousTargets(); this.removeListeners(); if (isPresent(this.hintElem)) { this.destroyHint(); } } else { if (isPresent(this.wrapper) || isPresent(this.currentDragTarget)) { this.subscribe(); } this.initializeDragTargets(); } } get dragDisabled() { return this._dragDisabled; } /** * Specifies whether the default dragging behavior will be performed or the developer will manually handle the drag action. * * @default 'auto' */ mode = 'auto'; /** * Specifies the cursor style of the drag targets. Accepts same values as the [CSS `cursor` property](https://developer.mozilla.org/en-US/docs/Web/CSS/cursor#values). * * @default 'move' */ cursorStyle = 'move'; /** * @hidden */ hintContext; /** * Fires when a DragTarget's `dragDelay` has passed and the user is able to drag the element. */ onDragReady = new EventEmitter(); /** * Fires when the user presses a DragTarget element. */ onPress = new EventEmitter(); /** * Fires when the dragging of a DragTarget element begins. */ onDragStart = new EventEmitter(); /** * Fires while the user drags a DragTarget element. */ onDrag = new EventEmitter(); /** * Fires when the user releases a DragTarget element after being pressed. */ onRelease = new EventEmitter(); /** * Fires when the dragging of a DragTarget ends and the element is released. */ onDragEnd = new EventEmitter(); /** * Used for notifying the DragTargetContainer that its content has changed. */ notify() { this.cdr.detectChanges(); this.initializeDragTargets(); } currentDragTarget = null; dragTimeout = null; pressed = false; dragStarted = false; hintComponent = null; defaultHint = null; currentDragTargetElement = null; scrollableParent = null; previousDragTargets = []; initialPosition = { x: 0, y: 0 }; position = { x: 0, y: 0 }; positionsMap = new Map(); _dragTargetFilter = null; _dragDisabled = false; _dragData = () => null; _dragTargetId = () => null; prevUserSelect; get allDragTargets() { return this.queryHost(this.dragTargetFilter); } get dragHandles() { return this.isHandleSelectorValid ? this.queryHost(this.dragHandle) : null; } get hintTemplate() { return isPresent(this.hint) && typeof this.hint === 'object' ? this.hint.hintTemplate : null; } constructor(wrapper, ngZone, renderer, service, viewContainer, cdr) { this.wrapper = wrapper; this.ngZone = ngZone; this.renderer = renderer; this.service = service; this.viewContainer = viewContainer; this.cdr = cdr; validatePackage(packageMetadata); } ngAfterViewInit() { const isTargetPresent = isPresent(this.wrapper) || isPresent(this.currentDragTarget); if (!this.dragDisabled && isTargetPresent) { this.subscribe(); } !this.dragDisabled && this.initializeDragTargets(); } ngOnDestroy() { this.removeListeners(); } onPointerDown(event) { const filterElement = closestBySelector(event.target, this.isHandleSelectorValid ? this.dragHandle : this.dragTargetFilter); if (this.dragTargetFilter === '' || !isPresent(filterElement)) { return; } if (isPresent(this.dragHandles) && !this.isDragHandle(event.target)) { return; } const action = getAction(event, this.currentDragTarget); this.service.handleDragAndDrop(action); this.subscribe(); } onTouchStart(event) { const filterElement = closestBySelector(event.target, this.isHandleSelectorValid ? this.dragHandle : this.dragTargetFilter); if (this.dragTargetFilter === '' || !isPresent(filterElement)) { return; } if (isPresent(this.dragHandles) && !this.isDragHandle(event.target)) { return; } event.preventDefault(); const action = getAction(event, this.currentDragTarget); this.service.handleDragAndDrop(action); this.subscribe(); } onPointerMove(event) { const action = getAction(event, this.currentDragTarget); this.service.handleDragAndDrop(action); } onTouchMove(event) { event.preventDefault(); const action = getAction(event, this.currentDragTarget); this.service.handleDragAndDrop(action); } onPointerUp(event) { const action = getAction(event, this.currentDragTarget); this.service.handleDragAndDrop(action); this.subscribe(); } onContextMenu(event) { event.preventDefault(); const action = getAction(event, this.currentDragTarget); this.service.handleDragAndDrop(action); this.subscribe(); } handlePress(event) { if (this.dragDelay > 0) { this.dragTimeout = window.setTimeout(() => { this.pressed = true; this.emitZoneAwareEvent('onDragReady', event); }, this.dragDelay); } else { this.pressed = true; } const eventTarget = event.originalEvent.target; this.currentDragTargetElement = closestBySelector(eventTarget, this.dragTargetFilter); this.currentDragTarget.element = this.currentDragTargetElement; this.service.dragIndex = this.getDragIndex(); this.scrollableParent = this.hintTemplate ? document.body : this.currentDragTargetElement ? getScrollableParent(this.currentDragTargetElement) : null; this.prevUserSelect = this.currentDragTargetElement.style.userSelect; this.renderer.setStyle(this.currentDragTargetElement, 'user-select', 'none'); this.emitZoneAwareEvent('onPress', event); } handleDragStart(event) { if (!this.pressed) { if (this.dragTimeout) { window.clearTimeout(this.dragTimeout); this.dragTimeout = null; } return; } isDragStartPrevented = this.emitZoneAwareEvent('onDragStart', event).isDefaultPrevented(); if (isDragStartPrevented) { return; } this.position = this.positionsMap.has(this.currentDragTargetElement) ? this.positionsMap.get(this.currentDragTargetElement) : { x: 0, y: 0 }; if (this.hint) { this.createHint(); if (this.mode === 'auto') { this.renderer.setStyle(this.currentDragTargetElement, 'opacity', '0.7'); } } else { this.initialPosition = { x: event.clientX - this.position.x, y: event.clientY - this.position.y }; } this.dragStarted = this.threshold === 0; this.service.dragTarget = this.currentDragTarget; const targetIdArgs = { dragTarget: this.currentDragTargetElement, dragTargetIndex: this.service.dragIndex }; this.service.dragTargetId = this.dragTargetId(targetIdArgs); const targetDataArgs = Object.assign({ dragTargetId: this.service.dragTargetId }, targetIdArgs); this.service.dragData = this.dragData(targetDataArgs); } handleDrag(event) { if (!this.pressed || isDragStartPrevented) { return; } const elem = this.hint ? this.hintElem : this.currentDragTargetElement; 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.dragStarted) { this.positionsMap.set(this.currentDragTargetElement, this.position); } if (this.dragTimeout) { clearTimeout(this.dragTimeout); this.dragTimeout = null; } this.pressed = false; this.prevUserSelect ? this.renderer.setStyle(this.currentDragTargetElement, 'user-select', this.prevUserSelect) : this.renderer.removeStyle(this.currentDragTargetElement, 'user-select'); this.prevUserSelect = null; this.emitZoneAwareEvent('onRelease', event); } handleDragEnd(event) { if (!this.dragStarted) { return; } 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.currentDragTargetElement; if (isDroppedOverParentTarget || this.service.dropTargets.length > 0 && isPresent(elem)) { this.renderer.removeStyle(elem, 'transform'); setElementStyles(this.renderer, elem, { transition: dragTargetTransition }); this.positionsMap.delete(this.currentDragTargetElement); } } if (this.hint && isPresent(this.hintElem)) { this.destroyHint(); if (this.mode === 'auto') { this.renderer.removeStyle(this.currentDragTargetElement, 'opacity'); } } this.service.dragTarget = null; this.service.dragIndex = null; this.currentDragTarget.element = null; this.emitZoneAwareEvent('onDragEnd', event); if (isDragStartPrevented || isDragPrevented) { return; } this.dragStarted = false; } get nativeElement() { return this.wrapper.nativeElement; } get hintElem() { return this.hintTemplate && isPresent(this.hintComponent) ? this.hintComponent.instance.element.nativeElement : this.defaultHint; } 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('pointercancel', this.onPointerUp); document.removeEventListener('contextmenu', this.onContextMenu); 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); } get supportPointerEvent() { return Boolean(typeof window !== 'undefined' && window.PointerEvent); } subscribe() { this.ngZone.runOutsideAngular(() => { this.removeListeners(); if (!(isDocumentAvailable() && isPresent(this.wrapper))) { 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)) { 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 }); } } }); } emitZoneAwareEvent(event, normalizedEvent) { const targetIdArgs = { dragTarget: this.currentDragTargetElement, dragTargetIndex: this.service.dragIndex }; const eventProps = { dragTarget: this.currentDragTargetElement, dragEvent: normalizedEvent, dragTargetIndex: this.service.dragIndex, dragTargetId: this.dragTargetId(targetIdArgs) }; if (this.hint && isPresent(this.hintElem)) { eventProps.hintElement = this.hintElem; } 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; } createHint() { if (!(isDocumentAvailable() && isPresent(this.wrapper))) { return; } if (isPresent(this.hint) && typeof this.hint === 'object') { if (isPresent(this.hint.hintTemplate)) { this.createCustomHint(); } else { this.createDefaultHint(); } } else { this.createDefaultHint(); } this.currentDragTarget.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.currentDragTargetElement.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.instance.targetIndex = this.service.dragIndex; const targetDataArgs = { dragTarget: this.currentDragTargetElement, dragTargetId: this.service.dragTargetId, dragTargetIndex: this.service.dragIndex }; this.hintComponent.instance.contextData = this.dragData(targetDataArgs); this.hintComponent.instance.customContext = this.hintContext; this.hintComponent.changeDetectorRef.detectChanges(); } destroyHint() { if (isPresent(this.hintTemplate)) { this.hintComponent.destroy(); this.hintComponent.changeDetectorRef.detectChanges(); this.hintComponent = null; } else { document.body.removeChild(this.defaultHint); this.defaultHint = null; } this.currentDragTarget.hint = null; } getDragIndex() { return this.allDragTargets.indexOf(this.currentDragTargetElement); } initializeDragTargets() { if (!isPresent(this.allDragTargets)) { if (this.previousDragTargets.length > 0) { this.clearPreviousTargets(); } return; } this.allDragTargets.forEach(dragTargetEl => { const isDragTargetInitialized = this.service.dragTargets.find(dt => dt.element === dragTargetEl); if (!isDragTargetInitialized) { this.service.dragTargets.push({ element: dragTargetEl, 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) }); } }); if (this.previousDragTargets.length > 0) { const dragTargetsToRemove = this.previousDragTargets.filter(dt => !this.allDragTargets.includes(dt)); dragTargetsToRemove.forEach(dragTarget => { const idx = this.service.dragTargets.findIndex(serviceDragTarget => serviceDragTarget.element === dragTarget); if (idx > -1) { this.service.dragTargets.splice(idx, 1); } }); } this.previousDragTargets = this.allDragTargets; this.currentDragTarget = { element: null, 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) }; this.setTargetStyles(); } isDragHandle(el) { return this.dragHandles.some(dh => contains(dh, el, true)); } get isHandleSelectorValid() { return isPresent(this.dragHandle) && this.dragHandle !== ''; } setTargetStyles() { if (!isDocumentAvailable()) { return; } if (isPresent(this.dragHandle) && this.dragHandle !== '') { if (isPresent(this.dragHandles) && this.dragHandles.length > 0) { this.dragHandles.forEach(handle => { this.renderer.setStyle(handle, 'cursor', this.cursorStyle); this.renderer.setStyle(handle, 'touch-action', 'none'); }); } } else { this.allDragTargets.forEach(target => { this.renderer.setStyle(target, 'cursor', this.cursorStyle); this.renderer.setStyle(target, 'touch-action', 'none'); }); } } queryHost(selector) { if (isPresent(selector) && selector !== "") { return Array.from(this.nativeElement.querySelectorAll(selector)); } } clearPreviousTargets() { this.previousDragTargets.forEach(dragTarget => { const idx = this.service.dragTargets.findIndex(serviceDragTarget => serviceDragTarget.element === dragTarget); if (idx > -1) { this.service.dragTargets.splice(idx, 1); } }); this.previousDragTargets = []; } performDrag() { const elem = this.hint ? this.hintElem : this.currentDragTargetElement; if (elem) { const styles = this.getStylesPerElement(elem); setElementStyles(this.renderer, elem, styles); } } calculatePosition(element, event) { let position = null; if (!isDocumentAvailable()) { return { x: 0, y: 0 }; } 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 }; } return position; } getStylesPerElement(element) { if (element === this.hintElem) { return { top: `${this.position.y}px`, left: `${this.position.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: DragTargetContainerDirective, deps: [{ token: i0.ElementRef }, { token: i0.NgZone }, { token: i0.Renderer2 }, { token: i1.DragStateService }, { token: i0.ViewContainerRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.12", type: DragTargetContainerDirective, isStandalone: true, selector: "[kendoDragTargetContainer]", inputs: { hint: "hint", dragTargetFilter: "dragTargetFilter", dragHandle: "dragHandle", dragDelay: "dragDelay", threshold: "threshold", dragTargetId: "dragTargetId", dragData: "dragData", dragDisabled: "dragDisabled", mode: "mode", cursorStyle: "cursorStyle", hintContext: "hintContext" }, outputs: { onDragReady: "onDragReady", onPress: "onPress", onDragStart: "onDragStart", onDrag: "onDrag", onRelease: "onRelease", onDragEnd: "onDragEnd" }, exportAs: ["kendoDragTargetContainer"], ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: DragTargetContainerDirective, decorators: [{ type: Directive, args: [{ selector: '[kendoDragTargetContainer]', exportAs: 'kendoDragTargetContainer', standalone: true }] }], ctorParameters: function () { return [{ type: i0.ElementRef }, { type: i0.NgZone }, { type: i0.Renderer2 }, { type: i1.DragStateService }, { type: i0.ViewContainerRef }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { hint: [{ type: Input }], dragTargetFilter: [{ type: Input }], dragHandle: [{ type: Input }], dragDelay: [{ type: Input }], threshold: [{ type: Input }], dragTargetId: [{ type: Input }], dragData: [{ type: Input }], dragDisabled: [{ type: Input }], mode: [{ type: Input }], cursorStyle: [{ type: Input }], hintContext: [{ type: Input }], onDragReady: [{ type: Output }], onPress: [{ type: Output }], onDragStart: [{ type: Output }], onDrag: [{ type: Output }], onRelease: [{ type: Output }], onDragEnd: [{ type: Output }] } });