Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion src/global.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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,
Expand Down
24 changes: 24 additions & 0 deletions src/models/interactions.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
215 changes: 91 additions & 124 deletions src/plugins/slick.draggablegrouping.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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_;

/**
*
Expand Down Expand Up @@ -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;
Expand All @@ -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[] = [];

/**
Expand Down Expand Up @@ -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<HTMLDivElement>('.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<HTMLDivElement>('.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<HTMLDivElement>('.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<HTMLDivElement>('.slick-placeholder');
Expand All @@ -192,147 +209,96 @@ export class SlickDraggableGrouping {
groupTogglerElm.style.display = 'none';
}
},
onEnd: (e: SortableEvent) => {
e.item.classList.remove('slick-header-column-active');
const draggablePlaceholderElm = dropzoneElm.querySelector<HTMLDivElement>('.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<HTMLDivElement>('.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) {
finalReorderedColumns.push(reorderedColumns[getColumnIndex.call(grid, reorderedId)]);
}
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) {
setTimeout(() => this.setDroppedGroups(this._options.initialGroupBy || []), 0);
}
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
};
}

/**
* 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<HTMLDivElement>('.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<HTMLDivElement>) => {
const target = event.target;
Expand All @@ -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';
Expand Down
Loading
Loading