@progress/kendo-angular-sortable
Version:
A Sortable Component for Angular
1,178 lines (1,173 loc) • 46.1 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Component, Input, Output, QueryList, ContentChildren, ViewChild, ViewChildren, TemplateRef, ElementRef, EventEmitter, HostBinding, NgZone, ChangeDetectorRef, forwardRef, Renderer2 } from '@angular/core';
import { Subject, merge } from 'rxjs';
import { isDocumentAvailable, isChanged, Keys, EventsOutsideAngularDirective } from '@progress/kendo-angular-common';
import { getAllFocusableChildren, keepFocusWithinComponent, relativeContextElement } from './util';
import { filter, take } from 'rxjs/operators';
import { LocalizationService, L10N_PREFIX } from '@progress/kendo-angular-l10n';
import { validatePackage } from '@progress/kendo-licensing';
import { packageMetadata } from './package-metadata';
import { SortableService } from './sortable.service';
import { DraggableDirective } from './draggable.directive';
import { SortableContainer } from './sortable-container';
import { ItemTemplateDirective, PlaceholderTemplateDirective } from './item-template.directive';
import { NavigateEvent } from './navigate-event';
import { DraggableEvent } from './draggable-event';
import { DragStartEvent, DragOverEvent, DragEndEvent } from './sortable-events';
import { Draggable } from '@progress/kendo-draggable';
import { NgFor, NgClass, NgStyle, NgIf, NgTemplateOutlet } from '@angular/common';
import * as i0 from "@angular/core";
import * as i1 from "@progress/kendo-angular-l10n";
import * as i2 from "./sortable.service";
const KEY_SHORTCUTS = 'Control+ArrowLeft Control+ArrowRight Meta+ArrowLeft Meta+ArrowRight';
/**
* Represents the [Kendo UI Sortable component for Angular]({% slug overview_sortable %}).
*
* @example
* ```html
* <kendo-sortable [data]="['Item 1', 'Item 2', 'Item 3']"></kendo-sortable>
* ```
*/
/**
* Represents the Kendo UI Sortable component for Angular.
*/
export class SortableComponent {
ngZone;
renderer;
changeDetector;
localization;
cdr;
/**
* Specifies the tab index of the Sortable component.
*/
tabIndex = null;
/**
* Configures how the Sortable component tracks changes in its items collection.
*/
trackBy = (_, idx) => idx;
/**
* Sets an array of any data that is used as a data source for the Sortable.
*/
set data(data) {
this._data = data;
//Cache each _data item instance locally to avoid repaint due to the ngTemplateOutletContext (generated by itemData)
//This prevents destroying the kendoDraggable instance, which otherwise leads to losing the dragEnd event
//due to non-exisitng HTML element
this.cacheData();
}
get data() {
return this._data;
}
/**
* Sets a boolean value that determines whether the Sortable items are navigable using the keyboard. [See example]({% slug keyboard_navigation_sortable %}).
* @default true
*/
navigable = true;
/**
* Enables or disables built-in animations.
* @default false
*/
animation = false;
/**
* Sets an array of integers that represent the indexes of the disabled items from the data array. [See example](slug:items_sortable#toc-disabling-items).
*/
disabledIndexes = [];
/**
* Sets a string that represents the name of the zone to which the Sortable belongs
* ([see example](slug:items_sortable#toc-transferring-of-items)). Items can be transferred
* between Sortables in the same zone.
*/
zone = undefined;
/**
* Defines the zones from which items can be transferred onto the current Sortable component
* ([see example](slug:items_sortable#toc-transferring-of-items)). If the `acceptZones` property
* of the target Sortable is set, you can transfer items between Sortables in different zones.
*/
acceptZones = undefined;
/**
* Represents the CSS styles applied to each Sortable item.
*/
itemStyle = {};
/**
* Defines the CSS styles applied to an empty item ([see example]({% slug templates_sortable %})).
*/
emptyItemStyle = undefined;
/**
* Defines the CSS styles which are applied to the currently dragged item ([see example]({% slug templates_sortable %})).
*/
activeItemStyle = undefined;
/**
* Defines the CSS styles which are applied to all disabled items.
*/
disabledItemStyle = undefined;
/**
* Defines the class which is applied to each Sortable item.
*/
itemClass = "";
/**
* Defines the class which is applied to the active Sortable item.
*/
activeItemClass = null;
/**
* Defines the class which is applied to the empty item when the Sortable has empty data.
*/
emptyItemClass = null;
/**
* Defines the class which is applied to each disabled Sortable item.
*/
disabledItemClass = null;
/**
* Sets the text message that will be displayed when the Sortable has no items.
*/
emptyText = "Empty";
/**
* @hidden
*/
defaultTemplateRef = null;
/**
* Defines the template that will be used for rendering the items.
* @hidden
*/
itemTemplateDirectiveRef = null;
/**
* Defines the template that will be used for rendering the placeholder.
* @hidden
*/
placeholderTemplateDirectiveRef = null;
itemWrappers = new QueryList();
draggables;
noDataContainer;
hint;
/**
* Fires when the dragging of an item is started.
*/
dragStart = new EventEmitter();
/**
* Fires when the dragging of an item is completed.
*/
dragEnd = new EventEmitter();
/**
* Fires while the dragging of an item is in progress.
*/
dragOver = new EventEmitter();
/**
* Fires when dragging an item outside of the component.
*/
dragLeave = new EventEmitter();
/**
* Fires while the moving an item from one position to another.
*/
dataMove = new EventEmitter();
/**
* Fires when a new item is added to the Sortable.
*/
dataAdd = new EventEmitter();
/**
* Fires when an item is removed from the Sortable.
*/
dataRemove = new EventEmitter();
/**
* Fires when navigating using the keyboard.
*/
navigate = new EventEmitter();
/**
* The index of the currently focused item.
* If no item is focused, set to `-1`.
*/
activeIndex = -1;
get touchAction() {
return "none";
}
get dir() {
return this.direction;
}
hostRole = 'list';
/**
* Flag indicating if the component is currently playing animations.
* @hidden
*/
animating = false;
/**
* The index of the currently dragged item.
*/
dragIndex = -1;
/**
* The index of the item above which the dragged item is.
*/
dragOverIndex = -1;
onDragStartSubject = new Subject();
onDragOverSubject = new Subject();
onDragLeaveSubject = new Subject();
onDragEndSubject = new Subject();
/**
* The SortableComponent's HTMLElement.
*/
wrapper;
/**
* The location of the hint indicator when dragging on mobile devices.
*/
hintLocation = null;
id;
itemTemplateRef;
placeholderTemplateRef;
_data;
_localData = [];
/**
* @hidden
*/
ariaKeyShortcuts = KEY_SHORTCUTS;
localizationChangeSubscription;
dragStartSubscription;
dragOverSubscription;
dragLeaveSubscription;
dragEndSubscription;
childrenTabindexSubscription;
focusableItems = [];
animationDuration = 300;
afterKeyPress = false;
sortableService = null;
_hideActiveItem = false;
prevActiveIndex = 0;
direction;
_animating;
draggable;
offsetParent;
setItemData(data, i) {
this._localData[i].item = data.item;
this._localData[i].index = data.index;
this._localData[i].hidden = data.hidden;
}
/**
* @hidden
*/
itemTemplate(index) {
let template = this.itemTemplateRef;
if (index === this.dragOverIndex) {
template = this.placeholderTemplateRef;
}
else if (index === this.dragIndex) {
template = this.itemTemplateRef;
}
return template;
}
constructor(ngZone, renderer, changeDetector, localization, wrapper, sortableService, cdr) {
this.ngZone = ngZone;
this.renderer = renderer;
this.changeDetector = changeDetector;
this.localization = localization;
this.cdr = cdr;
validatePackage(packageMetadata);
this.wrapper = wrapper.nativeElement;
this.direction = localization.rtl ? 'rtl' : 'ltr';
this.sortableService = sortableService;
this.subscribeEvents();
}
ngOnInit() {
if (!this.data) {
this.data = [];
}
this.id = this.sortableService.registerComponent(this);
this.dragIndex = -1;
const display = "display";
if (this.activeItemStyle && !this.activeItemStyle[display]) {
this.activeItemStyle[display] = "";
}
if (!this.itemStyle[display]) {
this.itemStyle[display] = "";
}
if (this.wrapper) {
this.draggable = new Draggable({
press: (e) => this.sortableService.onPress(e),
drag: (e) => this.sortableService.onDrag(e),
release: (e) => this.sortableService.onRelease(e)
});
this.ngZone.runOutsideAngular(() => {
this.draggable.bindTo(this.wrapper);
});
}
}
ngAfterViewInit() {
if (this.navigable) {
this.setInitialItemTabindex();
this.setFocusableChildren();
}
this.childrenTabindexSubscription = this.itemWrappers.changes.subscribe(() => {
if (this.navigable) {
this.setInitialItemTabindex();
this.setFocusableChildren();
}
});
}
ngOnChanges(changes) {
if (this.data && isChanged('disabledIndexes', changes, false)) {
this.cacheData();
}
}
ngOnDestroy() {
this.unsubscribeEvents();
this.sortableService.unregisterComponent(this.id);
if (this.draggable) {
this.draggable.destroy();
}
}
ngAfterContentInit() {
this.itemTemplateRef = this.itemTemplateDirectiveRef.first || this.defaultTemplateRef.first;
this.placeholderTemplateRef = this.placeholderTemplateDirectiveRef.first || this.defaultTemplateRef.first;
}
ngAfterViewChecked() {
if (this.navigable) {
if (this.afterKeyPress) {
const elems = this.itemWrappers.toArray();
if (elems && elems.length > 0 && this.activeIndex > -1) {
const currentItem = elems[this.activeIndex].nativeElement;
const prevItem = elems[this.prevActiveIndex].nativeElement;
this.renderer.setAttribute(prevItem, 'tabindex', '-1');
this.renderer.setAttribute(currentItem, 'tabindex', '0');
currentItem.focus();
}
}
this.afterKeyPress = false;
}
}
/**
* @hidden
*/
setFocusableChildren() {
this.itemWrappers.toArray().forEach((item) => {
const itemEl = item.nativeElement;
const focusableChildren = getAllFocusableChildren(itemEl);
if (focusableChildren.length > 0) {
this.focusableItems.push(focusableChildren);
focusableChildren.forEach(focusableChild => {
this.renderer.setAttribute(focusableChild, 'tabindex', '-1');
});
}
});
}
/**
* @hidden
*/
updateCacheIndices() {
this._localData.forEach((item, index) => {
item.index = index;
});
}
/**
* @hidden
*/
cacheData() {
this._localData = [];
this._data.forEach((item, index) => {
this._localData.push({ item: item, active: false, disabled: !this.itemEnabled(index), index: index, hidden: false });
});
}
/**
* @hidden
*/
startDrag(event) {
const startEvent = new DraggableEvent(event);
this.onDragStartSubject.next(startEvent);
const prevented = startEvent.isDefaultPrevented();
if (!prevented) {
this.offsetParent = relativeContextElement(this.wrapper);
}
return prevented;
}
/**
* @hidden
*/
setInitialItemTabindex() {
this.itemWrappers.toArray().forEach((item, index) => {
if (this.itemEnabled(index)) {
const isFirstItem = index === 0 ? 0 : -1;
const tabIndexValue = `${this.navigable ? this.tabIndex || isFirstItem : this.tabIndex}`;
const hasItemTabindex = item.nativeElement.getAttribute('tabindex');
if (!hasItemTabindex) {
this.renderer.setAttribute(item.nativeElement, 'tabindex', tabIndexValue);
}
}
});
}
/**
* @hidden
*/
drag(event) {
const dragEvent = new DraggableEvent(event);
this.onDragOverSubject.next(dragEvent);
return dragEvent.isDefaultPrevented();
}
/**
* @hidden
*/
leave(event) {
const leaveEvent = new DraggableEvent(event);
this.onDragLeaveSubject.next(leaveEvent);
return leaveEvent.isDefaultPrevented();
}
/**
* @hidden
*/
endDrag(event) {
const endEvent = new DraggableEvent(event);
this.onDragEndSubject.next(endEvent);
return endEvent.isDefaultPrevented();
}
/**
* @hidden
*/
hintVisible() {
return this.dragIndex >= 0 && this.hintLocation && this === this.sortableService.getSource();
}
/**
* @hidden
*/
currentItemStyle(index) {
if (index === -1) {
return this.emptyItemStyle ? this.emptyItemStyle : this.itemStyle;
}
if (!this.itemEnabled(index) && this.disabledItemStyle) {
return this.disabledItemStyle;
}
if (index === this.dragIndex || (this.dragIndex === -1 && index === this.activeIndex)) {
if (this.hideActiveItem) {
return { "display": "none" };
}
if (this.activeItemStyle) {
return this.activeItemStyle;
}
}
return this.itemStyle;
}
/**
* @hidden
*/
currentItemClass(index) {
if (index === -1) {
return this.emptyItemClass ? this.emptyItemClass : this.itemClass;
}
if (!this.itemEnabled(index) && this.disabledItemClass) {
return this.disabledItemClass;
}
if ((index === this.dragIndex || this.dragIndex === -1 && index === this.activeIndex) && this.activeItemClass) {
return this.activeItemClass;
}
return this.itemClass;
}
/**
* @hidden
*/
hintStyle() {
const position = {
"left": this.hintLocation.x + 10 + "px",
"position": "fixed",
"top": this.hintLocation.y + 10 + "px"
};
const style = {};
Object.assign(style, this.currentItemStyle(this.dragIndex), position);
return style;
}
/**
* @hidden
*/
itemEnabled(index) {
return this.disabledIndexes.indexOf(index) === -1;
}
/**
* @hidden
*/
acceptDragFrom(sortableComponent) {
if (this.acceptZones === undefined) {
return (this.zone === sortableComponent.zone);
}
else if (sortableComponent.zone !== undefined) {
return (this.acceptZones.indexOf(sortableComponent.zone) !== -1);
}
return false;
}
/**
* @hidden
*/
ariaDropEffect(index) {
return this.itemEnabled(index) ? "move" : "none";
}
/**
* @hidden
*/
focusHandler(index) {
if (this.navigable) {
this.activeIndex = index;
}
}
/**
* @hidden
*/
blurHandler() {
if (this.navigable && !this.afterKeyPress) {
this.prevActiveIndex = this.activeIndex;
this.activeIndex = -1;
}
}
/**
* @hidden
*/
onArrowHandler(event, keyCode) {
const leftKey = this.direction === 'rtl' ? Keys.ArrowRight : Keys.ArrowLeft;
const dir = keyCode === Keys.ArrowUp || keyCode === leftKey ? -1 : 1;
const limit = this.data.length - 1;
let targetIndex = this.activeIndex + dir;
while (!this.itemEnabled(targetIndex) && targetIndex <= limit) {
targetIndex += dir;
}
targetIndex = Math.min(Math.max(targetIndex, 0), limit);
this.prevActiveIndex = this.activeIndex;
if (!this.itemEnabled(targetIndex)) {
return;
}
const ctrl = event.ctrlKey || event.metaKey;
const navigateEvent = new NavigateEvent({ index: targetIndex, oldIndex: this.activeIndex, ctrlKey: ctrl });
this.navigate.emit(navigateEvent);
if (!navigateEvent.isDefaultPrevented()) {
this.activeIndex = targetIndex;
}
this.dragIndex = -1;
this.dragOverIndex = -1;
event.stopPropagation();
event.preventDefault();
this.afterKeyPress = true;
}
/**
* @hidden
*/
onEnterHandler(item) {
const focusableItems = this.focusableItems[this.activeIndex];
focusableItems.forEach(focusableItem => {
this.renderer.setAttribute(focusableItem, 'tabindex', '0');
});
this.renderer.setAttribute(item, 'tabindex', '-1');
focusableItems[0].focus();
}
/**
* @hidden
*/
onEscapeHandler(event) {
const focusableItems = this.focusableItems[this.prevActiveIndex];
const item = (event?.target).closest('[data-sortable-item]');
focusableItems.forEach(focusableItem => {
this.renderer.setAttribute(focusableItem, 'tabindex', '-1');
});
this.renderer.setAttribute(item, 'tabindex', '0');
item.focus();
}
/**
* @hidden
*/
keydownHandler = (event) => {
if (!this.navigable) {
return;
}
this.cdr.markForCheck();
const targetIsWrapper = this.itemWrappers.toArray().some((item) => item.nativeElement === event.target);
const index = this.activeIndex === -1 ? this.prevActiveIndex : this.activeIndex;
const item = this.itemWrappers.toArray()[index]?.nativeElement;
const isItemFocused = document.activeElement === item;
const hasFocus = this.activeIndex !== -1;
const keyCode = event.keyCode;
if (keyCode === Keys.Tab && !isItemFocused) {
keepFocusWithinComponent(event, item);
return;
}
if (keyCode === Keys.Escape && this.focusableItems.length > 0 && this.activeIndex === -1) {
this.onEscapeHandler(event);
return;
}
if (!targetIsWrapper) {
return;
}
if (this.navigable && hasFocus) {
if (keyCode >= Keys.ArrowLeft && keyCode <= Keys.ArrowDown) {
this.ngZone.run(() => this.onArrowHandler(event, keyCode));
}
else if (keyCode === Keys.Enter && isItemFocused && this.focusableItems.length > 0) {
this.onEnterHandler(item);
}
}
};
/**
* Removes the currently active item from the Data collection that the Sortable uses.
*/
removeDataItem(index) {
this.dragIndex = -1;
this.dragOverIndex = -1;
this._localData.splice(index, 1);
this.data.splice(index, 1);
this.updateCacheIndices();
}
/**
* Sets a boolean value that indicates whether the item will be hidden or not.
* @hidden
*/
hideItem(index, hidden = true) {
this._localData[index].hidden = hidden;
}
/**
* Gets or sets a boolean value that indicates whether the currently dragged item will be hidden.
*
* If the currently dragged item is hidden, returns `true`.
* If the currently dragged item is visible, returns `false`.
*/
get hideActiveItem() {
return this._hideActiveItem;
}
set hideActiveItem(value) {
this.activeIndex = -1;
this._hideActiveItem = value;
}
/**
* Clears the active item.
* An active item is the one that is currently focused when the user navigates with the keyboard.
*/
clearActiveItem() {
if (this.navigable) {
this.fixFocus();
}
else {
this.activeIndex = -1;
}
this.dragIndex = -1;
}
/**
* Returns the currently active item when the user navigates with the keyboard.
* @return - The data item which is currently active.
*/
getActiveItem() {
if (this.data && this.dragIndex >= 0 && this.dragIndex < this.data.length) {
return this.data[this.dragIndex];
}
}
/**
* Inserts a new data item at a particular index in the Sortable component.
* @param dataItem - The data item.
* @param index - The index at which the data item is inserted.
*/
addDataItem(dataItem, index) {
const originDraggable = this.sortableService.originDraggable;
if (originDraggable && originDraggable.parent === this) {
const animation = this.animation;
this.hideItem(originDraggable.index, false);
this.animation = false;
this.moveItem(originDraggable.index, index);
this.animation = animation;
}
else {
this.data.splice(index, 0, dataItem);
this._localData.splice(index, 0, { item: dataItem, active: false, disabled: !this.itemEnabled(index), index: index, hidden: false });
this.updateCacheIndices();
}
this.dragIndex = index;
this.dragOverIndex = index;
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
this.sortableService.target = this;
this.sortableService.setSource(this);
this.sortableService.activeDraggable = this.draggables.toArray()[index];
this.sortableService.lastDraggable = null;
});
}
/**
* Moves a data item from one index to another in the Sortable component.
* @param fromIndex - The data item's index.
* @param toIndex - The index which the data item should be moved to. Item currently sitting at that index is pushed back one position.
*/
moveItem(fromIndex, toIndex) {
if (toIndex === fromIndex) {
return;
}
let dragIndex = fromIndex;
const d = toIndex > dragIndex ? 1 : -1;
const originalIndexAnimate = dragIndex;
const toAnimate = [];
let prevIndex = dragIndex;
let tmp;
while (dragIndex !== toIndex) {
dragIndex += d;
if (this.itemEnabled(dragIndex) || dragIndex === toIndex) {
if (this.animation) {
toAnimate.push({ next: dragIndex, prev: prevIndex });
}
tmp = this._localData[prevIndex].index;
this._localData[prevIndex].index = this._localData[dragIndex].index;
this._localData[dragIndex].index = tmp;
tmp = this._localData[prevIndex];
this._localData[prevIndex] = this._localData[dragIndex];
this._localData[dragIndex] = tmp;
tmp = this.data[prevIndex];
this.data[prevIndex] = this.data[dragIndex];
this.data[dragIndex] = tmp;
prevIndex = dragIndex;
}
}
this.dragIndex = dragIndex;
this.dragOverIndex = dragIndex;
this.activeIndex = dragIndex;
if (this.focusableItems.length > 0) {
this.swapFocusableChildren(fromIndex, toIndex);
}
if (this.animation) {
setTimeout(() => {
toAnimate.push({ next: originalIndexAnimate, prev: dragIndex });
this.animating = true;
this.animate(toAnimate);
});
}
this.ngZone.onStable.pipe(take(1)).subscribe(() => {
this.sortableService.activeDraggable = this.draggables.toArray()[dragIndex];
this.sortableService.lastDraggable = null;
});
}
/**
* @hidden
*/
animate(draggables) {
const itemArray = this.itemWrappers.toArray();
const prevClientRect = [];
const nextClientRect = [];
clearTimeout(this._animating);
for (let i = 0; i < draggables.length; i++) {
prevClientRect.push(itemArray[draggables[i].prev].nativeElement.getBoundingClientRect());
nextClientRect.push(itemArray[draggables[i].next].nativeElement.getBoundingClientRect());
}
for (let i = 0; i < draggables.length; i++) {
const nextIndex = draggables[i].prev;
const targetRect = nextClientRect[i];
const currentRect = prevClientRect[i];
const target = itemArray[nextIndex].nativeElement;
this.applyAnimationStyle(target, 'transition', 'none');
this.applyAnimationStyle(target, 'transform', 'translate3d('
+ (targetRect.left - currentRect.left).toString() + 'px,'
+ (targetRect.top - currentRect.top).toString() + 'px,0)');
this.reflow(target);
}
for (let i = 0; i < draggables.length; i++) {
const nextIndex = draggables[i].prev;
const target = itemArray[nextIndex].nativeElement;
this.applyAnimationStyle(target, 'transition', 'all ' + this.animationDuration + 'ms');
this.applyAnimationStyle(target, 'transform', 'translate3d(0,0,0)');
clearTimeout(target.animated);
target.animated = setTimeout(() => {
this.applyAnimationStyle(target, 'transition', '');
this.applyAnimationStyle(target, 'transform', '');
target.animated = false;
}, this.animationDuration);
}
this._animating = setTimeout(() => {
this.animating = false;
}, this.animationDuration);
}
/**
* @hidden
*/
positionHintFromEvent(event) {
const offset = this.parentOffset();
this.hintLocation = event ? { x: event.clientX - offset.left, y: event.clientY - offset.top } : null;
}
/**
* @hidden
*/
parentOffset() {
const offsetParent = this.offsetParent;
if (offsetParent) {
const rect = offsetParent.getBoundingClientRect();
return {
left: rect.left - offsetParent.scrollLeft,
top: rect.top - offsetParent.scrollTop
};
}
return { left: 0, top: 0 };
}
/**
* @hidden
*/
markForCheck() {
this.changeDetector.markForCheck();
}
/**
* @hidden
*/
reflow(element) {
return element.offsetWidth;
}
/**
* @hidden
*/
swapFocusableChildren(firstItemIndex, secondItemIndex) {
[this.focusableItems[firstItemIndex], this.focusableItems[secondItemIndex]] = [this.focusableItems[secondItemIndex], this.focusableItems[firstItemIndex]];
}
/**
* @hidden
*/
applyAnimationStyle(el, prop, val) {
const style = el && el.style;
if (style) {
if (!(prop in style)) {
prop = '-webkit-' + prop;
}
style[prop] = val;
}
}
subscribeEvents() {
this.localizationChangeSubscription = this.localization
.changes
.subscribe(({ rtl }) => this.direction = rtl ? 'rtl' : 'ltr');
this.dragStartSubscription = this.onDragStartSubject
.subscribe((event) => {
if (!event.target) {
return;
}
this.sortableService.originDraggable = event.target;
this.sortableService.originIndex = event.target.index;
this.sortableService.activeDraggable = event.target;
this.sortableService.lastDraggable = event.target;
this.sortableService.target = this;
this.sortableService.setSource(this);
const dragStartEvent = new DragStartEvent({ index: event.target.index });
this.dragStart.emit(dragStartEvent);
if (dragStartEvent.isDefaultPrevented()) {
event.preventDefault();
}
else {
if (!event.target.disabled) {
if (this.sortableService.target) {
this.sortableService.target.dragOverIndex = -1;
this.sortableService.target.dragIndex = -1;
}
this.dragOverIndex = event.target.index;
this.dragIndex = event.target.index;
}
}
});
this.dragOverSubscription = this.onDragOverSubject.pipe(filter(event => event.target && event.target.el.nativeElement.style.transition.length === 0), filter(() => {
// Drag started from a disabled item
return this.sortableService.originDraggable && !this.sortableService.originDraggable.disabled;
}), filter(() => {
return this.sortableService && this.acceptDragFrom(this.sortableService.getSource());
}), filter((event) => {
return event.target !== this.sortableService.lastDraggable;
}))
.subscribe((event) => {
this.sortableService.lastDraggable = event.target;
const originDraggable = this.sortableService.originDraggable;
let targetIndex = event.target.index;
if (originDraggable.hidden && originDraggable.parent === this) {
if (originDraggable.index < event.target.index) {
targetIndex = event.target.index - 1;
}
}
this.sortableService.target = this;
const oldIndex = this.sortableService.activeDraggable ? this.sortableService.activeDraggable.index : 0;
const dragOverEvent = new DragOverEvent({ index: targetIndex, oldIndex: oldIndex });
this.dragOver.emit(dragOverEvent);
if (!dragOverEvent.isDefaultPrevented() && event.target && event.target.index >= 0) {
this.dragOverIndex = event.target.index;
this.placeHolderItemData(event.target);
}
});
this.dragEndSubscription = this.onDragEndSubject
.subscribe((event) => {
const source = this.sortableService.getSource();
if (!source) {
return;
}
const target = this.sortableService.target;
const index = event.target ? event.target.index : -1;
const oldIndex = this.sortableService.originDraggable ? this.sortableService.originIndex : -1;
this.hintLocation = null;
const dragEndEvent = new DragEndEvent({ index: index, oldIndex: oldIndex });
this.dragEnd.emit(dragEndEvent);
if (!dragEndEvent.isDefaultPrevented()) {
source.dragIndex = -1;
source.dragOverIndex = -1;
source.activeIndex = -1;
if (target && target !== source) {
target.dragIndex = -1;
target.dragOverIndex = -1;
}
setTimeout(() => {
this.sortableService.activeDraggable = null;
this.sortableService.lastDraggable = null;
this.sortableService.originDraggable = null;
this.sortableService.target = null;
this.sortableService.setSource(null);
});
}
});
this.dragLeaveSubscription = this.onDragLeaveSubject.pipe(filter((e) => {
if (!isDocumentAvailable()) {
return false;
}
return this.wrapper !== document.elementFromPoint(e.originalEvent.pageX, e.originalEvent.pageY);
}), filter((_e) => {
return !this.animating;
}), filter(_ => this.sortableService.target && this.sortableService.target.dragOverIndex > -1))
.subscribe(() => {
this.dragLeave.emit({ index: this.sortableService.originDraggable.index });
this.sortableService.lastDraggable = null;
this.dragOverIndex = -1;
this.sortableService.target = null;
});
}
unsubscribeEvents() {
if (this.localizationChangeSubscription) {
this.localizationChangeSubscription.unsubscribe();
}
if (this.childrenTabindexSubscription) {
this.childrenTabindexSubscription.unsubscribe();
}
this.dragStartSubscription.unsubscribe();
this.dragOverSubscription.unsubscribe();
this.dragEndSubscription.unsubscribe();
this.dragLeaveSubscription.unsubscribe();
}
placeHolderItemData(draggable) {
if (draggable.disabled) {
return;
}
const target = this.sortableService.target;
const source = this.sortableService.getSource();
const originalData = Object.assign({}, this._localData[draggable.index]);
const newData = source._localData[source.dragIndex];
this.setItemData(newData, draggable.index);
const endSub = source.onDragEndSubject.pipe(take(1)).subscribe(() => {
this.setItemData(originalData, draggable.index);
});
const leaveSub = target.onDragLeaveSubject.pipe(take(1)).subscribe(() => {
this.setItemData(originalData, draggable.index);
});
const overSub = merge(this.onDragOverSubject.pipe(filter(() => {
return draggable.index !== this.dragOverIndex;
})), this.onDragLeaveSubject).subscribe(() => {
this.setItemData(originalData, draggable.index);
endSub.unsubscribe();
overSub.unsubscribe();
leaveSub.unsubscribe();
});
}
fixFocus() {
if (this.itemWrappers) {
const itemArray = this.itemWrappers.toArray();
if (this.dragIndex > -1 && itemArray && itemArray.length > 0) {
itemArray[this.dragIndex].nativeElement.focus();
this.activeIndex = this.dragIndex;
}
}
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SortableComponent, deps: [{ token: i0.NgZone }, { token: i0.Renderer2 }, { token: i0.ChangeDetectorRef }, { token: i1.LocalizationService }, { token: i0.ElementRef }, { token: i2.SortableService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: SortableComponent, isStandalone: true, selector: "kendo-sortable", inputs: { tabIndex: "tabIndex", trackBy: "trackBy", data: "data", navigable: "navigable", animation: "animation", disabledIndexes: "disabledIndexes", zone: "zone", acceptZones: "acceptZones", itemStyle: "itemStyle", emptyItemStyle: "emptyItemStyle", activeItemStyle: "activeItemStyle", disabledItemStyle: "disabledItemStyle", itemClass: "itemClass", activeItemClass: "activeItemClass", emptyItemClass: "emptyItemClass", disabledItemClass: "disabledItemClass", emptyText: "emptyText", activeIndex: "activeIndex" }, outputs: { dragStart: "dragStart", dragEnd: "dragEnd", dragOver: "dragOver", dragLeave: "dragLeave", dataMove: "dataMove", dataAdd: "dataAdd", dataRemove: "dataRemove", navigate: "navigate" }, host: { properties: { "style.touch-action": "this.touchAction", "attr.dir": "this.dir", "attr.role": "this.hostRole" } }, providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.sortable'
},
{
provide: SortableContainer,
useExisting: forwardRef(() => SortableComponent)
}
], queries: [{ propertyName: "defaultTemplateRef", predicate: TemplateRef }, { propertyName: "itemTemplateDirectiveRef", predicate: ItemTemplateDirective, read: TemplateRef }, { propertyName: "placeholderTemplateDirectiveRef", predicate: PlaceholderTemplateDirective, read: TemplateRef }], viewQueries: [{ propertyName: "noDataContainer", first: true, predicate: ["noDataRef"], descendants: true }, { propertyName: "hint", first: true, predicate: ["hint"], descendants: true }, { propertyName: "itemWrappers", predicate: ["itemWrapper"], descendants: true }, { propertyName: "draggables", predicate: DraggableDirective, descendants: true }], exportAs: ["kendoSortable"], usesOnChanges: true, ngImport: i0, template: `
<div #itemWrapper *ngFor="let item of _localData; let i=index; trackBy: trackBy;"
kendoDraggable
role="listitem"
[attr.aria-grabbed]="i===dragIndex"
[attr.aria-disabled]="!itemEnabled(i)"
[attr.aria-keyshortcuts]="navigable ? ariaKeyShortcuts : ''"
[attr.aria-dropeffect]="ariaDropEffect(i)"
[attr.data-sortable-item] = "true"
[attr.data-sortable-index]="i"
[attr.data-sortable-id]="id"
[index]="i"
[hidden]="item.hidden"
[disabled]="!itemEnabled(i)"
[ngClass]="currentItemClass(i)"
[ngStyle]="currentItemStyle(i)"
(focus)="focusHandler(i)"
(blur)="blurHandler()"
[kendoEventsOutsideAngular]="{
keydown: keydownHandler
}"
>
<ng-container *ngIf="itemTemplateRef"
[ngTemplateOutlet]="itemTemplate(i)"
[ngTemplateOutletContext]="item">
</ng-container>
<ng-container *ngIf="!itemTemplateRef">{{item.item}}</ng-container>
</div>
<ng-container #noDataRef *ngIf="!_data.length || _localData.length === 1 && _localData[0].hidden">
<div
kendoDraggable
[index]="0"
[disabled]="true"
[attr.data-sortable-id]="id"
[attr.data-sortable-index]="0"
[ngStyle]="currentItemStyle(-1)"
[ngClass]="currentItemClass(-1)"
>{{emptyText}}</div>
</ng-container>
<div *ngIf="hintVisible()" [ngStyle]="hintStyle()" [ngClass]="currentItemClass(dragIndex)">
<ng-container *ngIf="itemTemplateRef"
[ngTemplateOutlet]="itemTemplateRef"
[ngTemplateOutletContext]="{item: _localData[dragIndex].item}">
</ng-container>
<ng-container *ngIf="!itemTemplateRef">{{_localData[dragIndex].item}}</ng-container>
</div>
`, isInline: true, dependencies: [{ kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: DraggableDirective, selector: "[kendoDraggable]", inputs: ["index", "disabled", "hidden"] }, { kind: "directive", type: NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SortableComponent, decorators: [{
type: Component,
args: [{
exportAs: 'kendoSortable',
providers: [
LocalizationService,
{
provide: L10N_PREFIX,
useValue: 'kendo.sortable'
},
{
provide: SortableContainer,
useExisting: forwardRef(() => SortableComponent)
}
],
selector: 'kendo-sortable',
template: `
<div #itemWrapper *ngFor="let item of _localData; let i=index; trackBy: trackBy;"
kendoDraggable
role="listitem"
[attr.aria-grabbed]="i===dragIndex"
[attr.aria-disabled]="!itemEnabled(i)"
[attr.aria-keyshortcuts]="navigable ? ariaKeyShortcuts : ''"
[attr.aria-dropeffect]="ariaDropEffect(i)"
[attr.data-sortable-item] = "true"
[attr.data-sortable-index]="i"
[attr.data-sortable-id]="id"
[index]="i"
[hidden]="item.hidden"
[disabled]="!itemEnabled(i)"
[ngClass]="currentItemClass(i)"
[ngStyle]="currentItemStyle(i)"
(focus)="focusHandler(i)"
(blur)="blurHandler()"
[kendoEventsOutsideAngular]="{
keydown: keydownHandler
}"
>
<ng-container *ngIf="itemTemplateRef"
[ngTemplateOutlet]="itemTemplate(i)"
[ngTemplateOutletContext]="item">
</ng-container>
<ng-container *ngIf="!itemTemplateRef">{{item.item}}</ng-container>
</div>
<ng-container #noDataRef *ngIf="!_data.length || _localData.length === 1 && _localData[0].hidden">
<div
kendoDraggable
[index]="0"
[disabled]="true"
[attr.data-sortable-id]="id"
[attr.data-sortable-index]="0"
[ngStyle]="currentItemStyle(-1)"
[ngClass]="currentItemClass(-1)"
>{{emptyText}}</div>
</ng-container>
<div *ngIf="hintVisible()" [ngStyle]="hintStyle()" [ngClass]="currentItemClass(dragIndex)">
<ng-container *ngIf="itemTemplateRef"
[ngTemplateOutlet]="itemTemplateRef"
[ngTemplateOutletContext]="{item: _localData[dragIndex].item}">
</ng-container>
<ng-container *ngIf="!itemTemplateRef">{{_localData[dragIndex].item}}</ng-container>
</div>
`,
standalone: true,
imports: [NgFor, DraggableDirective, NgClass, NgStyle, NgIf, NgTemplateOutlet, EventsOutsideAngularDirective]
}]
}], ctorParameters: function () { return [{ type: i0.NgZone }, { type: i0.Renderer2 }, { type: i0.ChangeDetectorRef }, { type: i1.LocalizationService }, { type: i0.ElementRef }, { type: i2.SortableService }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { tabIndex: [{
type: Input
}], trackBy: [{
type: Input
}], data: [{
type: Input
}], navigable: [{
type: Input
}], animation: [{
type: Input
}], disabledIndexes: [{
type: Input
}], zone: [{
type: Input
}], acceptZones: [{
type: Input
}], itemStyle: [{
type: Input
}], emptyItemStyle: [{
type: Input
}], activeItemStyle: [{
type: Input
}], disabledItemStyle: [{
type: Input
}], itemClass: [{
type: Input
}], activeItemClass: [{
type: Input
}], emptyItemClass: [{
type: Input
}], disabledItemClass: [{
type: Input
}], emptyText: [{
type: Input
}], defaultTemplateRef: [{
type: ContentChildren,
args: [TemplateRef, { descendants: false }]
}], itemTemplateDirectiveRef: [{
type: ContentChildren,
args: [ItemTemplateDirective, { read: TemplateRef, descendants: false }]
}], placeholderTemplateDirectiveRef: [{
type: ContentChildren,
args: [PlaceholderTemplateDirective, { read: TemplateRef, descendants: false }]
}], itemWrappers: [{
type: ViewChildren,
args: ['itemWrapper']
}], draggables: [{
type: ViewChildren,
args: [DraggableDirective]
}], noDataContainer: [{
type: ViewChild,
args: ['noDataRef']
}], hint: [{
type: ViewChild,
args: ['hint']
}], dragStart: [{
type: Output
}], dragEnd: [{
type: Output
}], dragOver: [{
type: Output
}], dragLeave: [{
type: Output
}], dataMove: [{
type: Output
}], dataAdd: [{
type: Output
}], dataRemove: [{
type: Output
}], navigate: [{
type: Output
}], activeIndex: [{
type: Input
}], touchAction: [{
type: HostBinding,
args: ['style.touch-action']
}], dir: [{
type: HostBinding,
args: ['attr.dir']
}], hostRole: [{
type: HostBinding,
args: ['attr.role']
}] } });