UNPKG

handsontable

Version:

Handsontable is a JavaScript Data Grid available for React, Angular and Vue.

423 lines (401 loc) • 13.9 kB
"use strict"; exports.__esModule = true; require("core-js/modules/es.error.cause.js"); var _baseEditor = require("../baseEditor"); var _eventManager = _interopRequireDefault(require("../../eventManager")); var _browser = require("../../helpers/browser"); var _element = require("../../helpers/dom/element"); var _number = require("../../helpers/number"); var _autoResize = require("../../utils/autoResize"); var _mixed = require("../../helpers/mixed"); var _caretPositioner = require("./caretPositioner"); var _a11y = require("../../helpers/a11y"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _defineProperty(e, r, t) { return (r = _toPropertyKey(r)) in e ? Object.defineProperty(e, r, { value: t, enumerable: !0, configurable: !0, writable: !0 }) : e[r] = t, e; } function _toPropertyKey(t) { var i = _toPrimitive(t, "string"); return "symbol" == typeof i ? i : i + ""; } function _toPrimitive(t, r) { if ("object" != typeof t || !t) return t; var e = t[Symbol.toPrimitive]; if (void 0 !== e) { var i = e.call(t, r || "default"); if ("object" != typeof i) return i; throw new TypeError("@@toPrimitive must return a primitive value."); } return ("string" === r ? String : Number)(t); } const EDITOR_VISIBLE_CLASS_NAME = 'ht_editor_visible'; const EDITOR_HIDDEN_CLASS_NAME = 'ht_editor_hidden'; const SHORTCUTS_GROUP = 'textEditor'; const EDITOR_TYPE = exports.EDITOR_TYPE = 'text'; /** * @private * @class TextEditor */ class TextEditor extends _baseEditor.BaseEditor { static get EDITOR_TYPE() { return EDITOR_TYPE; } /** * Instance of {@link EventManager}. * * @private * @type {EventManager} */ /** * @param {Core} hotInstance The Handsontable instance. */ constructor(hotInstance) { super(hotInstance); _defineProperty(this, "eventManager", new _eventManager.default(this)); /** * Autoresize instance. Automagically resizes editor after changes. * * @private * @type {Function} */ _defineProperty(this, "autoResize", (0, _autoResize.createInputElementResizer)(this.hot.rootDocument)); /** * An TEXTAREA element. * * @private * @type {HTMLTextAreaElement} */ _defineProperty(this, "TEXTAREA", void 0); /** * Style declaration object of the TEXTAREA element. * * @private * @type {CSSStyleDeclaration} */ _defineProperty(this, "textareaStyle", void 0); /** * Parent element of the TEXTAREA. * * @private * @type {HTMLDivElement} */ _defineProperty(this, "TEXTAREA_PARENT", void 0); /** * Style declaration object of the TEXTAREA_PARENT element. * * @private * @type {CSSStyleDeclaration} */ _defineProperty(this, "textareaParentStyle", void 0); /** * Z-index class style for the editor. * * @private * @type {string} */ _defineProperty(this, "layerClass", void 0); this.eventManager = new _eventManager.default(this); this.createElements(); this.bindEvents(); this.hot.addHookOnce('afterDestroy', () => this.destroy()); } /** * Gets current value from editable element. * * @returns {number} */ getValue() { return this.TEXTAREA.value; } /** * Sets new value into editable element. * * @param {*} newValue The editor value. */ setValue(newValue) { this.TEXTAREA.value = newValue; } /** * Opens the editor and adjust its size. */ open() { this._opened = true; this.refreshDimensions(); // need it instantly, to prevent https://github.com/handsontable/handsontable/issues/348 this.showEditableElement(); this.hot.getShortcutManager().setActiveContextName('editor'); this.registerShortcuts(); } /** * Closes the editor. */ close() { this._opened = false; this.autoResize.unObserve(); if ((0, _element.isInternalElement)(this.hot.rootDocument.activeElement, this.hot.rootElement)) { this.hot.listen(); // don't refocus the table if user focused some cell outside of HT on purpose } this.hideEditableElement(); this.unregisterShortcuts(); } /** * Prepares editor's meta data. * * @param {number} row The visual row index. * @param {number} col The visual column index. * @param {number|string} prop The column property (passed when datasource is an array of objects). * @param {HTMLTableCellElement} td The rendered cell element. * @param {*} value The rendered value. * @param {object} cellProperties The cell meta object (see {@link Core#getCellMeta}). */ prepare(row, col, prop, td, value, cellProperties) { const previousState = this.state; super.prepare(row, col, prop, td, value, cellProperties); if (!cellProperties.readOnly) { this.refreshDimensions(true); const { allowInvalid } = cellProperties; if (allowInvalid && !this.isOpened()) { // Remove an empty space from textarea (added by copyPaste plugin to make copy/paste // functionality work with IME) this.TEXTAREA.value = ''; } if (previousState !== _baseEditor.EDITOR_STATE.FINISHED && !this.isOpened()) { this.hideEditableElement(); } } } /** * Begins editing on a highlighted cell and hides fillHandle corner if was present. * * @param {*} newInitialValue The editor initial value. * @param {Event} event The keyboard event object. */ beginEditing(newInitialValue, event) { if (this.state !== _baseEditor.EDITOR_STATE.VIRGIN) { return; } this.TEXTAREA.value = ''; // Remove an empty space from textarea (added by copyPaste plugin to make copy/paste functionality work with IME). super.beginEditing(newInitialValue, event); } /** * Sets focus state on the select element. */ focus() { // For IME editor textarea element must be focused using ".select" method. // Using ".focus" browser automatically scroll into the focused element which // is undesired effect. this.TEXTAREA.select(); (0, _element.setCaretPosition)(this.TEXTAREA, this.TEXTAREA.value.length); } /** * Creates an editor's elements and adds necessary CSS classnames. */ createElements() { const { rootDocument } = this.hot; this.TEXTAREA = rootDocument.createElement('TEXTAREA'); // Makes the element recognizable by Hot as its own // component's element. (0, _element.setAttribute)(this.TEXTAREA, [['data-hot-input', ''], (0, _a11y.A11Y_TABINDEX)(-1)]); (0, _element.addClass)(this.TEXTAREA, 'handsontableInput'); this.textareaStyle = this.TEXTAREA.style; this.textareaStyle.width = 0; this.textareaStyle.height = 0; this.textareaStyle.overflowY = 'visible'; this.TEXTAREA_PARENT = rootDocument.createElement('DIV'); (0, _element.addClass)(this.TEXTAREA_PARENT, 'handsontableInputHolder'); if ((0, _element.hasClass)(this.TEXTAREA_PARENT, this.layerClass)) { (0, _element.removeClass)(this.TEXTAREA_PARENT, this.layerClass); } (0, _element.addClass)(this.TEXTAREA_PARENT, EDITOR_HIDDEN_CLASS_NAME); this.textareaParentStyle = this.TEXTAREA_PARENT.style; this.TEXTAREA_PARENT.appendChild(this.TEXTAREA); this.hot.rootElement.appendChild(this.TEXTAREA_PARENT); } /** * Moves an editable element out of the viewport, but element must be able to hold focus for IME support. * * @private */ hideEditableElement() { if ((0, _browser.isEdge)()) { this.textareaStyle.textIndent = '-99999px'; } this.textareaStyle.overflowY = 'visible'; this.textareaParentStyle.opacity = '0'; this.textareaParentStyle.height = '1px'; (0, _element.removeClass)(this.TEXTAREA_PARENT, this.layerClass); (0, _element.addClass)(this.TEXTAREA_PARENT, EDITOR_HIDDEN_CLASS_NAME); } /** * Resets an editable element position. * * @private */ showEditableElement() { this.textareaParentStyle.height = ''; this.textareaParentStyle.overflow = ''; this.textareaParentStyle.position = ''; this.textareaParentStyle[this.hot.isRtl() ? 'left' : 'right'] = 'auto'; this.textareaParentStyle.opacity = '1'; this.textareaStyle.textIndent = ''; const childNodes = this.TEXTAREA_PARENT.childNodes; let hasClassHandsontableEditor = false; (0, _number.rangeEach)(childNodes.length - 1, index => { const childNode = childNodes[index]; if ((0, _element.hasClass)(childNode, 'handsontableEditor')) { hasClassHandsontableEditor = true; return false; } }); if ((0, _element.hasClass)(this.TEXTAREA_PARENT, EDITOR_HIDDEN_CLASS_NAME)) { (0, _element.removeClass)(this.TEXTAREA_PARENT, EDITOR_HIDDEN_CLASS_NAME); } if (hasClassHandsontableEditor) { this.layerClass = EDITOR_VISIBLE_CLASS_NAME; (0, _element.addClass)(this.TEXTAREA_PARENT, this.layerClass); } else { this.layerClass = this.getEditedCellsLayerClass(); (0, _element.addClass)(this.TEXTAREA_PARENT, this.layerClass); } } /** * Refreshes editor's value using source data. * * @private */ refreshValue() { const physicalRow = this.hot.toPhysicalRow(this.row); const sourceData = this.hot.getSourceDataAtCell(physicalRow, this.col); this.originalValue = sourceData; this.setValue(sourceData); this.refreshDimensions(); } /** * Refreshes editor's size and position. * * @private * @param {boolean} force Indicates if the refreshing editor dimensions should be triggered. */ refreshDimensions() { let force = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; if (this.state !== _baseEditor.EDITOR_STATE.EDITING && !force) { return; } this.TD = this.getEditedCell(); // TD is outside of the viewport. if (!this.TD) { if (!force) { this.close(); // TODO shouldn't it be this.finishEditing() ? } return; } const { top, start, width, maxWidth, height, maxHeight } = this.getEditedCellRect(); this.textareaParentStyle.top = `${top}px`; this.textareaParentStyle[this.hot.isRtl() ? 'right' : 'left'] = `${start}px`; this.showEditableElement(); const cellComputedStyle = this.hot.rootWindow.getComputedStyle(this.TD); this.TEXTAREA.style.fontSize = cellComputedStyle.fontSize; this.TEXTAREA.style.fontFamily = cellComputedStyle.fontFamily; this.TEXTAREA.style.backgroundColor = this.TD.style.backgroundColor; this.autoResize.init(this.TEXTAREA, { minWidth: Math.min(width, maxWidth), minHeight: Math.min(height, maxHeight), // TEXTAREA should never be wider than visible part of the viewport (should not cover the scrollbar) maxWidth, maxHeight }, true); } /** * Binds events and hooks. * * @private */ bindEvents() { if ((0, _browser.isIOS)()) { // on iOS after click "Done" the edit isn't hidden by default, so we need to handle it manually. this.eventManager.addEventListener(this.TEXTAREA, 'focusout', () => this.finishEditing(false)); } this.addHook('afterScrollHorizontally', () => this.refreshDimensions()); this.addHook('afterScrollVertically', () => this.refreshDimensions()); this.addHook('afterColumnResize', () => { this.refreshDimensions(); if (this.state === _baseEditor.EDITOR_STATE.EDITING) { this.focus(); } }); this.addHook('afterRowResize', () => { this.refreshDimensions(); if (this.state === _baseEditor.EDITOR_STATE.EDITING) { this.focus(); } }); } /** * Destroys the internal event manager and clears attached hooks. * * @private */ destroy() { this.eventManager.destroy(); this.clearHooks(); } /** * Register shortcuts responsible for handling editor. * * @private */ registerShortcuts() { const shortcutManager = this.hot.getShortcutManager(); const editorContext = shortcutManager.getContext('editor'); const contextConfig = { runOnlyIf: () => (0, _mixed.isDefined)(this.hot.getSelected()), group: SHORTCUTS_GROUP }; const insertNewLine = () => { this.hot.rootDocument.execCommand('insertText', false, '\n'); }; editorContext.addShortcuts([{ keys: [['Control', 'Enter']], callback: () => { insertNewLine(); return false; // Will block closing editor. }, runOnlyIf: event => !this.hot.selection.isMultiple() && // We trigger a data population for multiple selection. // catch CTRL but not right ALT (which in some systems triggers ALT+CTRL) !event.altKey }, { keys: [['Meta', 'Enter']], callback: () => { insertNewLine(); return false; // Will block closing editor. }, runOnlyIf: () => !this.hot.selection.isMultiple() // We trigger a data population for multiple selection. }, { keys: [['Alt', 'Enter']], callback: () => { insertNewLine(); return false; // Will block closing editor. } }, { keys: [['Home']], callback: (event, _ref) => { let [keyName] = _ref; (0, _caretPositioner.updateCaretPosition)(keyName, this.TEXTAREA); } }, { keys: [['End']], callback: (event, _ref2) => { let [keyName] = _ref2; (0, _caretPositioner.updateCaretPosition)(keyName, this.TEXTAREA); } }], contextConfig); } /** * Unregister shortcuts responsible for handling editor. * * @private */ unregisterShortcuts() { const shortcutManager = this.hot.getShortcutManager(); const editorContext = shortcutManager.getContext('editor'); editorContext.removeShortcutsByGroup(SHORTCUTS_GROUP); } } exports.TextEditor = TextEditor;