1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 | import { Plugin } from 'ckeditor5/src/core';
|
9 | import { first } from 'ckeditor5/src/utils';
|
10 | import TableWalker from './tablewalker';
|
11 | import TableUtils from './tableutils';
|
12 | import { cropTableToDimensions, adjustLastRowIndex, adjustLastColumnIndex } from './utils/structure';
|
13 | import '../theme/tableselection.css';
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | export default class TableSelection extends Plugin {
|
19 | |
20 |
|
21 |
|
22 | static get pluginName() {
|
23 | return 'TableSelection';
|
24 | }
|
25 | |
26 |
|
27 |
|
28 | static get requires() {
|
29 | return [TableUtils, TableUtils];
|
30 | }
|
31 | |
32 |
|
33 |
|
34 | init() {
|
35 | const editor = this.editor;
|
36 | const model = editor.model;
|
37 | const view = editor.editing.view;
|
38 | this.listenTo(model, 'deleteContent', (evt, args) => this._handleDeleteContent(evt, args), { priority: 'high' });
|
39 | this.listenTo(view.document, 'insertText', (evt, data) => this._handleInsertTextEvent(evt, data), { priority: 'high' });
|
40 | this._defineSelectionConverter();
|
41 | this._enablePluginDisabling();
|
42 | }
|
43 | |
44 |
|
45 |
|
46 | getSelectedTableCells() {
|
47 | const tableUtils = this.editor.plugins.get(TableUtils);
|
48 | const selection = this.editor.model.document.selection;
|
49 | const selectedCells = tableUtils.getSelectedTableCells(selection);
|
50 | if (selectedCells.length == 0) {
|
51 | return null;
|
52 | }
|
53 |
|
54 |
|
55 |
|
56 |
|
57 | return selectedCells;
|
58 | }
|
59 | |
60 |
|
61 |
|
62 | getSelectionAsFragment() {
|
63 | const tableUtils = this.editor.plugins.get(TableUtils);
|
64 | const selectedCells = this.getSelectedTableCells();
|
65 | if (!selectedCells) {
|
66 | return null;
|
67 | }
|
68 | return this.editor.model.change(writer => {
|
69 | const documentFragment = writer.createDocumentFragment();
|
70 | const { first: firstColumn, last: lastColumn } = tableUtils.getColumnIndexes(selectedCells);
|
71 | const { first: firstRow, last: lastRow } = tableUtils.getRowIndexes(selectedCells);
|
72 | const sourceTable = selectedCells[0].findAncestor('table');
|
73 | let adjustedLastRow = lastRow;
|
74 | let adjustedLastColumn = lastColumn;
|
75 |
|
76 |
|
77 | if (tableUtils.isSelectionRectangular(selectedCells)) {
|
78 | const dimensions = {
|
79 | firstColumn,
|
80 | lastColumn,
|
81 | firstRow,
|
82 | lastRow
|
83 | };
|
84 | adjustedLastRow = adjustLastRowIndex(sourceTable, dimensions);
|
85 | adjustedLastColumn = adjustLastColumnIndex(sourceTable, dimensions);
|
86 | }
|
87 | const cropDimensions = {
|
88 | startRow: firstRow,
|
89 | startColumn: firstColumn,
|
90 | endRow: adjustedLastRow,
|
91 | endColumn: adjustedLastColumn
|
92 | };
|
93 | const table = cropTableToDimensions(sourceTable, cropDimensions, writer);
|
94 | writer.insert(table, documentFragment, 0);
|
95 | return documentFragment;
|
96 | });
|
97 | }
|
98 | |
99 |
|
100 |
|
101 |
|
102 |
|
103 |
|
104 |
|
105 |
|
106 |
|
107 |
|
108 |
|
109 |
|
110 |
|
111 | setCellSelection(anchorCell, targetCell) {
|
112 | const cellsToSelect = this._getCellsToSelect(anchorCell, targetCell);
|
113 | this.editor.model.change(writer => {
|
114 | writer.setSelection(cellsToSelect.cells.map(cell => writer.createRangeOn(cell)), { backward: cellsToSelect.backward });
|
115 | });
|
116 | }
|
117 | |
118 |
|
119 |
|
120 | getFocusCell() {
|
121 | const selection = this.editor.model.document.selection;
|
122 | const focusCellRange = [...selection.getRanges()].pop();
|
123 | const element = focusCellRange.getContainedElement();
|
124 | if (element && element.is('element', 'tableCell')) {
|
125 | return element;
|
126 | }
|
127 | return null;
|
128 | }
|
129 | |
130 |
|
131 |
|
132 | getAnchorCell() {
|
133 | const selection = this.editor.model.document.selection;
|
134 | const anchorCellRange = first(selection.getRanges());
|
135 | const element = anchorCellRange.getContainedElement();
|
136 | if (element && element.is('element', 'tableCell')) {
|
137 | return element;
|
138 | }
|
139 | return null;
|
140 | }
|
141 | |
142 |
|
143 |
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 |
|
150 | _defineSelectionConverter() {
|
151 | const editor = this.editor;
|
152 | const highlighted = new Set();
|
153 | editor.conversion.for('editingDowncast').add(dispatcher => dispatcher.on('selection', (evt, data, conversionApi) => {
|
154 | const viewWriter = conversionApi.writer;
|
155 | clearHighlightedTableCells(viewWriter);
|
156 | const selectedCells = this.getSelectedTableCells();
|
157 | if (!selectedCells) {
|
158 | return;
|
159 | }
|
160 | for (const tableCell of selectedCells) {
|
161 | const viewElement = conversionApi.mapper.toViewElement(tableCell);
|
162 | viewWriter.addClass('ck-editor__editable_selected', viewElement);
|
163 | highlighted.add(viewElement);
|
164 | }
|
165 | const lastViewCell = conversionApi.mapper.toViewElement(selectedCells[selectedCells.length - 1]);
|
166 | viewWriter.setSelection(lastViewCell, 0);
|
167 | }, { priority: 'lowest' }));
|
168 | function clearHighlightedTableCells(viewWriter) {
|
169 | for (const previouslyHighlighted of highlighted) {
|
170 | viewWriter.removeClass('ck-editor__editable_selected', previouslyHighlighted);
|
171 | }
|
172 | highlighted.clear();
|
173 | }
|
174 | }
|
175 | |
176 |
|
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 | _enablePluginDisabling() {
|
183 | const editor = this.editor;
|
184 | this.on('change:isEnabled', () => {
|
185 | if (!this.isEnabled) {
|
186 | const selectedCells = this.getSelectedTableCells();
|
187 | if (!selectedCells) {
|
188 | return;
|
189 | }
|
190 | editor.model.change(writer => {
|
191 | const position = writer.createPositionAt(selectedCells[0], 0);
|
192 | const range = editor.model.schema.getNearestSelectionRange(position);
|
193 | writer.setSelection(range);
|
194 | });
|
195 | }
|
196 | });
|
197 | }
|
198 | |
199 |
|
200 |
|
201 |
|
202 |
|
203 | _handleDeleteContent(event, args) {
|
204 | const tableUtils = this.editor.plugins.get(TableUtils);
|
205 | const selection = args[0];
|
206 | const options = args[1];
|
207 | const model = this.editor.model;
|
208 | const isBackward = !options || options.direction == 'backward';
|
209 | const selectedTableCells = tableUtils.getSelectedTableCells(selection);
|
210 | if (!selectedTableCells.length) {
|
211 | return;
|
212 | }
|
213 | event.stop();
|
214 | model.change(writer => {
|
215 | const tableCellToSelect = selectedTableCells[isBackward ? selectedTableCells.length - 1 : 0];
|
216 | model.change(writer => {
|
217 | for (const tableCell of selectedTableCells) {
|
218 | model.deleteContent(writer.createSelection(tableCell, 'in'));
|
219 | }
|
220 | });
|
221 | const rangeToSelect = model.schema.getNearestSelectionRange(writer.createPositionAt(tableCellToSelect, 0));
|
222 |
|
223 |
|
224 | if (selection.is('documentSelection')) {
|
225 | writer.setSelection(rangeToSelect);
|
226 | }
|
227 | else {
|
228 | selection.setTo(rangeToSelect);
|
229 | }
|
230 | });
|
231 | }
|
232 | |
233 |
|
234 |
|
235 |
|
236 |
|
237 |
|
238 |
|
239 |
|
240 |
|
241 |
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 |
|
248 | _handleInsertTextEvent(evt, data) {
|
249 | const editor = this.editor;
|
250 | const selectedCells = this.getSelectedTableCells();
|
251 | if (!selectedCells) {
|
252 | return;
|
253 | }
|
254 | const view = editor.editing.view;
|
255 | const mapper = editor.editing.mapper;
|
256 | const viewRanges = selectedCells.map(tableCell => view.createRangeOn(mapper.toViewElement(tableCell)));
|
257 | data.selection = view.createSelection(viewRanges);
|
258 | }
|
259 | |
260 |
|
261 |
|
262 |
|
263 |
|
264 |
|
265 | _getCellsToSelect(anchorCell, targetCell) {
|
266 | const tableUtils = this.editor.plugins.get('TableUtils');
|
267 | const startLocation = tableUtils.getCellLocation(anchorCell);
|
268 | const endLocation = tableUtils.getCellLocation(targetCell);
|
269 | const startRow = Math.min(startLocation.row, endLocation.row);
|
270 | const endRow = Math.max(startLocation.row, endLocation.row);
|
271 | const startColumn = Math.min(startLocation.column, endLocation.column);
|
272 | const endColumn = Math.max(startLocation.column, endLocation.column);
|
273 |
|
274 | const selectionMap = new Array(endRow - startRow + 1).fill(null).map(() => []);
|
275 | const walkerOptions = {
|
276 | startRow,
|
277 | endRow,
|
278 | startColumn,
|
279 | endColumn
|
280 | };
|
281 | for (const { row, cell } of new TableWalker(anchorCell.findAncestor('table'), walkerOptions)) {
|
282 | selectionMap[row - startRow].push(cell);
|
283 | }
|
284 | const flipVertically = endLocation.row < startLocation.row;
|
285 | const flipHorizontally = endLocation.column < startLocation.column;
|
286 | if (flipVertically) {
|
287 | selectionMap.reverse();
|
288 | }
|
289 | if (flipHorizontally) {
|
290 | selectionMap.forEach(row => row.reverse());
|
291 | }
|
292 | return {
|
293 | cells: selectionMap.flat(),
|
294 | backward: flipVertically || flipHorizontally
|
295 | };
|
296 | }
|
297 | }
|