1 | /**
|
2 | * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
|
3 | * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-oss-license
|
4 | */
|
5 | /**
|
6 | * @module table/tablemouse
|
7 | */
|
8 | import { Plugin } from 'ckeditor5/src/core';
|
9 | import TableSelection from './tableselection';
|
10 | import MouseEventsObserver from './tablemouse/mouseeventsobserver';
|
11 | import TableUtils from './tableutils';
|
12 | /**
|
13 | * This plugin enables a table cells' selection with the mouse.
|
14 | * It is loaded automatically by the {@link module:table/table~Table} plugin.
|
15 | */
|
16 | export default class TableMouse extends Plugin {
|
17 | /**
|
18 | * @inheritDoc
|
19 | */
|
20 | static get pluginName() {
|
21 | return 'TableMouse';
|
22 | }
|
23 | /**
|
24 | * @inheritDoc
|
25 | */
|
26 | static get requires() {
|
27 | return [TableSelection, TableUtils];
|
28 | }
|
29 | /**
|
30 | * @inheritDoc
|
31 | */
|
32 | init() {
|
33 | const editor = this.editor;
|
34 | // Currently the MouseObserver only handles `mousedown` and `mouseup` events.
|
35 | // TODO move to the engine?
|
36 | editor.editing.view.addObserver(MouseEventsObserver);
|
37 | this._enableShiftClickSelection();
|
38 | this._enableMouseDragSelection();
|
39 | }
|
40 | /**
|
41 | * Enables making cells selection by <kbd>Shift</kbd>+click. Creates a selection from the cell which previously held
|
42 | * the selection to the cell which was clicked. It can be the same cell, in which case it selects a single cell.
|
43 | */
|
44 | _enableShiftClickSelection() {
|
45 | const editor = this.editor;
|
46 | const tableUtils = editor.plugins.get(TableUtils);
|
47 | let blockSelectionChange = false;
|
48 | const tableSelection = editor.plugins.get(TableSelection);
|
49 | this.listenTo(editor.editing.view.document, 'mousedown', (evt, domEventData) => {
|
50 | const selection = editor.model.document.selection;
|
51 | if (!this.isEnabled || !tableSelection.isEnabled) {
|
52 | return;
|
53 | }
|
54 | if (!domEventData.domEvent.shiftKey) {
|
55 | return;
|
56 | }
|
57 | const anchorCell = tableSelection.getAnchorCell() || tableUtils.getTableCellsContainingSelection(selection)[0];
|
58 | if (!anchorCell) {
|
59 | return;
|
60 | }
|
61 | const targetCell = this._getModelTableCellFromDomEvent(domEventData);
|
62 | if (targetCell && haveSameTableParent(anchorCell, targetCell)) {
|
63 | blockSelectionChange = true;
|
64 | tableSelection.setCellSelection(anchorCell, targetCell);
|
65 | domEventData.preventDefault();
|
66 | }
|
67 | });
|
68 | this.listenTo(editor.editing.view.document, 'mouseup', () => {
|
69 | blockSelectionChange = false;
|
70 | });
|
71 | // We need to ignore a `selectionChange` event that is fired after we render our new table cells selection.
|
72 | // When downcasting table cells selection to the view, we put the view selection in the last selected cell
|
73 | // in a place that may not be natively a "correct" location. This is – we put it directly in the `<td>` element.
|
74 | // All browsers fire the native `selectionchange` event.
|
75 | // However, all browsers except Safari return the selection in the exact place where we put it
|
76 | // (even though it's visually normalized). Safari returns `<td><p>^foo` that makes our selection observer
|
77 | // fire our `selectionChange` event (because the view selection that we set in the first step differs from the DOM selection).
|
78 | // Since `selectionChange` is fired, we automatically update the model selection that moves it that paragraph.
|
79 | // This breaks our dear cells selection.
|
80 | //
|
81 | // Theoretically this issue concerns only Safari that is the only browser that do normalize the selection.
|
82 | // However, to avoid code branching and to have a good coverage for this event blocker, I enabled it for all browsers.
|
83 | //
|
84 | // Note: I'm keeping the `blockSelectionChange` state separately for shift+click and mouse drag (exact same logic)
|
85 | // so I don't have to try to analyze whether they don't overlap in some weird cases. Probably they don't.
|
86 | // But I have other things to do, like writing this comment.
|
87 | this.listenTo(editor.editing.view.document, 'selectionChange', evt => {
|
88 | if (blockSelectionChange) {
|
89 | // @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' );
|
90 | evt.stop();
|
91 | }
|
92 | }, { priority: 'highest' });
|
93 | }
|
94 | /**
|
95 | * Enables making cells selection by dragging.
|
96 | *
|
97 | * The selection is made only on mousemove. Mouse tracking is started on mousedown.
|
98 | * However, the cells selection is enabled only after the mouse cursor left the anchor cell.
|
99 | * Thanks to that normal text selection within one cell works just fine. However, you can still select
|
100 | * just one cell by leaving the anchor cell and moving back to it.
|
101 | */
|
102 | _enableMouseDragSelection() {
|
103 | const editor = this.editor;
|
104 | let anchorCell, targetCell;
|
105 | let beganCellSelection = false;
|
106 | let blockSelectionChange = false;
|
107 | const tableSelection = editor.plugins.get(TableSelection);
|
108 | this.listenTo(editor.editing.view.document, 'mousedown', (evt, domEventData) => {
|
109 | if (!this.isEnabled || !tableSelection.isEnabled) {
|
110 | return;
|
111 | }
|
112 | // Make sure to not conflict with the shift+click listener and any other possible handler.
|
113 | if (domEventData.domEvent.shiftKey || domEventData.domEvent.ctrlKey || domEventData.domEvent.altKey) {
|
114 | return;
|
115 | }
|
116 | anchorCell = this._getModelTableCellFromDomEvent(domEventData);
|
117 | });
|
118 | this.listenTo(editor.editing.view.document, 'mousemove', (evt, domEventData) => {
|
119 | if (!domEventData.domEvent.buttons) {
|
120 | return;
|
121 | }
|
122 | if (!anchorCell) {
|
123 | return;
|
124 | }
|
125 | const newTargetCell = this._getModelTableCellFromDomEvent(domEventData);
|
126 | if (newTargetCell && haveSameTableParent(anchorCell, newTargetCell)) {
|
127 | targetCell = newTargetCell;
|
128 | // Switch to the cell selection mode after the mouse cursor left the anchor cell.
|
129 | // Switch off only on mouseup (makes selecting a single cell possible).
|
130 | if (!beganCellSelection && targetCell != anchorCell) {
|
131 | beganCellSelection = true;
|
132 | }
|
133 | }
|
134 | // Yep, not making a cell selection yet. See method docs.
|
135 | if (!beganCellSelection) {
|
136 | return;
|
137 | }
|
138 | blockSelectionChange = true;
|
139 | tableSelection.setCellSelection(anchorCell, targetCell);
|
140 | domEventData.preventDefault();
|
141 | });
|
142 | this.listenTo(editor.editing.view.document, 'mouseup', () => {
|
143 | beganCellSelection = false;
|
144 | blockSelectionChange = false;
|
145 | anchorCell = null;
|
146 | targetCell = null;
|
147 | });
|
148 | // See the explanation in `_enableShiftClickSelection()`.
|
149 | this.listenTo(editor.editing.view.document, 'selectionChange', evt => {
|
150 | if (blockSelectionChange) {
|
151 | // @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' );
|
152 | evt.stop();
|
153 | }
|
154 | }, { priority: 'highest' });
|
155 | }
|
156 | /**
|
157 | * Returns the model table cell element based on the target element of the passed DOM event.
|
158 | *
|
159 | * @returns Returns the table cell or `undefined`.
|
160 | */
|
161 | _getModelTableCellFromDomEvent(domEventData) {
|
162 | // Note: Work with positions (not element mapping) because the target element can be an attribute or other non-mapped element.
|
163 | const viewTargetElement = domEventData.target;
|
164 | const viewPosition = this.editor.editing.view.createPositionAt(viewTargetElement, 0);
|
165 | const modelPosition = this.editor.editing.mapper.toModelPosition(viewPosition);
|
166 | const modelElement = modelPosition.parent;
|
167 | return modelElement.findAncestor('tableCell', { includeSelf: true });
|
168 | }
|
169 | }
|
170 | function haveSameTableParent(cellA, cellB) {
|
171 | return cellA.parent.parent == cellB.parent.parent;
|
172 | }
|