diff --git a/examples/example-spreadsheet.html b/examples/example-spreadsheet.html
index 293cf8da..ef490450 100644
--- a/examples/example-spreadsheet.html
+++ b/examples/example-spreadsheet.html
@@ -83,7 +83,7 @@
Range Selection
enableAddRow: true,
enableCellNavigation: true,
asyncEditorLoading: false,
- autoEdit: false
+ autoEdit: true
};
var columns = [
diff --git a/src/global.d.ts b/src/global.d.ts
index 4f2d5663..30ae2e2a 100644
--- a/src/global.d.ts
+++ b/src/global.d.ts
@@ -7,6 +7,7 @@ import type {
keyCode,
preClickClassName,
RowSelectionMode,
+ CellSelectionMode,
SlickEvent,
SlickEventData,
SlickEventHandler,
@@ -108,9 +109,11 @@ declare global {
},
preClickClassName: typeof preClickClassName,
Range: typeof SlickRange,
+ DragExtendHandle: typeof SlickDragExtendHandle,
Resizable: typeof Resizable,
RowMoveManager: typeof SlickRowMoveManager,
RowSelectionMode: typeof RowSelectionMode,
+ CellSelectionMode: typeof CellSelectionMode,
RowSelectionModel: typeof SlickRowSelectionModel,
State: typeof SlickState,
Utils: typeof Utils,
diff --git a/src/models/drag.interface.ts b/src/models/drag.interface.ts
index 64a8cda7..90c3e2d0 100644
--- a/src/models/drag.interface.ts
+++ b/src/models/drag.interface.ts
@@ -15,6 +15,7 @@ export interface DragPosition {
startX: number;
startY: number;
range: DragRange;
+ matchClassTag: string;
}
export interface DragRange {
@@ -50,4 +51,5 @@ export interface DragRowMove {
startX: number;
startY: number;
range: DragRange;
+ matchClassTag: string;
}
\ No newline at end of file
diff --git a/src/models/interactions.interface.ts b/src/models/interactions.interface.ts
index 4ecdc96d..53b37cbe 100644
--- a/src/models/interactions.interface.ts
+++ b/src/models/interactions.interface.ts
@@ -6,6 +6,18 @@ import type { DragPosition } from './drag.interface.js';
export interface InteractionBase {
destroy: () => void;
}
+export interface ClassDetectElement {
+ /** tag to be returned on match */
+ tag: string
+
+ /** element id to match */
+ id?: string;
+
+ /** element cssSelector to match */
+ cssSelector?: string;
+}
+
+
export interface DraggableOption {
/** container DOM element, defaults to "document" */
containerElement?: HTMLElement | Document;
@@ -30,6 +42,8 @@ export interface DraggableOption {
/** drag ended callback */
onDragEnd?: (e: DragEvent, dd: DragPosition) => boolean | void;
+
+ dragFromClassDetectArr?: Array
}
export interface MouseWheelOption {
diff --git a/src/plugins/slick.cellrangedecorator.ts b/src/plugins/slick.cellrangedecorator.ts
index 2e3ca97c..c9f8885a 100644
--- a/src/plugins/slick.cellrangedecorator.ts
+++ b/src/plugins/slick.cellrangedecorator.ts
@@ -25,6 +25,7 @@ export class SlickCellRangeDecorator implements SlickPlugin {
// protected props
protected _options: CellRangeDecoratorOption;
protected _elem?: HTMLDivElement | null;
+ protected _selectionCss: CSSStyleDeclaration;
protected _defaults = {
selectionCssClass: 'slick-range-decorator',
selectionCss: {
@@ -36,12 +37,21 @@ export class SlickCellRangeDecorator implements SlickPlugin {
constructor(protected readonly grid: SlickGrid, options?: Partial) {
this._options = Utils.extend(true, {}, this._defaults, options);
+ this._selectionCss = options?.selectionCss || {} as CSSStyleDeclaration;
}
destroy() {
this.hide();
}
+ getSelectionCss() {
+ return this._selectionCss;
+ }
+
+ setSelectionCss(cssProps: CSSStyleDeclaration) {
+ this._selectionCss = cssProps;
+ }
+
init() { }
hide() {
@@ -53,8 +63,8 @@ export class SlickCellRangeDecorator implements SlickPlugin {
if (!this._elem) {
this._elem = document.createElement('div');
this._elem.className = this._options.selectionCssClass;
- Object.keys(this._options.selectionCss as CSSStyleDeclaration).forEach((cssStyleKey) => {
- this._elem!.style[cssStyleKey as CSSStyleDeclarationWritable] = this._options.selectionCss[cssStyleKey as CSSStyleDeclarationWritable];
+ Object.keys(this._selectionCss as CSSStyleDeclaration).forEach((cssStyleKey) => {
+ this._elem!.style[cssStyleKey as CSSStyleDeclarationWritable] = this._selectionCss[cssStyleKey as CSSStyleDeclarationWritable];
});
this._elem.style.position = 'absolute';
const canvasNode = this.grid.getActiveCanvasNode();
diff --git a/src/plugins/slick.cellrangeselector.ts b/src/plugins/slick.cellrangeselector.ts
index 109a4236..680f37ed 100644
--- a/src/plugins/slick.cellrangeselector.ts
+++ b/src/plugins/slick.cellrangeselector.ts
@@ -1,4 +1,4 @@
-import { SlickEvent as SlickEvent_, type SlickEventData, SlickEventHandler as SlickEventHandler_, SlickRange as SlickRange_, Utils as Utils_ } from '../slick.core.js';
+import { CellSelectionMode as CellSelectionMode_, SlickEvent as SlickEvent_, type SlickEventData, SlickEventHandler as SlickEventHandler_, SlickRange as SlickRange_, Utils as Utils_ } from '../slick.core.js';
import { Draggable as Draggable_ } from '../slick.interactions.js';
import { SlickCellRangeDecorator as SlickCellRangeDecorator_ } from './slick.cellrangedecorator.js';
import type { CellRangeSelectorOption, DragPosition, DragRange, DragRowMove, GridOption, MouseOffsetViewport, OnScrollEventArgs, SlickPlugin } from '../models/index.js';
@@ -11,14 +11,15 @@ const SlickRange = IIFE_ONLY ? Slick.Range : SlickRange_;
const Draggable = IIFE_ONLY ? Slick.Draggable : Draggable_;
const SlickCellRangeDecorator = IIFE_ONLY ? Slick.CellRangeDecorator : SlickCellRangeDecorator_;
const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
+const CellSelectionMode = IIFE_ONLY ? Slick.CellSelectionMode : CellSelectionMode_;
export class SlickCellRangeSelector implements SlickPlugin {
// --
// public API
pluginName = 'CellRangeSelector' as const;
onBeforeCellRangeSelected = new SlickEvent<{ row: number; cell: number; }>('onBeforeCellRangeSelected');
- onCellRangeSelected = new SlickEvent<{ range: SlickRange_; }>('onCellRangeSelected');
- onCellRangeSelecting = new SlickEvent<{ range: SlickRange_; }>('onCellRangeSelecting');
+ onCellRangeSelected = new SlickEvent<{ range: SlickRange_; selectionMode?: string; }>('onCellRangeSelected');
+ onCellRangeSelecting = new SlickEvent<{ range: SlickRange_; selectionMode?: string; }>('onCellRangeSelecting');
// --
// protected props
@@ -31,6 +32,7 @@ export class SlickCellRangeSelector implements SlickPlugin {
protected _dragging = false;
protected _handler = new SlickEventHandler();
protected _options: CellRangeSelectorOption;
+ protected _selectionMode: string = CellSelectionMode.Select;
protected _defaults = {
autoScroll: true,
minIntervalToShowNextCell: 30,
@@ -96,6 +98,14 @@ export class SlickCellRangeSelector implements SlickPlugin {
return this._decorator;
}
+ getSelectionMode() {
+ return this._selectionMode;
+ }
+
+ setSelectionMode(mode: string) {
+ this._selectionMode = mode;
+ }
+
protected handleScroll(_e: SlickEventData, args: OnScrollEventArgs) {
this._scrollTop = args.scrollTop;
this._scrollLeft = args.scrollLeft;
@@ -138,12 +148,19 @@ export class SlickCellRangeSelector implements SlickPlugin {
}
}
+ console.log('CellRangeSelector.handleDragInit() _activeViewport is ' + (this._activeViewport ? 'defined' : 'undefined'));
+
// prevent the grid from cancelling drag'n'drop by default
e.stopImmediatePropagation();
e.preventDefault();
}
protected handleDragStart(e: SlickEventData, dd: DragRowMove) {
+ console.log('CellRangeSelector.handleDragStart() _activeViewport is ' + (this._activeViewport ? 'defined' : 'undefined'));
+ if (!this._activeViewport) {
+ // const x = 1;
+ }
+
const cell = this._grid.getCellFromEvent(e);
if (cell && this.onBeforeCellRangeSelected.notify(cell).getReturnValue() !== false && this._grid.canCellBeSelected(cell.row, cell.cell)) {
this._dragging = true;
@@ -167,11 +184,18 @@ export class SlickCellRangeSelector implements SlickPlugin {
startY += this._scrollTop;
}
- const start = this._grid.getCellFromPoint(startX, startY);
+ let start: { row: number | undefined, cell: number | undefined; } | null;
+ this._selectionMode = CellSelectionMode.Select;
+ if (dd.matchClassTag !== 'dragReplaceHandle') {
+ start = this._grid.getCellFromPoint(startX, startY);
+ } else {
+ start = this._grid.getActiveCell() || { row: undefined, cell: undefined };
+ this._selectionMode = CellSelectionMode.Replace;
+ }
dd.range = { start, end: {} };
this._currentlySelectedRange = dd.range;
- return this._decorator.show(new SlickRange(start.row, start.cell));
+ return this._decorator.show(new SlickRange(start.row ?? 0, start.cell ?? 0));
}
protected handleDrag(evt: SlickEventData, dd: DragRowMove) {
@@ -194,6 +218,11 @@ export class SlickCellRangeSelector implements SlickPlugin {
}
protected getMouseOffsetViewport(e: MouseEvent | TouchEvent, dd: DragRowMove): MouseOffsetViewport {
+ console.log('Drag.getMouseOffsetViewport() _activeViewport is ' + (this._activeViewport ? 'defined' : 'undefined'));
+ if (!this._activeViewport) {
+ // const x = 1;
+ }
+
const targetEvent: MouseEvent | Touch = (e as TouchEvent)?.touches?.[0] ?? e;
const viewportLeft = this._activeViewport.scrollLeft;
const viewportTop = this._activeViewport.scrollTop;
@@ -356,7 +385,7 @@ export class SlickCellRangeSelector implements SlickPlugin {
const range = new SlickRange(dd.range.start.row ?? 0, dd.range.start.cell ?? 0, end.row, end.cell);
this._decorator.show(range);
this.onCellRangeSelecting.notify({
- range
+ range,
});
}
}
@@ -381,8 +410,10 @@ export class SlickCellRangeSelector implements SlickPlugin {
dd.range.start.cell ?? 0,
dd.range.end.row,
dd.range.end.cell
- )
+ ),
+ selectionMode: this._selectionMode
});
+ console.log('handleDragEnd');
}
getCurrentRange() {
diff --git a/src/plugins/slick.cellselectionmodel.ts b/src/plugins/slick.cellselectionmodel.ts
index 4f8e53d0..2ec12350 100644
--- a/src/plugins/slick.cellselectionmodel.ts
+++ b/src/plugins/slick.cellselectionmodel.ts
@@ -102,7 +102,7 @@ export class SlickCellSelectionModel {
this._cachedPageRowCount = 0;
}
- setSelectedRanges(ranges: SlickRange_[], caller = 'SlickCellSelectionModel.setSelectedRanges') {
+ setSelectedRanges(ranges: SlickRange_[], caller = 'SlickCellSelectionModel.setSelectedRanges', selectionMode: string) {
// simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
if ((!this._ranges || this._ranges.length === 0) && (!ranges || ranges.length === 0)) { return; }
@@ -113,7 +113,7 @@ export class SlickCellSelectionModel {
if (rangeHasChanged) {
// provide extra "caller" argument through SlickEventData event to avoid breaking the previous pubsub event structure
// that only accepts an array of selected range `SlickRange[]`, the SlickEventData args will be merged and used later by `onSelectedRowsChanged`
- const eventData = new SlickEventData(new CustomEvent('click', { detail: { caller } }), this._ranges);
+ const eventData = new SlickEventData(new CustomEvent('click', { detail: { caller, selectionMode } }), this._ranges);
this.onSelectedRangesChanged.notify(this._ranges, eventData);
}
}
@@ -123,7 +123,7 @@ export class SlickCellSelectionModel {
}
refreshSelections() {
- this.setSelectedRanges(this.getSelectedRanges());
+ this.setSelectedRanges(this.getSelectedRanges(), undefined, '');
}
protected handleBeforeCellRangeSelected(e: SlickEventData_): boolean | void {
@@ -133,9 +133,9 @@ export class SlickCellSelectionModel {
}
}
- protected handleCellRangeSelected(_e: SlickEventData_, args: { range: SlickRange_; }) {
+ protected handleCellRangeSelected(_e: SlickEventData_, args: { range: SlickRange_; selectionMode: string; }) {
this._grid.setActiveCell(args.range.fromRow, args.range.fromCell, false, false, true);
- this.setSelectedRanges([args.range]);
+ this.setSelectedRanges([args.range], undefined, args.selectionMode);
}
protected handleActiveCellChange(_e: SlickEventData_, args: OnActiveCellChangedEventArgs) {
@@ -144,10 +144,10 @@ export class SlickCellSelectionModel {
const isRowDefined = Utils.isDefined(args.row);
if (this._options?.selectActiveCell && isRowDefined && isCellDefined) {
- this.setSelectedRanges([new SlickRange(args.row, args.cell)]);
+ this.setSelectedRanges([new SlickRange(args.row, args.cell)], undefined, '');
} else if (!this._options?.selectActiveCell || (!isRowDefined && !isCellDefined)) {
// clear the previous selection once the cell changes
- this.setSelectedRanges([]);
+ this.setSelectedRanges([], undefined, '');
}
}
@@ -272,7 +272,7 @@ export class SlickCellSelectionModel {
ranges.push(last);
}
- this.setSelectedRanges(ranges);
+ this.setSelectedRanges(ranges, undefined, '');
e.preventDefault();
e.stopPropagation();
diff --git a/src/plugins/slick.hybridselectionmodel.ts b/src/plugins/slick.hybridselectionmodel.ts
new file mode 100644
index 00000000..776897e1
--- /dev/null
+++ b/src/plugins/slick.hybridselectionmodel.ts
@@ -0,0 +1,518 @@
+import { keyCode as keyCode_, SlickEvent as SlickEvent_, SlickEventData as SlickEventData_, SlickRange as SlickRange_, Utils as Utils_ } from '../slick.core.js';
+import { Draggable as Draggable_ } from '../slick.interactions.js';
+import { SlickCellRangeDecorator as SlickCellRangeDecorator_ } from './slick.cellrangedecorator.js';
+import { SlickCellRangeSelector as SlickCellRangeSelector_ } from './slick.cellrangeselector.js';
+import type { Column, CustomDataView, OnActiveCellChangedEventArgs } from '../models/index.js';
+import type { SlickDataView } from '../slick.dataview.js';
+import type { SlickCrossGridRowMoveManager as SlickCrossGridRowMoveManager_ } from './slick.crossgridrowmovemanager.js';
+import type { SlickRowMoveManager as SlickRowMoveManager_ } from './slick.rowmovemanager.js';
+import type { SlickGrid } from '../slick.grid.js';
+
+// for (iife) load Slick methods from global Slick object, or use imports for (esm)
+const Draggable = IIFE_ONLY ? Slick.Draggable : Draggable_;
+const keyCode = IIFE_ONLY ? Slick.keyCode : keyCode_;
+const SlickEvent = IIFE_ONLY ? Slick.Event : SlickEvent_;
+const SlickEventData = IIFE_ONLY ? Slick.EventData : SlickEventData_;
+const SlickRange = IIFE_ONLY ? Slick.Range : SlickRange_;
+const SlickCellRangeDecorator = IIFE_ONLY ? Slick.CellRangeDecorator : SlickCellRangeDecorator_;
+const SlickCellRangeSelector = IIFE_ONLY ? Slick.CellRangeSelector : SlickCellRangeSelector_;
+const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
+
+export declare type RowSelectOverride = (data: OnActiveCellChangedEventArgs, selectionModel: SlickHybridSelectionModel, grid: SlickGrid) => boolean;
+
+export interface HybridSelectionModelOption {
+ selectActiveCell: boolean;
+ selectActiveRow: boolean;
+ cellRangeSelector?: SlickCellRangeSelector_;
+ dragToSelect: boolean;
+ autoScrollWhenDrag: boolean;
+ handleRowMoveManagerColumn: boolean; // Row Selection on RowMoveManage column
+ rowSelectColumnObjectArr: Column[]; // Row Selection on these columns
+ rowSelectOverride: RowSelectOverride | undefined; // function to toggle Row Selection Models
+}
+
+export class SlickHybridSelectionModel {
+ // hybrid selection model is CellSelectionModel except when selecting
+ // specific columns, which behave as RowSelectionModel
+
+ // --
+ // public API
+ pluginName = 'HybridSelectionModel' as const;
+ onSelectedRangesChanged = new SlickEvent('onSelectedRangesChanged');
+
+ // --
+ // protected props
+ protected _cachedPageRowCount = 0;
+ protected _dataView?: CustomDataView | SlickDataView;
+ protected _grid!: SlickGrid;
+ protected _prevSelectedRow?: number;
+ protected _prevKeyDown = '';
+ protected _ranges: SlickRange_[] = [];
+ protected _selector: SlickCellRangeSelector_;
+ protected _isRowMoveManagerHandler: any;
+ protected _activeSelectionIsRow = false;
+ protected _options?: HybridSelectionModelOption;
+ protected _defaults: HybridSelectionModelOption = {
+ selectActiveCell: true,
+ selectActiveRow: true,
+ dragToSelect: false,
+ autoScrollWhenDrag: true,
+ handleRowMoveManagerColumn: true, // Row Selection on RowMoveManage column
+ rowSelectColumnObjectArr: [], // Row Selection on these columns
+ rowSelectOverride: undefined, // function to toggle Row Selection Models
+ cellRangeSelector: undefined
+
+ };
+
+ constructor(options?: { selectActiveCell: boolean; cellRangeSelector: SlickCellRangeSelector_; }) {
+ if (options === undefined || options.cellRangeSelector === undefined) {
+ this._selector = new SlickCellRangeSelector({ selectionCss: { border: '2px solid black' } as CSSStyleDeclaration });
+ } else {
+ this._selector = options.cellRangeSelector;
+ }
+ }
+
+ // Region: Setup
+ // -----------------------------------------------------------------------------
+
+ init(grid: SlickGrid) {
+ if (Draggable === undefined) {
+ throw new Error('Slick.Draggable is undefined, make sure to import "slick.interactions.js"');
+ }
+
+ this._options = Utils.extend(true, {}, this._defaults, this._options);
+ this._grid = grid;
+ Utils.addSlickEventPubSubWhenDefined(grid.getPubSubService(), this);
+
+ if (!this._selector && this._options.dragToSelect) {
+ if (!SlickCellRangeDecorator) {
+ throw new Error('Slick.CellRangeDecorator is required when option dragToSelect set to true');
+ }
+ this._selector = new SlickCellRangeSelector({
+ selectionCss: { border: 'none' } as CSSStyleDeclaration,
+ autoScroll: this._options.autoScrollWhenDrag
+ });
+ }
+
+ if (grid.hasDataView()) {
+ this._dataView = grid.getData();
+ }
+ this._grid.onActiveCellChanged.subscribe(this.handleActiveCellChange.bind(this));
+ this._grid.onKeyDown.subscribe(this.handleKeyDown.bind(this));
+ this._grid.onClick.subscribe(this.handleClick.bind(this));
+ if (this._selector) {
+ grid.registerPlugin(this._selector);
+ this._selector.onCellRangeSelecting.subscribe(this.handleCellRangeSelected.bind(this));
+ this._selector.onCellRangeSelected.subscribe(this.handleCellRangeSelected.bind(this));
+ this._selector.onBeforeCellRangeSelected.subscribe(this.handleBeforeCellRangeSelected.bind(this));
+ }
+ }
+
+ destroy() {
+ this._grid.onActiveCellChanged.unsubscribe(this.handleActiveCellChange.bind(this));
+ this._grid.onKeyDown.unsubscribe(this.handleKeyDown.bind(this));
+ this._grid.onClick.unsubscribe(this.handleClick.bind(this));
+ this._selector.onCellRangeSelecting.unsubscribe(this.handleCellRangeSelected.bind(this));
+ this._selector.onCellRangeSelected.unsubscribe(this.handleCellRangeSelected.bind(this));
+ this._selector.onBeforeCellRangeSelected.unsubscribe(this.handleBeforeCellRangeSelected.bind(this));
+ this._grid.unregisterPlugin(this._selector);
+ this._selector?.destroy();
+ }
+
+ // Region: CellSelectionModel Members
+ // -----------------------------------------------------------------------------
+
+ protected removeInvalidRanges(ranges: SlickRange_[]) {
+ const result: SlickRange_[] = [];
+
+ for (let i = 0; i < ranges.length; i++) {
+ const r = ranges[i];
+ if (this._grid.canCellBeSelected(r.fromRow, r.fromCell) && this._grid.canCellBeSelected(r.toRow, r.toCell)) {
+ result.push(r);
+ }
+ }
+
+ return result;
+ }
+
+ protected rangesAreEqual(range1: SlickRange_[], range2: SlickRange_[]) {
+ let areDifferent = (range1.length !== range2.length);
+ if (!areDifferent) {
+ for (let i = 0; i < range1.length; i++) {
+ if (
+ range1[i].fromCell !== range2[i].fromCell
+ || range1[i].fromRow !== range2[i].fromRow
+ || range1[i].toCell !== range2[i].toCell
+ || range1[i].toRow !== range2[i].toRow
+ ) {
+ areDifferent = true;
+ break;
+ }
+ }
+ }
+ return !areDifferent;
+ }
+
+ // Region: RowSelectionModel Members
+ // -----------------------------------------------------------------------------
+
+ protected rangesToRows(ranges: SlickRange_[]): number[] {
+ const rows: number[] = [];
+ for (let i = 0; i < ranges.length; i++) {
+ for (let j = ranges[i].fromRow; j <= ranges[i].toRow; j++) {
+ rows.push(j);
+ }
+ }
+ return rows;
+ }
+
+ protected rowsToRanges(rows: number[]) {
+ const ranges: SlickRange_[] = [];
+ const lastCell = this._grid.getColumns().length - 1;
+ rows.forEach(row => ranges.push(new SlickRange(row, 0, row, lastCell)));
+ return ranges;
+ }
+
+ protected getRowsRange(from: number, to: number) {
+ let i;
+ const rows: number[] = [];
+ for (i = from; i <= to; i++) {
+ rows.push(i);
+ }
+ for (i = to; i < from; i++) {
+ rows.push(i);
+ }
+ return rows;
+ }
+
+ getSelectedRows() {
+ return this.rangesToRows(this._ranges);
+ }
+
+ setSelectedRows(rows: number[]) {
+ this.setSelectedRanges(this.rowsToRanges(rows), 'SlickRowSelectionModel.setSelectedRows', '');
+ }
+
+ // Region: Shared Members
+ // -----------------------------------------------------------------------------
+
+ /** Provide a way to force a recalculation of page row count (for example on grid resize) */
+ resetPageRowCount() {
+ this._cachedPageRowCount = 0;
+ }
+
+ setSelectedRanges(ranges: SlickRange_[], caller = 'SlickHybridSelectionModel.setSelectedRanges', selectionMode: string) {
+ // simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
+ if ((!this._ranges || this._ranges.length === 0) && (!ranges || ranges.length === 0)) { return; }
+
+ // if range has not changed, don't fire onSelectedRangesChanged
+ const rangeHasChanged = !this.rangesAreEqual(this._ranges, ranges);
+
+ if (this._activeSelectionIsRow) {
+ this._ranges = ranges;
+
+ // provide extra "caller" argument through SlickEventData event to avoid breaking the previous pubsub event structure
+ // that only accepts an array of selected range `SlickRange[]`, the SlickEventData args will be merged and used later by `onSelectedRowsChanged`
+ const eventData = new SlickEventData(new CustomEvent('click', { detail: { caller, selectionMode } }), this._ranges);
+ this.onSelectedRangesChanged.notify(this._ranges, eventData);
+ } else {
+ this._ranges = this.removeInvalidRanges(ranges);
+ if (rangeHasChanged) {
+ // provide extra "caller" argument through SlickEventData event to avoid breaking the previous pubsub event structure
+ // that only accepts an array of selected range `SlickRange[]`, the SlickEventData args will be merged and used later by `onSelectedRowsChanged`
+ const eventData = new SlickEventData(new CustomEvent('click', { detail: { caller, selectionMode } }), this._ranges);
+ this.onSelectedRangesChanged.notify(this._ranges, eventData);
+ }
+ }
+ }
+
+ currentSelectionModeIsRow() {
+ return this._activeSelectionIsRow;
+ }
+
+ getSelectedRanges() {
+ return this._ranges;
+ }
+
+ refreshSelections() {
+ if (this._activeSelectionIsRow) {
+ this.setSelectedRows(this.getSelectedRows());
+ } else {
+ this.setSelectedRanges(this.getSelectedRanges(), undefined, '');
+ }
+ }
+
+ getRowMoveManagerPlugin(): SlickRowMoveManager_ | SlickCrossGridRowMoveManager_ | undefined {
+ return this._grid.getPluginByName('RowMoveManager') || this._grid.getPluginByName('CrossGridRowMoveManager');
+ }
+
+ rowSelectionModelIsActive(data: OnActiveCellChangedEventArgs): boolean {
+ // work out required selection mode
+ if (this._options?.rowSelectOverride) {
+ return this._options?.rowSelectOverride(data, this, this._grid);
+ }
+
+ if (this._options?.handleRowMoveManagerColumn) {
+ const rowMoveManager = this.getRowMoveManagerPlugin();
+ if (rowMoveManager?.isHandlerColumn(data.cell)) { return true; }
+ }
+
+ const targetColumn = this._grid.getVisibleColumns()[data.cell];
+ return this._options?.rowSelectColumnObjectArr.includes(targetColumn) || false;
+ }
+
+ protected handleActiveCellChange(_e: SlickEventData_, args: OnActiveCellChangedEventArgs) {
+ this._prevSelectedRow = undefined;
+ const isCellDefined = Utils.isDefined(args.cell);
+ const isRowDefined = Utils.isDefined(args.row);
+ this._activeSelectionIsRow = this.rowSelectionModelIsActive(args);
+
+ if (this._activeSelectionIsRow) {
+ if (this._options?.selectActiveRow && args.row !== null) {
+ this.setSelectedRanges([new Slick.Range(args.row, 0, args.row, this._grid.getColumns().length - 1)], undefined, '');
+ }
+ } else {
+ if (this._options?.selectActiveCell && isRowDefined && isCellDefined) {
+ this.setSelectedRanges([new SlickRange(args.row, args.cell)], undefined, '');
+ } else if (!this._options?.selectActiveCell || (!isRowDefined && !isCellDefined)) {
+ // clear the previous selection once the cell changes
+ this.setSelectedRanges([], undefined, '');
+ }
+ }
+ }
+
+ protected isKeyAllowed(key: string) {
+ return ['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'PageDown', 'PageUp', 'Home', 'End'].some(k => k === key);
+ }
+
+ protected handleKeyDown(e: SlickEventData_) {
+ if (!this._activeSelectionIsRow) {
+ let ranges: SlickRange_[], last: SlickRange_;
+ const colLn = this._grid.getColumns().length;
+ const active = this._grid.getActiveCell();
+ let dataLn = 0;
+ if (this._dataView && 'getPagingInfo' in this._dataView) {
+ dataLn = this._dataView?.getPagingInfo().pageSize || this._dataView.getLength();
+ } else {
+ dataLn = this._grid.getDataLength();
+ }
+
+ if (active && (e.shiftKey || e.ctrlKey) && !e.altKey && this.isKeyAllowed(e.key as string)) {
+ ranges = this.getSelectedRanges().slice();
+ if (!ranges.length) {
+ ranges.push(new SlickRange(active.row, active.cell));
+ }
+ // keyboard can work with last range only
+ last = ranges.pop() as SlickRange_;
+
+ // can't handle selection out of active cell
+ if (!last.contains(active.row, active.cell)) {
+ last = new SlickRange(active.row, active.cell);
+ }
+
+ let dRow = last.toRow - last.fromRow;
+ let dCell = last.toCell - last.fromCell;
+
+ // walking direction
+ const dirRow = active.row === last.fromRow ? 1 : -1;
+ const dirCell = active.cell === last.fromCell ? 1 : -1;
+ const isSingleKeyMove = e.key!.startsWith('Arrow');
+ let toCell: undefined | number = undefined;
+ let toRow = 0;
+
+ if (isSingleKeyMove && !e.ctrlKey) {
+ // single cell move: (Arrow{Up/ArrowDown/ArrowLeft/ArrowRight})
+ if (e.key === 'ArrowLeft') {
+ dCell -= dirCell;
+ } else if (e.key === 'ArrowRight') {
+ dCell += dirCell;
+ } else if (e.key === 'ArrowUp') {
+ dRow -= dirRow;
+ } else if (e.key === 'ArrowDown') {
+ dRow += dirRow;
+ }
+ toRow = active.row + dirRow * dRow;
+ } else {
+ // multiple cell moves: (Home, End, Page{Up/Down})
+ if (this._cachedPageRowCount < 1) {
+ this._cachedPageRowCount = this._grid.getViewportRowCount();
+ }
+ if (this._prevSelectedRow === undefined) {
+ this._prevSelectedRow = active.row;
+ }
+
+ if (e.shiftKey && !e.ctrlKey && e.key === 'Home') {
+ toCell = 0;
+ toRow = active.row;
+ } else if (e.shiftKey && !e.ctrlKey && e.key === 'End') {
+ toCell = colLn - 1;
+ toRow = active.row;
+ } else if (e.ctrlKey && e.shiftKey && e.key === 'Home') {
+ toCell = 0;
+ toRow = 0;
+ } else if (e.ctrlKey && e.shiftKey && e.key === 'End') {
+ toCell = colLn - 1;
+ toRow = dataLn - 1;
+ } else if (e.key === 'PageUp') {
+ if (this._prevSelectedRow >= 0) {
+ toRow = this._prevSelectedRow - this._cachedPageRowCount;
+ }
+ if (toRow < 0) {
+ toRow = 0;
+ }
+ } else if (e.key === 'PageDown') {
+ if (this._prevSelectedRow <= dataLn - 1) {
+ toRow = this._prevSelectedRow + this._cachedPageRowCount;
+ }
+ if (toRow > dataLn - 1) {
+ toRow = dataLn - 1;
+ }
+ }
+ this._prevSelectedRow = toRow;
+ }
+
+ // define new selection range
+ toCell ??= active.cell + dirCell * dCell;
+ const new_last = new SlickRange(active.row, active.cell, toRow, toCell);
+ if (this.removeInvalidRanges([new_last]).length) {
+ ranges.push(new_last);
+ const viewRow = dirRow > 0 ? new_last.toRow : new_last.fromRow;
+ const viewCell = dirCell > 0 ? new_last.toCell : new_last.fromCell;
+
+ if (isSingleKeyMove) {
+ this._grid.scrollRowIntoView(viewRow);
+ this._grid.scrollCellIntoView(viewRow, viewCell);
+ } else {
+ this._grid.scrollRowIntoView(toRow);
+ this._grid.scrollCellIntoView(toRow, viewCell);
+ }
+ } else {
+ ranges.push(last);
+ }
+
+ this.setSelectedRanges(ranges, undefined, '');
+
+ e.preventDefault();
+ e.stopPropagation();
+ this._prevKeyDown = e.key as string;
+ }
+ } else {
+ const activeRow = this._grid.getActiveCell();
+ if (this._grid.getOptions().multiSelect && activeRow
+ && e.shiftKey && !e.ctrlKey && !e.altKey && !e.metaKey
+ && (e.which === keyCode.UP || e.which === keyCode.DOWN)) {
+ let selectedRows = this.getSelectedRows();
+ selectedRows.sort(function (x, y) {
+ return x - y;
+ });
+
+ if (!selectedRows.length) {
+ selectedRows = [activeRow.row];
+ }
+
+ let top = selectedRows[0];
+ let bottom = selectedRows[selectedRows.length - 1];
+ let active: number;
+
+ if (e.which === keyCode.DOWN) {
+ active = activeRow.row < bottom || top === bottom ? ++bottom : ++top;
+ } else {
+ active = activeRow.row < bottom ? --bottom : --top;
+ }
+
+ if (active >= 0 && active < this._grid.getDataLength()) {
+ this._grid.scrollRowIntoView(active);
+ const tempRanges = this.rowsToRanges(this.getRowsRange(top, bottom));
+ this.setSelectedRanges(tempRanges, undefined, '');
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+ }
+ }
+ }
+
+ protected handleClick(e: SlickEventData_): boolean | void {
+ if (!this._activeSelectionIsRow) { return; }
+
+ const cell = this._grid.getCellFromEvent(e);
+ if (!cell || !this._grid.canCellBeActive(cell.row, cell.cell)) {
+ return false;
+ }
+
+ if (!this._grid.getOptions().multiSelect || (
+ !e.ctrlKey && !e.shiftKey && !e.metaKey)) {
+ return false;
+ }
+
+ let selection = this.rangesToRows(this._ranges);
+ const idx = selection.indexOf(cell.row);
+
+ if (idx === -1 && (e.ctrlKey || e.metaKey)) {
+ selection.push(cell.row);
+ this._grid.setActiveCell(cell.row, cell.cell);
+ } else if (idx !== -1 && (e.ctrlKey || e.metaKey)) {
+ selection = selection.filter((o) => o !== cell.row);
+ this._grid.setActiveCell(cell.row, cell.cell);
+ } else if (selection.length && e.shiftKey) {
+ const last = selection.pop() as number;
+ const from = Math.min(cell.row, last);
+ const to = Math.max(cell.row, last);
+ selection = [];
+ for (let i = from; i <= to; i++) {
+ if (i !== last) {
+ selection.push(i);
+ }
+ }
+ selection.push(last);
+ this._grid.setActiveCell(cell.row, cell.cell);
+ }
+
+ const tempRanges = this.rowsToRanges(selection);
+ this.setSelectedRanges(tempRanges, undefined, '');
+ e.stopImmediatePropagation();
+
+ return true;
+ }
+
+ protected handleBeforeCellRangeSelected(e: SlickEventData_, cell: { row: number; cell: number; }): boolean | void {
+ if (this._activeSelectionIsRow) {
+ if (!this._isRowMoveManagerHandler) {
+ const rowMoveManager = this._grid.getPluginByName('RowMoveManager') || this._grid.getPluginByName('CrossGridRowMoveManager');
+ this._isRowMoveManagerHandler = rowMoveManager ? rowMoveManager.isHandlerColumn : Utils.noop;
+ }
+ if (this._grid.getEditorLock().isActive() || this._isRowMoveManagerHandler(cell.cell)) {
+ e.stopPropagation();
+ return false;
+ }
+ this._grid.setActiveCell(cell.row, cell.cell);
+ } else {
+ if (this._grid.getEditorLock().isActive()) {
+ e.stopPropagation();
+ return false;
+ }
+ }
+ }
+
+ protected handleCellRangeSelected(_e: SlickEventData_, args: { range: SlickRange_; selectionMode: string; }) {
+ if (this._activeSelectionIsRow) {
+ if (!this._grid.getOptions().multiSelect || !this._options?.selectActiveRow) {
+ return false;
+ }
+ this.setSelectedRanges([new SlickRange(args.range.fromRow, 0, args.range.toRow, this._grid.getColumns().length - 1)], undefined, args.selectionMode);
+ } else {
+ this._grid.setActiveCell(args.range.fromRow, args.range.fromCell, false, false, true);
+ this.setSelectedRanges([args.range], undefined, args.selectionMode);
+ }
+ return true;
+ }
+}
+
+// extend Slick namespace on window object when building as iife
+if (IIFE_ONLY && window.Slick) {
+ Utils.extend(true, window, {
+ Slick: {
+ HybridSelectionModel: SlickHybridSelectionModel
+ }
+ });
+}
diff --git a/src/plugins/slick.rowselectionmodel.ts b/src/plugins/slick.rowselectionmodel.ts
index 80ecaff7..db0460d6 100644
--- a/src/plugins/slick.rowselectionmodel.ts
+++ b/src/plugins/slick.rowselectionmodel.ts
@@ -1,4 +1,5 @@
import {
+ // CellSelectionMode as CellSelectionMode_,
keyCode as keyCode_,
SlickEvent as SlickEvent_,
SlickEventData as SlickEventData_,
@@ -141,7 +142,7 @@ export class SlickRowSelectionModel {
this.setSelectedRanges(this.rowsToRanges(rows), 'SlickRowSelectionModel.setSelectedRows');
}
- setSelectedRanges(ranges: SlickRange_[], caller = 'SlickRowSelectionModel.setSelectedRanges') {
+ setSelectedRanges(ranges: SlickRange_[], caller = 'SlickRowSelectionModel.setSelectedRanges', selectionMode?: string) {
// simple check for: empty selection didn't change, prevent firing onSelectedRangesChanged
if ((!this._ranges || this._ranges.length === 0) && (!ranges || ranges.length === 0)) {
return;
@@ -150,7 +151,7 @@ export class SlickRowSelectionModel {
// provide extra "caller" argument through SlickEventData event to avoid breaking the previous pubsub event structure
// that only accepts an array of selected range `SlickRange[]`, the SlickEventData args will be merged and used later by `onSelectedRowsChanged`
- const eventData = new SlickEventData(new CustomEvent('click', { detail: { caller } }), this._ranges);
+ const eventData = new SlickEventData(new CustomEvent('click', { detail: { caller, selectionMode } }), this._ranges);
this.onSelectedRangesChanged.notify(this._ranges, eventData);
}
@@ -256,11 +257,11 @@ export class SlickRowSelectionModel {
this._grid.setActiveCell(cell.row, cell.cell);
}
- protected handleCellRangeSelected(_e: SlickEventData_, args: { range: SlickRange_; }): boolean | void {
+ protected handleCellRangeSelected(_e: SlickEventData_, args: { range: SlickRange_; selectionMode: string; }): boolean | void {
if (!this._grid.getOptions().multiSelect || !this._options.selectActiveRow) {
return false;
}
- this.setSelectedRanges([new SlickRange(args.range.fromRow, 0, args.range.toRow, this._grid.getColumns().length - 1)]);
+ this.setSelectedRanges([new SlickRange(args.range.fromRow, 0, args.range.toRow, this._grid.getColumns().length - 1)], undefined, args.selectionMode);
}
}
diff --git a/src/slick.core.ts b/src/slick.core.ts
index 85ef6aa3..6bbddd16 100644
--- a/src/slick.core.ts
+++ b/src/slick.core.ts
@@ -354,6 +354,39 @@ export class SlickRange {
};
}
+/**
+ * Create a handle element for Excel style drag-replace
+ * @class DragExtendHandle
+ * @constructor
+ * @param gridUid {String} string UID of parent grid
+ */
+export class SlickDragExtendHandle {
+ id: string;
+ cssClass = 'slick-drag-replace-handle';
+
+ constructor(gridUid: string) {
+ this.id = gridUid + "_drag_replace_handle";
+ }
+
+ getHandleHtml() {
+ return '';
+ //console.log('DragReplaceEl.getStringEl');
+ }
+
+ removeEl() {
+ const dragReplaceEl = document.getElementById(this.id);
+ if (dragReplaceEl) { dragReplaceEl.remove(); }
+ //console.log('DragReplaceEl.removeEl');
+ }
+
+ createEl(activeCellNode: any) {
+ const dragReplaceEl = document.createElement("div");
+ dragReplaceEl.classList.add("slick-drag-replace-handle");
+ dragReplaceEl.setAttribute("id", this.id);
+ activeCellNode.appendChild(dragReplaceEl);
+ console.log('DragReplaceEl.createEl');
+ }
+}
/**
* A base class that all special / non-data rows (like Group and GroupTotals) derive from.
@@ -1094,6 +1127,7 @@ const SlickCore = {
EventData: SlickEventData,
EventHandler: SlickEventHandler,
Range: SlickRange,
+ DragExtendHandle: SlickDragExtendHandle,
NonDataRow: SlickNonDataItem,
Group: SlickGroup,
GroupTotals: SlickGroupTotals,
@@ -1152,6 +1186,11 @@ const SlickCore = {
LastRow: 'LS1'
},
+ 'CellSelectionMode': {
+ Select: "SEL",
+ Replace: "REP"
+ },
+
'ValueFilterMode': {
None: 'NONE',
DeDuplicate: 'DEDP',
@@ -1168,9 +1207,9 @@ const SlickCore = {
};
export const {
- EditorLock, Event, EventData, EventHandler, Group, GroupTotals, NonDataRow, Range,
+ EditorLock, Event, EventData, EventHandler, Group, GroupTotals, NonDataRow, Range, DragExtendHandle,
RegexSanitizer, GlobalEditorLock, keyCode, preClickClassName, GridAutosizeColsMode, ColAutosizeMode,
- RowSelectionMode, ValueFilterMode, WidthEvalMode
+ RowSelectionMode, CellSelectionMode,ValueFilterMode, WidthEvalMode
} = SlickCore;
// also add to global object when exist
diff --git a/src/slick.grid.ts b/src/slick.grid.ts
index 3ad1046e..3b5c6f4f 100644
--- a/src/slick.grid.ts
+++ b/src/slick.grid.ts
@@ -82,6 +82,7 @@ import {
keyCode as keyCode_,
preClickClassName as preClickClassName_,
RowSelectionMode as RowSelectionMode_,
+ CellSelectionMode as CellSelectionMode_,
type SlickEditorLock,
SlickEvent as SlickEvent_,
SlickEventData as SlickEventData_,
@@ -89,6 +90,7 @@ import {
Utils as Utils_,
ValueFilterMode as ValueFilterMode_,
WidthEvalMode as WidthEvalMode_,
+ DragExtendHandle as DragExtendHandle_
} from './slick.core.js';
import { Draggable as Draggable_, MouseWheel as MouseWheel_, Resizable as Resizable_ } from './slick.interactions.js';
@@ -103,12 +105,14 @@ const keyCode = IIFE_ONLY ? Slick.keyCode : keyCode_;
const preClickClassName = IIFE_ONLY ? Slick.preClickClassName : preClickClassName_;
const SlickRange = IIFE_ONLY ? Slick.Range : SlickRange_;
const RowSelectionMode = IIFE_ONLY ? Slick.RowSelectionMode : RowSelectionMode_;
+const CellSelectionMode = IIFE_ONLY ? Slick.CellSelectionMode : CellSelectionMode_;
const ValueFilterMode = IIFE_ONLY ? Slick.ValueFilterMode : ValueFilterMode_;
const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
const WidthEvalMode = IIFE_ONLY ? Slick.WidthEvalMode : WidthEvalMode_;
const Draggable = IIFE_ONLY ? Slick.Draggable : Draggable_;
const MouseWheel = IIFE_ONLY ? Slick.MouseWheel : MouseWheel_;
const Resizable = IIFE_ONLY ? Slick.Resizable : Resizable_;
+const DragExtendHandle = IIFE_ONLY ? Slick.DragExtendHandle : DragExtendHandle_;
/**
* @license
@@ -358,6 +362,7 @@ export class SlickGrid = Column, O e
protected initialized = false;
protected _container!: HTMLElement;
protected uid = `slickgrid_${Math.round(1000000 * Math.random())}`;
+ protected dragReplaceEl = new DragExtendHandle(this.uid);
protected _focusSink!: HTMLDivElement;
protected _focusSink2!: HTMLDivElement;
protected _groupHeaders: HTMLDivElement[] = [];
@@ -422,6 +427,8 @@ export class SlickGrid = Column, O e
protected activePosY!: number;
protected activeRow!: number;
protected activeCell!: number;
+ protected selectionBottomRow!: number;
+ protected selectionRightCell!: number;
protected activeCellNode: HTMLDivElement | null = null;
protected currentEditor: Editor | null = null;
protected serializedEditorValue: any;
@@ -443,6 +450,7 @@ export class SlickGrid = Column, O e
protected selectionModel?: SelectionModel;
protected selectedRows: number[] = [];
+ protected selectedRanges: SlickRange_[] = [];
protected plugins: SlickPlugin[] = [];
protected cellCssClasses: CssStyleHash = {};
@@ -980,7 +988,8 @@ export class SlickGrid = Column, O e
if (Draggable) {
this.slickDraggableInstance = Draggable({
containerElement: this._container,
- allowDragFrom: 'div.slick-cell',
+ allowDragFrom: 'div.slick-cell, div.' + this.dragReplaceEl.cssClass,
+ dragFromClassDetectArr: [{ tag: 'dragReplaceHandle', id: this.dragReplaceEl.id }],
// the slick cell parent must always contain `.dnd` and/or `.cell-reorder` class to be identified as draggable
allowDragFromClosest: 'div.slick-cell.dnd, div.slick-cell.cell-reorder',
preventDragFromKeys: this._options.preventDragFromKeys,
@@ -3481,7 +3490,87 @@ export class SlickGrid = Column, O e
protected handleSelectedRangesChanged(e: SlickEventData_, ranges: SlickRange_[]) {
const ne = e.getNativeEvent();
+ const selectionMode = ne?.detail?.selectionMode ?? '';
+
+ // drag and replace functionality
+ const prevSelectedRanges = this.selectedRanges.slice(0);
+ this.selectedRanges = ranges;
+
+ if (selectionMode === CellSelectionMode.Replace
+ && prevSelectedRanges && prevSelectedRanges.length === 1
+ && this.selectedRanges && this.selectedRanges.length === 1) {
+ const prevSelectedRange = prevSelectedRanges[0];
+
+ const prevSelectedRange_rowCount = prevSelectedRange.toRow - prevSelectedRange.fromRow + 1;
+ const prevSelectedRange_cellCount = prevSelectedRange.toCell - prevSelectedRange.fromCell + 1;
+
+ const selectedRange = this.selectedRanges[0];
+ const selectedRange_rowCount = selectedRange.toRow - selectedRange.fromRow + 1;
+ const selectedRange_cellCount = selectedRange.toCell - selectedRange.fromCell + 1;
+
+ // |---0----|---1----|---2----|---3----|---4----|---5----|
+ // 0 | | | | ^ | | |
+ // |--------|--------|--------|--------|--------|--------|
+ // 1 | | | | | | |
+ // |--------|--------|--------|--------|--------|--------|
+ // 2 | | | 1 | 2 | | |
+ // |--------|--------|--------|--------|--------|--------|
+ // 3 | < | | 4 | 5 x| | > |
+ // |--------|--------|--------|--------|--------|--------|
+ // 4 | | | | | | |
+ // |--------|--------|--------|--------|--------|--------|
+ // 5 | | | | v | | |
+ // |--------|--------|--------|--------|--------|--------|
+
+ // check range has expanded
+ if (selectedRange_rowCount >= prevSelectedRange_rowCount
+ && selectedRange_cellCount >= prevSelectedRange_cellCount) {
+ const copyUp = selectedRange.fromRow < prevSelectedRange.fromRow;
+ //var copyLeft = selectedRange.fromCell < prevSelectedRange.fromCell;
+
+ const copyToRange = {
+ fromRow: copyUp ? selectedRange.fromRow : prevSelectedRange.toRow + 1
+ , rowCount: selectedRange_rowCount - prevSelectedRange_rowCount
+ , fromCell: selectedRange.fromCell // copyLeft ? selectedRange.fromCell : prevSelectedRange.toCell + 1
+ , cellCount: selectedRange_cellCount // - prevSelectedRange_cellCount
+ };
+
+ let fromRowOffset = 0;
+ let fromCellOffset = 0;
+ for (let i = 0; i < copyToRange.rowCount; i++) {
+ const toRow = this.getDataItem(copyToRange.fromRow + i);
+ const fromRow = this.getDataItem(prevSelectedRange.fromRow + fromRowOffset);
+ fromCellOffset = 0;
+
+ for (let j = 0; j < copyToRange.cellCount; j++) {
+ const toColDef = this.columns[copyToRange.fromCell + j];
+ const fromColDef = this.columns[prevSelectedRange.fromCell + fromCellOffset];
+
+ if (!toColDef.hidden && !fromColDef.hidden) {
+ let val = fromRow[fromColDef.field as keyof TData];
+ if (this._options.dataItemColumnValueExtractor) {
+ val = this._options.dataItemColumnValueExtractor(fromRow, fromColDef);
+ }
+ toRow[toColDef.field as keyof TData] = val;
+ }
+
+ fromCellOffset++;
+ if (fromCellOffset >= prevSelectedRange_cellCount) {fromCellOffset = 0;}
+ }
+
+ fromRowOffset++;
+ if (fromRowOffset >= prevSelectedRange_rowCount) {fromRowOffset = 0;}
+ }
+ this.invalidate();
+ }
+ }
+
const previousSelectedRows = this.selectedRows.slice(0); // shallow copy previously selected rows for later comparison
+
+ this.selectionBottomRow = -1;
+ this.selectionRightCell = -1;
+ this.dragReplaceEl.removeEl();
+
this.selectedRows = [];
const hash: CssStyleHash = {};
for (let i = 0; i < ranges.length; i++) {
@@ -3496,11 +3585,18 @@ export class SlickGrid = Column, O e
}
}
}
+ if (this.selectionBottomRow < ranges[i].toRow) {this.selectionBottomRow = ranges[i].toRow;}
+ if (this.selectionRightCell < ranges[i].toCell) {this.selectionRightCell = ranges[i].toCell;}
}
this.setCellCssStyles(this._options.selectedCellCssClass || '', hash);
- if (this.simpleArrayEquals(previousSelectedRows, this.selectedRows)) {
+ if (this.selectionBottomRow >= 0 && this.selectionRightCell >= 0) {
+ const lowerRightCell = this.getCellNode(this.selectionBottomRow, this.selectionRightCell)
+ this.dragReplaceEl.createEl(lowerRightCell);
+ }
+
+ if (this.simpleArraysNotEqual(previousSelectedRows, this.selectedRows)) {
const caller = ne?.detail?.caller ?? 'click';
// Use Set for faster performance
const selectedRowsSet = new Set(this.getSelectedRows());
@@ -3520,7 +3616,7 @@ export class SlickGrid = Column, O e
}
// compare 2 simple arrays (integers or strings only, do not use to compare object arrays)
- simpleArrayEquals(arr1: any[], arr2: any[]) {
+ simpleArraysNotEqual(arr1: any[], arr2: any[]) {
return Array.isArray(arr1) && Array.isArray(arr2) && arr2.sort().toString() !== arr1.sort().toString();
}
@@ -4148,6 +4244,11 @@ export class SlickGrid = Column, O e
if (item) {
const cellResult = (Object.prototype.toString.call(formatterResult) !== '[object Object]' ? formatterResult : (formatterResult as FormatterResultWithHtml).html || (formatterResult as FormatterResultWithText).text);
this.applyHtmlCode(cellDiv, cellResult as string | HTMLElement);
+
+ // add drag-to-replace handle
+ if (row === this.selectionBottomRow && cell === this.selectionRightCell && this._options.showCellSelection) {
+ this.dragReplaceEl.createEl(cellDiv);
+ }
}
divRow.appendChild(cellDiv);
@@ -5730,6 +5831,8 @@ export class SlickGrid = Column, O e
}
protected handleDragInit(e: DragEvent, dd: DragPosition) {
+ console.log('SlickGrid.handleDragInit ' + dd.matchClassTag);
+
const cell = this.getCellFromEvent(e);
if (!cell || !this.cellExists(cell.row, cell.cell)) {
return false;
@@ -5746,6 +5849,8 @@ export class SlickGrid = Column, O e
}
protected handleDragStart(e: DragEvent, dd: DragPosition) {
+ console.log('SlickGrid.handleDragStart ' + dd.matchClassTag);
+
const cell = this.getCellFromEvent(e);
if (!cell || !this.cellExists(cell.row, cell.cell)) {
return false;
@@ -5760,10 +5865,17 @@ export class SlickGrid = Column, O e
}
protected handleDrag(e: DragEvent, dd: DragPosition) {
+ console.log('SlickGrid.handleDrag ' + dd.matchClassTag);
+
return this.trigger(this.onDrag, dd, e).getReturnValue();
}
protected handleDragEnd(e: DragEvent, dd: DragPosition) {
+ console.log('SlickGrid.handleDragEnd ' + dd.matchClassTag);
+
+ if (dd.matchClassTag === 'dragReplaceHandle') {
+ this.dragReplaceEl.removeEl();
+ }
this.trigger(this.onDragEnd, dd, e);
}
diff --git a/src/slick.interactions.ts b/src/slick.interactions.ts
index b08ac89b..99a90e6a 100644
--- a/src/slick.interactions.ts
+++ b/src/slick.interactions.ts
@@ -1,4 +1,4 @@
-import type { DragItem, DragPosition, DraggableOption, MouseWheelOption, ResizableOption } from './models/index.js';
+import type { DragItem, DragPosition, ClassDetectElement, DraggableOption, 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)
@@ -18,6 +18,8 @@ const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
* https://betterprogramming.pub/perfecting-drag-and-drop-in-pure-vanilla-javascript-a761184b797a
* available optional options:
* - containerElement: container DOM element, defaults to "document"
+ * - dragFromClassDetectArr: array of tags and query selectors/ids to match on dragstart, used to determine
+ * drag source element. eg: [ { tag: 'B', id: 'myElement' }, { tag: 'A', cssSelector: 'div.myClass' } ]
* - allowDragFrom: when defined, only allow dragging from an element that matches a specific query selector
* - allowDragFromClosest: when defined, only allow dragging from an element or its parent matching a specific .closest() query selector
* - onDragInit: drag initialized callback
@@ -37,6 +39,7 @@ export function Draggable(options: DraggableOption) {
let deltaX: number;
let deltaY: number;
let dragStarted: boolean;
+ let matchClassTag: string;
if (!containerElement) {
containerElement = document.body;
@@ -88,12 +91,25 @@ export function Draggable(options: DraggableOption) {
if (!options.allowDragFrom || (options.allowDragFrom && (element.matches(options.allowDragFrom)) || (options.allowDragFromClosest && element.closest(options.allowDragFromClosest)))) {
originaldd.dragHandle = element as HTMLElement;
+
+ matchClassTag = '';
+ if (options.dragFromClassDetectArr) {
+ for (let o: ClassDetectElement, i = 0; i < options.dragFromClassDetectArr.length; i++) {
+ o = options.dragFromClassDetectArr[i];
+
+ if ((o.id && element.id === o.id) || (o.cssSelector && element.matches(o.cssSelector))) {
+ matchClassTag = o.tag;
+ break;
+ }
+ }
+ }
+
const winScrollPos = Utils.windowScrollPosition();
startX = winScrollPos.left + targetEvent.clientX;
startY = winScrollPos.top + targetEvent.clientY;
deltaX = targetEvent.clientX - targetEvent.clientX;
deltaY = targetEvent.clientY - targetEvent.clientY;
- originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target });
+ originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target, matchClassTag });
const result = executeDragCallbackWhenDefined(onDragInit as (e: DragEvent, dd: DragPosition) => boolean | void, event, originaldd as DragItem);
if (result !== false) {
@@ -103,6 +119,7 @@ export function Draggable(options: DraggableOption) {
document.body.addEventListener('touchend', userReleased);
document.body.addEventListener('touchcancel', userReleased);
}
+ console.log('userPressed.matchClassTag: ' + matchClassTag);
}
}
}
@@ -115,12 +132,12 @@ export function Draggable(options: DraggableOption) {
const { target } = targetEvent;
if (!dragStarted) {
- originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target });
+ originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target, matchClassTag });
executeDragCallbackWhenDefined(onDragStart, event, originaldd as DragItem);
dragStarted = true;
}
- originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target });
+ originaldd = Object.assign(originaldd, { deltaX, deltaY, startX, startY, target, matchClassTag });
executeDragCallbackWhenDefined(onDrag, event, originaldd as DragItem);
}
}
diff --git a/src/styles/slick-alpine-theme.scss b/src/styles/slick-alpine-theme.scss
index 3fc660ea..3427af04 100644
--- a/src/styles/slick-alpine-theme.scss
+++ b/src/styles/slick-alpine-theme.scss
@@ -857,4 +857,12 @@ button.slick-btn {
background-color: var(--alpine-button-primary-hover-color, v.$alpine-button-primary-hover-color);
}
}
-}
\ No newline at end of file
+}
+.slick-drag-replace-handle {
+ height: 7px;
+ width: 7px;
+ background: gray;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+}
diff --git a/src/styles/slick-default-theme.scss b/src/styles/slick-default-theme.scss
index d3d61f60..c3ef11b3 100644
--- a/src/styles/slick-default-theme.scss
+++ b/src/styles/slick-default-theme.scss
@@ -159,6 +159,15 @@ classes should alter those!
-webkit-animation-name: slickgrid-invalid-hilite;
}
+.slick-drag-replace-handle {
+ height: 7px;
+ width: 7px;
+ background: gray;
+ position: absolute;
+ bottom: 0;
+ right: 0;
+}
+
@-moz-keyframes slickgrid-invalid-hilite {
from { box-shadow: 0 0 6px red; }
to { box-shadow: none; }
diff --git a/src/styles/slick.grid.scss b/src/styles/slick.grid.scss
index 39ccd548..0aea7db8 100644
--- a/src/styles/slick.grid.scss
+++ b/src/styles/slick.grid.scss
@@ -273,3 +273,4 @@ classes should alter those!
outline: 0;
width: 100%;
}
+