/
rowMoveController.js
320 lines (269 loc) · 11 KB
/
rowMoveController.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
import { isUndefined } from '../../../helpers/mixed';
import { warn } from '../../../helpers/console';
import { toSingleLine } from '../../../helpers/templateLiteralTag';
/**
* Helper class for the row-move-related operations.
*
* @private
* @class RowMoveController
*/
export default class RowMoveController {
/**
* Reference to the Nested Rows plugin instance.
*
* @type {NestedRows}
*/
plugin;
/**
* Reference to the Handsontable instance.
*
* @type {Handsontable.Core}
*/
hot;
/**
* Reference to the Data Manager class instance.
*
* @type {DataManager}
*/
dataManager;
/**
* Reference to the Collapsing UI class instance.
*
* @type {CollapsingUI}
*/
collapsingUI;
constructor(plugin) {
this.plugin = plugin;
this.hot = plugin.hot;
this.dataManager = plugin.dataManager;
this.collapsingUI = plugin.collapsingUI;
}
/**
* `beforeRowMove` hook callback.
*
* @param {Array} rows Array of visual row indexes to be moved.
* @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements
* will be placed after the moving action. To check the visualization of the final index, please take a look at
* [documentation](@/guides/rows/row-moving/row-moving.md).
* @param {undefined|number} dropIndex Visual row index, being a drop index for the moved rows. Points to where we
* are going to drop the moved elements. To check visualization of drop index please take a look at
* [documentation](@/guides/rows/row-moving/row-moving.md).
* @param {boolean} movePossible Indicates if it's possible to move rows to the desired position.
* @fires Hooks#afterRowMove
* @returns {boolean}
*/
onBeforeRowMove(rows, finalIndex, dropIndex, movePossible) {
const improperUsage = this.displayAPICompatibilityWarning({ rows, finalIndex, dropIndex, movePossible });
if (improperUsage) {
return false;
}
this.movedToCollapsed = false;
const dropToLastRow = dropIndex === this.hot.countRows();
const physicalDropIndex = dropToLastRow ?
this.hot.countSourceRows() :
this.dataManager.translateTrimmedRow(dropIndex);
let allowMove = true;
const physicalStartIndexes = rows.map((rowIndex) => {
// Don't do the logic for the rest of the rows, as it's bound to fail anyway.
if (!allowMove) {
return false;
}
const physicalRowIndex = this.dataManager.translateTrimmedRow(rowIndex);
allowMove = this.shouldAllowMoving(physicalRowIndex, physicalDropIndex);
return physicalRowIndex;
});
const willDataChange = physicalStartIndexes.indexOf(physicalDropIndex) === -1;
if (!allowMove || !willDataChange) {
return false;
}
const baseParent = this.getBaseParent(physicalStartIndexes);
const targetParent = this.getTargetParent(dropToLastRow, physicalDropIndex);
const sameParent = baseParent === targetParent;
this.movedToCollapsed = this.collapsingUI.areChildrenCollapsed(targetParent);
// Stash the current state of collapsed rows
this.collapsingUI.collapsedRowsStash.stash();
this.shiftCollapsibleParentsLocations(physicalStartIndexes, physicalDropIndex, sameParent);
this.moveRows(physicalStartIndexes, physicalDropIndex, targetParent);
this.dataManager.rewriteCache();
this.moveCellsMeta(physicalStartIndexes, physicalDropIndex);
this.collapsingUI.collapsedRowsStash.applyStash(false);
// TODO: Trying to mock real work of the `ManualRowMove` plugin. It was blocked by returning `false` below.
this.hot.runHooks('afterRowMove',
rows, finalIndex, dropIndex, movePossible, movePossible && this.isRowOrderChanged(rows, finalIndex));
// Not necessary - added to keep compatibility with other plugins (namely: columnSummary).
this.hot.render();
this.selectCells(rows, dropIndex);
return false;
}
/**
* Display a `dragRows`/`moveRows` method compatibility warning if needed.
*
* @param {object} beforeMoveRowHookArgs A set of arguments from the `beforeMoveRow` hook.
* @returns {boolean} `true` if is a result of an improper usage of the moving API.
*/
displayAPICompatibilityWarning(beforeMoveRowHookArgs) {
const {
rows,
finalIndex,
dropIndex,
movePossible
} = beforeMoveRowHookArgs;
let shouldTerminate = false;
if (isUndefined(dropIndex)) {
warn(toSingleLine`Since version 8.0.0 of the Handsontable the 'moveRows' method isn't used for moving rows\x20
when the NestedRows plugin is enabled. Please use the 'dragRows' method instead.`);
// TODO: Trying to mock real work of the `ManualRowMove` plugin. It was blocked by returning `false` below.
this.hot.runHooks('afterRowMove', rows, finalIndex, dropIndex, movePossible, false);
shouldTerminate = true;
}
return shouldTerminate;
}
/**
* Check if the moving action should be allowed.
*
* @param {number} physicalRowIndex Physical start row index.
* @param {number} physicalDropIndex Physical drop index.
* @returns {boolean} `true` if it should continue with the moving action.
*/
shouldAllowMoving(physicalRowIndex, physicalDropIndex) {
/*
We can't move rows when any of them is:
- a parent
- a top-level element
- is being moved to the top level
- is being moved to the position of any of the moved rows (not changing position)
*/
return !(
this.dataManager.isParent(physicalRowIndex) ||
this.dataManager.isRowHighestLevel(physicalRowIndex) ||
physicalRowIndex === physicalDropIndex ||
physicalDropIndex === 0
);
}
/**
* Get the base row parent.
*
* @param {number} physicalStartIndexes Physical start row index.
* @returns {object|null} The base row parent.
*/
getBaseParent(physicalStartIndexes) {
return this.dataManager.getRowParent(physicalStartIndexes[0]);
}
/**
* Get the target row parent.
*
* @param {boolean} dropToLastRow `true` if the row is moved to the last row of the table.
* @param {number} physicalDropIndex Physical drop row index.
* @returns {object|null} The target row parent.
*/
getTargetParent(dropToLastRow, physicalDropIndex) {
let targetParent = this.dataManager.getRowParent(dropToLastRow ? physicalDropIndex - 1 : physicalDropIndex);
// If we try to move an element to the place of a top-level parent, snap the element to the previous top-level
// parent's children instead
if (targetParent === null || targetParent === undefined) {
targetParent = this.dataManager.getRowParent(physicalDropIndex - 1);
}
return targetParent;
}
/**
* Shift the cached collapsible rows position according to the move action.
*
* @param {number[]} physicalStartIndexes Physical start row indexes.
* @param {number} physicalDropIndex Physical drop index.
* @param {boolean} sameParent `true` if the row's being moved between siblings of the same parent.
*/
shiftCollapsibleParentsLocations(physicalStartIndexes, physicalDropIndex, sameParent) {
if (!sameParent) {
if (Math.max(...physicalStartIndexes) <= physicalDropIndex) {
this.collapsingUI.collapsedRowsStash.shiftStash(physicalStartIndexes[0], physicalDropIndex,
(-1) * physicalStartIndexes.length);
} else {
this.collapsingUI.collapsedRowsStash.shiftStash(physicalDropIndex, physicalStartIndexes[0],
physicalStartIndexes.length);
}
}
}
/**
* Move the rows at the provided coordinates.
*
* @param {number[]} physicalStartIndexes Physical indexes of the rows about to be moved.
* @param {number} physicalDropIndex Physical drop index.
* @param {object} targetParent Parent of the destination row.
*/
moveRows(physicalStartIndexes, physicalDropIndex, targetParent) {
const moveToLastChild = physicalDropIndex === this.dataManager.getRowIndex(targetParent) +
this.dataManager.countChildren(targetParent) + 1;
this.hot.batchRender(() => {
physicalStartIndexes.forEach((physicalStartIndex) => {
this.dataManager.moveRow(physicalStartIndex, physicalDropIndex, this.movedToCollapsed, moveToLastChild);
});
});
}
/**
* Move the cell meta for multiple rows.
*
* @param {number[]} baseIndexes Array of indexes for the rows being moved.
* @param {number} targetIndex Index of the destination of the move.
*/
moveCellsMeta(baseIndexes, targetIndex) {
const rowsOfMeta = [];
const movingDown = Math.max(...baseIndexes) < targetIndex;
baseIndexes.forEach((baseIndex) => {
rowsOfMeta.push(this.hot.getCellMetaAtRow(baseIndex));
});
this.hot.spliceCellsMeta(baseIndexes[0], baseIndexes.length);
this.hot.spliceCellsMeta(targetIndex - (movingDown ? rowsOfMeta.length : 0), 0, ...rowsOfMeta);
}
/**
* Select cells after the move.
*
* @param {Array} rows Array of visual row indexes to be moved.
* @param {undefined|number} dropIndex Visual row index, being a drop index for the moved rows. Points to where we
* are going to drop the moved elements. To check visualization of drop index please take a look at
* [documentation](@/guides/rows/row-moving/row-moving.md).
*/
selectCells(rows, dropIndex) {
const rowsLen = rows.length;
let startRow = 0;
let endRow = 0;
let selection = null;
let lastColIndex = null;
if (this.movedToCollapsed) {
let physicalDropIndex = null;
if (rows[rowsLen - 1] < dropIndex) {
physicalDropIndex = this.dataManager.translateTrimmedRow(dropIndex - rowsLen);
} else {
physicalDropIndex = this.dataManager.translateTrimmedRow(dropIndex);
}
const parentObject = this.dataManager.getRowParent(
physicalDropIndex === null ? this.hot.countSourceRows() - 1 : physicalDropIndex - 1
);
const parentIndex = this.dataManager.getRowIndex(parentObject);
startRow = this.dataManager.untranslateTrimmedRow(parentIndex);
endRow = startRow;
} else if (rows[rowsLen - 1] < dropIndex) {
endRow = dropIndex - 1;
startRow = endRow - rowsLen + 1;
} else {
startRow = dropIndex;
endRow = startRow + rowsLen - 1;
}
selection = this.hot.selection;
lastColIndex = this.hot.countCols() - 1;
selection.setRangeStart(this.hot._createCellCoords(startRow, 0));
selection.setRangeEnd(this.hot._createCellCoords(endRow, lastColIndex), true);
}
// TODO: Reimplementation of function which is inside the `ManualRowMove` plugin.
/**
* Indicates if order of rows was changed.
*
* @param {Array} movedRows Array of visual row indexes to be moved.
* @param {number} finalIndex Visual row index, being a start index for the moved rows. Points to where the elements
* will be placed after the moving action. To check the visualization of the final index, please take a look at
* [documentation](@/guides/rows/row-moving/row-moving.md).
* @returns {boolean}
*/
isRowOrderChanged(movedRows, finalIndex) {
return movedRows.some((row, nrOfMovedElement) => row - nrOfMovedElement !== finalIndex);
}
}