@progress/kendo-angular-utils
Version:
Kendo UI Angular utils component
592 lines (591 loc) • 25.3 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* 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 }]
}] } });