UNPKG

handsontable

Version:

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

270 lines (260 loc) • 10.8 kB
"use strict"; exports.__esModule = true; exports.cacheColumnWidthAndRegisterResizeHook = cacheColumnWidthAndRegisterResizeHook; exports.createChipElement = createChipElement; exports.createOverflowIndicator = createOverflowIndicator; exports.getItemProperty = getItemProperty; exports.handleChipsOverflow = handleChipsOverflow; exports.parseValue = parseValue; exports.registerChipRemovingEvents = registerChipRemovingEvents; exports.removeValueByKey = removeValueByKey; var _object = require("../../../helpers/object"); var _a11y = require("../../../helpers/a11y"); var _element = require("../../../helpers/dom/element"); var _event = require("../../../helpers/dom/event"); var _eventManager = _interopRequireDefault(require("../../../eventManager")); function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } const CLASS_PREFIX = exports.CLASS_PREFIX = 'ht-multi-select'; const CHIP_CLASS = exports.CHIP_CLASS = `${CLASS_PREFIX}-chip`; const CHIP_REMOVE_CLASS = exports.CHIP_REMOVE_CLASS = `${CLASS_PREFIX}-chip-remove`; const CHIP_LABEL_CLASS = `${CLASS_PREFIX}-chip-label`; const OVERFLOW_INDICATOR_CLASS = `${CLASS_PREFIX}-overflow`; const beforeColumnResizeHookRegistered = new WeakSet(); const latestColumnWidthCache = new WeakMap(); const chipsEventManagers = new WeakMap(); /** * Extracts the property from a value item - default to the item itself. * * @param {string|object} item The value item (string or object with key/value). * @param {string} property The property to extract. * @returns {string} The property value. */ function getItemProperty(item, property) { return (0, _object.isKeyValueObject)(item) ? item[property] : item; } /** * Parses a stringified value if necessary. * * @param {*} value The value to parse. * @returns {Array} The parsed array of values. */ function parseValue(value) { if (value === null || value === undefined || value === '') { return []; } if (Array.isArray(value)) { return value; } if (typeof value === 'string') { try { const parsed = JSON.parse(value); return Array.isArray(parsed) ? parsed : [parsed]; } catch { return value.trim() ? [value] : []; } } return [value]; } /** * Creates a chip element for a single value. * * @param {Document} rootDocument The document object. * @param {string|object} item The value item. * @param {boolean} isAriaEnabled Whether ARIA is enabled. * @param {number} row The row index. * @param {number} prop The property index. * @returns {HTMLElement} The chip element. */ function createChipElement(rootDocument, item, isAriaEnabled, row, prop) { const chip = rootDocument.createElement('span'); const textContent = getItemProperty(item, 'value'); (0, _element.addClass)(chip, CHIP_CLASS); chip.dataset.row = row; chip.dataset.prop = prop; chip.title = textContent; const label = rootDocument.createElement('span'); (0, _element.addClass)(label, CHIP_LABEL_CLASS); label.textContent = textContent; chip.appendChild(label); const removeBtn = rootDocument.createElement('span'); (0, _element.addClass)(removeBtn, CHIP_REMOVE_CLASS); if (isAriaEnabled) { removeBtn.setAttribute(...(0, _a11y.A11Y_HIDDEN)()); } chip.dataset.key = getItemProperty(item, 'key'); chip.appendChild(removeBtn); return chip; } /** * Creates an overflow indicator element. * * @param {Document} rootDocument The document object. * @param {number} count The number of hidden items. * @returns {HTMLElement} The overflow indicator element. */ function createOverflowIndicator(rootDocument, count) { const indicator = rootDocument.createElement('span'); (0, _element.addClass)(indicator, OVERFLOW_INDICATOR_CLASS); indicator.textContent = `+${count}`; return indicator; } /** * Registers a single click listener responsible for removing chips. * Uses a per-instance EventManager cache to avoid duplicate listeners. * * @param {Core} hotInstance The Handsontable instance. * @param {string} rendererType The renderer type (used as source id). */ function registerChipRemovingEvents(hotInstance, rendererType) { if (chipsEventManagers.has(hotInstance)) { return; } chipsEventManagers.set(hotInstance, new _eventManager.default(hotInstance)); const eventManager = chipsEventManagers.get(hotInstance); eventManager.addEventListener(hotInstance.rootElement, 'click', event => { if (!(0, _element.hasClass)(event.target, CHIP_REMOVE_CLASS)) { return; } event.preventDefault(); event.stopPropagation(); const chip = event.target.closest(`.${CHIP_CLASS}`); if (!chip) { return; } const rowIndex = chip.dataset.row; const columnProp = chip.dataset.prop; const physicalRow = hotInstance.toPhysicalRow(rowIndex); const physicalColumn = typeof columnProp === 'string' ? columnProp : hotInstance.toPhysicalColumn(columnProp); const visualColumn = hotInstance.propToCol(columnProp); const currentData = hotInstance.getSourceDataAtCell(physicalRow, visualColumn); const keyToRemove = chip.dataset.key; const newData = removeValueByKey(parseValue(currentData), keyToRemove); hotInstance.setSourceDataAtCell(physicalRow, physicalColumn, newData, `${rendererType}-renderer`); hotInstance.render(); }); hotInstance.addHook('beforeOnCellMouseDown', event => { if ((0, _element.hasClass)(event.target, CHIP_REMOVE_CLASS)) { (0, _event.stopImmediatePropagation)(event); } }); } /** * Caches the latest column width for a specific column and registers a one-time hook * for keeping the cache in sync during column resizing (e.g. ManualColumnResize). * * @param {Core} hotInstance The Handsontable instance. * @param {number} col The visual column index. * @returns {number} The cached column width. */ function cacheColumnWidthAndRegisterResizeHook(hotInstance, col) { var _latestColumnWidthCac, _latestColumnWidthCac3, _latestColumnWidthCac4; const currentWidth = hotInstance.getColWidth(col); // Cache the column width (required to know the column width before it's rendered - e.g. ManualColumnResize) if (!latestColumnWidthCache.has(hotInstance)) { latestColumnWidthCache.set(hotInstance, { [col]: { width: currentWidth } }); } else if (((_latestColumnWidthCac = latestColumnWidthCache.get(hotInstance)) === null || _latestColumnWidthCac === void 0 || (_latestColumnWidthCac = _latestColumnWidthCac[col]) === null || _latestColumnWidthCac === void 0 ? void 0 : _latestColumnWidthCac.width) !== currentWidth) { latestColumnWidthCache.set(hotInstance, { ...latestColumnWidthCache.get(hotInstance), [col]: { width: currentWidth } }); } if (!beforeColumnResizeHookRegistered.has(hotInstance)) { hotInstance.addHook('beforeColumnResize', (newSize, columnIndex) => { var _latestColumnWidthCac2; if (((_latestColumnWidthCac2 = latestColumnWidthCache.get(hotInstance)) === null || _latestColumnWidthCac2 === void 0 || (_latestColumnWidthCac2 = _latestColumnWidthCac2[columnIndex]) === null || _latestColumnWidthCac2 === void 0 ? void 0 : _latestColumnWidthCac2.width) !== newSize) { latestColumnWidthCache.set(hotInstance, { ...latestColumnWidthCache.get(hotInstance), [columnIndex]: { width: newSize } }); } }); beforeColumnResizeHookRegistered.add(hotInstance); } return (_latestColumnWidthCac3 = (_latestColumnWidthCac4 = latestColumnWidthCache.get(hotInstance)) === null || _latestColumnWidthCac4 === void 0 || (_latestColumnWidthCac4 = _latestColumnWidthCac4[col]) === null || _latestColumnWidthCac4 === void 0 ? void 0 : _latestColumnWidthCac4.width) !== null && _latestColumnWidthCac3 !== void 0 ? _latestColumnWidthCac3 : currentWidth; } /** * Recalculates which chips are visible and updates the overflow indicator. * This function is called both on initial render and when container size changes. * * @param {number} columnWidth The width of the column. * @param {HTMLElement} chipsContainer The container holding the chips. * @param {Document} rootDocument The document object. */ function recalculateChipsVisibility(columnWidth, chipsContainer, rootDocument) { const containerWidth = columnWidth; const chips = chipsContainer.querySelectorAll(`.${CHIP_CLASS}`); for (let i = 0; i < chips.length; i++) { chips[i].style.display = ''; } if (containerWidth === null || chips.length === 0) { return; } let indicator = chipsContainer.querySelector(`.${OVERFLOW_INDICATOR_CLASS}`); if (!indicator) { indicator = createOverflowIndicator(rootDocument, chips.length); indicator.style.visibility = 'hidden'; chipsContainer.appendChild(indicator); } else { indicator.style.display = ''; indicator.style.visibility = 'hidden'; } const containerStyles = rootDocument.defaultView.getComputedStyle(chipsContainer); const gap = parseFloat(containerStyles.gap) || 0; const indicatorWidth = indicator.offsetWidth; let totalWidth = 0; let visibleCount = 0; for (let i = 0; i < chips.length; i++) { const chipWidth = chips[i].offsetWidth; const chipGap = i < chips.length - 1 ? gap : 0; const nextWidth = totalWidth + chipWidth + chipGap; const needsIndicatorSpace = i < chips.length - 1; const availableWidth = containerWidth - (needsIndicatorSpace ? indicatorWidth + gap : 0); if (nextWidth <= availableWidth) { totalWidth = nextWidth; visibleCount += 1; } else { break; } } const hiddenCount = chips.length - visibleCount; if (hiddenCount > 0) { for (let i = visibleCount; i < chips.length; i++) { chips[i].style.display = 'none'; } indicator.textContent = `+${hiddenCount}`; indicator.style.visibility = 'visible'; } else { indicator.style.display = 'none'; } } /** * Handles overflow by hiding chips that don't fit and showing a "+N" indicator. * Uses ResizeObserver to recalculate when container dimensions change. * * @param {number} columnWidth The width of the column. * @param {HTMLElement} chipsContainer The container holding the chips. * @param {Document} rootDocument The document object. */ function handleChipsOverflow(columnWidth, chipsContainer, rootDocument) { recalculateChipsVisibility(columnWidth, chipsContainer, rootDocument); } /** * Removes a value from an array by its key. * * @param {Array} array The source array. * @param {string} keyToRemove The key of the item to remove. * @returns {Array} A new array with the item removed. */ function removeValueByKey(array, keyToRemove) { return array.filter(item => { return getItemProperty(item, 'key') !== keyToRemove; }); }