diff --git a/src/global.d.ts b/src/global.d.ts index 24755df8..385f6a9c 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -50,7 +50,7 @@ import type { SlickState } from './plugins/slick.state.js'; import type { SlickGroupItemMetadataProvider } from './slick.groupitemmetadataprovider.js'; import type { SlickRemoteModel } from './slick.remotemodel.js'; import type { SlickRemoteModelYahoo } from './slick.remotemodel-yahoo.js'; -import type { Draggable, MouseWheel, Resizable, setupColumnReorderDrag } from './slick.interactions.js'; +import type { Draggable, MouseWheel, Resizable, setupColumnReorderDrag, setupDropzonePillDrag } from './slick.interactions.js'; import type { Aggregators } from './slick.dataview.js'; import type { Editors } from './slick.editors.js'; import type { Formatters } from './slick.formatters.js'; @@ -116,6 +116,7 @@ declare global { DragExtendHandle: typeof SlickDragExtendHandle, Resizable: typeof Resizable, setupColumnReorderDrag: typeof setupColumnReorderDrag, + setupDropzonePillDrag: typeof setupDropzonePillDrag, RowMoveManager: typeof SlickRowMoveManager, RowSelectionMode: typeof RowSelectionMode, CellSelectionMode: typeof CellSelectionMode, diff --git a/src/models/interactions.interface.ts b/src/models/interactions.interface.ts index d7490be3..5ad82465 100644 --- a/src/models/interactions.interface.ts +++ b/src/models/interactions.interface.ts @@ -100,6 +100,30 @@ export interface ColumnReorderDragOption { onDrop?: (draggedEl: HTMLElement, event: DragEvent | MouseEvent | TouchEvent, draggedColumnId?: string) => void; } +/** Dropzone pill drag (used by SlickDraggableGrouping to reorder group pills and accept column-header drops) */ +export interface DropzonePillDragOption { + /** The dropzone container element */ + dropzoneElm: HTMLElement; + + /** CSS selector for draggable pill elements inside the dropzone (default: `.slick-dropped-grouping`) */ + itemSelector?: string; + + /** CSS class added to a pill while it is being dragged via the mouse/touch fallback */ + draggingCssClass?: string; + + /** Called when a pill reorder drag-and-drop is complete */ + onPillDragEnd?: (pill: HTMLElement) => void; + + /** Called when an external drag (e.g. column header) enters the dropzone */ + onColumnDragEnter?: (e: DragEvent) => void; + + /** Called when an external drag leaves the dropzone */ + onColumnDragLeave?: (e: DragEvent) => void; + + /** Called when a column header is natively dropped onto the dropzone */ + onColumnDrop?: (columnDataId: string, e: DragEvent) => void; +} + export interface MouseWheelOption { /** optional DOM element to attach mousewheel values, if undefined we'll attach it to the "window" object */ element: HTMLElement | Document; diff --git a/src/plugins/slick.draggablegrouping.ts b/src/plugins/slick.draggablegrouping.ts index e578bc9d..24a13bb0 100644 --- a/src/plugins/slick.draggablegrouping.ts +++ b/src/plugins/slick.draggablegrouping.ts @@ -1,8 +1,6 @@ -// @ts-ignore -import type { SortableEvent, SortableInstance, SortableOptions } from 'sortablejs'; - -import type { Column, DOMMouseOrTouchEvent, DraggableGroupingOption, Grouping, GroupingGetterFunction } from '../models/index.js'; +import type { Column, DOMMouseOrTouchEvent, DraggableGroupingOption, Grouping, GroupingGetterFunction, InteractionBase } from '../models/index.js'; import { BindingEventService as BindingEventService_, SlickEvent as SlickEvent_, SlickEventHandler as SlickEventHandler_, Utils as Utils_ } from '../slick.core.js'; +import { setupColumnReorderDrag as setupColumnReorderDrag_, setupDropzonePillDrag as setupDropzonePillDrag_ } from '../slick.interactions.js'; import type { SlickDataView } from '../slick.dataview.js'; import type { SlickGrid } from '../slick.grid.js'; @@ -11,6 +9,8 @@ const BindingEventService = IIFE_ONLY ? Slick.BindingEventService : BindingEvent const SlickEvent = IIFE_ONLY ? Slick.Event : SlickEvent_; const SlickEventHandler = IIFE_ONLY ? Slick.EventHandler : SlickEventHandler_; const Utils = IIFE_ONLY ? Slick.Utils : Utils_; +const setupColumnReorderDrag = IIFE_ONLY ? Slick.setupColumnReorderDrag : setupColumnReorderDrag_; +const setupDropzonePillDrag = IIFE_ONLY ? Slick.setupDropzonePillDrag : setupDropzonePillDrag_; /** * @@ -51,7 +51,7 @@ export class SlickDraggableGrouping { protected _gridColumns: Column[] = []; protected _dataView!: SlickDataView; protected _dropzoneElm!: HTMLDivElement; - protected _droppableInstance?: SortableInstance; + protected _dropzonePillDrag?: InteractionBase; protected _dropzonePlaceholder!: HTMLDivElement; protected _groupToggler?: HTMLDivElement; protected _isInitialized = false; @@ -65,8 +65,7 @@ export class SlickDraggableGrouping { }; protected _bindingEventService = new BindingEventService(); protected _handler = new SlickEventHandler(); - protected _sortableLeftInstance?: SortableInstance; - protected _sortableRightInstance?: SortableInstance; + protected _columnReorderDrag?: InteractionBase; protected _columnsGroupBy: Column[] = []; /** @@ -155,30 +154,48 @@ export class SlickDraggableGrouping { * @param uid - grid UID * @param trigger - callback to execute when triggering a column grouping */ - getSetupColumnReorder(grid: SlickGrid, headers: any, _headerColumnWidthDiff: any, setColumns: (columns: Column[]) => void, setupColumnResize: () => void, _columns: Column[], getColumnIndex: (columnId: string) => number, _uid: string, trigger: (slickEvent: SlickEvent_, data?: any) => void) { - this.destroySortableInstances(); + getSetupColumnReorder(grid: SlickGrid, _headers: any, _headerColumnWidthDiff: any, setColumns: (columns: Column[]) => void, setupColumnResize: () => void, _columns: Column[], getColumnIndex: (columnId: string) => number, _uid: string, trigger: (slickEvent: SlickEvent_, data?: any) => void) { + this.destroyColumnReorderInstance(); const dropzoneElm = grid.getTopHeaderPanel() || grid.getPreHeaderPanel(); const groupTogglerElm = dropzoneElm.querySelector('.slick-group-toggle-all'); + const headerLeft = document.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-left`) as HTMLDivElement; + const headerRight = document.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-right`) as HTMLDivElement; - const sortableOptions = { - animation: 50, - // chosenClass: 'slick-header-column-active', - ghostClass: 'slick-sortable-placeholder', - draggable: '.slick-header-column', - dataIdAttr: 'data-id', - group: { - name: 'shared', - pull: 'clone', - put: false, - }, - revertClone: true, - // filter: function (_e, target) { - // // block column from being able to be dragged if it's already a grouped column - // // NOTE: need to disable for now since it also blocks the column reordering - // return this.columnsGroupBy.some(c => c.id === target.getAttribute('data-id')); - // }, - onStart: (e: SortableEvent) => { - e.item.classList.add('slick-header-column-active'); + // restore the dropzone UI that gets altered while a header drag is in flight (hidden pills, shown placeholder, hover classes) + const restoreDropzoneUI = () => { + const draggablePlaceholderElm = dropzoneElm.querySelector('.slick-placeholder'); + dropzoneElm.classList.remove('slick-dropzone-hover'); + draggablePlaceholderElm?.classList.remove('slick-dropzone-placeholder-hover'); + + if (this._dropzonePlaceholder) { + this._dropzonePlaceholder.style.display = 'none'; + } + if (draggablePlaceholderElm) { + draggablePlaceholderElm.parentElement?.classList.remove('slick-dropzone-placeholder-hover'); + } + + const droppedGroupingElms = dropzoneElm.querySelectorAll('.slick-dropped-grouping'); + if (droppedGroupingElms.length) { + droppedGroupingElms.forEach(droppedGroupingElm => droppedGroupingElm.style.display = 'inline-flex'); + if (draggablePlaceholderElm) { + draggablePlaceholderElm.style.display = 'none'; + } + if (groupTogglerElm) { + groupTogglerElm.style.display = 'inline-block'; + } + } + }; + + this._columnReorderDrag = setupColumnReorderDrag({ + headerLeft, + headerRight, + container: grid.getContainerNode(), + viewportScrollContainerX: grid.getViewportNode() ?? grid.getContainerNode(), + hasFrozenColumns: () => (grid.getOptions().frozenColumn ?? -1) >= 0, + draggableSelector: '.slick-header-column', + dragActiveClass: 'slick-header-column-active', + unorderableColumnCssClass: grid.getOptions().unorderableColumnCssClass, + onDragStart: () => { dropzoneElm.classList.add('slick-dropzone-hover'); dropzoneElm.classList.add('slick-dropzone-placeholder-hover'); const draggablePlaceholderElm = dropzoneElm.querySelector('.slick-placeholder'); @@ -192,50 +209,13 @@ export class SlickDraggableGrouping { groupTogglerElm.style.display = 'none'; } }, - onEnd: (e: SortableEvent) => { - e.item.classList.remove('slick-header-column-active'); - const draggablePlaceholderElm = dropzoneElm.querySelector('.slick-placeholder'); - dropzoneElm.classList.remove('slick-dropzone-hover'); - draggablePlaceholderElm?.classList.remove('slick-dropzone-placeholder-hover'); - - - if (this._dropzonePlaceholder) { - this._dropzonePlaceholder.style.display = 'none'; - } - if (draggablePlaceholderElm) { - draggablePlaceholderElm.parentElement?.classList.remove('slick-dropzone-placeholder-hover'); - } - - const droppedGroupingElms = dropzoneElm.querySelectorAll('.slick-dropped-grouping'); - if (droppedGroupingElms.length) { - droppedGroupingElms.forEach(droppedGroupingElm => droppedGroupingElm.style.display = 'inline-flex'); - if (draggablePlaceholderElm) { - draggablePlaceholderElm.style.display = 'none'; - } - if (groupTogglerElm) { - groupTogglerElm.style.display = 'inline-block'; - } - } + onDragEnd: (reorderedIds) => { + restoreDropzoneUI(); if (!grid.getEditorLock().commitCurrentEdit()) { return; } - const reorderedIds = this._sortableLeftInstance?.toArray() ?? []; - - // when frozen columns are used, headers has more than one entry and we need the ids from all of them. - // though there is only really a left and right header, this will work even if that should change. - if (headers.length > 1) { - const ids = this._sortableRightInstance?.toArray() ?? []; - - // Note: the loop below could be simplified with: - // reorderedIds.push.apply(reorderedIds,ids); - // However, the loop is more in keeping with way-backward compatibility - for (const id of ids) { - reorderedIds.push(id); - } - } - const finalReorderedColumns: Column[] = []; const reorderedColumns = grid.getColumns(); for (const reorderedId of reorderedIds) { @@ -243,13 +223,15 @@ export class SlickDraggableGrouping { } setColumns.call(grid, finalReorderedColumns); trigger.call(grid, grid.onColumnsReordered, { grid, impactedColumns: finalReorderedColumns }); - e.stopPropagation(); setupColumnResize.call(grid); - } - } as SortableOptions; - - this._sortableLeftInstance = Sortable.create(document.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-left`) as HTMLDivElement, sortableOptions); - this._sortableRightInstance = Sortable.create(document.querySelector(`.${grid.getUID()} .slick-header-columns.slick-header-columns-right`) as HTMLDivElement, sortableOptions); + }, + onDrop: (draggedEl) => { + // column header dropped onto the dropzone: group by that column (the engine already restored + // the header element to its original position in the header row) + restoreDropzoneUI(); + this.handleGroupByDrop(dropzoneElm, draggedEl as HTMLDivElement); + }, + }); // user can optionally provide initial groupBy columns if (this._options.initialGroupBy && !this._isInitialized) { @@ -257,9 +239,10 @@ export class SlickDraggableGrouping { } this._isInitialized = true; + // NOTE: breaking change - this previously returned `{ sortableLeftInstance, sortableRightInstance }` + // (SortableJS instances); it now returns the single native drag instance which only exposes `destroy()` return { - sortableLeftInstance: this._sortableLeftInstance, - sortableRightInstance: this._sortableRightInstance + columnReorderDragInstance: this._columnReorderDrag }; } @@ -267,72 +250,55 @@ export class SlickDraggableGrouping { * Destroy plugin. */ destroy() { - this.destroySortableInstances(); - if (this._droppableInstance?.el) { - this._droppableInstance?.destroy(); - } + this.destroyColumnReorderInstance(); + this._dropzonePillDrag?.destroy(); + this._dropzonePillDrag = undefined; this.onGroupChanged.unsubscribe(); this._handler.unsubscribeAll(); this._bindingEventService.unbindAll(); Utils.emptyElement(document.querySelector(`.${this._gridUid} .slick-preheader-panel,.${this._gridUid} .slick-topheader-panel`)); } - protected destroySortableInstances() { - if (this._sortableLeftInstance?.el) { - this._sortableLeftInstance?.destroy(); - } - if (this._sortableRightInstance?.el) { - this._sortableRightInstance?.destroy(); - } - } - - protected addDragOverDropzoneListeners() { - const draggablePlaceholderElm = this._dropzoneElm.querySelector('.slick-placeholder'); - - if (draggablePlaceholderElm && this._dropzoneElm) { - this._bindingEventService.bind(draggablePlaceholderElm, 'dragover', (e) => e.preventDefault()); - this._bindingEventService.bind(draggablePlaceholderElm, 'dragenter', () => this._dropzoneElm.classList.add('slick-dropzone-hover')); - this._bindingEventService.bind(draggablePlaceholderElm, 'dragleave', () => this._dropzoneElm.classList.remove('slick-dropzone-hover')); - } + protected destroyColumnReorderInstance() { + this._columnReorderDrag?.destroy(); + this._columnReorderDrag = undefined; } protected setupColumnDropbox() { const dropzoneElm = this._dropzoneElm; - this._droppableInstance = Sortable.create(dropzoneElm, { - group: 'shared', - // chosenClass: 'slick-header-column-active', - ghostClass: 'slick-droppable-sortitem-hover', - draggable: '.slick-dropped-grouping', - dragoverBubble: true, - onAdd: (evt: MouseEvent & { item: any; clone: HTMLElement; originalEvent: MouseEvent; }) => { - const el = evt.item; - const elId = el.getAttribute('id'); - if (elId?.replace(this._gridUid, '')) { - this.handleGroupByDrop(dropzoneElm, (Sortable.utils).clone(evt.item)); - } - evt.clone.style.opacity = '.5'; - el.parentNode?.removeChild(el); - }, - onUpdate: () => { - const sortArray = this._droppableInstance?.toArray() ?? []; + this._dropzonePillDrag = setupDropzonePillDrag({ + dropzoneElm, + itemSelector: '.slick-dropped-grouping', + draggingCssClass: 'slick-droppable-sortitem-hover', + onPillDragEnd: () => { + // read the new pill order from the DOM and reapply the grouping when it changed + const sortArray = Array.from(dropzoneElm.querySelectorAll('.slick-dropped-grouping')).map(elm => elm.dataset.id ?? ''); const newGroupingOrder: Column[] = []; - for (let i = 0, l = sortArray.length; i < l; i++) { - for (let a = 0, b = this._columnsGroupBy.length; a < b; a++) { - if (this._columnsGroupBy[a].id === sortArray[i]) { - newGroupingOrder.push(this._columnsGroupBy[a]); - break; - } + for (const id of sortArray) { + const columnGroupBy = this._columnsGroupBy.find(col => String(col.id) === id); + if (columnGroupBy) { + newGroupingOrder.push(columnGroupBy); } } - this._columnsGroupBy = newGroupingOrder; - this.updateGroupBy('sort-group'); + const isSameOrder = newGroupingOrder.length === this._columnsGroupBy.length && newGroupingOrder.every((col, idx) => col === this._columnsGroupBy[idx]); + if (!isSameOrder) { + this._columnsGroupBy = newGroupingOrder; + this.updateGroupBy('sort-group'); + } + }, + onColumnDragEnter: () => dropzoneElm.classList.add('slick-dropzone-hover'), + onColumnDragLeave: () => dropzoneElm.classList.remove('slick-dropzone-hover'), + onColumnDrop: (columnDataId) => { + // native HTML5 drop of a column header onto the dropzone; also handled (and deduplicated by + // handleGroupByDrop) through the column reorder drag's own onDrop for the non-native fallbacks + const headerColumnElm = this._grid.getHeaderColumn(columnDataId); + if (headerColumnElm) { + this.handleGroupByDrop(dropzoneElm, headerColumnElm as HTMLDivElement); + } }, }); - // Sortable doesn't have onOver, we need to implement it ourselves - this.addDragOverDropzoneListeners(); - if (this._groupToggler) { this._bindingEventService.bind(this._groupToggler, 'click', ((event: DOMMouseOrTouchEvent) => { const target = event.target; @@ -359,6 +325,7 @@ export class SlickDraggableGrouping { entryElm.id = `${this._gridUid}_${col.id}_entry`; entryElm.className = 'slick-dropped-grouping'; entryElm.dataset.id = `${col.id}`; + entryElm.draggable = true; // needed for the native HTML5 pill drag const groupTextElm = document.createElement('div'); groupTextElm.className = 'slick-dropped-grouping-title'; diff --git a/src/slick.interactions.ts b/src/slick.interactions.ts index 329b9b0d..0a4f69e1 100644 --- a/src/slick.interactions.ts +++ b/src/slick.interactions.ts @@ -1,4 +1,4 @@ -import type { ColumnReorderDragOption, DragItem, DragPosition, ClassDetectElement, DraggableOption, MouseWheelOption, ResizableOption } from './models/index.js'; +import type { ColumnReorderDragOption, DragItem, DragPosition, ClassDetectElement, DraggableOption, DropzonePillDragOption, MouseWheelOption, ResizableOption } from './models/index.js'; import { Utils as Utils_ } from './slick.core.js'; // for (iife) load Slick methods from global Slick object, or use imports for (esm) @@ -840,6 +840,221 @@ export function setupColumnReorderDrag(options: ColumnReorderDragOption) { return { destroy }; } +/** + * Sets up drag-and-drop for reordering group pills inside a dropzone (used by SlickDraggableGrouping), + * plus accepting column-header drops that create new group pills. + * Ported from the Slickgrid-Universal implementation of the same feature. + * + * All Firefox+Linux detection and mouse-based fallback are handled internally; + * callers do not need to know about the browser quirk. Touch screens (which never + * fire HTML5 drag events) use the same pointer-based fallback. + * + * @param {Object} options + * @returns - setupDropzonePillDrag instance which includes destroy method + * @class setupDropzonePillDrag + */ +export function setupDropzonePillDrag(options: DropzonePillDragOption) { + const { dropzoneElm } = options; + const itemSelector = options.itemSelector ?? '.slick-dropped-grouping'; + const draggingCssClass = options.draggingCssClass ?? ''; + const DRAG_THRESHOLD = 5; // pixels before we consider it a drag, not a click + + const userAgent = (typeof window !== 'undefined' ? window.navigator : navigator)?.userAgent ?? ''; + const isFfLinux = /firefox/i.test(userAgent) && /linux/i.test(userAgent); + + let draggedPill: HTMLElement | null = null; + let fallbackActive = false; + let dragStartX: number | null = null; + let dragStartY: number | null = null; + + // ── Native pill drag ────────────────────────────────────────────────────── + + const onDragStart = (e: DragEvent) => { + const pill = (e.target as HTMLElement).closest(itemSelector); + if (pill) { + draggedPill = pill; + if (e.dataTransfer) { + e.dataTransfer.effectAllowed = 'move'; + if (typeof e.dataTransfer.setData === 'function') { + e.dataTransfer.setData('text/plain', pill.dataset.id ?? ''); + } + // Explicit drag image avoids Firefox+Linux ghost rendering issues + if (typeof e.dataTransfer.setDragImage === 'function') { + const rect = pill.getBoundingClientRect(); + e.dataTransfer.setDragImage(pill, e.clientX - rect.left, e.clientY - rect.top); + } + } + } + }; + + const onDragOver = (e: DragEvent) => { + e.preventDefault(); + const target = (e.target as HTMLElement).closest(itemSelector); + if (draggedPill && target && target !== draggedPill) { + const rect = target.getBoundingClientRect(); + dropzoneElm.insertBefore(draggedPill, e.clientX < rect.left + rect.width / 2 ? target : target.nextSibling); + } + }; + + const onDragEnd = () => { + if (draggedPill) { + const currentPill = draggedPill; + if (draggingCssClass) { + currentPill.classList.remove(draggingCssClass); + } + draggedPill = null; + fallbackActive = false; + options.onPillDragEnd?.(currentPill); + } + }; + + // ── Column-header drop visual feedback & acceptance ─────────────────────── + + const onDragEnter = (e: DragEvent) => { + if (!draggedPill) { + options.onColumnDragEnter?.(e); + } + e.preventDefault(); + }; + + const onDragLeave = (e: DragEvent) => { + if (!draggedPill) { + options.onColumnDragLeave?.(e); + } + }; + + const onDrop = (e: DragEvent) => { + e.preventDefault(); + if (!draggedPill) { + const columnDataId = e.dataTransfer?.getData('text/plain'); + if (columnDataId) { + options.onColumnDrop?.(columnDataId, e); + } + } + }; + + // Firefox/Linux native drag is broken; touch screens never fire HTML5 drag events. + // Both cases share onPointerDown/Move/Up - coordinates come from getPointerPos(). + + const onPointerDown = (e: MouseEvent | TouchEvent) => { + const pill = (e.target as HTMLElement).closest(itemSelector); + if (pill) { + // Track start position for drag threshold + dragStartX = getPointerPos(e).clientX; + dragStartY = getPointerPos(e).clientY; + draggedPill = pill; + // Add visual feedback immediately for pills (no menu conflict) + if (draggingCssClass) { + pill.classList.add(draggingCssClass); + } + // Disable text selection during drag + document.body.style.userSelect = 'none'; + if ('touches' in e) { + document.addEventListener('touchmove', onPointerMove as EventListener, { passive: false }); + document.addEventListener('touchend', onPointerUp as EventListener); + document.addEventListener('touchcancel', onPointerUp as EventListener); + } else { + document.addEventListener('mousemove', onPointerMove as EventListener); + document.addEventListener('mouseup', onPointerUp as EventListener); + } + } + }; + + const onPointerMove = (e: MouseEvent | TouchEvent) => { + if (draggedPill && dragStartX !== null && dragStartY !== null) { + const { clientX, clientY } = getPointerPos(e); + + // Check if we've exceeded the drag threshold + if (!fallbackActive) { + const deltaX = Math.abs(clientX - dragStartX); + const deltaY = Math.abs(clientY - dragStartY); + if (deltaX < DRAG_THRESHOLD && deltaY < DRAG_THRESHOLD) { + // Haven't moved far enough yet - don't commit to drag + return; + } + // Threshold exceeded - now commit to drag by preventing default + e.preventDefault(); + fallbackActive = true; + } + + // Now handle the actual drag movement + if (fallbackActive) { + e.preventDefault(); + const target = (document.elementFromPoint(clientX, clientY) as HTMLElement | null)?.closest(itemSelector); + if (target && target !== draggedPill && dropzoneElm.contains(target)) { + const rect = target.getBoundingClientRect(); + const insertBefore = clientX < rect.left + rect.width / 2; + const insertTarget = insertBefore ? target : target.nextSibling; + if (draggedPill.parentElement === dropzoneElm && insertTarget !== draggedPill) { + dropzoneElm.insertBefore(draggedPill, insertTarget); + } + } + } + } + }; + + const cleanupPointerListeners = () => { + document.removeEventListener('mousemove', onPointerMove as EventListener); + document.removeEventListener('mouseup', onPointerUp as EventListener); + document.removeEventListener('touchmove', onPointerMove as EventListener); + document.removeEventListener('touchend', onPointerUp as EventListener); + document.removeEventListener('touchcancel', onPointerUp as EventListener); + }; + + const onPointerUp = () => { + cleanupPointerListeners(); + const currentPill = draggedPill; + if (currentPill && draggingCssClass) { + currentPill.classList.remove(draggingCssClass); + } + draggedPill = null; + dragStartX = null; + dragStartY = null; + fallbackActive = false; + // Re-enable text selection after drag completes + document.body.style.userSelect = ''; + if (currentPill) { + options.onPillDragEnd?.(currentPill); + } + }; + + // ── Register listeners ──────────────────────────────────────────────────── + + dropzoneElm.addEventListener('dragstart', onDragStart as EventListener); + dropzoneElm.addEventListener('dragover', onDragOver as EventListener); + dropzoneElm.addEventListener('dragend', onDragEnd); + dropzoneElm.addEventListener('dragenter', onDragEnter as EventListener); + dropzoneElm.addEventListener('dragleave', onDragLeave as EventListener); + dropzoneElm.addEventListener('drop', onDrop as EventListener); + + if (isFfLinux) { + dropzoneElm.addEventListener('mousedown', onPointerDown as EventListener); + } + // Touch support for pill reordering (all platforms) + dropzoneElm.addEventListener('touchstart', onPointerDown as EventListener, { passive: false }); + + function destroy() { + dropzoneElm.removeEventListener('dragstart', onDragStart as EventListener); + dropzoneElm.removeEventListener('dragover', onDragOver as EventListener); + dropzoneElm.removeEventListener('dragend', onDragEnd); + dropzoneElm.removeEventListener('dragenter', onDragEnter as EventListener); + dropzoneElm.removeEventListener('dragleave', onDragLeave as EventListener); + dropzoneElm.removeEventListener('drop', onDrop as EventListener); + dropzoneElm.removeEventListener('touchstart', onPointerDown as EventListener); + if (isFfLinux) { + dropzoneElm.removeEventListener('mousedown', onPointerDown as EventListener); + } + cleanupPointerListeners(); + draggedPill = null; + dragStartX = null; + dragStartY = null; + fallbackActive = false; + } + + // public API + return { destroy }; +} + // extend Slick namespace on window object when building as iife if (IIFE_ONLY && window.Slick) { Utils.extend(Slick, { @@ -847,5 +1062,6 @@ if (IIFE_ONLY && window.Slick) { MouseWheel, Resizable, setupColumnReorderDrag, + setupDropzonePillDrag, }); }