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%; } +