UNPKG

@ckeditor/ckeditor5-table

Version:

Table feature for CKEditor 5.

179 lines (178 loc) 8.43 kB
/** * @license Copyright (c) 2003-2025, CKSource Holding sp. z o.o. All rights reserved. * For licensing, see LICENSE.md or https://ckeditor.com/legal/ckeditor-licensing-options */ /** * @module table/tablemouse */ import { Plugin } from 'ckeditor5/src/core.js'; import TableSelection from './tableselection.js'; import MouseEventsObserver from './tablemouse/mouseeventsobserver.js'; import TableUtils from './tableutils.js'; /** * This plugin enables a table cells' selection with the mouse. * It is loaded automatically by the {@link module:table/table~Table} plugin. */ export default class TableMouse extends Plugin { /** * @inheritDoc */ static get pluginName() { return 'TableMouse'; } /** * @inheritDoc */ static get isOfficialPlugin() { return true; } /** * @inheritDoc */ static get requires() { return [TableSelection, TableUtils]; } /** * @inheritDoc */ init() { const editor = this.editor; // Currently the MouseObserver only handles `mousedown` and `mouseup` events. // TODO move to the engine? editor.editing.view.addObserver(MouseEventsObserver); this._enableShiftClickSelection(); this._enableMouseDragSelection(); } /** * Enables making cells selection by <kbd>Shift</kbd>+click. Creates a selection from the cell which previously held * the selection to the cell which was clicked. It can be the same cell, in which case it selects a single cell. */ _enableShiftClickSelection() { const editor = this.editor; const tableUtils = editor.plugins.get(TableUtils); let blockSelectionChange = false; const tableSelection = editor.plugins.get(TableSelection); this.listenTo(editor.editing.view.document, 'mousedown', (evt, domEventData) => { const selection = editor.model.document.selection; if (!this.isEnabled || !tableSelection.isEnabled) { return; } if (!domEventData.domEvent.shiftKey) { return; } const anchorCell = tableSelection.getAnchorCell() || tableUtils.getTableCellsContainingSelection(selection)[0]; if (!anchorCell) { return; } const targetCell = this._getModelTableCellFromDomEvent(domEventData); if (targetCell && haveSameTableParent(anchorCell, targetCell)) { blockSelectionChange = true; tableSelection.setCellSelection(anchorCell, targetCell); domEventData.preventDefault(); } }); this.listenTo(editor.editing.view.document, 'mouseup', () => { blockSelectionChange = false; }); // We need to ignore a `selectionChange` event that is fired after we render our new table cells selection. // When downcasting table cells selection to the view, we put the view selection in the last selected cell // in a place that may not be natively a "correct" location. This is – we put it directly in the `<td>` element. // All browsers fire the native `selectionchange` event. // However, all browsers except Safari return the selection in the exact place where we put it // (even though it's visually normalized). Safari returns `<td><p>^foo` that makes our selection observer // fire our `selectionChange` event (because the view selection that we set in the first step differs from the DOM selection). // Since `selectionChange` is fired, we automatically update the model selection that moves it that paragraph. // This breaks our dear cells selection. // // Theoretically this issue concerns only Safari that is the only browser that do normalize the selection. // However, to avoid code branching and to have a good coverage for this event blocker, I enabled it for all browsers. // // Note: I'm keeping the `blockSelectionChange` state separately for shift+click and mouse drag (exact same logic) // so I don't have to try to analyze whether they don't overlap in some weird cases. Probably they don't. // But I have other things to do, like writing this comment. this.listenTo(editor.editing.view.document, 'selectionChange', evt => { if (blockSelectionChange) { // @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' ); evt.stop(); } }, { priority: 'highest' }); } /** * Enables making cells selection by dragging. * * The selection is made only on mousemove. Mouse tracking is started on mousedown. * However, the cells selection is enabled only after the mouse cursor left the anchor cell. * Thanks to that normal text selection within one cell works just fine. However, you can still select * just one cell by leaving the anchor cell and moving back to it. */ _enableMouseDragSelection() { const editor = this.editor; let anchorCell, targetCell; let beganCellSelection = false; let blockSelectionChange = false; const tableSelection = editor.plugins.get(TableSelection); this.listenTo(editor.editing.view.document, 'mousedown', (evt, domEventData) => { if (!this.isEnabled || !tableSelection.isEnabled) { return; } // Make sure to not conflict with the shift+click listener and any other possible handler. if (domEventData.domEvent.shiftKey || domEventData.domEvent.ctrlKey || domEventData.domEvent.altKey) { return; } anchorCell = this._getModelTableCellFromDomEvent(domEventData); }); this.listenTo(editor.editing.view.document, 'mousemove', (evt, domEventData) => { if (!domEventData.domEvent.buttons) { return; } if (!anchorCell) { return; } const newTargetCell = this._getModelTableCellFromDomEvent(domEventData); if (newTargetCell && haveSameTableParent(anchorCell, newTargetCell)) { targetCell = newTargetCell; // Switch to the cell selection mode after the mouse cursor left the anchor cell. // Switch off only on mouseup (makes selecting a single cell possible). if (!beganCellSelection && targetCell != anchorCell) { beganCellSelection = true; } } // Yep, not making a cell selection yet. See method docs. if (!beganCellSelection) { return; } blockSelectionChange = true; tableSelection.setCellSelection(anchorCell, targetCell); domEventData.preventDefault(); }); this.listenTo(editor.editing.view.document, 'mouseup', () => { beganCellSelection = false; blockSelectionChange = false; anchorCell = null; targetCell = null; }); // See the explanation in `_enableShiftClickSelection()`. this.listenTo(editor.editing.view.document, 'selectionChange', evt => { if (blockSelectionChange) { // @if CK_DEBUG // console.log( 'Blocked selectionChange to avoid breaking table cells selection.' ); evt.stop(); } }, { priority: 'highest' }); } /** * Returns the model table cell element based on the target element of the passed DOM event. * * @returns Returns the table cell or `undefined`. */ _getModelTableCellFromDomEvent(domEventData) { // Note: Work with positions (not element mapping) because the target element can be an attribute or other non-mapped element. const viewTargetElement = domEventData.target; const viewPosition = this.editor.editing.view.createPositionAt(viewTargetElement, 0); const modelPosition = this.editor.editing.mapper.toModelPosition(viewPosition); const modelElement = modelPosition.parent; return modelElement.findAncestor('tableCell', { includeSelf: true }); } } function haveSameTableParent(cellA, cellB) { return cellA.parent.parent == cellB.parent.parent; }