@progress/kendo-angular-utils
Version:
Kendo UI Angular utils component
709 lines (708 loc) • 29.4 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 { 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
}] } });