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