handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
509 lines (494 loc) • 20.7 kB
JavaScript
"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);
}
}
}