UNPKG

handsontable

Version:

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

569 lines (549 loc) • 23.3 kB
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 _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"); } import { mixin } from "../../../helpers/object.mjs"; import localHooks from "../../../mixins/localHooks.mjs"; import { getCheckboxElement, includesValue } from "../utils/utils.mjs"; import EventManager from "../../../eventManager.mjs"; import { InputController } from "./inputController.mjs"; import { selectItem, deselectItem, createSearchInputWrapper, createSearchIcon, createSearchInputElement, createSeparatorElement, createListElement, getDropdownWidth, deselectAllItems, defocusItem, focusItemAt, disableUncheckedCheckboxes, enableAllCheckboxes, createListItemElement } from "./utils.mjs"; /** * Renders and manages the dropdown list used by the `MultiSelectEditor`. * Responsible for rendering checkbox rows and emitting hooks when values change. * * @private * @class DropdownController */ var _instanceId = /*#__PURE__*/new WeakMap(); var _containerElement = /*#__PURE__*/new WeakMap(); var _dropdownListElement = /*#__PURE__*/new WeakMap(); var _searchInputElement = /*#__PURE__*/new WeakMap(); var _separatorElement = /*#__PURE__*/new WeakMap(); var _searchInputWrapper = /*#__PURE__*/new WeakMap(); var _inputController = /*#__PURE__*/new WeakMap(); var _rootDocument = /*#__PURE__*/new WeakMap(); var _eventManager = /*#__PURE__*/new WeakMap(); var _cache = /*#__PURE__*/new WeakMap(); var _DropdownController_brand = /*#__PURE__*/new WeakSet(); export class DropdownController { /** * Creates a dropdown renderer attached to the provided container. * * @param {HTMLDivElement} containerElement Host element created by the editor. * @param {string} instanceId Handsontable instance id. */ constructor(containerElement, instanceId) { /** * Selects the item at the given index. * * @param {number} index Index of the item to focus. */ _classPrivateMethodInitSpec(this, _DropdownController_brand); /** * Handsontable instance id. */ _classPrivateFieldInitSpec(this, _instanceId, null); /** * Element that wraps the dropdown list inside the editor UI. * * @private * @type {HTMLDivElement|null} */ _classPrivateFieldInitSpec(this, _containerElement, null); /** * `<ul>` element containing all checkbox rows. * * @private * @type {HTMLUListElement|null} */ _classPrivateFieldInitSpec(this, _dropdownListElement, null); /** * Search input element for filtering dropdown entries. * * @private * @type {HTMLInputElement|null} */ _classPrivateFieldInitSpec(this, _searchInputElement, null); /** * Separator element between the search input and the dropdown list. * * @private * @type {HTMLDivElement|null} */ _classPrivateFieldInitSpec(this, _separatorElement, null); /** * Wrapper element for the search input. * * @private * @type {HTMLDivElement|null} */ _classPrivateFieldInitSpec(this, _searchInputWrapper, null); /** * Input controller for managing the search input. * * @private * @type {InputController|null} */ _classPrivateFieldInitSpec(this, _inputController, null); /** * Cached document reference used for DOM operations. * * @private * @type {Document|null} */ _classPrivateFieldInitSpec(this, _rootDocument, null); /** * Event manager for handling checkbox change events. * * @private * @type {EventManager} */ _classPrivateFieldInitSpec(this, _eventManager, new EventManager(this)); /** * Cache for the dropdown controller. * * @private * @type {object} */ _classPrivateFieldInitSpec(this, _cache, { visibleRowsNumber: null, entriesCount: 0, flippedVertically: false, currentlySelectedItemIndex: 0, checkboxChangeListeners: new Map(), areCheckboxesDisabled: false, sourceSortFunction: null }); _classPrivateFieldSet(_containerElement, this, containerElement); _classPrivateFieldSet(_rootDocument, this, _classPrivateFieldGet(_containerElement, this).ownerDocument); _classPrivateFieldSet(_instanceId, this, instanceId); this.init(); } /** * Builds required DOM elements and inserts them into the container. */ init() { _classPrivateFieldSet(_dropdownListElement, this, createListElement({ root: _classPrivateFieldGet(_rootDocument, this) })); _classPrivateFieldSet(_searchInputElement, this, createSearchInputElement({ root: _classPrivateFieldGet(_rootDocument, this) })); const searchIcon = createSearchIcon({ root: _classPrivateFieldGet(_rootDocument, this) }); _classPrivateFieldSet(_searchInputWrapper, this, createSearchInputWrapper({ root: _classPrivateFieldGet(_rootDocument, this) })); _classPrivateFieldSet(_separatorElement, this, createSeparatorElement({ root: _classPrivateFieldGet(_rootDocument, this) })); _classPrivateFieldGet(_searchInputWrapper, this).appendChild(searchIcon); _classPrivateFieldGet(_searchInputWrapper, this).appendChild(_classPrivateFieldGet(_searchInputElement, this)); _classPrivateFieldGet(_containerElement, this).appendChild(_classPrivateFieldGet(_searchInputWrapper, this)); _classPrivateFieldGet(_containerElement, this).appendChild(_classPrivateFieldGet(_separatorElement, this)); _classPrivateFieldGet(_containerElement, this).appendChild(_classPrivateFieldGet(_dropdownListElement, this)); _classPrivateFieldSet(_inputController, this, new InputController({ input: _classPrivateFieldGet(_searchInputElement, this), eventManager: _classPrivateFieldGet(_eventManager, this) })); } /** * Sets the number of visible rows in the dropdown. * * @param {number} visibleRowsNumber Number of visible rows. */ setVisibleRowsNumberSetting(visibleRowsNumber) { _classPrivateFieldGet(_cache, this).visibleRowsNumberSetting = visibleRowsNumber; } /** * Sets the source sort function. * * @param {Function} sourceSortFunction Source sort function. */ setSourceSortFunction(sourceSortFunction) { _classPrivateFieldGet(_cache, this).sourceSortFunction = sourceSortFunction !== null && sourceSortFunction !== void 0 ? sourceSortFunction : null; } /** * Sets the visibility of the search input. * * @param {boolean} [searchInput=true] If true, the search input will be displayed. */ setSearchInputVisibility() { let searchInput = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; _classPrivateFieldGet(_searchInputWrapper, this).style.display = searchInput ? '' : 'none'; _classPrivateFieldGet(_separatorElement, this).style.display = searchInput ? '' : 'none'; _classPrivateFieldGet(_inputController, this).toggle(searchInput); } /** * Populates the dropdown with provided entries and marks selected ones. * * @param {string[]|object[]} entries Collection of primitive values or `[value, label]` tuples. * @param {Array<*>} [checkedValues=[]] Values that should be rendered as checked. */ fillDropdown(entries) { let checkedValues = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; this.removeAllDropdownItems(); _classPrivateFieldGet(_cache, this).currentlySelectedItemIndex = 0; if (!Array.isArray(checkedValues)) { checkedValues = []; } if (_classPrivateFieldGet(_cache, this).sourceSortFunction) { entries = _classPrivateFieldGet(_cache, this).sourceSortFunction(entries); } entries.forEach((elem, indexWithinList) => { var _elem$value; _assertClassBrand(_DropdownController_brand, this, _addDropdownItem).call(this, { rootDocument: _classPrivateFieldGet(_rootDocument, this), itemKey: elem === null || elem === void 0 ? void 0 : elem.key, itemValue: (_elem$value = elem === null || elem === void 0 ? void 0 : elem.value) !== null && _elem$value !== void 0 ? _elem$value : elem, indexWithinList, checked: includesValue(checkedValues, elem), disabled: _classPrivateFieldGet(_cache, this).areCheckboxesDisabled }); }); _classPrivateFieldGet(_cache, this).entriesCount = entries.length; this.runLocalHooks('afterDropdownFill'); } /** * Controls dropdown height based on entry count and configured visible rows. * * @param {object} availableSpace Available space object. * @param {boolean} noFlip If true, the dropdown will not be flipped vertically. */ updateDimensions(availableSpace) { let noFlip = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; const entryHeight = _assertClassBrand(_DropdownController_brand, this, _getEntryHeight).call(this); const requiresFlippingVertically = _assertClassBrand(_DropdownController_brand, this, _requiresFlippingVertically).call(this, availableSpace); const availableHeight = requiresFlippingVertically ? availableSpace.spaceAbove : availableSpace.spaceBelow; if (!noFlip && requiresFlippingVertically) { _classPrivateFieldGet(_cache, this).flippedVertically = true; } if (_classPrivateFieldGet(_cache, this).entriesCount > 0 && availableHeight < this.getHeight(true)) { const maxRenderableItems = Math.max(Math.floor((availableHeight - _assertClassBrand(_DropdownController_brand, this, _getSearchInputWrapperHeight).call(this)) / entryHeight) - 1, 1); _classPrivateFieldGet(_cache, this).actualRenderedItemsCount = _classPrivateFieldGet(_cache, this).visibleRowsNumberSetting ? Math.min(maxRenderableItems, _classPrivateFieldGet(_cache, this).visibleRowsNumberSetting) : maxRenderableItems; } else { _classPrivateFieldGet(_cache, this).actualRenderedItemsCount = Math.min(_classPrivateFieldGet(_cache, this).entriesCount, _classPrivateFieldGet(_cache, this).visibleRowsNumberSetting); } if (_classPrivateFieldGet(_cache, this).actualRenderedItemsCount && _classPrivateFieldGet(_cache, this).entriesCount > _classPrivateFieldGet(_cache, this).actualRenderedItemsCount) { _classPrivateFieldGet(_containerElement, this).style.height = `${this.getHeight()}px`; } else { _classPrivateFieldGet(_containerElement, this).style.height = ''; } _assertClassBrand(_DropdownController_brand, this, _toggleVerticalFlip).call(this, availableSpace); _classPrivateFieldGet(_containerElement, this).scrollTop = 0; } /** * Gets the width of the dropdown. * * @returns {number} Width of the dropdown. */ getDropdownWidth() { return getDropdownWidth({ dropdownListElement: _classPrivateFieldGet(_dropdownListElement, this) }); } /** * Deselects all items in the dropdown. */ deselectAllItems() { deselectAllItems({ dropdownListElement: _classPrivateFieldGet(_dropdownListElement, this) }); } /** * Focuses the first item in the dropdown. */ focusFirstItem() { if (_classPrivateFieldGet(_cache, this).entriesCount > 0) { _assertClassBrand(_DropdownController_brand, this, _focusItem).call(this, 0); } } /** * Focuses the item at the given index. * * @param {number} index Index of the item to focus. */ focusItem(index) { _assertClassBrand(_DropdownController_brand, this, _focusItem).call(this, index); } /** * Selects the previous item in the dropdown. */ focusPreviousItem() { if (_classPrivateFieldGet(_cache, this).currentlySelectedItemIndex === 0) { this.focusSearchInput(); return; } defocusItem({ dropdownListElement: _classPrivateFieldGet(_dropdownListElement, this), index: _classPrivateFieldGet(_cache, this).currentlySelectedItemIndex }); _assertClassBrand(_DropdownController_brand, this, _focusItem).call(this, _classPrivateFieldGet(_cache, this).currentlySelectedItemIndex - 1); } /** * Selects the next item in the dropdown. */ focusNextItem() { if (_classPrivateFieldGet(_cache, this).currentlySelectedItemIndex === _classPrivateFieldGet(_cache, this).entriesCount - 1) { return; } defocusItem({ dropdownListElement: _classPrivateFieldGet(_dropdownListElement, this), index: _classPrivateFieldGet(_cache, this).currentlySelectedItemIndex }); _assertClassBrand(_DropdownController_brand, this, _focusItem).call(this, _classPrivateFieldGet(_cache, this).currentlySelectedItemIndex + 1); } /** * Focuses the search input element. */ focusSearchInput() { if (_classPrivateFieldGet(_searchInputElement, this)) { _classPrivateFieldGet(_searchInputElement, this).focus(); } } /** * Resets the cache and the dropdown position and height. */ reset() { this.removeAllDropdownItems(); _assertClassBrand(_DropdownController_brand, this, _resetCache).call(this); if (_classPrivateFieldGet(_searchInputElement, this)) { _classPrivateFieldGet(_searchInputElement, this).value = ''; } _classPrivateFieldGet(_containerElement, this).style.position = ''; _classPrivateFieldGet(_containerElement, this).style.top = ''; _classPrivateFieldGet(_containerElement, this).style.height = ''; _classPrivateFieldGet(_containerElement, this).scrollTop = 0; } /** * Checks if the dropdown is flipped vertically. * * @returns {boolean} */ isFlippedVertically() { return _classPrivateFieldGet(_cache, this).flippedVertically; } /** * Gets the height of the dropdown. * * @param {boolean} maxRowsCalculation If true, the height will be calculated for the maximum number of rows. * @param {boolean} outerWidth If true, the width will be calculated for the outer width. * @returns {number} Height of the dropdown. */ getHeight() { let maxRowsCalculation = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; let outerWidth = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false; const computedStyle = _classPrivateFieldGet(_rootDocument, this).defaultView.getComputedStyle(_classPrivateFieldGet(_containerElement, this)); return _assertClassBrand(_DropdownController_brand, this, _getListHeight).call(this, maxRowsCalculation) + _assertClassBrand(_DropdownController_brand, this, _getSearchInputWrapperHeight).call(this) + (outerWidth === true ? 2 * parseInt(computedStyle.getPropertyValue('--ht-menu-vertical-padding'), 10) : 0); } /** * Gets the input controller instance. * * @returns {InputController|null} The input controller instance. */ getInputController() { return _classPrivateFieldGet(_inputController, this); } /** * Removes all dropdown rows. */ removeAllDropdownItems() { Array.from(_classPrivateFieldGet(_dropdownListElement, this).children).forEach(itemElement => _assertClassBrand(_DropdownController_brand, this, _unregisterEvents).call(this, itemElement)); _classPrivateFieldGet(_cache, this).checkboxChangeListeners.clear(); _classPrivateFieldGet(_dropdownListElement, this).innerHTML = ''; } /** * Disables the unchecked checkboxes. */ disableCheckboxes() { _classPrivateFieldGet(_cache, this).areCheckboxesDisabled = true; disableUncheckedCheckboxes({ dropdownListElement: _classPrivateFieldGet(_dropdownListElement, this) }); } /** * Enables the checkboxes. */ enableCheckboxes() { _classPrivateFieldGet(_cache, this).areCheckboxesDisabled = false; enableAllCheckboxes({ dropdownListElement: _classPrivateFieldGet(_dropdownListElement, this) }); } } function _focusItem(index) { _classPrivateFieldGet(_cache, this).currentlySelectedItemIndex = index; focusItemAt({ dropdownListElement: _classPrivateFieldGet(_dropdownListElement, this), index }); } /** * Checks if the dropdown requires flipping vertically. * * @param {object} availableSpace Available space object. * @returns {boolean} */ function _requiresFlippingVertically(availableSpace) { const { spaceAbove, spaceBelow, cellHeight } = availableSpace; return this.getHeight(true) > spaceBelow && spaceAbove > spaceBelow + cellHeight; } /** * Toggles the vertical flip. * * @param {object} availableSpace Available space object. */ function _toggleVerticalFlip(availableSpace) { const { cellHeight } = availableSpace; const flipNeeded = _classPrivateFieldGet(_cache, this).flippedVertically; if (flipNeeded) { _classPrivateFieldGet(_containerElement, this).style.position = 'absolute'; _classPrivateFieldGet(_containerElement, this).style.top = `${-this.getHeight(false, true) - cellHeight - 2}px`; } else { _classPrivateFieldGet(_containerElement, this).style.position = ''; _classPrivateFieldGet(_containerElement, this).style.top = ''; } } /** * Gets the height of an entry. * * @returns {number} Height of an entry. */ function _getEntryHeight() { const computedStyle = _classPrivateFieldGet(_rootDocument, this).defaultView.getComputedStyle(_classPrivateFieldGet(_containerElement, this)); return 2 * parseInt(computedStyle.getPropertyValue('--ht-menu-item-vertical-padding'), 10) + parseInt(computedStyle.getPropertyValue('--ht-line-height'), 10); } /** * Gets the height of the list. * * @param {boolean} maxRowsCalculation If true, the height will be calculated for the maximum number of rows. * @returns {number} Height of the list. */ function _getListHeight() { var _classPrivateFieldGet2; let maxRowsCalculation = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; const maxRenderedItems = _classPrivateFieldGet(_cache, this).entriesCount; const actualRenderedItems = maxRowsCalculation ? maxRenderedItems : (_classPrivateFieldGet2 = _classPrivateFieldGet(_cache, this).actualRenderedItemsCount) !== null && _classPrivateFieldGet2 !== void 0 ? _classPrivateFieldGet2 : maxRenderedItems; const entryHeight = _assertClassBrand(_DropdownController_brand, this, _getEntryHeight).call(this); const listHeight = actualRenderedItems * entryHeight; return listHeight; } /** * Gets the height of the search input wrapper. * * @returns {number} Height of the search input wrapper. */ function _getSearchInputWrapperHeight() { if (!_classPrivateFieldGet(_inputController, this).enabled) { return 0; } const computedStyle = _classPrivateFieldGet(_rootDocument, this).defaultView.getComputedStyle(_classPrivateFieldGet(_containerElement, this)); const searchInputWrapperHeight = _classPrivateFieldGet(_searchInputWrapper, this).offsetHeight; const separatorHeight = _classPrivateFieldGet(_separatorElement, this).offsetHeight + 2 * parseInt(computedStyle.getPropertyValue('--ht-menu-vertical-padding'), 10); return searchInputWrapperHeight + separatorHeight; } /** * Adds a single row to the dropdown and optionally marks it as checked. * * @param {object} options Options object. * @param {Document} options.rootDocument Root document element. * @param {string} options.itemKey Key stored in the associated checkbox dataset. * @param {string} options.itemValue Text content rendered next to the checkbox. * @param {number} options.indexWithinList Index of the item within the list. * @param {boolean} [options.checked=false] Flag indicating whether the checkbox starts selected. * @param {boolean} [options.disabled=false] Flag indicating whether the checkbox starts disabled. */ function _addDropdownItem(_ref) { let { rootDocument, itemKey, itemValue, indexWithinList, checked = false, disabled = false } = _ref; const itemElement = createListItemElement({ rootDocument, instanceId: _classPrivateFieldGet(_instanceId, this), itemKey, itemValue, indexWithinList, checked, disabled }); if (checked) { selectItem(itemElement); } _assertClassBrand(_DropdownController_brand, this, _registerEvents).call(this, itemElement); _classPrivateFieldGet(_dropdownListElement, this).appendChild(itemElement); } /** * Wires checkbox change events to toggle selection and emit hooks. * * @param {HTMLLIElement} itemElement Dropdown row element. */ function _registerEvents(itemElement) { const checkbox = getCheckboxElement(itemElement); const checkboxChangeListener = () => { // Checkbox was just natively checked (so checked = was unchecked when clicked) if (checkbox.dataset.disabled === 'true' && checkbox.checked) { checkbox.checked = false; return; } if (checkbox.checked) { selectItem(itemElement); this.runLocalHooks('afterDropdownItemChecked', checkbox.dataset.key, checkbox.dataset.value); } else { deselectItem(itemElement); this.runLocalHooks('afterDropdownItemUnchecked', checkbox.dataset.key, checkbox.dataset.value); } }; const itemClickListener = event => { if (event.target === checkbox || checkbox.dataset.disabled === 'true' || event.target.tagName === 'LABEL') { return; } checkbox.checked = !checkbox.checked; checkbox.dispatchEvent(new Event('change')); }; _classPrivateFieldGet(_cache, this).checkboxChangeListeners.set(checkbox, { change: checkboxChangeListener, click: itemClickListener }); _classPrivateFieldGet(_eventManager, this).addEventListener(checkbox, 'change', checkboxChangeListener); _classPrivateFieldGet(_eventManager, this).addEventListener(itemElement, 'click', itemClickListener); } /** * Unregisters events from the item element. * * @param {HTMLLIElement} itemElement Dropdown row element. */ function _unregisterEvents(itemElement) { const checkbox = getCheckboxElement(itemElement); const checkboxListeners = _classPrivateFieldGet(_cache, this).checkboxChangeListeners.get(checkbox); _classPrivateFieldGet(_eventManager, this).removeEventListener(checkbox, 'change', checkboxListeners.change); _classPrivateFieldGet(_eventManager, this).removeEventListener(itemElement, 'click', checkboxListeners.click); _classPrivateFieldGet(_cache, this).checkboxChangeListeners.delete(checkbox); } /** * Resets the cache. */ function _resetCache() { _classPrivateFieldGet(_cache, this).visibleRowsNumberSetting = null; _classPrivateFieldGet(_cache, this).entriesCount = 0; _classPrivateFieldGet(_cache, this).flippedVertically = false; _classPrivateFieldGet(_cache, this).currentlySelectedItemIndex = 0; _classPrivateFieldGet(_cache, this).checkboxChangeListeners.clear(); _classPrivateFieldGet(_cache, this).areCheckboxesDisabled = false; _classPrivateFieldGet(_cache, this).sourceSortFunction = null; } mixin(DropdownController, localHooks);