UNPKG

8.31 kBJavaScriptView Raw
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 */
8import { Plugin } from 'ckeditor5/src/core';
9import TableSelection from './tableselection';
10import MouseEventsObserver from './tablemouse/mouseeventsobserver';
11import 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 */
16export 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}
170function haveSameTableParent(cellA, cellB) {
171 return cellA.parent.parent == cellB.parent.parent;
172}