diff --git a/demos/aurelia/src/examples/slickgrid/example16.ts b/demos/aurelia/src/examples/slickgrid/example16.ts index feae3c3fb..ceec9eab3 100644 --- a/demos/aurelia/src/examples/slickgrid/example16.ts +++ b/demos/aurelia/src/examples/slickgrid/example16.ts @@ -96,8 +96,18 @@ export class Example16 { cancelEditOnDrag: true, hideRowMoveShadow: false, width: 30, - onBeforeMoveRows: this.onBeforeMoveRow.bind(this), - onMoveRows: this.onMoveRows.bind(this), + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, // you can change the move icon position of any extension (RowMove, RowDetail or RowSelector icon) // note that you might have to play with the position when using multiple extension @@ -137,74 +147,6 @@ export class Example16 { this.dataset = mockDataset; } - onBeforeMoveRow(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const rowIdx of data.rows) { - // no point in moving before or after itself - if ( - rowIdx === data.insertBefore || - (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== this.aureliaGrid.dataView.getItemCount()) - ) { - e.preventDefault(); // OR eventData.preventDefault(); - return false; - } - } - return true; - } - - onMoveRows(_e: MouseEvent | TouchEvent, args: any) { - // rows and insertBefore references, - // note that these references are assuming that the dataset isn't filtered at all - // which is not always the case so we will recalcualte them and we won't use these reference afterward - const rows = args.rows as number[]; - const insertBefore = args.insertBefore; - const extractedRows: number[] = []; - - // when moving rows, we need to cancel any sorting that might happen - // we can do this by providing an undefined sort comparer - // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting - this.aureliaGrid.dataView.sort(undefined as any, true); - - // the dataset might be filtered/sorted, - // so we need to get the same dataset as the one that the SlickGrid DataView uses - const tmpDataset = this.aureliaGrid.dataView.getItems(); - const filteredItems = this.aureliaGrid.dataView.getFilteredItems(); - - const itemOnRight = this.aureliaGrid.dataView.getItem(insertBefore); - const insertBeforeFilteredIdx = itemOnRight - ? this.aureliaGrid.dataView.getIdxById(itemOnRight.id) - : this.aureliaGrid.dataView.getItemCount(); - - const filteredRowItems: any[] = []; - rows.forEach((row) => filteredRowItems.push(filteredItems[row])); - const filteredRows = filteredRowItems.map((item) => this.aureliaGrid.dataView.getIdxById(item.id)); - - const left = tmpDataset.slice(0, insertBeforeFilteredIdx); - const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); - - // convert into a final new dataset that has the new order - // we need to resort with - rows.sort((a: number, b: number) => a - b); - for (const filteredRow of filteredRows) { - if (filteredRow) { - extractedRows.push(tmpDataset[filteredRow]); - } - } - filteredRows.reverse(); - for (const row of filteredRows) { - if (row !== undefined && insertBeforeFilteredIdx !== undefined) { - if (row < insertBeforeFilteredIdx) { - left.splice(row, 1); - } else { - right.splice(row - insertBeforeFilteredIdx, 1); - } - } - } - - // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order - const finalDataset = left.concat(extractedRows.concat(right)); - this.dataset = finalDataset; // update dataset and re-render the grid - } - hideDurationColumnDynamically() { // -- you can hide by one Id or multiple Ids: // hideColumnById(id, options), hideColumnByIds([ids], options) diff --git a/demos/aurelia/src/examples/slickgrid/example41.ts b/demos/aurelia/src/examples/slickgrid/example41.ts index fd27bfc61..12873330b 100644 --- a/demos/aurelia/src/examples/slickgrid/example41.ts +++ b/demos/aurelia/src/examples/slickgrid/example41.ts @@ -61,8 +61,14 @@ export class Example41 { cancelEditOnDrag: true, disableRowSelection: true, hideRowMoveShadow: false, - onBeforeMoveRows: this.onBeforeMoveRows.bind(this), - onMoveRows: this.onMoveRows.bind(this), + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, // usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 @@ -79,51 +85,6 @@ export class Example41 { ]; } - onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const dataRow of data.rows) { - // no point in moving before or after itself - if (dataRow === data.insertBefore || dataRow === data.insertBefore - 1) { - e.stopPropagation(); - return false; - } - } - return true; - } - - onMoveRows(_e: MouseEvent | TouchEvent, args: { rows: number[]; insertBefore: number }) { - const extractedRows: any[] = []; - const rows = args.rows; - const insertBefore = args.insertBefore; - const left = this.dataset.slice(0, insertBefore); - const right = this.dataset.slice(insertBefore, this.dataset.length); - - rows.sort((a, b) => a - b); - - for (const row of rows) { - extractedRows.push(this.dataset[row]); - } - - rows.reverse(); - - for (const row of rows) { - if (row < insertBefore) { - left.splice(row, 1); - } else { - right.splice(row - insertBefore, 1); - } - } - - this.dataset = left.concat(extractedRows.concat(right)); - - const selectedRows: number[] = []; - for (let i = 0; i < rows.length; i++) { - selectedRows.push(left.length + i); - } - - this.aureliaGrid.slickGrid?.resetActiveCell(); - this.aureliaGrid.slickGrid?.invalidate(); - } - handleOnDragInit(e: CustomEvent) { // prevent the grid from cancelling drag'n'drop by default e.stopImmediatePropagation(); diff --git a/demos/react-fluent/src/examples/slickgrid/Example02.tsx b/demos/react-fluent/src/examples/slickgrid/Example02.tsx index 071a19f0a..df2e29d0e 100644 --- a/demos/react-fluent/src/examples/slickgrid/Example02.tsx +++ b/demos/react-fluent/src/examples/slickgrid/Example02.tsx @@ -98,8 +98,14 @@ const Example02: React.FC = () => { disableRowSelection: true, cancelEditOnDrag: true, width: 30, - onBeforeMoveRows, - onMoveRows, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + setDataset(args.updatedItems); + }, // you can change the move icon position of any extension (RowMove, RowDetail or RowSelector icon) // note that you might have to play with the position when using multiple extension @@ -143,75 +149,6 @@ const Example02: React.FC = () => { return mockDataset; } - function onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const rowIdx of data.rows) { - // no point in moving before or after itself - if ( - rowIdx === data.insertBefore || - (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== reactGridRef.current?.dataView.getItemCount()) - ) { - e.stopPropagation(); - return false; - } - } - return true; - } - - function onMoveRows(_e: MouseEvent | TouchEvent, args: any) { - // rows and insertBefore references, - // note that these references are assuming that the dataset isn't filtered at all - // which is not always the case so we will recalcualte them and we won't use these reference afterward - const rows = args.rows as number[]; - const insertBefore = args.insertBefore; - const extractedRows: number[] = []; - - // when moving rows, we need to cancel any sorting that might happen - // we can do this by providing an undefined sort comparer - // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting - reactGridRef.current?.dataView.sort(undefined as any, true); - - // the dataset might be filtered/sorted, - // so we need to get the same dataset as the one that the SlickGrid DataView uses - const tmpDataset = reactGridRef.current?.dataView.getItems() || []; - const filteredItems = reactGridRef.current?.dataView.getFilteredItems() || []; - - const itemOnRight = reactGridRef.current?.dataView.getItem(insertBefore); - const insertBeforeFilteredIdx = itemOnRight - ? reactGridRef.current?.dataView.getIdxById(itemOnRight.id) - : reactGridRef.current?.dataView.getItemCount(); - - const filteredRowItems: any[] = []; - rows.forEach((row) => filteredRowItems.push(filteredItems[row])); - const filteredRows = filteredRowItems.map((item) => reactGridRef.current?.dataView.getIdxById(item.id)); - - const left = tmpDataset.slice(0, insertBeforeFilteredIdx); - const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); - - // convert into a final new dataset that has the new order - // we need to resort with - rows.sort((a: number, b: number) => a - b); - for (const filteredRow of filteredRows) { - if (filteredRow) { - extractedRows.push(tmpDataset[filteredRow]); - } - } - filteredRows.reverse(); - for (const row of filteredRows) { - if (row !== undefined && insertBeforeFilteredIdx !== undefined) { - if (row < insertBeforeFilteredIdx) { - left.splice(row, 1); - } else { - right.splice(row - insertBeforeFilteredIdx, 1); - } - } - } - - // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order - const finalDataset = left.concat(extractedRows.concat(right)); - - setDataset(finalDataset); - } - function hideDurationColumnDynamically() { // -- you can hide by one Id or multiple Ids: // hideColumnById(id, options), hideColumnByIds([ids], options) diff --git a/demos/react/src/examples/slickgrid/Example16.tsx b/demos/react/src/examples/slickgrid/Example16.tsx index 655609a95..c7bfde9db 100644 --- a/demos/react/src/examples/slickgrid/Example16.tsx +++ b/demos/react/src/examples/slickgrid/Example16.tsx @@ -97,9 +97,14 @@ const Example16: React.FC = () => { disableRowSelection: true, cancelEditOnDrag: true, width: 30, - onBeforeMoveRows, - onMoveRows, - + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + setDataset(args.updatedItems); + }, // you can change the move icon position of any extension (RowMove, RowDetail or RowSelector icon) // note that you might have to play with the position when using multiple extension // since it really depends on which extension get created first to know what their real position are @@ -141,75 +146,6 @@ const Example16: React.FC = () => { return mockDataset; } - function onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const rowIdx of data.rows) { - // no point in moving before or after itself - if ( - rowIdx === data.insertBefore || - (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== reactGridRef.current?.dataView.getItemCount()) - ) { - e.stopPropagation(); - return false; - } - } - return true; - } - - function onMoveRows(_e: MouseEvent | TouchEvent, args: any) { - // rows and insertBefore references, - // note that these references are assuming that the dataset isn't filtered at all - // which is not always the case so we will recalcualte them and we won't use these reference afterward - const rows = args.rows as number[]; - const insertBefore = args.insertBefore; - const extractedRows: number[] = []; - - // when moving rows, we need to cancel any sorting that might happen - // we can do this by providing an undefined sort comparer - // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting - reactGridRef.current?.dataView.sort(undefined as any, true); - - // the dataset might be filtered/sorted, - // so we need to get the same dataset as the one that the SlickGrid DataView uses - const tmpDataset = reactGridRef.current?.dataView.getItems() || []; - const filteredItems = reactGridRef.current?.dataView.getFilteredItems() || []; - - const itemOnRight = reactGridRef.current?.dataView.getItem(insertBefore); - const insertBeforeFilteredIdx = itemOnRight - ? reactGridRef.current?.dataView.getIdxById(itemOnRight.id) - : reactGridRef.current?.dataView.getItemCount(); - - const filteredRowItems: any[] = []; - rows.forEach((row) => filteredRowItems.push(filteredItems[row])); - const filteredRows = filteredRowItems.map((item) => reactGridRef.current?.dataView.getIdxById(item.id)); - - const left = tmpDataset.slice(0, insertBeforeFilteredIdx); - const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); - - // convert into a final new dataset that has the new order - // we need to resort with - rows.sort((a: number, b: number) => a - b); - for (const filteredRow of filteredRows) { - if (filteredRow) { - extractedRows.push(tmpDataset[filteredRow]); - } - } - filteredRows.reverse(); - for (const row of filteredRows) { - if (row !== undefined && insertBeforeFilteredIdx !== undefined) { - if (row < insertBeforeFilteredIdx) { - left.splice(row, 1); - } else { - right.splice(row - insertBeforeFilteredIdx, 1); - } - } - } - - // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order - const finalDataset = left.concat(extractedRows.concat(right)); - - setDataset(finalDataset); - } - function hideDurationColumnDynamically() { // -- you can hide by one Id or multiple Ids: // hideColumnById(id, options), hideColumnByIds([ids], options) diff --git a/demos/react/src/examples/slickgrid/Example41.tsx b/demos/react/src/examples/slickgrid/Example41.tsx index bbc32486e..f2705d090 100644 --- a/demos/react/src/examples/slickgrid/Example41.tsx +++ b/demos/react/src/examples/slickgrid/Example41.tsx @@ -65,8 +65,14 @@ const Example41: React.FC = () => { cancelEditOnDrag: true, disableRowSelection: true, hideRowMoveShadow: false, - onBeforeMoveRows, - onMoveRows, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + setDataset(args.updatedItems); + }, // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, // usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 @@ -86,52 +92,6 @@ const Example41: React.FC = () => { ]; } - function onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const dataRow of data.rows) { - // no point in moving before or after itself - if (dataRow === data.insertBefore || dataRow === data.insertBefore - 1) { - e.stopPropagation(); - return false; - } - } - return true; - } - - function onMoveRows(_e: MouseEvent | TouchEvent, args: { rows: number[]; insertBefore: number }) { - const extractedRows: any[] = []; - const rows = args.rows; - const insertBefore = args.insertBefore; - const tmpDataset = dataset || []; - const left = tmpDataset.slice(0, insertBefore); - const right = tmpDataset.slice(insertBefore, tmpDataset.length); - - rows.sort((a, b) => a - b); - - for (const row of rows) { - extractedRows.push(tmpDataset[row]); - } - - rows.reverse(); - - for (const row of rows) { - if (row < insertBefore) { - left.splice(row, 1); - } else { - right.splice(row - insertBefore, 1); - } - } - - const finalDataset = left.concat(extractedRows.concat(right)); - const selectedRows: number[] = []; - for (let i = 0; i < rows.length; i++) { - selectedRows.push(left.length + i); - } - - reactGridRef.current?.dataView.setItems(finalDataset); - reactGridRef.current?.slickGrid?.resetActiveCell(); - reactGridRef.current?.slickGrid?.invalidate(); - } - function handleOnDragInit(e: CustomEvent) { // prevent the grid from cancelling drag'n'drop by default e.stopImmediatePropagation(); diff --git a/demos/vanilla/src/examples/example07.ts b/demos/vanilla/src/examples/example07.ts index 9cf5e77bb..b5466caab 100644 --- a/demos/vanilla/src/examples/example07.ts +++ b/demos/vanilla/src/examples/example07.ts @@ -402,8 +402,14 @@ export default class Example07 { disableRowSelection: true, cancelEditOnDrag: true, hideRowMoveShadow: false, - onBeforeMoveRows: this.onBeforeMoveRow.bind(this), - onMoveRows: this.onMoveRows.bind(this), + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, // usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 @@ -559,71 +565,6 @@ export default class Example07 { return collection.sort((item1, item2) => item1.value - item2.value); } - onBeforeMoveRow(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const rowIdx of data.rows) { - // no point in moving before or after itself - if ( - rowIdx === data.insertBefore || - (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== this.sgb.dataView?.getItemCount()) - ) { - e.stopPropagation(); - return false; - } - } - return true; - } - - onMoveRows(_e: MouseEvent | TouchEvent, args: { rows: number[]; insertBefore: number }) { - // rows and insertBefore references, - // note that these references are assuming that the dataset isn't filtered at all - // which is not always the case so we will recalcualte them and we won't use these reference afterward - const rows = args.rows as number[]; - const insertBefore = args.insertBefore; - const extractedRows: any[] = []; - - // when moving rows, we need to cancel any sorting that might happen - // we can do this by providing an undefined sort comparer - // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting - this.sgb.dataView?.sort(undefined as any, true); - - // the dataset might be filtered/sorted, - // so we need to get the same dataset as the one that the SlickGrid DataView uses - const tmpDataset = this.sgb.dataView?.getItems() as any[]; - const filteredItems = this.sgb.dataView?.getFilteredItems() as any[]; - - const itemOnRight = this.sgb.dataView?.getItem(insertBefore); - const insertBeforeFilteredIdx = ( - itemOnRight ? this.sgb.dataView?.getIdxById(itemOnRight.id) : this.sgb.dataView?.getItemCount() - ) as number; - - const filteredRowItems: any[] = []; - rows.forEach((row) => filteredRowItems.push(filteredItems[row] as any)); - const filteredRows = filteredRowItems.map((item) => this.sgb.dataView?.getIdxById(item.id)) as number[]; - - const left = tmpDataset.slice(0, insertBeforeFilteredIdx); - const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); - - // convert into a final new dataset that has the new order - // we need to resort with - rows.sort((a: number, b: number) => a - b); - for (const filteredRow of filteredRows) { - extractedRows.push(tmpDataset[filteredRow as number]); - } - filteredRows.reverse(); - for (const row of filteredRows) { - if (row < insertBeforeFilteredIdx) { - left.splice(row, 1); - } else { - right.splice(row - insertBeforeFilteredIdx, 1); - } - } - - // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order - const finalDataset = left.concat(extractedRows.concat(right)); - this.dataset = finalDataset; - this.sgb.dataset = this.dataset; // update dataset and re-render the grid - } - handleOnCellChange(event) { console.log('onCellChanged', event.detail, event.detail.args.item.start); } diff --git a/demos/vanilla/src/examples/example29.ts b/demos/vanilla/src/examples/example29.ts index 54a919667..3dcaf9597 100644 --- a/demos/vanilla/src/examples/example29.ts +++ b/demos/vanilla/src/examples/example29.ts @@ -83,8 +83,14 @@ export default class Example29 { cancelEditOnDrag: true, disableRowSelection: true, hideRowMoveShadow: false, - onBeforeMoveRows: this.onBeforeMoveRows.bind(this), - onMoveRows: this.onMoveRows.bind(this), + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, // usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 @@ -111,51 +117,6 @@ export default class Example29 { } } - onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const dataRow of data.rows) { - // no point in moving before or after itself - if (dataRow === data.insertBefore || dataRow === data.insertBefore - 1) { - e.stopPropagation(); - return false; - } - } - return true; - } - - onMoveRows(_e: MouseEvent | TouchEvent, args: { rows: number[]; insertBefore: number }) { - const extractedRows: any[] = []; - const rows = args.rows; - const insertBefore = args.insertBefore; - const left = this.sgb.dataset.slice(0, insertBefore); - const right = this.sgb.dataset.slice(insertBefore, this.sgb.dataset.length); - - rows.sort((a, b) => a - b); - - for (const row of rows) { - extractedRows.push(this.sgb.dataset[row]); - } - - rows.reverse(); - - for (const row of rows) { - if (row < insertBefore) { - left.splice(row, 1); - } else { - right.splice(row - insertBefore, 1); - } - } - - this.dataset = left.concat(extractedRows.concat(right)); - - const selectedRows: number[] = []; - for (let i = 0; i < rows.length; i++) { - selectedRows.push(left.length + i); - } - - this.sgb.slickGrid?.resetActiveCell(); - this.sgb.dataset = this.dataset; // update dataset and re-render the grid - } - handleOnDragInit(e: CustomEvent) { // prevent the grid from cancelling drag'n'drop by default const { eventData } = e.detail; diff --git a/demos/vanilla/src/examples/example34.ts b/demos/vanilla/src/examples/example34.ts index 4542d6a90..03d3d678d 100644 --- a/demos/vanilla/src/examples/example34.ts +++ b/demos/vanilla/src/examples/example34.ts @@ -150,8 +150,6 @@ export default class Example34 { disableRowSelection: true, cancelEditOnDrag: true, hideRowMoveShadow: false, - onBeforeMoveRows: this.onBeforeMoveRow.bind(this), - onMoveRows: this.onMoveRows.bind(this), // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, // usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 @@ -159,71 +157,6 @@ export default class Example34 { }; } - onBeforeMoveRow(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const rowIdx of data.rows) { - // no point in moving before or after itself - if ( - rowIdx === data.insertBefore || - (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== this.sgb.dataView?.getItemCount()) - ) { - e.stopPropagation(); - return false; - } - } - return true; - } - - onMoveRows(_e: MouseEvent | TouchEvent, args: { rows: number[]; insertBefore: number }) { - // rows and insertBefore references, - // note that these references are assuming that the dataset isn't filtered at all - // which is not always the case so we will recalcualte them and we won't use these reference afterward - const rows = args.rows as number[]; - const insertBefore = args.insertBefore; - const extractedRows: any[] = []; - - // when moving rows, we need to cancel any sorting that might happen - // we can do this by providing an undefined sort comparer - // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting - this.sgb.dataView?.sort(undefined as any, true); - - // the dataset might be filtered/sorted, - // so we need to get the same dataset as the one that the SlickGrid DataView uses - const tmpDataset = this.sgb.dataView?.getItems() as any[]; - const filteredItems = this.sgb.dataView?.getFilteredItems() as any[]; - - const itemOnRight = this.sgb.dataView?.getItem(insertBefore); - const insertBeforeFilteredIdx = ( - itemOnRight ? this.sgb.dataView?.getIdxById(itemOnRight.id) : this.sgb.dataView?.getItemCount() - ) as number; - - const filteredRowItems: any[] = []; - rows.forEach((row) => filteredRowItems.push(filteredItems[row] as any)); - const filteredRows = filteredRowItems.map((item) => this.sgb.dataView?.getIdxById(item.id)) as number[]; - - const left = tmpDataset.slice(0, insertBeforeFilteredIdx); - const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); - - // convert into a final new dataset that has the new order - // we need to resort with - rows.sort((a: number, b: number) => a - b); - for (const filteredRow of filteredRows) { - extractedRows.push(tmpDataset[filteredRow as number]); - } - filteredRows.reverse(); - for (const row of filteredRows) { - if (row < insertBeforeFilteredIdx) { - left.splice(row, 1); - } else { - right.splice(row - insertBeforeFilteredIdx, 1); - } - } - - // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order - const finalDataset = left.concat(extractedRows.concat(right)); - this.dataset = finalDataset; - this.sgb.dataset = this.dataset; // update dataset and re-render the grid - } - // add onScroll listener which will detect when we reach the scroll end // if so, then append items to the dataset handleOnScroll(event) { diff --git a/demos/vue/src/components/Example16.vue b/demos/vue/src/components/Example16.vue index 4ef09fc3d..9bd59c87d 100644 --- a/demos/vue/src/components/Example16.vue +++ b/demos/vue/src/components/Example16.vue @@ -87,8 +87,14 @@ function defineGrid() { cancelEditOnDrag: true, hideRowMoveShadow: false, width: 30, - onBeforeMoveRows, - onMoveRows, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + dataset.value = args.updatedItems; + }, // you can change the move icon position of any extension (RowMove, RowDetail or RowSelector icon) // note that you might have to play with the position when using multiple extension @@ -132,69 +138,6 @@ function getData(count: number) { return mockDataset; } -function onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const rowIdx of data.rows) { - // no point in moving before or after itself - if (rowIdx === data.insertBefore || (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== vueGrid.dataView.getItemCount())) { - e.preventDefault(); // OR eventData.preventDefault(); - return false; - } - } - return true; -} - -function onMoveRows(_e: MouseEvent | TouchEvent, args: any) { - // rows and insertBefore references, - // note that these references are assuming that the dataset isn't filtered at all - // which is not always the case so we will recalcualte them and we won't use these reference afterward - const rows = args.rows as number[]; - const insertBefore = args.insertBefore; - const extractedRows: number[] = []; - - // when moving rows, we need to cancel any sorting that might happen - // we can do this by providing an undefined sort comparer - // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting - vueGrid.dataView.sort(undefined as any, true); - - // the dataset might be filtered/sorted, - // so we need to get the same dataset as the one that the SlickGrid DataView uses - const tmpDataset = vueGrid.dataView.getItems(); - const filteredItems = vueGrid.dataView.getFilteredItems(); - - const itemOnRight = vueGrid.dataView.getItem(insertBefore); - const insertBeforeFilteredIdx = itemOnRight ? vueGrid.dataView.getIdxById(itemOnRight.id) : vueGrid.dataView.getItemCount(); - - const filteredRowItems: any[] = []; - rows.forEach((row) => filteredRowItems.push(filteredItems[row])); - const filteredRows = filteredRowItems.map((item) => vueGrid.dataView.getIdxById(item.id)); - - const left = tmpDataset.slice(0, insertBeforeFilteredIdx); - const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); - - // convert into a final new dataset that has the new order - // we need to resort with - rows.sort((a: number, b: number) => a - b); - for (const filteredRow of filteredRows) { - if (filteredRow) { - extractedRows.push(tmpDataset[filteredRow]); - } - } - filteredRows.reverse(); - for (const row of filteredRows) { - if (row !== undefined && insertBeforeFilteredIdx !== undefined) { - if (row < insertBeforeFilteredIdx) { - left.splice(row, 1); - } else { - right.splice(row - insertBeforeFilteredIdx, 1); - } - } - } - - // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order - const finalDataset = left.concat(extractedRows.concat(right)); - dataset.value = finalDataset; // update dataset and re-render the grid -} - function hideDurationColumnDynamically() { // -- you can hide by one Id or multiple Ids: // hideColumnById(id, options), hideColumnByIds([ids], options) diff --git a/demos/vue/src/components/Example41.vue b/demos/vue/src/components/Example41.vue index 78e81bcfe..5faa89079 100644 --- a/demos/vue/src/components/Example41.vue +++ b/demos/vue/src/components/Example41.vue @@ -57,8 +57,14 @@ function defineGrid() { cancelEditOnDrag: true, disableRowSelection: true, hideRowMoveShadow: false, - onBeforeMoveRows: onBeforeMoveRows, - onMoveRows: onMoveRows, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + dataset.value = args.updatedItems; + }, // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, // usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 @@ -75,51 +81,6 @@ function mockData() { ]; } -function onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const dataRow of data.rows) { - // no point in moving before or after itself - if (dataRow === data.insertBefore || dataRow === data.insertBefore - 1) { - e.stopPropagation(); - return false; - } - } - return true; -} - -function onMoveRows(_e: MouseEvent | TouchEvent, args: { rows: number[]; insertBefore: number }) { - const extractedRows: any[] = []; - const rows = args.rows; - const insertBefore = args.insertBefore; - const left = dataset.value.slice(0, insertBefore); - const right = dataset.value.slice(insertBefore, dataset.value.length); - - rows.sort((a, b) => a - b); - - for (const row of rows) { - extractedRows.push(dataset.value[row]); - } - - rows.reverse(); - - for (const row of rows) { - if (row < insertBefore) { - left.splice(row, 1); - } else { - right.splice(row - insertBefore, 1); - } - } - - dataset.value = left.concat(extractedRows.concat(right)); - - const selectedRows: number[] = []; - for (let i = 0; i < rows.length; i++) { - selectedRows.push(left.length + i); - } - - vueGrid.slickGrid?.resetActiveCell(); - vueGrid.slickGrid?.invalidate(); -} - function handleOnDragInit(e: CustomEvent) { // prevent the grid from cancelling drag'n'drop by default e.stopImmediatePropagation(); diff --git a/docs/TOC.md b/docs/TOC.md index 80e2365b2..78f179bab 100644 --- a/docs/TOC.md +++ b/docs/TOC.md @@ -68,6 +68,7 @@ * [Infinite Scroll](grid-functionalities/infinite-scroll.md) * [Pinning (frozen) of Columns/Rows](grid-functionalities/frozen-columns-rows.md) * [Row Detail](grid-functionalities/row-detail.md) +* [Row Move (dragging)](grid-functionalities/row-move.md) * [Row Selection](grid-functionalities/row-selection.md) * [Tree Data Grid](grid-functionalities/tree-data-grid.md) * [Row Based Editing Plugin](grid-functionalities/row-based-edit.md) diff --git a/docs/grid-functionalities/row-move.md b/docs/grid-functionalities/row-move.md new file mode 100644 index 000000000..cc67c6bfe --- /dev/null +++ b/docs/grid-functionalities/row-move.md @@ -0,0 +1,185 @@ +### Description +Row Move feature allow you to drag a row and move it to another position in the grid. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example07) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vanilla/src/examples/example07.ts) + +## Basic Usage +By default the lib will fallback to its default logic implementation to move the item and readjust the dataset with the updated row positions. If the default logic doesn't work for you, you can provide your own logic for `onBeforeMoveRows` and/or `onMoveRows`. + +#### ViewModel +```ts +export class Example1 { + attached() { + this.initializeGrid(); + this.dataset = this.loadData(500); + const gridContainerElm = document.querySelector(`.grid3`); + + gridContainerElm.addEventListener('onselectedrows', this.handleOnClick.bind(this)); + this.sgb = new Slicker.GridBundle(gridContainerElm, this.columns, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); + } + + initializeGrid() { + // define columns + ... + + // grid options + this.gridOptions = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + // ... list of different Row Move options + }, + } + } +} +``` + +## List of all Row Move options + +#### ViewModel +```ts +export class Example1 { + attached() { + this.initializeGrid(); + this.dataset = this.loadData(500); + const gridContainerElm = document.querySelector(`.grid3`); + + gridContainerElm.addEventListener('onselectedrows', this.handleOnClick.bind(this)); + this.sgb = new Slicker.GridBundle(gridContainerElm, this.columns, { ...ExampleGridOptions, ...this.gridOptions }, this.dataset); + } + + initializeGrid() { + // define columns + ... + + // grid options + this.gridOptions = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + // list of different Row Move options + columnIndexPosition: 0, // provide a different column position if default doesn't work for you + // when using Row Move + Row Selection, you want to move only a single row and we will enable the following flags so it doesn't cancel row selection + singleRowMove: true, // allow multi-row dragging? + disableRowSelection: true, // should we disable row selection? + cancelEditOnDrag: true, + hideRowMoveShadow: false, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, + + // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, + usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 + }, + } + } +} +``` + +## Provide your own callback (`onBeforeMoveRows` and `onMoveRows`) +Typical code logic when providing your own custom logic, the code below is literraly the default callback logic (it is only shown as demo and logic that can be used to get started). + +#### ViewModel +```ts +export class Example1 { + attached() { + this.initializeGrid(); + } + + initializeGrid() { + // define columns + ... + + // grid options + this.gridOptions = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + onBeforeMoveRows: this.onBeforeMoveRow.bind(this), + onMoveRows: this.onMoveRows.bind(this) + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, + }, + } + } + + onBeforeMoveRow(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { + for (const rowIdx of data.rows) { + // no point in moving before or after itself + if ( + rowIdx === data.insertBefore || + (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== this.aureliaGrid.dataView.getItemCount()) + ) { + e.preventDefault(); // OR eventData.preventDefault(); + return false; + } + } + return true; + } + + onMoveRows(_e: MouseEvent | TouchEvent, args: any) { + // rows and insertBefore references, + // note that these references are assuming that the dataset isn't filtered at all + // which is not always the case so we will recalcualte them and we won't use these reference afterward + const rows = args.rows as number[]; + const insertBefore = args.insertBefore; + const extractedRows: number[] = []; + + // when moving rows, we need to cancel any sorting that might happen + // we can do this by providing an undefined sort comparer + // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting + this.aureliaGrid.dataView.sort(undefined as any, true); + + // the dataset might be filtered/sorted, + // so we need to get the same dataset as the one that the SlickGrid DataView uses + const tmpDataset = this.aureliaGrid.dataView.getItems(); + const filteredItems = this.aureliaGrid.dataView.getFilteredItems(); + + const itemOnRight = this.aureliaGrid.dataView.getItem(insertBefore); + const insertBeforeFilteredIdx = itemOnRight + ? this.aureliaGrid.dataView.getIdxById(itemOnRight.id) + : this.aureliaGrid.dataView.getItemCount(); + + const filteredRowItems: any[] = []; + rows.forEach((row) => filteredRowItems.push(filteredItems[row])); + const filteredRows = filteredRowItems.map((item) => this.aureliaGrid.dataView.getIdxById(item.id)); + + const left = tmpDataset.slice(0, insertBeforeFilteredIdx); + const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); + + // convert into a final new dataset that has the new order + // we need to resort with + rows.sort((a: number, b: number) => a - b); + for (const filteredRow of filteredRows) { + if (filteredRow) { + extractedRows.push(tmpDataset[filteredRow]); + } + } + filteredRows.reverse(); + for (const row of filteredRows) { + if (row !== undefined && insertBeforeFilteredIdx !== undefined) { + if (row < insertBeforeFilteredIdx) { + left.splice(row, 1); + } else { + right.splice(row - insertBeforeFilteredIdx, 1); + } + } + } + + // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order + const finalDataset = left.concat(extractedRows.concat(right)); + this.dataset = finalDataset; // update dataset and re-render the grid + } +} +``` diff --git a/frameworks/angular-slickgrid/docs/TOC.md b/frameworks/angular-slickgrid/docs/TOC.md index 3e483fae3..f91dcb022 100644 --- a/frameworks/angular-slickgrid/docs/TOC.md +++ b/frameworks/angular-slickgrid/docs/TOC.md @@ -75,6 +75,7 @@ * [Pinning (frozen) of Columns/Rows](grid-functionalities/frozen-columns-rows.md) * [Providing data to the grid](grid-functionalities/providing-grid-data.md) * [Row Detail](grid-functionalities/row-detail.md) +* [Row Move (dragging)](grid-functionalities/row-move.md) * [Row Selection](grid-functionalities/row-selection.md) * [Tree Data Grid](grid-functionalities/tree-data-grid.md) * [Row Based Editing Plugin](grid-functionalities/row-based-edit.md) diff --git a/frameworks/angular-slickgrid/docs/grid-functionalities/row-move.md b/frameworks/angular-slickgrid/docs/grid-functionalities/row-move.md new file mode 100644 index 000000000..911aedd43 --- /dev/null +++ b/frameworks/angular-slickgrid/docs/grid-functionalities/row-move.md @@ -0,0 +1,184 @@ +### Description +Row Move feature allow you to drag a row and move it to another position in the grid. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example07) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vanilla/src/examples/example07.ts) + +## Basic Usage +By default the lib will fallback to its default logic implementation to move the item and readjust the dataset with the updated row positions. If the default logic doesn't work for you, you can provide your own logic for `onBeforeMoveRows` and/or `onMoveRows`. + +#### ViewModel +```ts +@Component({ + templateUrl: './grid-row-move.component.html' +}) +export class GridRowDetailComponent implements OnInit, OnDestroy { + constructor() { + this.defineGrid(); + } + + defineGrid() { + // define columns + ... + + // grid options + this.gridOptions = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + // ... list of different Row Move options + }, + } + } +} +``` + +## List of all Row Move options + +#### ViewModel +```ts +@Component({ + templateUrl: './grid-row-move.component.html' +}) +export class GridRowDetailComponent implements OnInit, OnDestroy { + constructor() { + this.defineGrid(); + } + + defineGrid() { + // define columns + ... + + // grid options + this.gridOptions = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + // list of different Row Move options + columnIndexPosition: 0, // provide a different column position if default doesn't work for you + // when using Row Move + Row Selection, you want to move only a single row and we will enable the following flags so it doesn't cancel row selection + singleRowMove: true, // allow multi-row dragging? + disableRowSelection: true, // should we disable row selection? + cancelEditOnDrag: true, + hideRowMoveShadow: false, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, + + // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, + usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 + }, + } + } +} +``` + +## Provide your own callback (`onBeforeMoveRows` and `onMoveRows`) +Typical code logic when providing your own custom logic, the code below is literraly the default callback logic (it is only shown as demo and logic that can be used to get started). + +#### ViewModel +```ts +@Component({ + templateUrl: './grid-row-move.component.html' +}) +export class GridRowDetailComponent implements OnInit, OnDestroy { + constructor() { + this.defineGrid(); + } + + defineGrid() { + // define columns + ... + + // grid options + this.gridOptions = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + onBeforeMoveRows: this.onBeforeMoveRow.bind(this), + onMoveRows: this.onMoveRows.bind(this) + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, + }, + } + } + + onBeforeMoveRow(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { + for (const rowIdx of data.rows) { + // no point in moving before or after itself + if ( + rowIdx === data.insertBefore || + (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== this.aureliaGrid.dataView.getItemCount()) + ) { + e.preventDefault(); // OR eventData.preventDefault(); + return false; + } + } + return true; + } + + onMoveRows(_e: MouseEvent | TouchEvent, args: any) { + // rows and insertBefore references, + // note that these references are assuming that the dataset isn't filtered at all + // which is not always the case so we will recalcualte them and we won't use these reference afterward + const rows = args.rows as number[]; + const insertBefore = args.insertBefore; + const extractedRows: number[] = []; + + // when moving rows, we need to cancel any sorting that might happen + // we can do this by providing an undefined sort comparer + // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting + this.aureliaGrid.dataView.sort(undefined as any, true); + + // the dataset might be filtered/sorted, + // so we need to get the same dataset as the one that the SlickGrid DataView uses + const tmpDataset = this.aureliaGrid.dataView.getItems(); + const filteredItems = this.aureliaGrid.dataView.getFilteredItems(); + + const itemOnRight = this.aureliaGrid.dataView.getItem(insertBefore); + const insertBeforeFilteredIdx = itemOnRight + ? this.aureliaGrid.dataView.getIdxById(itemOnRight.id) + : this.aureliaGrid.dataView.getItemCount(); + + const filteredRowItems: any[] = []; + rows.forEach((row) => filteredRowItems.push(filteredItems[row])); + const filteredRows = filteredRowItems.map((item) => this.aureliaGrid.dataView.getIdxById(item.id)); + + const left = tmpDataset.slice(0, insertBeforeFilteredIdx); + const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); + + // convert into a final new dataset that has the new order + // we need to resort with + rows.sort((a: number, b: number) => a - b); + for (const filteredRow of filteredRows) { + if (filteredRow) { + extractedRows.push(tmpDataset[filteredRow]); + } + } + filteredRows.reverse(); + for (const row of filteredRows) { + if (row !== undefined && insertBeforeFilteredIdx !== undefined) { + if (row < insertBeforeFilteredIdx) { + left.splice(row, 1); + } else { + right.splice(row - insertBeforeFilteredIdx, 1); + } + } + } + + // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order + const finalDataset = left.concat(extractedRows.concat(right)); + this.dataset = finalDataset; // update dataset and re-render the grid + } +} +``` diff --git a/frameworks/angular-slickgrid/src/demos/examples/example16.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example16.component.ts index 56934c7fe..ef9a9092b 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example16.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example16.component.ts @@ -95,8 +95,14 @@ export class Example16Component implements OnInit { cancelEditOnDrag: true, hideRowMoveShadow: false, width: 30, - onBeforeMoveRows: this.onBeforeMoveRows.bind(this), - onMoveRows: this.onMoveRows.bind(this), + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset.set(args.updatedItems); + }, // you can change the move icon position of any extension (RowMove, RowDetail or RowSelector icon) // note that you might have to play with the position when using multiple extension @@ -138,75 +144,6 @@ export class Example16Component implements OnInit { this.dataset.set(mockDataset); } - onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const rowIdx of data.rows) { - // no point in moving before or after itself - if ( - rowIdx === data.insertBefore || - (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== this.angularGrid.dataView.getItemCount()) - ) { - e.stopPropagation(); - return false; - } - } - return true; - } - - onMoveRows(_e: MouseEvent | TouchEvent, args: any) { - // rows and insertBefore references, - // note that these references are assuming that the dataset isn't filtered at all - // which is not always the case so we will recalcualte them and we won't use these reference afterward - const rows = args.rows as number[]; - const insertBefore = args.insertBefore; - const extractedRows = []; - - // when moving rows, we need to cancel any sorting that might happen - // we can do this by providing an undefined sort comparer - // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting - this.angularGrid.dataView.sort(undefined as any, true); - - // the dataset might be filtered/sorted, - // so we need to get the same dataset as the one that the SlickGrid DataView uses - const tmpDataset = this.angularGrid.dataView.getItems(); - const filteredItems = this.angularGrid.dataView.getFilteredItems(); - - const itemOnRight = this.angularGrid.dataView.getItem(insertBefore); - const insertBeforeFilteredIdx = itemOnRight - ? this.angularGrid.dataView.getIdxById(itemOnRight.id) - : this.angularGrid.dataView.getItemCount(); - - const filteredRowItems: any[] = []; - rows.forEach((row) => filteredRowItems.push(filteredItems[row])); - const filteredRows = filteredRowItems.map((item) => this.angularGrid.dataView.getIdxById(item.id)); - - const left = tmpDataset.slice(0, insertBeforeFilteredIdx); - const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); - - // convert into a final new dataset that has the new order - // we need to resort with - rows.sort((a: number, b: number) => a - b); - for (const filteredRow of filteredRows) { - if (filteredRow !== undefined) { - extractedRows.push(tmpDataset[filteredRow]); - } - } - filteredRows.reverse(); - for (const row of filteredRows) { - if (row !== undefined && insertBeforeFilteredIdx !== undefined) { - if (row < insertBeforeFilteredIdx) { - left.splice(row, 1); - } else { - right.splice(row - insertBeforeFilteredIdx, 1); - } - } - } - - // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order - const finalDataset = left.concat(extractedRows.concat(right)); - this.angularGrid.slickGrid?.invalidate(); - this.dataset.set(finalDataset); // assign new array reference to trigger Angular input change detection - } - hideDurationColumnDynamically() { // -- you can hide by one Id or multiple Ids: // hideColumnById(id, options), hideColumnByIds([ids], options) diff --git a/frameworks/angular-slickgrid/src/demos/examples/example41.component.ts b/frameworks/angular-slickgrid/src/demos/examples/example41.component.ts index 75197c586..60b8c19a1 100644 --- a/frameworks/angular-slickgrid/src/demos/examples/example41.component.ts +++ b/frameworks/angular-slickgrid/src/demos/examples/example41.component.ts @@ -83,8 +83,14 @@ export class Example41Component implements OnInit { cancelEditOnDrag: true, disableRowSelection: true, hideRowMoveShadow: false, - onBeforeMoveRows: this.onBeforeMoveRows.bind(this), - onMoveRows: this.onMoveRows.bind(this), + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, // usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 @@ -101,51 +107,6 @@ export class Example41Component implements OnInit { ]; } - onBeforeMoveRows(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { - for (const dataRow of data.rows) { - // no point in moving before or after itself - if (dataRow === data.insertBefore || dataRow === data.insertBefore - 1) { - e.stopPropagation(); - return false; - } - } - return true; - } - - onMoveRows(_e: MouseEvent | TouchEvent, args: { rows: number[]; insertBefore: number }) { - const extractedRows: any[] = []; - const rows = args.rows; - const insertBefore = args.insertBefore; - const left = this.dataset.slice(0, insertBefore); - const right = this.dataset.slice(insertBefore, this.dataset.length); - - rows.sort((a, b) => a - b); - - for (const row of rows) { - extractedRows.push(this.dataset[row]); - } - - rows.reverse(); - - for (const row of rows) { - if (row < insertBefore) { - left.splice(row, 1); - } else { - right.splice(row - insertBefore, 1); - } - } - - this.dataset = left.concat(extractedRows.concat(right)); - - const selectedRows: number[] = []; - for (let i = 0; i < rows.length; i++) { - selectedRows.push(left.length + i); - } - - this.angularGrid.slickGrid?.resetActiveCell(); - this.dataset = [...this.dataset]; - } - handleOnDragInit(e: CustomEvent) { // prevent the grid from cancelling drag'n'drop by default e.stopImmediatePropagation(); diff --git a/frameworks/aurelia-slickgrid/docs/TOC.md b/frameworks/aurelia-slickgrid/docs/TOC.md index bc886fd32..8644fa6f7 100644 --- a/frameworks/aurelia-slickgrid/docs/TOC.md +++ b/frameworks/aurelia-slickgrid/docs/TOC.md @@ -73,6 +73,7 @@ * [Pinning (frozen) of Columns/Rows](grid-functionalities/frozen-columns-rows.md) * [Providing data to the grid](grid-functionalities/providing-grid-data.md) * [Row Detail](grid-functionalities/row-detail.md) +* [Row Move (dragging)](grid-functionalities/row-move.md) * [Row Selection](grid-functionalities/row-selection.md) * [Tree Data Grid](grid-functionalities/tree-data-grid.md) * [Row Based Editing Plugin](grid-functionalities/row-based-edit.md) diff --git a/frameworks/aurelia-slickgrid/docs/grid-functionalities/row-move.md b/frameworks/aurelia-slickgrid/docs/grid-functionalities/row-move.md new file mode 100644 index 000000000..c594e2e21 --- /dev/null +++ b/frameworks/aurelia-slickgrid/docs/grid-functionalities/row-move.md @@ -0,0 +1,177 @@ +### Description +Row Move feature allow you to drag a row and move it to another position in the grid. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example07) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vanilla/src/examples/example07.ts) + +## Basic Usage +By default the lib will fallback to its default logic implementation to move the item and readjust the dataset with the updated row positions. If the default logic doesn't work for you, you can provide your own logic for `onBeforeMoveRows` and/or `onMoveRows`. + +#### ViewModel +```ts +import { AureliaGridInstance, GridState } from 'aurelia-slickgrid'; + +export class Example1 { + attached() { + this.initializeGrid(); + } + + initializeGrid() { + // define columns + ... + + // grid options + this.gridOptions = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + // ... list of different Row Move options + }, + } + } +} +``` + +## List of all Row Move options + +#### ViewModel +```ts +export class Example1 { + attached() { + this.initializeGrid(); + } + + initializeGrid() { + // define columns + ... + + // grid options + this.gridOptions = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + // list of different Row Move options + columnIndexPosition: 0, // provide a different column position if default doesn't work for you + // when using Row Move + Row Selection, you want to move only a single row and we will enable the following flags so it doesn't cancel row selection + singleRowMove: true, // allow multi-row dragging? + disableRowSelection: true, // should we disable row selection? + cancelEditOnDrag: true, + hideRowMoveShadow: false, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, + + // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, + usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 + }, + } + } +} +``` + +## Provide your own callback (`onBeforeMoveRows` and `onMoveRows`) +Typical code logic when providing your own custom logic, the code below is literraly the default callback logic (it is only shown as demo and logic that can be used to get started). + +#### ViewModel +```ts +export class Example1 { + attached() { + this.initializeGrid(); + } + + initializeGrid() { + // define columns + ... + + // grid options + this.gridOptions = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + onBeforeMoveRows: this.onBeforeMoveRow.bind(this), + onMoveRows: this.onMoveRows.bind(this) + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, + }, + } + } + + onBeforeMoveRow(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { + for (const rowIdx of data.rows) { + // no point in moving before or after itself + if ( + rowIdx === data.insertBefore || + (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== this.aureliaGrid.dataView.getItemCount()) + ) { + e.preventDefault(); // OR eventData.preventDefault(); + return false; + } + } + return true; + } + + onMoveRows(_e: MouseEvent | TouchEvent, args: any) { + // rows and insertBefore references, + // note that these references are assuming that the dataset isn't filtered at all + // which is not always the case so we will recalcualte them and we won't use these reference afterward + const rows = args.rows as number[]; + const insertBefore = args.insertBefore; + const extractedRows: number[] = []; + + // when moving rows, we need to cancel any sorting that might happen + // we can do this by providing an undefined sort comparer + // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting + this.aureliaGrid.dataView.sort(undefined as any, true); + + // the dataset might be filtered/sorted, + // so we need to get the same dataset as the one that the SlickGrid DataView uses + const tmpDataset = this.aureliaGrid.dataView.getItems(); + const filteredItems = this.aureliaGrid.dataView.getFilteredItems(); + + const itemOnRight = this.aureliaGrid.dataView.getItem(insertBefore); + const insertBeforeFilteredIdx = itemOnRight + ? this.aureliaGrid.dataView.getIdxById(itemOnRight.id) + : this.aureliaGrid.dataView.getItemCount(); + + const filteredRowItems: any[] = []; + rows.forEach((row) => filteredRowItems.push(filteredItems[row])); + const filteredRows = filteredRowItems.map((item) => this.aureliaGrid.dataView.getIdxById(item.id)); + + const left = tmpDataset.slice(0, insertBeforeFilteredIdx); + const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); + + // convert into a final new dataset that has the new order + // we need to resort with + rows.sort((a: number, b: number) => a - b); + for (const filteredRow of filteredRows) { + if (filteredRow) { + extractedRows.push(tmpDataset[filteredRow]); + } + } + filteredRows.reverse(); + for (const row of filteredRows) { + if (row !== undefined && insertBeforeFilteredIdx !== undefined) { + if (row < insertBeforeFilteredIdx) { + left.splice(row, 1); + } else { + right.splice(row - insertBeforeFilteredIdx, 1); + } + } + } + + // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order + const finalDataset = left.concat(extractedRows.concat(right)); + this.dataset = finalDataset; // update dataset and re-render the grid + } +} +``` diff --git a/frameworks/slickgrid-react/docs/TOC.md b/frameworks/slickgrid-react/docs/TOC.md index 54ec265cb..6c14a7ebb 100644 --- a/frameworks/slickgrid-react/docs/TOC.md +++ b/frameworks/slickgrid-react/docs/TOC.md @@ -73,6 +73,7 @@ * [Pinning (frozen) of Columns/Rows](grid-functionalities/frozen-columns-rows.md) * [Providing data to the grid](grid-functionalities/providing-grid-data.md) * [Row Detail](grid-functionalities/row-detail.md) +* [Row Move (dragging)](grid-functionalities/row-move.md) * [Row Selection](grid-functionalities/row-selection.md) * [Tree Data Grid](grid-functionalities/tree-data-grid.md) * [Row Based Editing Plugin](grid-functionalities/row-based-edit.md) diff --git a/frameworks/slickgrid-react/docs/grid-functionalities/row-move.md b/frameworks/slickgrid-react/docs/grid-functionalities/row-move.md new file mode 100644 index 000000000..31d0abf06 --- /dev/null +++ b/frameworks/slickgrid-react/docs/grid-functionalities/row-move.md @@ -0,0 +1,185 @@ +### Description +Row Move feature allow you to drag a row and move it to another position in the grid. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example07) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vanilla/src/examples/example07.ts) + +## Basic Usage +By default the lib will fallback to its default logic implementation to move the item and readjust the dataset with the updated row positions. If the default logic doesn't work for you, you can provide your own logic for `onBeforeMoveRows` and/or `onMoveRows`. + +#### ViewModel +```tsx +import { SlickRowDetailView } from 'slickgrid-react'; // for v9 and below + +const Example: React.FC = () => { + const [dataset, setDataset] = useState([]); + const [columns, setColumns] = useState([]); + const [detailViewRowCount, setDetailViewRowCount] = useState(8); + const [options, setOptions] = useState(undefined); + const reactGridRef = useRef(null); + + useEffect(() => defineGrid()); + + function reactGridReady(reactGrid: SlickgridReactInstance) { + reactGridRef.current = reactGrid; + } + + function defineGrid() { + // define columns + ... + + // grid options + setGridOptions({ + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + // ... list of different Row Move options + }, + }); + } +} +``` + +## List of all Row Move options + +#### ViewModel +```tsx +function defineGrid() { + // define columns + ... + + // grid options + setGridOptions({ + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + // list of different Row Move options + columnIndexPosition: 0, // provide a different column position if default doesn't work for you + // when using Row Move + Row Selection, you want to move only a single row and we will enable the following flags so it doesn't cancel row selection + singleRowMove: true, // allow multi-row dragging? + disableRowSelection: true, // should we disable row selection? + cancelEditOnDrag: true, + hideRowMoveShadow: false, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, + + // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, + usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 + }, + }); +} +``` + +## Provide your own callback (`onBeforeMoveRows` and `onMoveRows`) +Typical code logic when providing your own custom logic, the code below is literraly the default callback logic (it is only shown as demo and logic that can be used to get started). + +#### ViewModel +```tsx +import { SlickRowDetailView } from 'slickgrid-react'; // for v9 and below + +const Example: React.FC = () => { + const [dataset, setDataset] = useState([]); + const [columns, setColumns] = useState([]); + const [detailViewRowCount, setDetailViewRowCount] = useState(8); + const [options, setOptions] = useState(undefined); + const reactGridRef = useRef(null); + + useEffect(() => defineGrid()); + + function defineGrid() { + // define columns + ... + + // grid options + setGridOptions({ + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + onBeforeMoveRows: this.onBeforeMoveRow.bind(this), + onMoveRows: this.onMoveRows.bind(this) + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, + }, + }); + } + + function onBeforeMoveRow(e: MouseEvent | TouchEvent, data: { rows: number[]; insertBefore: number }) { + for (const rowIdx of data.rows) { + // no point in moving before or after itself + if ( + rowIdx === data.insertBefore || + (rowIdx === data.insertBefore - 1 && data.insertBefore - 1 !== this.aureliaGrid.dataView.getItemCount()) + ) { + e.preventDefault(); // OR eventData.preventDefault(); + return false; + } + } + return true; + } + + function onMoveRows(_e: MouseEvent | TouchEvent, args: any) { + // rows and insertBefore references, + // note that these references are assuming that the dataset isn't filtered at all + // which is not always the case so we will recalcualte them and we won't use these reference afterward + const rows = args.rows as number[]; + const insertBefore = args.insertBefore; + const extractedRows: number[] = []; + + // when moving rows, we need to cancel any sorting that might happen + // we can do this by providing an undefined sort comparer + // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting + this.aureliaGrid.dataView.sort(undefined as any, true); + + // the dataset might be filtered/sorted, + // so we need to get the same dataset as the one that the SlickGrid DataView uses + const tmpDataset = this.aureliaGrid.dataView.getItems(); + const filteredItems = this.aureliaGrid.dataView.getFilteredItems(); + + const itemOnRight = this.aureliaGrid.dataView.getItem(insertBefore); + const insertBeforeFilteredIdx = itemOnRight + ? this.aureliaGrid.dataView.getIdxById(itemOnRight.id) + : this.aureliaGrid.dataView.getItemCount(); + + const filteredRowItems: any[] = []; + rows.forEach((row) => filteredRowItems.push(filteredItems[row])); + const filteredRows = filteredRowItems.map((item) => this.aureliaGrid.dataView.getIdxById(item.id)); + + const left = tmpDataset.slice(0, insertBeforeFilteredIdx); + const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); + + // convert into a final new dataset that has the new order + // we need to resort with + rows.sort((a: number, b: number) => a - b); + for (const filteredRow of filteredRows) { + if (filteredRow) { + extractedRows.push(tmpDataset[filteredRow]); + } + } + filteredRows.reverse(); + for (const row of filteredRows) { + if (row !== undefined && insertBeforeFilteredIdx !== undefined) { + if (row < insertBeforeFilteredIdx) { + left.splice(row, 1); + } else { + right.splice(row - insertBeforeFilteredIdx, 1); + } + } + } + + // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order + const finalDataset = left.concat(extractedRows.concat(right)); + this.dataset = finalDataset; // update dataset and re-render the grid + } +} +``` diff --git a/frameworks/slickgrid-vue/docs/TOC.md b/frameworks/slickgrid-vue/docs/TOC.md index 59fc8dcc6..68a204bb7 100644 --- a/frameworks/slickgrid-vue/docs/TOC.md +++ b/frameworks/slickgrid-vue/docs/TOC.md @@ -73,6 +73,7 @@ * [Pinning (frozen) of Columns/Rows](grid-functionalities/frozen-columns-rows.md) * [Providing data to the grid](grid-functionalities/providing-grid-data.md) * [Row Detail](grid-functionalities/row-detail.md) +* [Row Move (dragging)](grid-functionalities/row-move.md) * [Row Selection](grid-functionalities/row-selection.md) * [Tree Data Grid](grid-functionalities/tree-data-grid.md) * [Row Based Editing Plugin](grid-functionalities/row-based-edit.md) diff --git a/frameworks/slickgrid-vue/docs/grid-functionalities/row-move.md b/frameworks/slickgrid-vue/docs/grid-functionalities/row-move.md new file mode 100644 index 000000000..459fa2bd9 --- /dev/null +++ b/frameworks/slickgrid-vue/docs/grid-functionalities/row-move.md @@ -0,0 +1,191 @@ +### Description +Row Move feature allow you to drag a row and move it to another position in the grid. + +### Demo +[Demo Page](https://ghiscoding.github.io/slickgrid-universal/#/example07) / [Demo ViewModel](https://github.com/ghiscoding/slickgrid-universal/blob/master/demos/vanilla/src/examples/example07.ts) + +## Basic Usage +By default the lib will fallback to its default logic implementation to move the item and readjust the dataset with the updated row positions. If the default logic doesn't work for you, you can provide your own logic for `onBeforeMoveRows` and/or `onMoveRows`. + +#### ViewModel +```vue + +``` + +## List of all Row Move options + +#### ViewModel +```ts +function defineGrid() { + // define columns + ... + + // grid options + gridOptions.value = { + enableAutoResize: true, + // ... + enableRowMoveManager: true, + rowMoveManager: { + // list of different Row Move options + columnIndexPosition: 0, // provide a different column position if default doesn't work for you + // when using Row Move + Row Selection, you want to move only a single row and we will enable the following flags so it doesn't cancel row selection + singleRowMove: true, // allow multi-row dragging? + disableRowSelection: true, // should we disable row selection? + cancelEditOnDrag: true, + hideRowMoveShadow: false, + // you can provide your own `onBeforeMoveRows` and/or `onMoveRows` implementation + // or use the default implementation, however the default won't work with Tree Data + // onBeforeMoveRows: () => {}, + // onMoveRows: () => {}, + onAfterMoveRows: (_e, args) => { + // update dataset for the ms-select list to be updated + this.dataset = args.updatedItems; + }, + + // you can also override the usability of the rows, for example make every 2nd row the only moveable rows, + usabilityOverride: (row, dataContext, grid) => dataContext.id % 2 === 1 + }, + } +} +``` + +## Provide your own callback (`onBeforeMoveRows` and `onMoveRows`) +Typical code logic when providing your own custom logic, the code below is literraly the default callback logic (it is only shown as demo and logic that can be used to get started). + +#### ViewModel +```vue + +``` diff --git a/packages/common/src/extensions/__tests__/rowMoveUtils.spec.ts b/packages/common/src/extensions/__tests__/rowMoveUtils.spec.ts new file mode 100644 index 000000000..df407ae3a --- /dev/null +++ b/packages/common/src/extensions/__tests__/rowMoveUtils.spec.ts @@ -0,0 +1,115 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { SlickEvent, type SlickGrid } from '../../core/index.js'; +import { defaultOnBeforeMoveRows, defaultOnMoveRows } from '../rowMoveUtils.js'; + +const dataViewStub = { + destroy: vi.fn(), + addItem: vi.fn(), + getItem: vi.fn(), + getItems: vi.fn(), + getIdxById: vi.fn(), + getFilteredItems: vi.fn(), + getItemCount: vi.fn(), + setItems: vi.fn(), + sort: vi.fn(), +} as unknown as SlickDataView; + +const gridStub = { + getCellNode: vi.fn(), + getCellFromEvent: vi.fn(), + getData: () => dataViewStub, + getOptions: vi.fn(), + registerPlugin: vi.fn(), + onHeaderMouseEnter: new SlickEvent(), + onMouseEnter: new SlickEvent(), + invalidate: vi.fn(), + resetActiveCell: vi.fn(), + setItems: vi.fn(), +} as unknown as SlickGrid; + +describe('rowMoveUtils', () => { + describe('defaultOnBeforeMoveRows() method', () => { + afterEach(() => { + dataViewStub.getItemCount = vi.fn(); + }); + + it('should return false when trying to move at same position', () => { + vi.spyOn(dataViewStub, 'getItemCount').mockReturnValue(4); + const output = defaultOnBeforeMoveRows(new CustomEvent('change'), { rows: [0, 1, 2, 3], insertBefore: 1, grid: gridStub }); + expect(output).toEqual(false); + }); + + it('should return true when trying to move at available spot', () => { + vi.spyOn(dataViewStub, 'getItemCount').mockReturnValue(500); + const output = defaultOnBeforeMoveRows(new CustomEvent('change'), { rows: [1], insertBefore: 3, grid: gridStub }); + expect(output).toEqual(true); + }); + + it('should return false when dataView.getItemCount() is undefined', () => { + dataViewStub.getItemCount = undefined; + const output = defaultOnBeforeMoveRows(new CustomEvent('change'), { rows: [1], insertBefore: 3, grid: gridStub }); + expect(output).toEqual(false); + }); + }); + + describe('defaultOnMoveRows() method', () => { + afterEach(() => { + dataViewStub.getItemCount = vi.fn(); + }); + + it('should insert after x when getItem() returns item', () => { + const setItemSpy = vi.spyOn(dataViewStub, 'setItems'); + const items = [ + { id: 0, firstName: 'John' }, + { id: 1, firstName: 'Jane' }, + { id: 2, firstName: 'Smith' }, + { id: 3, firstName: 'Bob' }, + ]; + vi.spyOn(dataViewStub, 'getItems').mockReturnValue(items); + vi.spyOn(dataViewStub, 'getFilteredItems').mockReturnValue(items); + vi.spyOn(dataViewStub, 'getItem').mockReturnValue(items[1]); + vi.spyOn(dataViewStub, 'getIdxById').mockReturnValue(0).mockReturnValueOnce(0).mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValueOnce(3); + vi.spyOn(dataViewStub, 'getItemCount').mockReturnValue(items.length); + defaultOnMoveRows(new CustomEvent('change'), { insertBefore: 2, rows: [0, 1, 2, 3], grid: gridStub }); + + expect(setItemSpy).toHaveBeenCalledWith([ + { id: 1, firstName: 'Jane' }, + { id: 2, firstName: 'Smith' }, + { id: 3, firstName: 'Bob' }, + { id: 0, firstName: 'John' }, + { id: 1, firstName: 'Jane' }, + ]); + }); + + it('should insert before x when getItem() returns null', () => { + const setItemSpy = vi.spyOn(dataViewStub, 'setItems'); + const items = [ + { id: 0, firstName: 'John' }, + { id: 1, firstName: 'Jane' }, + { id: 2, firstName: 'Smith' }, + { id: 3, firstName: 'Bob' }, + ]; + vi.spyOn(dataViewStub, 'getItems').mockReturnValue(items); + vi.spyOn(dataViewStub, 'getFilteredItems').mockReturnValue(items); + vi.spyOn(dataViewStub, 'getItem').mockReturnValue(null); + vi.spyOn(dataViewStub, 'getIdxById').mockReturnValue(0).mockReturnValueOnce(0).mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValueOnce(3); + vi.spyOn(dataViewStub, 'getItemCount').mockReturnValue(items.length); + defaultOnMoveRows(new CustomEvent('change'), { insertBefore: 2, rows: [0, 1, 2, 3], grid: gridStub }); + + expect(setItemSpy).toHaveBeenCalledWith([ + { id: 1, firstName: 'Jane' }, + { id: 2, firstName: 'Smith' }, + { id: 3, firstName: 'Bob' }, + { id: 0, firstName: 'John' }, + { id: 1, firstName: 'Jane' }, + ]); + }); + + it('should return false when dataView.getItemCount() is undefined', () => { + const consoleErrorSpy = vi.spyOn(console, 'error').mockReturnValue(); + dataViewStub.getItemCount = undefined; + defaultOnMoveRows(new CustomEvent('change'), { rows: [1], insertBefore: 3, grid: gridStub }); + expect(consoleErrorSpy).toHaveBeenCalledWith('Sorry `defaultOnMoveRows()` only works with SlickDataView'); + }); + }); +}); diff --git a/packages/common/src/extensions/__tests__/slickRowMoveManager.spec.ts b/packages/common/src/extensions/__tests__/slickRowMoveManager.spec.ts index aae3116ae..e3b9b54ac 100644 --- a/packages/common/src/extensions/__tests__/slickRowMoveManager.spec.ts +++ b/packages/common/src/extensions/__tests__/slickRowMoveManager.spec.ts @@ -27,6 +27,18 @@ const getEditorLockMock = { isActive: vi.fn(), }; +const dataViewStub = { + destroy: vi.fn(), + addItem: vi.fn(), + getItem: vi.fn(), + getItems: vi.fn(), + getIdxById: vi.fn(), + getFilteredItems: vi.fn(), + getItemCount: vi.fn(), + setItems: vi.fn(), + sort: vi.fn(), +} as unknown as SlickDataView; + const gridStub = { canCellBeActive: vi.fn(), getActiveCell: vi.fn(), @@ -44,7 +56,9 @@ const gridStub = { getEditorLock: () => getEditorLockMock, getOptions: () => mockGridOptions, getUID: () => GRID_UID, + getData: () => dataViewStub, focus: vi.fn(), + hasDataView: () => true, registerPlugin: vi.fn(), setActiveCell: vi.fn(), setSelectedRows: vi.fn(), @@ -340,6 +354,7 @@ describe('SlickRowMoveManager Plugin', () => { it('should create the plugin and trigger "dragStart" and "dragEnd" events, expect new row being moved when different and expect dragEnd to remove guide/proxy/shadow and finally onMoveRows to publish event and callback to be called', () => { const mockOnMoveRows = vi.fn(); + const mockOnAfterMoveRows = vi.fn(); const mockNewMovedRow = 0; const mockSlickRow = document.createElement('div'); mockSlickRow.className = 'slick-row'; @@ -348,9 +363,10 @@ describe('SlickRowMoveManager Plugin', () => { vi.spyOn(gridStub, 'getColumnByIdx').mockReturnValue(mockColumns[1]); vi.spyOn(gridStub, 'getCellNode').mockReturnValue(mockSlickRow); vi.spyOn(gridStub, 'getSelectedRows').mockReturnValue([2]); + vi.spyOn(dataViewStub, 'getItems').mockReturnValue([{ id: 1, first: 'John' }]); const setSelectRowSpy = vi.spyOn(gridStub, 'setSelectedRows'); - plugin.init(gridStub, { hideRowMoveShadow: false, onMoveRows: mockOnMoveRows }); + plugin.init(gridStub, { hideRowMoveShadow: false, onMoveRows: mockOnMoveRows, onAfterMoveRows: mockOnAfterMoveRows }); const onMoveRowNotifySpy = vi.spyOn(plugin.onMoveRows, 'notify'); const divElm = document.createElement('div'); @@ -383,6 +399,12 @@ describe('SlickRowMoveManager Plugin', () => { gridStub.onDragEnd.notify(mockArgs, mouseEvent); expect(onMoveRowNotifySpy).toHaveBeenCalledWith({ insertBefore: -1, rows: [mockNewMovedRow], grid: gridStub }); expect(mockOnMoveRows).toHaveBeenCalledWith(expect.anything(), { insertBefore: -1, rows: [mockNewMovedRow], grid: gridStub }); + expect(mockOnAfterMoveRows).toHaveBeenCalledWith(expect.anything(), { + insertBefore: -1, + rows: [mockNewMovedRow], + updatedItems: [{ id: 1, first: 'John' }], + grid: gridStub, + }); expect(stopImmediatePropagationSpy).toHaveBeenCalledTimes(2); }); @@ -555,4 +577,56 @@ describe('SlickRowMoveManager Plugin', () => { expect(mockArgs.canMove).toBe(false); expect(mockArgs.guide.style.top).toBe('-1000px'); }); + + it('should create the plugin and trigger "dragStart" and "drag" events, expect new row being moved with default functions', () => { + const mockNewMovedRow = 0; + const mockSlickRow = document.createElement('div'); + mockSlickRow.className = 'slick-row'; + vi.spyOn(gridStub, 'getCellFromEvent').mockReturnValue({ cell: 1, row: mockNewMovedRow }); + vi.spyOn(gridStub, 'getColumns').mockReturnValue(mockColumns); + vi.spyOn(gridStub, 'getColumnByIdx').mockReturnValue(mockColumns[1]); + vi.spyOn(gridStub, 'getCellNode').mockReturnValue(mockSlickRow); + vi.spyOn(gridStub, 'getDataLength').mockReturnValue(5); + vi.spyOn(gridStub, 'getSelectedRows').mockReturnValue([2]); + vi.spyOn(dataViewStub, 'getItemCount').mockReturnValue(4); + const setSelectRowSpy = vi.spyOn(gridStub, 'setSelectedRows'); + + plugin.init(gridStub, { hideRowMoveShadow: false }); + plugin.usabilityOverride(() => true); + vi.spyOn(gridStub.getEditorLock(), 'isActive').mockReturnValue(false); + + const divElm = document.createElement('div'); + const mouseEvent = addVanillaEventPropagation(new Event('mouseenter'), divElm); + const stopImmediatePropagationSpy = vi.spyOn(mouseEvent, 'stopImmediatePropagation'); + const mockArgs = { + deltaX: 0, + deltaY: 1, + offsetX: 2, + offsetY: 3, + row: 2, + rows: [2], + selectedRows: [2], + insertBefore: 4, + canMove: true, + } as any; + gridStub.onDragStart.notify(mockArgs, mouseEvent); + + expect(stopImmediatePropagationSpy).toHaveBeenCalled(); + expect(setSelectRowSpy).toHaveBeenCalledWith([mockNewMovedRow]); + expect(mockArgs.insertBefore).toBe(-1); + expect(mockArgs.selectedRows).toEqual([mockNewMovedRow]); + expect(mockArgs.clonedSlickRow).toBeTruthy(); + expect(mockArgs.guide).toBeTruthy(); + expect(mockArgs.selectionProxy).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-guide')).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-proxy')).toBeTruthy(); + expect(canvasTL.querySelector('.slick-reorder-shadow-row')).toBeTruthy(); + + Object.defineProperty(mouseEvent, 'pageY', { writable: true, configurable: true, value: 12 }); + gridStub.onDrag.notify(mockArgs, mouseEvent); + expect(mockArgs.selectionProxy.style.display).toBe('block'); + expect(mockArgs.selectionProxy.style.top).toBe('7px'); + expect(mockArgs.clonedSlickRow.style.display).toBe('block'); + expect(mockArgs.clonedSlickRow.style.top).toBe('6px'); + }); }); diff --git a/packages/common/src/extensions/rowMoveUtils.ts b/packages/common/src/extensions/rowMoveUtils.ts new file mode 100644 index 000000000..ea7798201 --- /dev/null +++ b/packages/common/src/extensions/rowMoveUtils.ts @@ -0,0 +1,76 @@ +import type { SlickDataView } from '../core/slickDataview.js'; +import type { SlickGrid } from '../core/slickGrid.js'; + +export function defaultOnBeforeMoveRows( + e: MouseEvent | TouchEvent, + args: { rows: number[]; insertBefore: number; grid: SlickGrid } +): boolean { + const dataView = args.grid.getData(); + if (dataView?.getItemCount) { + const itemCount = dataView.getItemCount(); + for (const rowIdx of args.rows) { + // no point in moving before or after itself + if (rowIdx === args.insertBefore || (rowIdx === args.insertBefore - 1 && args.insertBefore - 1 !== itemCount)) { + e.stopPropagation(); + return false; + } + } + return true; + } + return false; +} + +export function defaultOnMoveRows(_e: MouseEvent | TouchEvent, args: { rows: number[]; insertBefore: number; grid: SlickGrid }): void { + // rows and insertBefore references, + // note that these references are assuming that the dataset isn't filtered at all + // which is not always the case so we will recalcualte them and we won't use these reference afterward + const dataView = args.grid.getData(); + if (dataView?.getItemCount) { + const rows = args.rows as number[]; + const insertBefore = args.insertBefore; + const extractedRows: any[] = []; + + // when moving rows, we need to cancel any sorting that might happen + // we can do this by providing an undefined sort comparer + // which basically destroys the current sort comparer without resorting the dataset, it basically keeps the previous sorting + dataView?.sort(undefined as any, true); + + // the dataset might be filtered/sorted, + // so we need to get the same dataset as the one that the SlickGrid DataView uses + const tmpDataset = dataView?.getItems() as any[]; + const filteredItems = dataView?.getFilteredItems() as any[]; + + const itemOnRight = dataView?.getItem(insertBefore); + const insertBeforeFilteredIdx = (itemOnRight ? dataView?.getIdxById(itemOnRight.id) : dataView?.getItemCount()) as number; + + const filteredRowItems: any[] = []; + rows.forEach((row) => filteredRowItems.push(filteredItems[row] as any)); + const filteredRows = filteredRowItems.map((item) => dataView?.getIdxById(item.id)) as number[]; + + const left = tmpDataset.slice(0, insertBeforeFilteredIdx); + const right = tmpDataset.slice(insertBeforeFilteredIdx, tmpDataset.length); + + // convert into a final new dataset that has the new order + // we need to resort with + rows.sort((a: number, b: number) => a - b); + for (const filteredRow of filteredRows) { + extractedRows.push(tmpDataset[filteredRow as number]); + } + filteredRows.reverse(); + for (const row of filteredRows) { + if (row < insertBeforeFilteredIdx) { + left.splice(row, 1); + } else { + right.splice(row - insertBeforeFilteredIdx, 1); + } + } + + // final updated dataset, we need to overwrite the DataView dataset (and our local one) with this new dataset that has a new order + args.grid?.resetActiveCell(); + const finalDataset = left.concat(extractedRows.concat(right)); + dataView.setItems(finalDataset); + args.grid.invalidate(); + } else { + console.error('Sorry `defaultOnMoveRows()` only works with SlickDataView'); + } +} diff --git a/packages/common/src/extensions/slickRowMoveManager.ts b/packages/common/src/extensions/slickRowMoveManager.ts index 847861ae5..229f4cb02 100644 --- a/packages/common/src/extensions/slickRowMoveManager.ts +++ b/packages/common/src/extensions/slickRowMoveManager.ts @@ -2,14 +2,15 @@ import type { BasePubSubService } from '@slickgrid-universal/event-pub-sub'; import { createDomElement, findWidthOrDefault, getOffset, isDefined } from '@slickgrid-universal/utils'; import { SlickEvent, SlickEventData, SlickEventHandler, Utils as SlickUtils, type SlickGrid } from '../core/index.js'; import type { UsabilityOverrideFn } from '../enums/usabilityOverrideFn.type.js'; -import type { - Column, - DragRowMove, - FormatterResultWithHtml, - GridOption, - RowMoveManager, - RowMoveManagerOption, +import { + type Column, + type DragRowMove, + type FormatterResultWithHtml, + type GridOption, + type RowMoveManager, + type RowMoveManagerOption, } from '../interfaces/index.js'; +import { defaultOnBeforeMoveRows, defaultOnMoveRows } from './rowMoveUtils.js'; /** * Row Move Manager options: @@ -196,11 +197,17 @@ export class SlickRowMoveManager { rows: dd.selectedRows, insertBefore: dd.insertBefore, }; - // TODO: this._grid.remapCellCssClasses ? - if (typeof this._addonOptions.onMoveRows === 'function') { - this._addonOptions.onMoveRows(e instanceof SlickEventData ? e.getNativeEvent() : e, eventData); - } + + const evt: MouseEvent | TouchEvent = e instanceof SlickEventData ? e.getNativeEvent() : e; + typeof this._addonOptions.onMoveRows === 'function' + ? this._addonOptions.onMoveRows(evt, eventData) + : defaultOnMoveRows(evt, eventData); this.onMoveRows.notify(eventData); + + if (typeof this._addonOptions.onAfterMoveRows === 'function') { + const updatedItems = (this._grid.hasDataView() ? this._grid.getData().getItems() : this._grid.getData()) as any[]; + this._addonOptions.onAfterMoveRows(evt, { ...eventData, updatedItems }); + } } } @@ -228,10 +235,14 @@ export class SlickRowMoveManager { insertBefore, }; - dd.canMove = !( - this._addonOptions?.onBeforeMoveRows?.(e, eventData) === false || - this.onBeforeMoveRows.notify(eventData).getReturnValue() === false - ); + const evt: MouseEvent | TouchEvent = e instanceof SlickEventData ? e.getNativeEvent() : e; + let beforeMoveRowResult = + typeof this._addonOptions.onBeforeMoveRows === 'function' + ? this._addonOptions.onBeforeMoveRows(evt, eventData) + : defaultOnBeforeMoveRows(evt, eventData); + + // const beforeMoveRowResult = this._addonOptions?.onBeforeMoveRows?.(e, eventData) ?? defaultOnBeforeMoveRows(e, eventData); + dd.canMove = !(beforeMoveRowResult === false || this.onBeforeMoveRows.notify(eventData).getReturnValue() === false); // if there's a UsabilityOverride defined, we also need to verify that the condition is valid if (this._usabilityOverride && dd.canMove) { diff --git a/packages/common/src/interfaces/rowMoveManager.interface.ts b/packages/common/src/interfaces/rowMoveManager.interface.ts index 431899a0f..81104ed00 100644 --- a/packages/common/src/interfaces/rowMoveManager.interface.ts +++ b/packages/common/src/interfaces/rowMoveManager.interface.ts @@ -14,4 +14,10 @@ export interface RowMoveManager extends RowMoveManagerOption { /** SlickGrid Event fired while the row is moved. */ onMoveRows?: (e: MouseEvent | TouchEvent, args: { grid: SlickGrid; rows: number[]; insertBefore: number }) => void; + + /** SlickGrid Event fired after the row is moved. */ + onAfterMoveRows?: ( + e: MouseEvent | TouchEvent, + args: { grid: SlickGrid; rows: number[]; insertBefore: number; updatedItems: any[] } + ) => void; }