UNPKG

@progress/kendo-angular-sortable

Version:

A Sortable Component for Angular

1,205 lines (1,202 loc) 46.1 kB
/**----------------------------------------------------------------------------------------- * 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, normalizeKeys } 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 { NgClass, NgStyle, 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 = normalizeKeys(event); 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) { const isArrowKey = [Keys.ArrowUp, Keys.ArrowDown, Keys.ArrowLeft, Keys.ArrowRight].includes(keyCode); if (isArrowKey) { 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: "18.2.14", 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: "17.0.0", version: "18.2.14", 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: ` @for (item of _localData; track trackBy(i, item); let i = $index) { <div #itemWrapper 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 }" > @if (itemTemplateRef) { <ng-container [ngTemplateOutlet]="itemTemplate(i)" [ngTemplateOutletContext]="item"> </ng-container> } @if (!itemTemplateRef) { {{item.item}} } </div> } @if (!_data.length || _localData.length === 1 && _localData[0].hidden) { <ng-container #noDataRef> <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> } @if (hintVisible()) { <div [ngStyle]="hintStyle()" [ngClass]="currentItemClass(dragIndex)"> @if (itemTemplateRef) { <ng-container [ngTemplateOutlet]="itemTemplateRef" [ngTemplateOutletContext]="{item: _localData[dragIndex].item}"> </ng-container> } @if (!itemTemplateRef) { {{_localData[dragIndex].item}} } </div> } `, isInline: true, dependencies: [{ 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: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.14", 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: ` @for (item of _localData; track trackBy(i, item); let i = $index) { <div #itemWrapper 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 }" > @if (itemTemplateRef) { <ng-container [ngTemplateOutlet]="itemTemplate(i)" [ngTemplateOutletContext]="item"> </ng-container> } @if (!itemTemplateRef) { {{item.item}} } </div> } @if (!_data.length || _localData.length === 1 && _localData[0].hidden) { <ng-container #noDataRef> <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> } @if (hintVisible()) { <div [ngStyle]="hintStyle()" [ngClass]="currentItemClass(dragIndex)"> @if (itemTemplateRef) { <ng-container [ngTemplateOutlet]="itemTemplateRef" [ngTemplateOutletContext]="{item: _localData[dragIndex].item}"> </ng-container> } @if (!itemTemplateRef) { {{_localData[dragIndex].item}} } </div> } `, standalone: true, imports: [DraggableDirective, NgClass, NgStyle, NgTemplateOutlet, EventsOutsideAngularDirective] }] }], ctorParameters: () => [{ 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'] }] } });