UNPKG

handsontable

Version:

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

509 lines (494 loc) • 20.7 kB
"use strict"; exports.__esModule = true; var _baseEditor = require("../baseEditor"); var _eventManager = _interopRequireDefault(require("../../eventManager")); var _dropdownController = require("./controllers/dropdownController"); var _selectedItemsController = require("./controllers/selectedItemsController"); var _element = require("../../helpers/dom/element"); var _unicode = require("../../helpers/unicode"); var _a11y = require("../../helpers/a11y"); var _constants = require("../../shortcutContexts/constants"); var _utils = require("./utils/utils"); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _classPrivateMethodInitSpec(e, a) { _checkPrivateRedeclaration(e, a), a.add(e); } function _classPrivateFieldInitSpec(e, t, a) { _checkPrivateRedeclaration(e, t), t.set(e, a); } function _checkPrivateRedeclaration(e, t) { if (t.has(e)) throw new TypeError("Cannot initialize the same private elements twice on an object"); } 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); } function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); } function _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; } function _assertClassBrand(e, t, n) { if ("function" == typeof e ? e === t : e.has(t)) return arguments.length < 3 ? t : n; throw new TypeError("Private element is not present on this object"); } const EDITOR_TYPE = exports.EDITOR_TYPE = 'multiselect'; const DROPDOWN_ELEMENT_CSS_CLASSNAME = 'ht-multi-select-editor'; const DROPDOWN_ARIA_LABEL = 'Select options'; const SHORTCUTS_GROUP = 'multiselectEditor'; const EDITOR_VISIBLE_CLASS_NAME = 'ht_editor_visible'; /** * @private * @class MultiSelectEditor */ var _selectedItems = /*#__PURE__*/new WeakMap(); var _editorContainer = /*#__PURE__*/new WeakMap(); var _MultiSelectEditor_brand = /*#__PURE__*/new WeakSet(); class MultiSelectEditor extends _baseEditor.BaseEditor { /** * Returns the editor type. * * @returns {string} The editor type. */ static get EDITOR_TYPE() { return EDITOR_TYPE; } /** * @param {Core} hotInstance The Handsontable instance. */ constructor(hotInstance) { var _this$dropdownControl; super(hotInstance); /** * Gets the source (options) for the editor. * * @returns {Array} The source (options) for the editor. */ _classPrivateMethodInitSpec(this, _MultiSelectEditor_brand); /** * Prevent the editor from closing after data change. * * @type {boolean} */ _defineProperty(this, "_closeAfterDataChange", false); /** * Set of values that are currently checked in the dropdown. * * @private * @type {SelectedItemsController} */ _classPrivateFieldInitSpec(this, _selectedItems, new _selectedItemsController.SelectedItemsController()); /** * Container element that hosts the editor. * * @private * @type {HTMLDivElement} */ _classPrivateFieldInitSpec(this, _editorContainer, null); /** * Container element that hosts the dropdown with checkbox options. * * @private * @type {HTMLDivElement} */ _defineProperty(this, "dropdownContainerElement", null); /** * Dropdown controller responsible for rendering and syncing option states. * * @private * @type {DropdownController|null} */ _defineProperty(this, "dropdownController", (_this$dropdownControl = this.dropdownController) !== null && _this$dropdownControl !== void 0 ? _this$dropdownControl : null); this.eventManager = new _eventManager.default(this); this.createElements(); this.bindEvents(); } /** * Creates an editor's elements and adds necessary CSS classnames. */ createElements() { const { rootDocument } = this.hot; _classPrivateFieldSet(_editorContainer, this, rootDocument.createElement('div')); _classPrivateFieldGet(_editorContainer, this).style.display = 'none'; (0, _element.addClass)(_classPrivateFieldGet(_editorContainer, this), 'handsontableEditor'); this.dropdownContainerElement = this.hot.rootDocument.createElement('div'); (0, _element.addClass)(this.dropdownContainerElement, `${DROPDOWN_ELEMENT_CSS_CLASSNAME} handsontableEditor`); (0, _element.setAttribute)(this.dropdownContainerElement, [(0, _a11y.A11Y_LABEL)(DROPDOWN_ARIA_LABEL), (0, _a11y.A11Y_GROUP)()]); _classPrivateFieldGet(_editorContainer, this).appendChild(this.dropdownContainerElement); this.hot.rootElement.appendChild(_classPrivateFieldGet(_editorContainer, this)); this.dropdownController = new _dropdownController.DropdownController(this.dropdownContainerElement, this.hot.guid); } /** * 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) { super.prepare(row, col, prop, td, value, cellProperties); const parsedValue = (0, _utils.parseStringifiedValue)(value); const valuesArray = Array.isArray(parsedValue) ? parsedValue : [parsedValue]; const valuesIntersection = (0, _utils.getValuesIntersection)(valuesArray, _assertClassBrand(_MultiSelectEditor_brand, this, _getSource).call(this)); this.dropdownController.reset(); if (valuesIntersection.length >= _assertClassBrand(_MultiSelectEditor_brand, this, _getEditorSetting).call(this, 'maxSelections')) { _assertClassBrand(_MultiSelectEditor_brand, this, _blockNewSelections).call(this); } _assertClassBrand(_MultiSelectEditor_brand, this, _syncSelectedValues).call(this, valuesIntersection); this.dropdownController.setSourceSortFunction(this.cellProperties.sourceSortFunction); this.dropdownController.fillDropdown(_assertClassBrand(_MultiSelectEditor_brand, this, _getSource).call(this), valuesIntersection); this.dropdownController.setVisibleRowsNumberSetting(_assertClassBrand(_MultiSelectEditor_brand, this, _getEditorSetting).call(this, 'visibleRows')); this.dropdownController.setSearchInputVisibility(_assertClassBrand(_MultiSelectEditor_brand, this, _getEditorSetting).call(this, 'searchInput')); if (cellProperties.maxSelections !== undefined) { _classPrivateFieldGet(_selectedItems, this).setMaxSelectionCount(cellProperties.maxSelections); } } /** * Finishes editing and start saving or restoring process for editing cell. * * @param {boolean} restoreOriginalValue If true, then closes editor without saving value from the editor into a cell. * @param {boolean} ctrlDown If true, then saveValue will save editor's value to each cell in the last selected range. * @param {Function} callback The callback function, fired after editor closing. */ finishEditing(restoreOriginalValue, ctrlDown, callback) { super.finishEditing(restoreOriginalValue, ctrlDown, callback); } /** * Binds events to the editor. * * @private */ bindEvents() { var _this = this; this.dropdownController.addLocalHook('afterDropdownItemChecked', (selectedKey, selectedValue) => { _assertClassBrand(_MultiSelectEditor_brand, this, _addSelectedValue).call(this, selectedKey, selectedValue); if (_classPrivateFieldGet(_selectedItems, this).getSize() >= this.cellProperties.maxSelections) { _assertClassBrand(_MultiSelectEditor_brand, this, _blockNewSelections).call(this); } }); this.dropdownController.addLocalHook('afterDropdownItemUnchecked', (deselectedKey, deselectedValue) => { _assertClassBrand(_MultiSelectEditor_brand, this, _removeSelectedValue).call(this, deselectedKey, deselectedValue); if (_classPrivateFieldGet(_selectedItems, this).getSize() < this.cellProperties.maxSelections) { _assertClassBrand(_MultiSelectEditor_brand, this, _unblockNewSelections).call(this); } }); this.addHook('afterDestroy', () => this.destroy()); this.addHook('afterSetSourceDataAtCell', function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } return _assertClassBrand(_MultiSelectEditor_brand, _this, _onAfterSetSourceDataAtCell).call(_this, ...args); }); this.addHook('afterScrollHorizontally', () => this.refreshDimensions()); this.addHook('afterScrollVertically', () => this.refreshDimensions()); this.dropdownController.getInputController().addLocalHook('triggerFilter', value => _assertClassBrand(_MultiSelectEditor_brand, this, _filterEntries).call(this, value)); } /** * Opens the editor. * * @param {Event} event The event object. */ open(event) { if ((0, _unicode.isPrintableChar)(event === null || event === void 0 ? void 0 : event.keyCode)) { const character = event.key.length === 1 ? event.key : String.fromCharCode(event.keyCode); if (this.dropdownController.getInputController().enabled) { this.dropdownController.getInputController().setValue(character); _assertClassBrand(_MultiSelectEditor_brand, this, _filterEntries).call(this, character); } event.preventDefault(); this.enableFullEditMode(); } _assertClassBrand(_MultiSelectEditor_brand, this, _showEditableElement).call(this); this.refreshDimensions(); this.hot.getShortcutManager().setActiveContextName('editor'); _assertClassBrand(_MultiSelectEditor_brand, this, _registerShortcuts).call(this); this.dropdownController.getInputController().listen(); this.dropdownController.updateDimensions(_assertClassBrand(_MultiSelectEditor_brand, this, _getAvailableSpace).call(this)); } /** * Closes the editor. */ close() { _assertClassBrand(_MultiSelectEditor_brand, this, _hideEditableElement).call(this); _assertClassBrand(_MultiSelectEditor_brand, this, _unregisterShortcuts).call(this); this.dropdownController.getInputController().unlisten(); } /** * Returns the editor's value. * * @returns {string} The editor's value. */ getValue() { return _classPrivateFieldGet(_selectedItems, this).getItemsArray(); } /** * Sets the editor's value. */ setValue() { // Currently not implemented. // // As the MultiSelectEditor saves data after every change, there's no need to set the value when // the editor is closed. // // TODO: discuss this behavior and consider implementing an option for the data-saving strategy. } /** * Refreshes the editor's dimensions. */ refreshDimensions() { // TD is outside of the viewport. if (!this.getEditedCell()) { this.close(); return; } const { top, start, height } = this.getEditedCellRect(); const editorStyle = _classPrivateFieldGet(_editorContainer, this).style; editorStyle.top = `${top + height}px`; editorStyle[this.hot.isRtl() ? 'right' : 'left'] = `${start}px`; (0, _element.addClass)(_classPrivateFieldGet(_editorContainer, this), EDITOR_VISIBLE_CLASS_NAME); } /** * Focuses the editor. */ focus() { if (_assertClassBrand(_MultiSelectEditor_brand, this, _getEditorSetting).call(this, 'searchInput') === false) { this.dropdownController.focusFirstItem(); } else { this.dropdownController.focusSearchInput(); } } /** * Gets the input element. * * @returns {HTMLInputElement} The input element. */ getInputElement() { return this.dropdownController.getInputController().getInputElement(); } /** * Called when the editor is destroyed. * * @private */ destroy() { this.close(); this.dropdownController.reset(); } } exports.MultiSelectEditor = MultiSelectEditor; function _getSource() { var _this$cellProperties$; return (_this$cellProperties$ = this.cellProperties.source) !== null && _this$cellProperties$ !== void 0 ? _this$cellProperties$ : []; } /** * Register shortcuts responsible for handling editor. * * @private */ function _registerShortcuts() { const shortcutManager = this.hot.getShortcutManager(); const editorContext = shortcutManager.getContext('editor'); editorContext.addShortcuts([{ keys: [['ArrowUp']], callback: () => { this.dropdownController.focusPreviousItem(); } }, { keys: [['ArrowDown']], callback: () => { if (this.hot.rootDocument.activeElement === this.getInputElement()) { this.dropdownController.focusFirstItem(); } else { this.dropdownController.focusNextItem(); } } }], { group: SHORTCUTS_GROUP }); editorContext.addShortcuts([{ keys: [['enter'], ['shift', 'enter'], ['control/meta', 'enter'], ['control/meta', 'shift', 'enter']], runOnlyIf: () => !_assertClassBrand(_MultiSelectEditor_brand, this, _getEditorSetting).call(this, 'enterCommits'), callback: event => { const activeElement = this.hot.rootDocument.activeElement; if (activeElement.tagName === 'INPUT' && activeElement.type === 'checkbox') { activeElement.checked = !activeElement.checked; activeElement.dispatchEvent(new Event('change')); } event.preventDefault(); return false; } }, { keys: [['space']], runOnlyIf: () => !_assertClassBrand(_MultiSelectEditor_brand, this, _getEditorSetting).call(this, 'enterCommits'), callback: event => { event.preventDefault(); } }], { group: SHORTCUTS_GROUP, relativeToGroup: _constants.EDITOR_EDIT_GROUP, position: 'before' }); } /** * Unregister shortcuts responsible for handling editor. * * @private */ function _unregisterShortcuts() { const shortcutManager = this.hot.getShortcutManager(); const editorContext = shortcutManager.getContext('editor'); editorContext.removeShortcutsByGroup(SHORTCUTS_GROUP); } /** * Shows the editable element. */ function _showEditableElement() { _classPrivateFieldGet(_editorContainer, this).style.display = ''; } /** * Hides the editable element. */ function _hideEditableElement() { _classPrivateFieldGet(_editorContainer, this).style.display = 'none'; } /** * Blocks new selections. */ function _blockNewSelections() { this.dropdownController.disableCheckboxes(); } /** * Unblocks new selections. */ function _unblockNewSelections() { this.dropdownController.enableCheckboxes(); } /** * Filters the dropdown entries. * * @param {string} query The value of the input. * @param {boolean} filterSelectedItems If true, the selected items will be filtered out of the dropdown. */ function _filterEntries(query) { var _assertClassBrand$cal; let filterSelectedItems = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : (_assertClassBrand$cal = _assertClassBrand(_MultiSelectEditor_brand, this, _getEditorSetting).call(this, 'filterSelectedItems')) !== null && _assertClassBrand$cal !== void 0 ? _assertClassBrand$cal : true; const filteredItems = _assertClassBrand(_MultiSelectEditor_brand, this, _getSource).call(this).filter(item => { var _item$value; const value = (_item$value = item === null || item === void 0 ? void 0 : item.value) !== null && _item$value !== void 0 ? _item$value : item; if (!filterSelectedItems && _classPrivateFieldGet(_selectedItems, this).has(item)) { return true; } if (this.cellProperties.filteringCaseSensitive) { return value.includes(query); } return value.toLowerCase().includes(query.toLowerCase()); }); this.dropdownController.fillDropdown(filteredItems, _classPrivateFieldGet(_selectedItems, this).getItemsArray()); this.dropdownController.updateDimensions(_assertClassBrand(_MultiSelectEditor_brand, this, _getAvailableSpace).call(this), true); } /** * Gets the available space for the dropdown. * * @returns {{spaceAbove: number, spaceBelow: number, cellHeight: number}} The available space. */ function _getAvailableSpace() { const cellRect = this.getEditedCellRect(); const isVerticallyScrollableByWindow = this.hot.view.isVerticallyScrollableByWindow(); const workspaceHeight = this.hot.view.getWorkspaceHeight(); let spaceAbove = cellRect.top; if (isVerticallyScrollableByWindow) { const topOffset = this.hot.view.getTableOffset().top - this.hot.rootWindow.scrollY; spaceAbove = Math.max(spaceAbove + topOffset, 0); } const spaceBelow = workspaceHeight - spaceAbove - cellRect.height; return { spaceAbove, spaceBelow, cellHeight: cellRect.height }; } /** * Saves the current selection state to the cell without closing the editor. * * @private */ function _saveCurrentSelection() { const value = _classPrivateFieldGet(_selectedItems, this).getItemsArray(); this.saveValue([[value]]); } /** * Adds a value reported by the dropdown to the selection set and saves to the cell. * * @private * @param {string} selectedKey Key of the selected value. * @param {string} selectedValue Value of the selected value. */ function _addSelectedValue(selectedKey, selectedValue) { if (selectedKey) { _classPrivateFieldGet(_selectedItems, this).add({ key: selectedKey, value: selectedValue }); } else { _classPrivateFieldGet(_selectedItems, this).add(selectedValue); } _assertClassBrand(_MultiSelectEditor_brand, this, _saveCurrentSelection).call(this); this.refreshDimensions(); } /** * Removes a deselected dropdown value from the selection set and saves to the cell. * * @private * @param {string} deselectedKey Key of the deselected value. * @param {string} deselectedValue Value of the deselected value. */ function _removeSelectedValue(deselectedKey, deselectedValue) { if (deselectedKey) { _classPrivateFieldGet(_selectedItems, this).remove({ key: deselectedKey, value: deselectedValue }); } else { _classPrivateFieldGet(_selectedItems, this).remove(deselectedValue); } _assertClassBrand(_MultiSelectEditor_brand, this, _saveCurrentSelection).call(this); this.refreshDimensions(); } /** * Synchronizes the internal selection set with data deserialized from the cell value. * * @private * @param {Array<*>} valuesArray Values decoded from the stored JSON string. */ function _syncSelectedValues(valuesArray) { if (valuesArray.length > 0) { _classPrivateFieldSet(_selectedItems, this, new _selectedItemsController.SelectedItemsController(valuesArray)); } else { _classPrivateFieldSet(_selectedItems, this, new _selectedItemsController.SelectedItemsController()); } } /** * Helper for reading editor-related settings directly from the cell meta. * * @private * @param {string} settingKey Cell meta key to read. * @returns {*} Returns the stored value for the provided key. */ function _getEditorSetting(settingKey) { return this.cellProperties[settingKey]; } /** * `afterSetSourceDataAtCell` hook callback. * * @private * @param {Array<*>} changes The changes. * @param {string} source The source of the change. */ function _onAfterSetSourceDataAtCell(changes, source) { if (this.isOpened() && source === `${EDITOR_TYPE}-renderer` && parseInt(changes[0][0], 10) === this.cellProperties.visualRow && parseInt(changes[0][1], 10) === this.cellProperties.visualCol) { _assertClassBrand(_MultiSelectEditor_brand, this, _syncSelectedValues).call(this, changes[0][3]); this.dropdownController.fillDropdown(_assertClassBrand(_MultiSelectEditor_brand, this, _getSource).call(this), _classPrivateFieldGet(_selectedItems, this).getItemsArray()); this.dropdownController.focusItem(0); if (_classPrivateFieldGet(_selectedItems, this).getSize() >= this.cellProperties.maxSelections) { _assertClassBrand(_MultiSelectEditor_brand, this, _blockNewSelections).call(this); } else { _assertClassBrand(_MultiSelectEditor_brand, this, _unblockNewSelections).call(this); } } }