Files
railwaysystem/RuoYi-Vue/ruoyi-ui/node_modules1/handsontable/editorManager.mjs
T
2026-09-10 15:40:10 +08:00

456 lines
14 KiB
JavaScript

import { isFunctionKey, isCtrlMetaKey } from "./helpers/unicode.mjs";
import { stopImmediatePropagation } from "./helpers/dom/event.mjs";
import { isOutsideInput } from "./helpers/dom/element.mjs";
import { getEditorInstance } from "./editors/registry.mjs";
import EventManager from "./eventManager.mjs";
import { isDefined } from "./helpers/mixed.mjs";
export const SHORTCUTS_GROUP_NAVIGATION = 'editorManager.navigation';
export const SHORTCUTS_GROUP_EDITOR = 'editorManager.handlingEditor';
class EditorManager {
/**
* @param {Core} instance The Handsontable instance.
* @param {TableMeta} tableMeta The table meta instance.
* @param {Selection} selection The selection instance.
*/
constructor(instance, tableMeta, selection) {
/**
* Instance of {@link Handsontable}.
*
* @private
* @type {Handsontable}
*/
this.instance = instance;
/**
* Reference to an instance's private GridSettings object.
*
* @private
* @type {GridSettings}
*/
this.tableMeta = tableMeta;
/**
* Instance of {@link Selection}.
*
* @private
* @type {Selection}
*/
this.selection = selection;
/**
* Instance of {@link EventManager}.
*
* @private
* @type {EventManager}
*/
this.eventManager = new EventManager(instance);
/**
* Determines if EditorManager is destroyed.
*
* @private
* @type {boolean}
*/
this.destroyed = false;
/**
* Determines if EditorManager is locked.
*
* @private
* @type {boolean}
*/
this.lock = false;
/**
* A reference to an instance of the activeEditor.
*
* @private
* @type {BaseEditor}
*/
this.activeEditor = void 0;
/**
* Keeps a reference to the cell's properties object.
*
* @type {object}
*/
this.cellProperties = void 0;
const shortcutManager = this.instance.getShortcutManager();
shortcutManager.addContext('editor');
this.registerShortcuts();
this.instance.addHook('afterDocumentKeyDown', event => this.onAfterDocumentKeyDown(event));
// Open editor when text composition is started (IME editor)
this.eventManager.addEventListener(this.instance.rootDocument.documentElement, 'compositionstart', event => {
if (!this.destroyed && this.instance.isListening()) {
this.openEditor('', event);
}
});
this.instance.view._wt.update('onCellDblClick', (event, coords, elem) => this.onCellDblClick(event, coords, elem));
}
/**
* Register shortcuts responsible for handling some actions related to an editor.
*
* @private
*/
registerShortcuts() {
const shortcutManager = this.instance.getShortcutManager();
const gridContext = shortcutManager.getContext('grid');
const editorContext = shortcutManager.getContext('editor');
const config = {
group: SHORTCUTS_GROUP_EDITOR
};
editorContext.addShortcuts([{
keys: [['Enter'], ['Enter', 'Shift'], ['Enter', 'Control/Meta'], ['Enter', 'Control/Meta', 'Shift']],
callback: (event, keys) => {
this.closeEditorAndSaveChanges(shortcutManager.isCtrlPressed());
this.moveSelectionAfterEnter(keys.includes('shift'));
}
}, {
keys: [['Escape'], ['Escape', 'Control/Meta']],
callback: () => {
this.closeEditorAndRestoreOriginalValue(shortcutManager.isCtrlPressed());
this.activeEditor.focus();
}
}], config);
gridContext.addShortcuts([{
keys: [['F2']],
callback: event => {
this.openEditor(null, event, true);
}
}, {
keys: [['Backspace'], ['Delete']],
callback: () => {
this.instance.emptySelectedCells();
this.prepareEditor();
}
}, {
keys: [['Enter'], ['Enter', 'Shift']],
callback: (event, keys) => {
if (this.instance.getSettings().enterBeginsEditing) {
if (this.cellProperties.readOnly) {
this.moveSelectionAfterEnter();
} else {
this.openEditor(null, event, true);
}
} else {
this.moveSelectionAfterEnter(keys.includes('shift'));
}
stopImmediatePropagation(event); // required by HandsontableEditor
}
}], {
...config,
runOnlyIf: () => isDefined(this.instance.getSelected())
});
}
/**
* Lock the editor from being prepared and closed. Locking the editor prevents its closing and
* reinitialized after selecting the new cell. This feature is necessary for a mobile editor.
*/
lockEditor() {
this.lock = true;
}
/**
* Unlock the editor from being prepared and closed. This method restores the original behavior of
* the editors where for every new selection its instances are closed.
*/
unlockEditor() {
this.lock = false;
}
/**
* Destroy current editor, if exists.
*
* @param {boolean} revertOriginal If `false` and the cell using allowInvalid option,
* then an editor won't be closed until validation is passed.
*/
destroyEditor(revertOriginal) {
if (!this.lock) {
this.closeEditor(revertOriginal);
}
}
/**
* Get active editor.
*
* @returns {BaseEditor}
*/
getActiveEditor() {
return this.activeEditor;
}
/**
* Prepare text input to be displayed at given grid cell.
*/
prepareEditor() {
if (this.lock) {
return;
}
if (this.activeEditor && this.activeEditor.isWaiting()) {
this.closeEditor(false, false, dataSaved => {
if (dataSaved) {
this.prepareEditor();
}
});
return;
}
const {
row,
col
} = this.instance.getSelectedRangeLast().highlight;
const modifiedCellCoords = this.instance.runHooks('modifyGetCellCoords', row, col);
let visualRowToCheck = row;
let visualColumnToCheck = col;
if (Array.isArray(modifiedCellCoords)) {
[visualRowToCheck, visualColumnToCheck] = modifiedCellCoords;
}
// Getting values using the modified coordinates.
this.cellProperties = this.instance.getCellMeta(visualRowToCheck, visualColumnToCheck);
const {
activeElement
} = this.instance.rootDocument;
// Blurring the `activeElement` removes the unwanted border around the focusable element (#6877)
// and resets the `document.activeElement` property. The blurring should happen only when the
// previously selected input element has not belonged to the Handsontable editor. If blurring is
// triggered for all elements, there is a problem with the disappearing IME editor (#9672).
if (activeElement && isOutsideInput(activeElement)) {
activeElement.blur();
}
if (!this.isCellEditable()) {
this.clearActiveEditor();
return;
}
const td = this.instance.getCell(row, col, true);
// Skip the preparation when the cell is not rendered in the DOM. The cell is scrolled out of
// the table's viewport.
if (td) {
const editorClass = this.instance.getCellEditor(this.cellProperties);
const prop = this.instance.colToProp(visualColumnToCheck);
const originalValue = this.instance.getSourceDataAtCell(this.instance.toPhysicalRow(visualRowToCheck), visualColumnToCheck);
this.activeEditor = getEditorInstance(editorClass, this.instance);
// Using not modified coordinates, as we need to get the table element using selection coordinates.
// There is an extra translation in the editor for saving value.
this.activeEditor.prepare(row, col, prop, td, originalValue, this.cellProperties);
}
}
/**
* Check is editor is opened/showed.
*
* @returns {boolean}
*/
isEditorOpened() {
return this.activeEditor && this.activeEditor.isOpened();
}
/**
* Open editor with initial value.
*
* @param {null|string} newInitialValue New value from which editor will start if handled property it's not the `null`.
* @param {Event} event The event object.
* @param {boolean} [enableFullEditMode=false] When true, an editor works in full editing mode. Mode disallows closing an editor
* when arrow keys are pressed.
*/
openEditor(newInitialValue, event) {
let enableFullEditMode = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
if (!this.isCellEditable()) {
this.clearActiveEditor();
return;
}
if (!this.activeEditor) {
const {
row,
col
} = this.instance.getSelectedRangeLast().highlight;
const renderableRowIndex = this.instance.rowIndexMapper.getRenderableFromVisualIndex(row);
const renderableColumnIndex = this.instance.columnIndexMapper.getRenderableFromVisualIndex(col);
this.instance.view.scrollViewport(this.instance._createCellCoords(renderableRowIndex, renderableColumnIndex));
this.instance.view.render();
this.prepareEditor();
}
if (this.activeEditor) {
if (enableFullEditMode) {
this.activeEditor.enableFullEditMode();
}
this.activeEditor.beginEditing(newInitialValue, event);
}
}
/**
* Close editor, finish editing cell.
*
* @param {boolean} restoreOriginalValue If `true`, then closes editor without saving value from the editor into a cell.
* @param {boolean} isCtrlPressed If `true`, then editor will save value to each cell in the last selected range.
* @param {Function} callback The callback function, fired after editor closing.
*/
closeEditor(restoreOriginalValue, isCtrlPressed, callback) {
if (this.activeEditor) {
this.activeEditor.finishEditing(restoreOriginalValue, isCtrlPressed, callback);
} else if (callback) {
callback(false);
}
}
/**
* Close editor and save changes.
*
* @param {boolean} isCtrlPressed If `true`, then editor will save value to each cell in the last selected range.
*/
closeEditorAndSaveChanges(isCtrlPressed) {
this.closeEditor(false, isCtrlPressed);
}
/**
* Close editor and restore original value.
*
* @param {boolean} isCtrlPressed Indication of whether the CTRL button is pressed.
*/
closeEditorAndRestoreOriginalValue(isCtrlPressed) {
this.closeEditor(true, isCtrlPressed);
}
/**
* Clears reference to an instance of the active editor.
*
* @private
*/
clearActiveEditor() {
this.activeEditor = void 0;
}
/**
* Checks if the currently selected cell (pointed by selection highlight coords) is editable.
* Editable cell is when:
* - the cell has defined an editor type;
* - the cell is not marked as read-only;
* - the cell is not hidden.
*
* @private
* @returns {boolean}
*/
isCellEditable() {
const editorClass = this.instance.getCellEditor(this.cellProperties);
const {
row,
col
} = this.instance.getSelectedRangeLast().highlight;
const {
rowIndexMapper,
columnIndexMapper
} = this.instance;
const isCellHidden = rowIndexMapper.isHidden(this.instance.toPhysicalRow(row)) || columnIndexMapper.isHidden(this.instance.toPhysicalColumn(col));
if (this.cellProperties.readOnly || !editorClass || isCellHidden) {
return false;
}
return true;
}
/**
* Controls selection's behaviour after clicking `Enter`.
*
* @private
* @param {boolean} isShiftPressed If `true`, then the selection will move up after hit enter.
*/
moveSelectionAfterEnter(isShiftPressed) {
const enterMoves = typeof this.tableMeta.enterMoves === 'function' ? this.tableMeta.enterMoves(event) : this.tableMeta.enterMoves;
if (isShiftPressed) {
// move selection up
this.selection.transformStart(-enterMoves.row, -enterMoves.col);
} else {
// move selection down (add a new row if needed)
this.selection.transformStart(enterMoves.row, enterMoves.col, true);
}
}
/**
* OnAfterDocumentKeyDown callback.
*
* @private
* @param {KeyboardEvent} event The keyboard event object.
*/
onAfterDocumentKeyDown(event) {
if (!this.instance.isListening()) {
return;
}
const {
keyCode
} = event;
if (!this.selection.isSelected()) {
return;
}
// catch CTRL but not right ALT (which in some systems triggers ALT+CTRL)
const isCtrlPressed = (event.ctrlKey || event.metaKey) && !event.altKey;
if (!this.activeEditor || this.activeEditor && !this.activeEditor.isWaiting()) {
if (!isFunctionKey(keyCode) && !isCtrlMetaKey(keyCode) && !isCtrlPressed && !this.isEditorOpened()) {
const shortcutManager = this.instance.getShortcutManager();
const editorContext = shortcutManager.getContext('editor');
const runOnlySelectedConfig = {
runOnlyIf: () => isDefined(this.instance.getSelected()),
group: SHORTCUTS_GROUP_NAVIGATION
};
editorContext.addShortcuts([{
keys: [['ArrowUp']],
callback: () => {
this.instance.selection.transformStart(-1, 0);
}
}, {
keys: [['ArrowDown']],
callback: () => {
this.instance.selection.transformStart(1, 0);
}
}, {
keys: [['ArrowLeft']],
callback: () => {
this.instance.selection.transformStart(0, -1 * this.instance.getDirectionFactor());
}
}, {
keys: [['ArrowRight']],
callback: () => {
this.instance.selection.transformStart(0, this.instance.getDirectionFactor());
}
}], runOnlySelectedConfig);
this.openEditor('', event);
}
}
}
/**
* OnCellDblClick callback.
*
* @private
* @param {MouseEvent} event The mouse event object.
* @param {object} coords The cell coordinates.
* @param {HTMLTableCellElement|HTMLTableHeaderCellElement} elem The element which triggers the action.
*/
onCellDblClick(event, coords, elem) {
// may be TD or TH
if (elem.nodeName === 'TD') {
this.openEditor(null, event, true);
}
}
/**
* Destroy the instance.
*/
destroy() {
this.destroyed = true;
this.eventManager.destroy();
}
}
const instances = new WeakMap();
/**
* @param {Core} hotInstance The Handsontable instance.
* @param {TableMeta} tableMeta The table meta class instance.
* @param {Selection} selection The selection instance.
* @returns {EditorManager}
*/
EditorManager.getInstance = function (hotInstance, tableMeta, selection) {
let editorManager = instances.get(hotInstance);
if (!editorManager) {
editorManager = new EditorManager(hotInstance, tableMeta, selection);
instances.set(hotInstance, editorManager);
}
return editorManager;
};
export default EditorManager;