Skip to content

Commit c44eda8

Browse files
authored
fix: revamp all cell selection range with key combos, fixes #935 (#940)
* fix: revamp all cell selection range with key combos, fixes #935
1 parent 251c91e commit c44eda8

10 files changed

+126
-57
lines changed

cypress/e2e/example-auto-scroll-when-dragging.cy.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ describe('Example - Auto scroll when dragging', { retries: 1 }, () => {
2828
});
2929

3030
it('should select border shown in cell selection model, and hidden in row selection model when dragging', { scrollBehavior: false }, function () {
31-
cy.getCell(0, 1, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
31+
cy.getNthCell(0, 1, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
3232
.as('cell1')
3333
.dragStart();
3434

@@ -39,7 +39,7 @@ describe('Example - Auto scroll when dragging', { retries: 1 }, () => {
3939
cy.get('#myGrid .slick-range-decorator').should('not.be.exist');
4040
cy.get('#myGrid .slick-cell.selected').should('have.length', 6);
4141

42-
cy.getCell(0, 1, '', { parentSelector: "#myGrid2", rowHeight: cellHeight })
42+
cy.getNthCell(0, 1, '', { parentSelector: "#myGrid2", rowHeight: cellHeight })
4343
.as('cell2')
4444
.dragStart();
4545
cy.get('#myGrid2 .slick-range-decorator').should('be.exist').and('have.css', 'border-style').and('equal', 'none');
@@ -89,7 +89,7 @@ describe('Example - Auto scroll when dragging', { retries: 1 }, () => {
8989

9090
function getIntervalUntilRow16Displayed(selector, px) {
9191
const viewportSelector = (selector + ' .slick-viewport:first');
92-
cy.getCell(0, 1, '', { parentSelector: selector, rowHeight: cellHeight })
92+
cy.getNthCell(0, 1, '', { parentSelector: selector, rowHeight: cellHeight })
9393
.dragStart();
9494
return cy.get(viewportSelector).invoke('scrollTop').then(scrollBefore => {
9595
cy.dragOutside('bottom', 0, px, { parentSelector: selector, rowHeight: cellHeight });
@@ -275,7 +275,7 @@ describe('Example - Auto scroll when dragging', { retries: 1 }, () => {
275275
});
276276

277277
function testDragInGrouping(selector) {
278-
cy.getCell(7, 0, 'bottomRight', { parentSelector: selector, rowHeight: cellHeight })
278+
cy.getNthCell(7, 0, 'bottomRight', { parentSelector: selector, rowHeight: cellHeight })
279279
.dragStart();
280280
cy.get(selector + ' .slick-viewport:last').as('viewport').invoke('scrollTop').then(scrollBefore => {
281281
cy.dragOutside('bottom', 400, 300, { parentSelector: selector, rowHeight: cellHeight });

cypress/e2e/example-spreadsheet-dataview.cy.ts

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ describe('Example - Spreadsheet with DataView and Cell Selection', { retries: 0
6161
.should('have.text', '{"fromRow":10,"fromCell":4,"toCell":4,"toRow":46}');
6262
});
6363

64-
it('should click on cell E12 then End key w/selection E46-E99', () => {
64+
it('should click on cell E46 then Shift+End key with full row horizontal selection E46-CV46', () => {
6565
cy.getCell(46, 5, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
6666
.as('cell_E46')
6767
.click();
@@ -70,19 +70,43 @@ describe('Example - Spreadsheet with DataView and Cell Selection', { retries: 0
7070
.type('{shift}{end}');
7171

7272
cy.get('#selectionRange')
73-
.should('have.text', '{"fromRow":46,"fromCell":5,"toCell":5,"toRow":99}');
73+
.should('have.text', '{"fromRow":46,"fromCell":5,"toCell":100,"toRow":46}');
7474
});
7575

76-
it('should click on cell C85 then End key w/selection C0-C85', () => {
77-
cy.getCell(85, 3, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
78-
.as('cell_C85')
76+
it('should click on cell CP54 then Ctrl+Shift+End keys with selection E46-CV99', () => {
77+
cy.getCell(54, 94, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
78+
.as('cell_CP54')
7979
.click();
8080

81-
cy.get('@cell_C85')
82-
.type('{shift}{home}');
81+
cy.get('@cell_CP54')
82+
.type('{ctrl}{shift}{end}');
83+
84+
cy.get('#selectionRange')
85+
.should('have.text', '{"fromRow":54,"fromCell":94,"toCell":100,"toRow":99}');
86+
});
87+
88+
it('should click on cell CP95 then Ctrl+Shift+Home keys with selection C0-CP95', () => {
89+
cy.getCell(95, 98, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
90+
.as('cell_CP95')
91+
.click();
92+
93+
cy.get('@cell_CP95')
94+
.type('{ctrl}{shift}{home}');
95+
96+
cy.get('#selectionRange')
97+
.should('have.text', '{"fromRow":0,"fromCell":0,"toCell":98,"toRow":95}');
98+
});
99+
100+
it('should click on cell CR5 then Ctrl+Home keys and expect to scroll back to cell A0 without any selection range', () => {
101+
cy.getCell(5, 95, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
102+
.as('cell_CR95')
103+
.click();
104+
105+
cy.get('@cell_CR95')
106+
.type('{ctrl}{home}');
83107

84108
cy.get('#selectionRange')
85-
.should('have.text', '{"fromRow":0,"fromCell":3,"toCell":3,"toRow":85}');
109+
.should('have.text', '');
86110
});
87111
});
88112

@@ -92,7 +116,7 @@ describe('Example - Spreadsheet with DataView and Cell Selection', { retries: 0
92116
cy.get('[data-val="25"]').click();
93117
});
94118

95-
it('should click on cell B14 then Shift+End w/selection B14-24', () => {
119+
it('should click on cell B14 then Shift+End with selection B14-24', () => {
96120
cy.getCell(14, 2, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
97121
.as('cell_B14')
98122
.click();
@@ -101,45 +125,45 @@ describe('Example - Spreadsheet with DataView and Cell Selection', { retries: 0
101125
.type('{shift}{end}');
102126

103127
cy.get('#selectionRange')
104-
.should('have.text', '{"fromRow":14,"fromCell":2,"toCell":2,"toRow":24}');
128+
.should('have.text', '{"fromRow":14,"fromCell":2,"toCell":100,"toRow":14}');
105129
});
106130

107-
it('should click on cell C19 then Shift+End w/selection C0-19', () => {
108-
cy.getCell(19, 2, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
109-
.as('cell_C19')
131+
it('should click on cell CS14 then Shift+Home with selection A14-CS14', () => {
132+
cy.getCell(14, 97, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
133+
.as('cell_CS14')
110134
.click();
111135

112-
cy.get('@cell_C19')
136+
cy.get('@cell_CS14')
113137
.type('{shift}{home}');
114138

115139
cy.get('#selectionRange')
116-
.should('have.text', '{"fromRow":0,"fromCell":2,"toCell":2,"toRow":19}');
140+
.should('have.text', '{"fromRow":14,"fromCell":0,"toCell":97,"toRow":14}');
117141
});
118142

119-
it('should click on cell E3 then Shift+PageDown multiple times with current page selection starting at E3 w/selection E3-24', () => {
120-
cy.getCell(3, 5, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
121-
.as('cell_E3')
143+
it('should click on cell CN3 then Shift+PageDown multiple times with current page selection starting at E3 w/selection E3-24', () => {
144+
cy.getCell(3, 95, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
145+
.as('cell_CN3')
122146
.click();
123147

124-
cy.get('@cell_E3')
148+
cy.get('@cell_CN3')
125149
.type('{shift}{pagedown}{pagedown}{pagedown}');
126150

127151
cy.get('#selectionRange')
128-
.should('have.text', '{"fromRow":3,"fromCell":5,"toCell":5,"toRow":24}');
152+
.should('have.text', '{"fromRow":3,"fromCell":95,"toCell":95,"toRow":24}');
129153
});
130154

131-
it('should change to 2nd page then click on cell D41 then Shift+PageUp multiple times with current page selection w/selection D25-41', () => {
155+
it('should change to 2nd page then click on cell CN41 then Shift+PageUp multiple times with current page selection w/selection D25-41', () => {
132156
cy.get('.slick-pager .sgi-chevron-right').click();
133157

134-
cy.getCell(15, 4, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
135-
.as('cell_D41')
158+
cy.getCell(15, 92, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
159+
.as('cell_CN41')
136160
.click();
137161

138-
cy.get('@cell_D41')
162+
cy.get('@cell_CN41')
139163
.type('{shift}{pageup}{pageup}{pageup}');
140164

141165
cy.get('#selectionRange')
142-
.should('have.text', '{"fromRow":0,"fromCell":4,"toCell":4,"toRow":15}');
166+
.should('have.text', '{"fromRow":0,"fromCell":92,"toCell":92,"toRow":15}');
143167
});
144168
});
145169
});

cypress/e2e/example-spreadsheet.cy.ts

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ describe('Example - Spreadsheet and Cell Selection', { retries: 0 }, () => {
6060
.should('have.text', '{"fromRow":10,"fromCell":4,"toCell":4,"toRow":46}');
6161
});
6262

63-
it('should click on cell E12 then End key w/selection E46-E99', () => {
63+
it('should click on cell E46 then Shift+End key with full row horizontal selection E46-CV46', () => {
6464
cy.getCell(46, 5, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
6565
.as('cell_E46')
6666
.click();
@@ -69,18 +69,42 @@ describe('Example - Spreadsheet and Cell Selection', { retries: 0 }, () => {
6969
.type('{shift}{end}');
7070

7171
cy.get('#selectionRange')
72-
.should('have.text', '{"fromRow":46,"fromCell":5,"toCell":5,"toRow":99}');
72+
.should('have.text', '{"fromRow":46,"fromCell":5,"toCell":100,"toRow":46}');
7373
});
7474

75-
it('should click on cell C85 then End key w/selection C0-C85', () => {
76-
cy.getCell(85, 3, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
77-
.as('cell_C85')
75+
it('should click on cell CP54 then Ctrl+Shift+End keys with selection E46-CV99', () => {
76+
cy.getCell(54, 94, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
77+
.as('cell_CP54')
7878
.click();
7979

80-
cy.get('@cell_C85')
81-
.type('{shift}{home}');
80+
cy.get('@cell_CP54')
81+
.type('{ctrl}{shift}{end}');
8282

8383
cy.get('#selectionRange')
84-
.should('have.text', '{"fromRow":0,"fromCell":3,"toCell":3,"toRow":85}');
84+
.should('have.text', '{"fromRow":54,"fromCell":94,"toCell":100,"toRow":99}');
85+
});
86+
87+
it('should click on cell CP95 then Ctrl+Shift+Home keys with selection C0-CP95', () => {
88+
cy.getCell(95, 98, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
89+
.as('cell_CP95')
90+
.click();
91+
92+
cy.get('@cell_CP95')
93+
.type('{ctrl}{shift}{home}');
94+
95+
cy.get('#selectionRange')
96+
.should('have.text', '{"fromRow":0,"fromCell":0,"toCell":98,"toRow":95}');
97+
});
98+
99+
it('should click on cell CR5 then Ctrl+Home keys and expect to scroll back to cell A0 without any selection range', () => {
100+
cy.getCell(5, 95, '', { parentSelector: "#myGrid", rowHeight: cellHeight })
101+
.as('cell_CR95')
102+
.click();
103+
104+
cy.get('@cell_CR95')
105+
.type('{ctrl}{home}');
106+
107+
cy.get('#selectionRange')
108+
.should('have.text', '');
85109
});
86110
});

cypress/support/commands.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,30 @@ declare global {
3333
// triggerHover: (elements: NodeListOf<HTMLElement>) => void;
3434
convertPosition(viewport: string): Chainable<HTMLElement | JQuery<HTMLElement> | { x: string; y: string; }>;
3535
getCell(row: number, col: number, viewport?: string, options?: { parentSelector?: string, rowHeight?: number; }): Chainable<HTMLElement | JQuery<HTMLElement>>;
36+
getNthCell(row: number, nthCol: number, viewport?: string, options?: { parentSelector?: string, rowHeight?: number; }): Chainable<HTMLElement | JQuery<HTMLElement>>;
3637
restoreLocalStorage(): Chainable<HTMLElement | JQuery<HTMLElement>>;
3738
saveLocalStorage(): Chainable<HTMLElement | JQuery<HTMLElement>>;
3839
}
3940
}
4041
}
4142

4243
// convert position like 'topLeft' to the object { x: 'left|right', y: 'top|bottom' }
43-
Cypress.Commands.add('convertPosition', (viewport = 'topLeft') => cy.wrap(convertPosition(viewport)))
44+
Cypress.Commands.add('convertPosition', (viewport = 'topLeft') => cy.wrap(convertPosition(viewport)));
4445

4546
Cypress.Commands.add('getCell', (row, col, viewport = 'topLeft', { parentSelector = '', rowHeight = 25 } = {}) => {
4647
const position = convertPosition(viewport);
4748
const canvasSelectorX = position.x ? `.grid-canvas-${position.x}` : '';
4849
const canvasSelectorY = position.y ? `.grid-canvas-${position.y}` : '';
4950

50-
return cy.get(`${parentSelector} ${canvasSelectorX}${canvasSelectorY} [style="top: ${row * rowHeight}px;"] > .slick-cell:nth(${col})`);
51+
return cy.get(`${parentSelector} ${canvasSelectorX}${canvasSelectorY} [style="top: ${row * rowHeight}px;"] > .slick-cell.l${col}.r${col}`);
52+
});
53+
54+
Cypress.Commands.add('getNthCell', (row, nthCol, viewport = 'topLeft', { parentSelector = '', rowHeight = 25 } = {}) => {
55+
const position = convertPosition(viewport);
56+
const canvasSelectorX = position.x ? `.grid-canvas-${position.x}` : '';
57+
const canvasSelectorY = position.y ? `.grid-canvas-${position.y}` : '';
58+
59+
return cy.get(`${parentSelector} ${canvasSelectorX}${canvasSelectorY} [style="top: ${row * rowHeight}px;"] > .slick-cell:nth(${nthCol})`);
5160
});
5261

5362
const LOCAL_STORAGE_MEMORY = {};

cypress/support/drag.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,13 @@ Cypress.Commands.add('dragStart', { prevSubject: true }, (subject, { cellWidth =
1818
return cy.wrap(subject).click({ force: true })
1919
.trigger('mousedown', { which: 1 })
2020
.trigger('mousemove', cellWidth / 3, cellHeight / 3);
21-
})
21+
});
2222

2323
// use a different command name than "drag" so that it doesn't conflict with the "@4tw/cypress-drag-drop" lib
2424
// @ts-ignore
2525
Cypress.Commands.add('dragCell', { prevSubject: true }, (subject, addRow, addCell, { cellWidth = 80, cellHeight = 25 } = {}) => {
2626
return cy.wrap(subject).trigger('mousemove', cellWidth * (addCell + 0.5), cellHeight * (addRow + 0.5), { force: true });
27-
})
27+
});
2828

2929
Cypress.Commands.add('dragOutside', (viewport = 'topLeft', ms = 0, px = 0, { parentSelector = 'div[class^="slickgrid_"]', scrollbarDimension = 17 } = {}) => {
3030
const $parent = cy.$$(parentSelector);
@@ -49,17 +49,17 @@ Cypress.Commands.add('dragOutside', (viewport = 'topLeft', ms = 0, px = 0, { par
4949
cy.wait(ms);
5050
}
5151
return;
52-
})
52+
});
5353

5454
Cypress.Commands.add('dragEnd', { prevSubject: 'optional' }, (subject, gridSelector = 'div[class^="slickgrid_"]') => {
5555
cy.get(gridSelector).trigger('mouseup', { force: true });
5656
return;
57-
})
57+
});
5858

5959
export function getScrollDistanceWhenDragOutsideGrid(selector, viewport, dragDirection, fromRow, fromCol, px = 100) {
6060
return (cy as any).convertPosition(viewport).then((_viewportPosition: { x: number; y: number; }) => {
6161
const viewportSelector = `${selector} .slick-viewport-${_viewportPosition.x}.slick-viewport-${_viewportPosition.y}`;
62-
(cy as any).getCell(fromRow, fromCol, viewport, { parentSelector: selector })
62+
(cy as any).getNthCell(fromRow, fromCol, viewport, { parentSelector: selector })
6363
.dragStart();
6464
return cy.get(viewportSelector).then($viewport => {
6565
const scrollTopBefore = $viewport.scrollTop();

examples/example-frozen-columns-and-rows-spreadsheet.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ <h2>
3939
<li>Use Ctrl-C and Ctrl-V keyboard shortcuts to cut and paste cells</li>
4040
<li>Use Esc to cancel a copy and paste operation</li>
4141
<li>Edit the cell and select a cell range to paste the range</li>
42-
<li>Cell Selection using "Shift+{key}" where "key" can be any of:</li>
42+
<li>Cell Selection using "Shift+{key}" or "Ctrl+Shift+{key}" where "key" can be any of:</li>
4343
<ul>
4444
<li>Arrow Up/Down/Left/Right</li>
4545
<li>Page Up/Down</li>

examples/example-spreadsheet-dataview.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ <h2>
4040
<li>Use Ctrl-C and Ctrl-V keyboard shortcuts to cut and paste cells</li>
4141
<li>Use Esc to cancel a copy and paste operation</li>
4242
<li>Edit the cell and select a cell range to paste the range</li>
43-
<li>Cell Selection using "Shift+{key}" where "key" can be any of:</li>
43+
<li>Cell Selection using "Shift+{key}" or "Ctrl+Shift+{key}" where "key" can be any of:</li>
4444
<ul>
4545
<li>Arrow Up/Down/Left/Right</li>
4646
<li>Page Up/Down</li>

examples/example-spreadsheet.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ <h2>
3838
<li>Use Ctrl-C and Ctrl-V keyboard shortcuts to cut and paste cells</li>
3939
<li>Use Esc to cancel a copy and paste operation</li>
4040
<li>Edit the cell and select a cell range to paste the range</li>
41-
<li>Cell Selection using "Shift+{key}" where "key" can be any of:</li>
41+
<li>Cell Selection using "Shift+{key}" or "Ctrl+Shift+{key}" where "key" can be any of:</li>
4242
<ul>
4343
<li>Arrow Up/Down/Left/Right</li>
4444
<li>Page Up/Down</li>

src/plugins/slick.cellselectionmodel.ts

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,12 @@ export class SlickCellSelectionModel {
140140

141141
protected handleActiveCellChange(_e: Event, args: OnActiveCellChangedEventArgs) {
142142
this._prevSelectedRow = undefined;
143-
if (this._options?.selectActiveCell && Utils.isDefined(args.row) && Utils.isDefined(args.cell)) {
143+
const isCellDefined = Utils.isDefined(args.cell);
144+
const isRowDefined = Utils.isDefined(args.row);
145+
146+
if (this._options?.selectActiveCell && isRowDefined && isCellDefined) {
144147
this.setSelectedRanges([new SlickRange(args.row, args.cell)]);
145-
} else if (!this._options?.selectActiveCell) {
148+
} else if (!this._options?.selectActiveCell || (!isRowDefined && !isCellDefined)) {
146149
// clear the previous selection once the cell changes
147150
this.setSelectedRanges([]);
148151
}
@@ -154,17 +157,16 @@ export class SlickCellSelectionModel {
154157

155158
protected handleKeyDown(e: KeyboardEvent) {
156159
let ranges: SlickRange_[], last: SlickRange_;
160+
const colLn = this._grid.getColumns().length;
157161
const active = this._grid.getActiveCell();
158-
const metaKey = e.ctrlKey || e.metaKey;
159162
let dataLn = 0;
160163
if (this._dataView) {
161164
dataLn = this._dataView?.getPagingInfo().pageSize || this._dataView.getLength();
162165
} else {
163166
dataLn = this._grid.getDataLength();
164167
}
165168

166-
if (active && e.shiftKey && !metaKey && !e.altKey && this.isKeyAllowed(e.key)) {
167-
169+
if (active && (e.shiftKey || e.ctrlKey) && !e.altKey && this.isKeyAllowed(e.key)) {
168170
ranges = this.getSelectedRanges().slice();
169171
if (!ranges.length) {
170172
ranges.push(new SlickRange(active.row, active.cell));
@@ -184,9 +186,10 @@ export class SlickCellSelectionModel {
184186
const dirRow = active.row === last.fromRow ? 1 : -1;
185187
const dirCell = active.cell === last.fromCell ? 1 : -1;
186188
const isSingleKeyMove = e.key.startsWith('Arrow');
189+
let toCell: undefined | number = undefined;
187190
let toRow = 0;
188191

189-
if (isSingleKeyMove) {
192+
if (isSingleKeyMove && !e.ctrlKey) {
190193
// single cell move: (Arrow{Up/ArrowDown/ArrowLeft/ArrowRight})
191194
if (e.key === 'ArrowLeft') {
192195
dCell -= dirCell;
@@ -207,9 +210,17 @@ export class SlickCellSelectionModel {
207210
this._prevSelectedRow = active.row;
208211
}
209212

210-
if (e.key === 'Home') {
213+
if (e.shiftKey && !e.ctrlKey && e.key === 'Home') {
214+
toCell = 0;
215+
toRow = active.row;
216+
} else if (e.shiftKey && !e.ctrlKey && e.key === 'End') {
217+
toCell = colLn - 1;
218+
toRow = active.row;
219+
} else if (e.ctrlKey && e.shiftKey && e.key === 'Home') {
220+
toCell = 0;
211221
toRow = 0;
212-
} else if (e.key === 'End') {
222+
} else if (e.ctrlKey && e.shiftKey && e.key === 'End') {
223+
toCell = colLn - 1;
213224
toRow = dataLn - 1;
214225
} else if (e.key === 'PageUp') {
215226
if (this._prevSelectedRow >= 0) {
@@ -230,7 +241,8 @@ export class SlickCellSelectionModel {
230241
}
231242

232243
// define new selection range
233-
const new_last = new SlickRange(active.row, active.cell, toRow, active.cell + dirCell * dCell);
244+
toCell ??= active.cell + dirCell * dCell;
245+
const new_last = new SlickRange(active.row, active.cell, toRow, toCell);
234246
if (this.removeInvalidRanges([new_last]).length) {
235247
ranges.push(new_last);
236248
const viewRow = dirRow > 0 ? new_last.toRow : new_last.fromRow;

0 commit comments

Comments
 (0)