handsontable
Version:
Handsontable is a JavaScript Data Grid available for React, Angular and Vue.
1,095 lines (1,064 loc) • 41.7 kB
JavaScript
import "core-js/modules/es.error.cause.js";
import "core-js/modules/es.array.push.js";
import "core-js/modules/es.array.unscopables.flat.js";
import "core-js/modules/esnext.iterator.constructor.js";
import "core-js/modules/esnext.iterator.filter.js";
import "core-js/modules/esnext.iterator.for-each.js";
import "core-js/modules/esnext.iterator.map.js";
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 _classPrivateFieldSet(s, a, r) { return s.set(_assertClassBrand(s, a), r), r; }
function _classPrivateFieldGet(s, a) { return s.get(_assertClassBrand(s, a)); }
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 { BasePlugin } from "../base/index.mjs";
import { arrayEach, arrayMap } from "../../helpers/array.mjs";
import { toSingleLine } from "../../helpers/templateLiteralTag.mjs";
import { warn } from "../../helpers/console.mjs";
import { rangeEach } from "../../helpers/number.mjs";
import { addClass, removeClass } from "../../helpers/dom/element.mjs";
import { isKey } from "../../helpers/unicode.mjs";
import { SEPARATOR } from "../contextMenu/predefinedItems/index.mjs";
import * as constants from "../../i18n/constants.mjs";
import { ConditionComponent } from "./component/condition.mjs";
import { OperatorsComponent } from "./component/operators.mjs";
import { ValueComponent } from "./component/value.mjs";
import { ActionBarComponent } from "./component/actionBar.mjs";
import ConditionCollection from "./conditionCollection.mjs";
import DataFilter from "./dataFilter.mjs";
import ConditionUpdateObserver from "./conditionUpdateObserver.mjs";
import { createArrayAssertion, toEmptyString, unifyColumnValues } from "./utils.mjs";
import { createMenuFocusController } from "./menu/focusController.mjs";
import { CONDITION_NONE, CONDITION_BY_VALUE, OPERATION_AND, OPERATION_OR, OPERATION_OR_THEN_VARIABLE } from "./constants.mjs";
import { TrimmingMap } from "../../translations/index.mjs";
export const PLUGIN_KEY = 'filters';
export const PLUGIN_PRIORITY = 250;
const SHORTCUTS_GROUP = PLUGIN_KEY;
/**
* @plugin Filters
* @class Filters
*
* @description
* The plugin allows filtering the table data either by the built-in component or with the API.
*
* See [the filtering demo](@/guides/columns/column-filter/column-filter.md) for examples.
*
* @example
* ::: only-for javascript
* ```js
* const container = document.getElementById('example');
* const hot = new Handsontable(container, {
* data: getData(),
* colHeaders: true,
* rowHeaders: true,
* dropdownMenu: true,
* filters: true
* });
* ```
* :::
*
* ::: only-for react
* ```jsx
* <HotTable
* data={getData()}
* colHeaders={true}
* rowHeaders={true}
* dropdownMenu={true}
* filters={true}
* />
* ```
* :::
*
* ::: only-for angular
* ```ts
* settings = {
* data: getData(),
* colHeaders: true,
* rowHeaders: true,
* dropdownMenu: true,
* filters: true,
* };
* ```
*
* ```html
* <hot-table [settings]="settings"></hot-table>
* ```
* :::
*/
var _menuFocusNavigator = /*#__PURE__*/new WeakMap();
var _dropdownMenuTraces = /*#__PURE__*/new WeakMap();
var _previousConditionStack = /*#__PURE__*/new WeakMap();
var _Filters_brand = /*#__PURE__*/new WeakSet();
export class Filters extends BasePlugin {
static get PLUGIN_KEY() {
return PLUGIN_KEY;
}
static get PLUGIN_PRIORITY() {
return PLUGIN_PRIORITY;
}
static get PLUGIN_DEPS() {
return ['plugin:DropdownMenu', 'plugin:HiddenRows', 'cell-type:checkbox'];
}
/**
* Instance of {@link DropdownMenu}.
*
* @private
* @type {DropdownMenu}
*/
constructor(hotInstance) {
var _this;
// One listener for the enable/disable functionality
super(hotInstance);
_this = this;
/**
* `afterChange` listener.
*
* @param {Array} changes Array of changes.
*/
_classPrivateMethodInitSpec(this, _Filters_brand);
_defineProperty(this, "dropdownMenuPlugin", null);
/**
* Instance of {@link ConditionCollection}.
*
* @private
* @type {ConditionCollection}
*/
_defineProperty(this, "conditionCollection", null);
/**
* Instance of {@link ConditionUpdateObserver}.
*
* @private
* @type {ConditionUpdateObserver}
*/
_defineProperty(this, "conditionUpdateObserver", null);
/**
* Map, where key is component identifier and value represent `BaseComponent` element or it derivatives.
*
* @private
* @type {Map}
*/
_defineProperty(this, "components", new Map([['filter_by_condition', null], ['filter_operators', null], ['filter_by_condition2', null], ['filter_by_value', null], ['filter_action_bar', null]]));
/**
* Map of skipped rows by plugin.
*
* @private
* @type {null|TrimmingMap}
*/
_defineProperty(this, "filtersRowsMap", null);
/**
* Menu focus navigator allows switching the focus position through Tab and Shift Tab keys.
*
* @type {MenuFocusNavigator|undefined}
*/
_classPrivateFieldInitSpec(this, _menuFocusNavigator, void 0);
/**
* Traces the new menu instances to apply the focus navigation to the latest one.
*
* @type {WeakSet<Menu>}
*/
_classPrivateFieldInitSpec(this, _dropdownMenuTraces, new WeakSet());
/**
* Stores the previous state of the condition stack before the latest filter operation.
* This is used in the `beforeFilter` plugin to allow performing the undo operation.
*
* @type {Array}
*/
_classPrivateFieldInitSpec(this, _previousConditionStack, []);
this.hot.addHook('afterGetColHeader', function () {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return _assertClassBrand(_Filters_brand, _this, _onAfterGetColHeader).call(_this, ...args);
});
}
/**
* Checks if the plugin is enabled in the handsontable settings. This method is executed in {@link Hooks#beforeInit}
* hook and if it returns `true` then the {@link Filters#enablePlugin} method is called.
*
* @returns {boolean}
*/
isEnabled() {
/* eslint-disable no-unneeded-ternary */
return this.hot.getSettings()[PLUGIN_KEY] ? true : false;
}
/**
* Enables the plugin functionality for this Handsontable instance.
*/
enablePlugin() {
var _this2 = this;
if (this.enabled) {
return;
}
this.filtersRowsMap = this.hot.rowIndexMapper.registerMap(this.pluginName, new TrimmingMap());
this.dropdownMenuPlugin = this.hot.getPlugin('dropdownMenu');
const dropdownSettings = this.hot.getSettings().dropdownMenu;
const menuContainer = dropdownSettings && dropdownSettings.uiContainer || this.hot.rootPortalElement;
const addConfirmationHooks = component => {
component.addLocalHook('accept', () => _assertClassBrand(_Filters_brand, this, _onActionBarSubmit).call(this, 'accept'));
component.addLocalHook('cancel', () => _assertClassBrand(_Filters_brand, this, _onActionBarSubmit).call(this, 'cancel'));
component.addLocalHook('change', command => _assertClassBrand(_Filters_brand, this, _onComponentChange).call(this, component, command));
return component;
};
const filterByConditionLabel = () => `${this.hot.getTranslatedPhrase(constants.FILTERS_DIVS_FILTER_BY_CONDITION)}:`;
const filterValueLabel = () => `${this.hot.getTranslatedPhrase(constants.FILTERS_DIVS_FILTER_BY_VALUE)}:`;
if (!this.components.get('filter_by_condition')) {
const conditionComponent = new ConditionComponent(this.hot, {
id: 'filter_by_condition',
name: filterByConditionLabel,
addSeparator: false,
menuContainer
});
conditionComponent.addLocalHook('afterClose', () => _assertClassBrand(_Filters_brand, this, _onSelectUIClosed).call(this));
this.components.set('filter_by_condition', addConfirmationHooks(conditionComponent));
}
if (!this.components.get('filter_operators')) {
this.components.set('filter_operators', new OperatorsComponent(this.hot, {
id: 'filter_operators',
name: 'Operators'
}));
}
if (!this.components.get('filter_by_condition2')) {
const conditionComponent = new ConditionComponent(this.hot, {
id: 'filter_by_condition2',
name: '',
addSeparator: true,
menuContainer
});
conditionComponent.addLocalHook('afterClose', () => _assertClassBrand(_Filters_brand, this, _onSelectUIClosed).call(this));
this.components.set('filter_by_condition2', addConfirmationHooks(conditionComponent));
}
if (!this.components.get('filter_by_value')) {
this.components.set('filter_by_value', addConfirmationHooks(new ValueComponent(this.hot, {
id: 'filter_by_value',
name: filterValueLabel
})));
}
if (!this.components.get('filter_action_bar')) {
this.components.set('filter_action_bar', addConfirmationHooks(new ActionBarComponent(this.hot, {
id: 'filter_action_bar',
name: 'Action bar'
})));
}
if (!this.conditionCollection) {
this.conditionCollection = new ConditionCollection(this.hot);
}
if (!this.conditionUpdateObserver) {
this.conditionUpdateObserver = new ConditionUpdateObserver(this.hot, this.conditionCollection, physicalColumn => this.getDataMapAtColumn(physicalColumn));
this.conditionUpdateObserver.addLocalHook('update', conditionState => _assertClassBrand(_Filters_brand, this, _updateComponents).call(this, conditionState));
}
this.components.forEach(component => component.show());
this.addHook('afterDropdownMenuDefaultOptions', function () {
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
return _assertClassBrand(_Filters_brand, _this2, _onAfterDropdownMenuDefaultOptions).call(_this2, ...args);
});
this.addHook('beforeDropdownMenuShow', () => _assertClassBrand(_Filters_brand, this, _onBeforeDropdownMenuShow).call(this));
this.addHook('afterDropdownMenuShow', () => _assertClassBrand(_Filters_brand, this, _onAfterDropdownMenuShow).call(this));
this.addHook('afterDropdownMenuHide', () => _assertClassBrand(_Filters_brand, this, _onAfterDropdownMenuHide).call(this));
this.addHook('afterChange', changes => _assertClassBrand(_Filters_brand, this, _onAfterChange).call(this, changes));
// Temp. solution (extending menu items bug in contextMenu/dropdownMenu)
if (this.hot.getSettings().dropdownMenu && this.dropdownMenuPlugin) {
this.dropdownMenuPlugin.disablePlugin();
this.dropdownMenuPlugin.enablePlugin();
}
if (!_classPrivateFieldGet(_menuFocusNavigator, this) && this.dropdownMenuPlugin.enabled) {
const focusableItems = [
// A fake menu item that once focused allows escaping from the focus navigation (using Tab keys)
// to the menu navigation using arrow keys.
{
focus: () => {
const menu = _classPrivateFieldGet(_menuFocusNavigator, this).getMenu();
const menuNavigator = menu.getNavigator();
const lastSelectedMenuItem = _classPrivateFieldGet(_menuFocusNavigator, this).getLastMenuPage();
menu.focus();
if (lastSelectedMenuItem > 0) {
menuNavigator.setCurrentPage(lastSelectedMenuItem);
} else {
menuNavigator.toFirstItem();
}
}
}, ...Array.from(this.components).map(_ref => {
let [, component] = _ref;
return component.getElements();
}).flat()];
_classPrivateFieldSet(_menuFocusNavigator, this, createMenuFocusController(this.dropdownMenuPlugin.menu, focusableItems));
const forwardToFocusNavigation = event => {
_classPrivateFieldGet(_menuFocusNavigator, this).listen();
event.preventDefault();
if (isKey(event.keyCode, 'TAB')) {
if (event.shiftKey) {
_classPrivateFieldGet(_menuFocusNavigator, this).toPreviousItem();
} else {
_classPrivateFieldGet(_menuFocusNavigator, this).toNextItem();
}
}
};
this.components.get('filter_by_value').addLocalHook('listTabKeydown', forwardToFocusNavigation);
this.components.get('filter_by_condition').addLocalHook('selectTabKeydown', forwardToFocusNavigation);
}
this.registerShortcuts();
super.enablePlugin();
}
/**
* Disables the plugin functionality for this Handsontable instance.
*/
disablePlugin() {
if (this.enabled) {
var _this$dropdownMenuPlu;
if ((_this$dropdownMenuPlu = this.dropdownMenuPlugin) !== null && _this$dropdownMenuPlu !== void 0 && _this$dropdownMenuPlu.enabled) {
this.dropdownMenuPlugin.menu.clearLocalHooks();
}
this.components.forEach((component, key) => {
component.destroy();
this.components.set(key, null);
});
this.conditionCollection.destroy();
this.conditionCollection = null;
this.hot.rowIndexMapper.unregisterMap(this.pluginName);
}
this.unregisterShortcuts();
super.disablePlugin();
}
/**
* Register shortcuts responsible for clearing the filters.
*
* @private
*/
registerShortcuts() {
this.hot.getShortcutManager().getContext('grid').addShortcut({
keys: [['Alt', 'A']],
stopPropagation: true,
callback: () => {
const selection = this.hot.getSelected();
this.clearConditions();
this.filter();
if (selection) {
this.hot.selectCells(selection);
}
},
group: SHORTCUTS_GROUP
});
}
/**
* Unregister shortcuts responsible for clearing the filters.
*
* @private
*/
unregisterShortcuts() {
this.hot.getShortcutManager().getContext('grid').removeShortcutsByGroup(SHORTCUTS_GROUP);
}
/* eslint-disable jsdoc/require-description-complete-sentence */
/**
* @memberof Filters#
* @function addCondition
* @description
* Adds condition to the conditions collection at specified column index.
*
* Possible predefined conditions:
* * `begins_with` - Begins with
* * `between` - Between
* * `by_value` - By value
* * `contains` - Contains
* * `date_after` - After a date
* * `date_before` - Before a date
* * `date_today` - Today
* * `date_tomorrow` - Tomorrow
* * `date_yesterday` - Yesterday
* * `empty` - Empty
* * `ends_with` - Ends with
* * `eq` - Equal
* * `gt` - Greater than
* * `gte` - Greater than or equal
* * `lt` - Less than
* * `lte` - Less than or equal
* * `none` - None (no filter)
* * `not_between` - Not between
* * `not_contains` - Not contains
* * `not_empty` - Not empty
* * `neq` - Not equal.
*
* Possible operations on collection of conditions:
* * `conjunction` - [**Conjunction**](https://en.wikipedia.org/wiki/Logical_conjunction) on conditions collection (by default), i.e. for such operation: <br/> c1 AND c2 AND c3 AND c4 ... AND cn === TRUE, where c1 ... cn are conditions.
* * `disjunction` - [**Disjunction**](https://en.wikipedia.org/wiki/Logical_disjunction) on conditions collection, i.e. for such operation: <br/> c1 OR c2 OR c3 OR c4 ... OR cn === TRUE, where c1, c2, c3, c4 ... cn are conditions.
* * `disjunctionWithExtraCondition` - **Disjunction** on first `n - 1`\* conditions from collection with an extra requirement computed from the last condition, i.e. for such operation: <br/> c1 OR c2 OR c3 OR c4 ... OR cn-1 AND cn === TRUE, where c1, c2, c3, c4 ... cn are conditions.
*
* \* when `n` is collection size; it's used i.e. for one operation introduced from UI (when choosing from filter's drop-down menu two conditions with OR operator between them, mixed with choosing values from the multiple choice select)
*
* **Note**: Mind that you cannot mix different types of operations (for instance, if you use `conjunction`, use it consequently for a particular column).
*
* @example
* ::: only-for javascript
* ```js
* const container = document.getElementById('example');
* const hot = new Handsontable(container, {
* data: getData(),
* filters: true
* });
*
* // access to filters plugin instance
* const filtersPlugin = hot.getPlugin('filters');
*
* // add filter "Greater than" 95 to column at index 1
* filtersPlugin.addCondition(1, 'gt', [95]);
* filtersPlugin.filter();
*
* // add filter "By value" to column at index 1
* // in this case all value's that don't match will be filtered.
* filtersPlugin.addCondition(1, 'by_value', [['ing', 'ed', 'as', 'on']]);
* filtersPlugin.filter();
*
* // add filter "Begins with" with value "de" AND "Not contains" with value "ing"
* filtersPlugin.addCondition(1, 'begins_with', ['de'], 'conjunction');
* filtersPlugin.addCondition(1, 'not_contains', ['ing'], 'conjunction');
* filtersPlugin.filter();
*
* // add filter "Begins with" with value "de" OR "Not contains" with value "ing"
* filtersPlugin.addCondition(1, 'begins_with', ['de'], 'disjunction');
* filtersPlugin.addCondition(1, 'not_contains', ['ing'], 'disjunction');
* filtersPlugin.filter();
* ```
* :::
*
* ::: only-for react
* ```jsx
* const hotRef = useRef(null);
*
* ...
*
* <HotTable
* ref={hotRef}
* data={getData()}
* filters={true}
* />
*
* // access to filters plugin instance
* const hot = hotRef.current.hotInstance;
* const filtersPlugin = hot.getPlugin('filters');
*
* // add filter "Greater than" 95 to column at index 1
* filtersPlugin.addCondition(1, 'gt', [95]);
* filtersPlugin.filter();
*
* // add filter "By value" to column at index 1
* // in this case all value's that don't match will be filtered.
* filtersPlugin.addCondition(1, 'by_value', [['ing', 'ed', 'as', 'on']]);
* filtersPlugin.filter();
*
* // add filter "Begins with" with value "de" AND "Not contains" with value "ing"
* filtersPlugin.addCondition(1, 'begins_with', ['de'], 'conjunction');
* filtersPlugin.addCondition(1, 'not_contains', ['ing'], 'conjunction');
* filtersPlugin.filter();
*
* // add filter "Begins with" with value "de" OR "Not contains" with value "ing"
* filtersPlugin.addCondition(1, 'begins_with', ['de'], 'disjunction');
* filtersPlugin.addCondition(1, 'not_contains', ['ing'], 'disjunction');
* filtersPlugin.filter();
* ```
* :::
*
* ::: only-for angular
* ```ts
* import { AfterViewInit, Component, ViewChild } from "@angular/core";
* import {
* GridSettings,
* HotTableModule,
* HotTableComponent,
* } from "@handsontable/angular-wrapper";
*
* `@Component`({
* selector: "app-example",
* standalone: true,
* imports: [HotTableModule],
* template: ` <div>
* <hot-table themeName="ht-theme-main" [settings]="gridSettings" />
* </div>`,
* })
* export class ExampleComponent implements AfterViewInit {
* `@ViewChild`(HotTableComponent, { static: false })
* readonly hotTable!: HotTableComponent;
*
* readonly gridSettings = <GridSettings>{
* data: this.getData(),
* filters: true,
* };
*
* ngAfterViewInit(): void {
* // Access to filters plugin instance
* const hot = this.hotTable.hotInstance;
* const filtersPlugin = hot.getPlugin("filters");
*
* // Add filter "Greater than" 95 to column at index 1
* filtersPlugin.addCondition(1, "gt", [95]);
* filtersPlugin.filter();
*
* // Add filter "By value" to column at index 1
* // In this case, all values that don't match will be filtered.
* filtersPlugin.addCondition(1, "by_value", [["ing", "ed", "as", "on"]]);
* filtersPlugin.filter();
*
* // Add filter "Begins with" with value "de" AND "Not contains" with value "ing"
* filtersPlugin.addCondition(1, "begins_with", ["de"], "conjunction");
* filtersPlugin.addCondition(1, "not_contains", ["ing"], "conjunction");
* filtersPlugin.filter();
*
* // Add filter "Begins with" with value "de" OR "Not contains" with value "ing"
* filtersPlugin.addCondition(1, "begins_with", ["de"], "disjunction");
* filtersPlugin.addCondition(1, "not_contains", ["ing"], "disjunction");
* filtersPlugin.filter();
* }
*
* private getData(): any[] {
* // Get some data
* }
* }
* ```
* :::
*
* @param {number} column Visual column index.
* @param {string} name Condition short name.
* @param {Array} args Condition arguments.
* @param {string} [operationId=conjunction] `id` of operation which is performed on the column.
*/
/* eslint-enable jsdoc/require-description-complete-sentence */
addCondition(column, name, args) {
let operationId = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : OPERATION_AND;
const physicalColumn = this.hot.toPhysicalColumn(column);
this.conditionCollection.addCondition(physicalColumn, {
command: {
key: name
},
args
}, operationId);
}
/**
* Removes conditions at specified column index.
*
* @param {number} column Visual column index.
*/
removeConditions(column) {
const physicalColumn = this.hot.toPhysicalColumn(column);
this.conditionCollection.removeConditions(physicalColumn);
}
/**
* Clears all conditions previously added to the collection for the specified column index or, if the column index
* was not passed, clear the conditions for all columns.
*
* @param {number} [column] Visual column index.
*/
clearConditions(column) {
if (column === undefined) {
this.conditionCollection.clean();
} else {
const physicalColumn = this.hot.toPhysicalColumn(column);
this.conditionCollection.removeConditions(physicalColumn);
}
}
/**
* Imports filter conditions to all columns to the plugin. The method accepts
* the array of conditions with the same structure as the {@link Filters#exportConditions} method returns.
* Importing conditions will replace the current conditions. Once replaced, the state of the condition
* will be reflected in the UI. To apply the changes and filter the table, call
* the {@link Filters#filter} method eventually.
*
* @param {Array} conditions Array of conditions.
*/
importConditions(conditions) {
this.conditionCollection.importAllConditions(conditions);
}
/* eslint-disable jsdoc/require-description-complete-sentence */
/**
* Exports filter conditions for all columns from the plugin.
* The array represents the filter state for each column. For example:
*
* ```js
* [
* {
* column: 1,
* operation: 'conjunction',
* conditions: [
* { name: 'gt', args: [95] },
* ]
* },
* {
* column: 7,
* operation: 'conjunction',
* conditions: [
* { name: 'contains', args: ['mike'] },
* { name: 'begins_with', args: ['m'] },
* ]
* },
* ]
* ```
*
* @returns {Array}
*/
exportConditions() {
return this.conditionCollection.exportAllConditions();
}
/* eslint-enable jsdoc/require-description-complete-sentence */
/**
* Filters data based on added filter conditions.
*
* @fires Hooks#beforeFilter
* @fires Hooks#afterFilter
*/
filter() {
const {
navigableHeaders
} = this.hot.getSettings();
const dataFilter = this._createDataFilter();
const needToFilter = !this.conditionCollection.isEmpty();
let visibleVisualRows = [];
const conditions = this.exportConditions();
const allowFiltering = this.hot.runHooks('beforeFilter', conditions, _classPrivateFieldGet(_previousConditionStack, this));
if (allowFiltering !== false && needToFilter) {
const trimmedRows = [];
this.hot.batchExecution(() => {
this.filtersRowsMap.clear();
visibleVisualRows = arrayMap(dataFilter.filter(), rowData => rowData.meta.visualRow);
const visibleVisualRowsAssertion = createArrayAssertion(visibleVisualRows);
rangeEach(this.hot.countSourceRows() - 1, row => {
if (!visibleVisualRowsAssertion(row)) {
trimmedRows.push(row);
}
});
arrayEach(trimmedRows, physicalRow => {
this.filtersRowsMap.setValueAtIndex(physicalRow, true);
});
}, true);
if (!navigableHeaders && !visibleVisualRows.length) {
this.hot.deselectCell();
}
_classPrivateFieldSet(_previousConditionStack, this, this.exportConditions());
} else if (allowFiltering !== false && !needToFilter) {
_classPrivateFieldSet(_previousConditionStack, this, this.exportConditions());
this.filtersRowsMap.clear();
} else {
this.importConditions(_classPrivateFieldGet(_previousConditionStack, this));
}
if (this.hot.selection.isSelected()) {
this.hot.selectCell(navigableHeaders ? -1 : 0, this.hot.getSelectedRangeActive().highlight.col);
}
if (allowFiltering !== false) {
this.hot.runHooks('afterFilter', conditions);
this.hot.view.adjustElementsSize();
this.hot.render();
}
}
/**
* Gets last selected column index.
*
* @returns {{visualIndex: number, physicalIndex: number} | null} Returns `null` when a column is
* not selected. Otherwise, returns an object with `visualIndex` and `physicalIndex` properties containing
* the index of the column.
*/
getSelectedColumn() {
var _this$hot$getSelected;
const highlight = (_this$hot$getSelected = this.hot.getSelectedRangeActive()) === null || _this$hot$getSelected === void 0 ? void 0 : _this$hot$getSelected.highlight;
if (!highlight) {
return null;
}
return {
visualIndex: highlight.col,
physicalIndex: this.hot.toPhysicalColumn(highlight.col)
};
}
/**
* Returns handsontable source data with cell meta based on current selection.
*
* @param {number} [column] The physical column index. By default column index accept the value of the selected column.
* @returns {Array} Returns array of objects where keys as row index.
*/
getDataMapAtColumn(column) {
const visualColumn = this.hot.toVisualColumn(column);
const data = [];
arrayEach(this.hot.getSourceDataAtCol(visualColumn), (value, rowIndex) => {
var _this$hot$getDataAtCe;
const {
row,
col,
visualCol,
visualRow,
type,
instance,
dateFormat,
locale
} = this.hot.getCellMeta(rowIndex, visualColumn);
const dataValue = (_this$hot$getDataAtCe = this.hot.getDataAtCell(this.hot.toVisualRow(rowIndex), visualColumn)) !== null && _this$hot$getDataAtCe !== void 0 ? _this$hot$getDataAtCe : value;
data.push({
meta: {
row,
col,
visualCol,
visualRow,
type,
instance,
dateFormat,
locale
},
value: toEmptyString(dataValue)
});
});
return data;
}
/**
* Update the condition of ValueComponent, based on the handled changes.
*
* @private
* @param {number} columnIndex Column index of handled ValueComponent condition.
*/
updateValueComponentCondition(columnIndex) {
const dataAtCol = this.hot.getDataAtCol(columnIndex);
const selectedValues = unifyColumnValues(dataAtCol);
this.conditionUpdateObserver.updateStatesAtColumn(columnIndex, selectedValues);
}
/**
* Restores components to its saved state.
*
* @private
* @param {Array} components List of components.
*/
restoreComponents(components) {
var _this$getSelectedColu;
const physicalIndex = (_this$getSelectedColu = this.getSelectedColumn()) === null || _this$getSelectedColu === void 0 ? void 0 : _this$getSelectedColu.physicalIndex;
components.forEach(component => {
if (component.isHidden()) {
return;
}
component.restoreState(physicalIndex);
});
this.updateDependentComponentsVisibility();
}
/**
* After dropdown menu show listener.
*/
/**
* Get an operation, based on the number and types of arguments (where arguments are states of components).
*
* @param {string} suggestedOperation Operation which was chosen by user from UI.
* @param {object} byConditionState1 State of first condition component.
* @param {object} byConditionState2 State of second condition component.
* @param {object} byValueState State of value component.
* @private
* @returns {string}
*/
getOperationBasedOnArguments(suggestedOperation, byConditionState1, byConditionState2, byValueState) {
let operation = suggestedOperation;
if (operation === OPERATION_OR && byConditionState1.command.key !== CONDITION_NONE && byConditionState2.command.key !== CONDITION_NONE && byValueState.command.key !== CONDITION_NONE) {
operation = OPERATION_OR_THEN_VARIABLE;
} else if (byValueState.command.key !== CONDITION_NONE) {
if (byConditionState1.command.key === CONDITION_NONE || byConditionState2.command.key === CONDITION_NONE) {
operation = OPERATION_AND;
}
}
return operation;
}
/**
* On action bar submit listener.
*
* @private
* @param {string} submitType The submit type.
*/
/**
* Listen to the keyboard input on document body and forward events to instance of Handsontable
* created by DropdownMenu plugin.
*
* @private
*/
setListeningDropdownMenu() {
if (this.dropdownMenuPlugin) {
this.dropdownMenuPlugin.setListening();
}
}
/**
* Updates visibility of some of the components, based on the state of the parent component.
*
* @private
*/
updateDependentComponentsVisibility() {
const component = this.components.get('filter_by_condition');
const {
command
} = component.getState();
const componentsToShow = [this.components.get('filter_by_condition2'), this.components.get('filter_operators')];
if (command.showOperators) {
this.showComponents(...componentsToShow);
} else {
this.hideComponents(...componentsToShow);
}
}
/**
* On after get column header listener.
*
* @param {number} col Visual column index.
* @param {HTMLTableCellElement} TH Header's TH element.
* @param {number} headerLevel The index of header level counting from the top (positive
* values counting from 0 to N).
*
*/
/**
* Creates DataFilter instance based on condition collection.
*
* @private
* @param {ConditionCollection} conditionCollection Condition collection object.
* @returns {DataFilter}
*/
_createDataFilter() {
let conditionCollection = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : this.conditionCollection;
return new DataFilter(conditionCollection, physicalColumn => this.getDataMapAtColumn(physicalColumn));
}
/**
* It updates the components state. The state is triggered by ConditionUpdateObserver, which
* reacts to any condition added to the condition collection. It may be added through the UI
* components or by API call.
*
* @param {object} conditionsState An object with the state generated by UI components.
*/
/**
* Returns indexes of passed components inside list of `dropdownMenu` items.
*
* @private
* @param {...BaseComponent} components List of components.
* @returns {Array}
*/
getIndexesOfComponents() {
const indexes = [];
if (!this.dropdownMenuPlugin) {
return indexes;
}
const menu = this.dropdownMenuPlugin.menu;
for (var _len3 = arguments.length, components = new Array(_len3), _key3 = 0; _key3 < _len3; _key3++) {
components[_key3] = arguments[_key3];
}
arrayEach(components, component => {
arrayEach(menu.menuItems, (item, index) => {
if (item.key === component.getMenuItemDescriptor().key) {
indexes.push(index);
}
});
});
return indexes;
}
/**
* Changes visibility of component.
*
* @private
* @param {boolean} visible Determine if components should be visible.
* @param {...BaseComponent} components List of components.
*/
changeComponentsVisibility() {
let visible = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true;
if (!this.dropdownMenuPlugin) {
return;
}
const menu = this.dropdownMenuPlugin.menu;
const hotMenu = menu.hotMenu;
const hiddenRows = hotMenu.getPlugin('hiddenRows');
for (var _len4 = arguments.length, components = new Array(_len4 > 1 ? _len4 - 1 : 0), _key4 = 1; _key4 < _len4; _key4++) {
components[_key4 - 1] = arguments[_key4];
}
const indexes = this.getIndexesOfComponents(...components);
if (visible) {
hiddenRows.showRows(indexes);
} else {
hiddenRows.hideRows(indexes);
}
hotMenu.render();
}
/**
* Hides components of filters `dropdownMenu`.
*
* @private
* @param {...BaseComponent} components List of components.
*/
hideComponents() {
for (var _len5 = arguments.length, components = new Array(_len5), _key5 = 0; _key5 < _len5; _key5++) {
components[_key5] = arguments[_key5];
}
this.changeComponentsVisibility(false, ...components);
}
/**
* Shows components of filters `dropdownMenu`.
*
* @private
* @param {...BaseComponent} components List of components.
*/
showComponents() {
for (var _len6 = arguments.length, components = new Array(_len6), _key6 = 0; _key6 < _len6; _key6++) {
components[_key6] = arguments[_key6];
}
this.changeComponentsVisibility(true, ...components);
}
/**
* Destroys the plugin instance.
*/
destroy() {
if (this.enabled) {
this.components.forEach((component, key) => {
if (component !== null) {
component.destroy();
this.components.set(key, null);
}
});
this.conditionCollection.destroy();
this.conditionUpdateObserver.destroy();
this.hot.rowIndexMapper.unregisterMap(this.pluginName);
}
super.destroy();
}
}
function _onAfterChange(changes) {
if (changes) {
arrayEach(changes, change => {
const [, prop] = change;
const columnIndex = this.hot.propToCol(prop);
if (this.conditionCollection.hasConditions(columnIndex)) {
this.updateValueComponentCondition(columnIndex);
}
});
}
}
function _onAfterDropdownMenuShow() {
const menu = this.dropdownMenuPlugin.menu;
this.restoreComponents(Array.from(this.components.values()));
menu.updateMenuDimensions();
}
/**
* After dropdown menu hide listener.
*/
function _onAfterDropdownMenuHide() {
this.components.get('filter_by_condition').getSelectElement().closeOptions();
this.components.get('filter_by_condition2').getSelectElement().closeOptions();
}
/**
* Hooks applies the new dropdown menu instance to the focus navigator.
*/
function _onBeforeDropdownMenuShow() {
const mainMenu = this.dropdownMenuPlugin.menu;
if (!_classPrivateFieldGet(_dropdownMenuTraces, this).has(mainMenu)) {
_classPrivateFieldGet(_menuFocusNavigator, this).setMenu(mainMenu);
}
_classPrivateFieldGet(_dropdownMenuTraces, this).add(mainMenu);
}
/**
* After dropdown menu default options listener.
*
* @param {object} defaultOptions ContextMenu default item options.
*/
function _onAfterDropdownMenuDefaultOptions(defaultOptions) {
defaultOptions.items.push({
name: SEPARATOR
});
this.components.forEach(component => {
defaultOptions.items.push(component.getMenuItemDescriptor());
});
}
function _onActionBarSubmit(submitType) {
var _this$dropdownMenuPlu3;
if (submitType === 'accept') {
const selectedColumn = this.getSelectedColumn();
if (selectedColumn === null) {
var _this$dropdownMenuPlu2;
(_this$dropdownMenuPlu2 = this.dropdownMenuPlugin) === null || _this$dropdownMenuPlu2 === void 0 || _this$dropdownMenuPlu2.close();
return;
}
const {
physicalIndex
} = selectedColumn;
const byConditionState1 = this.components.get('filter_by_condition').getState();
const byConditionState2 = this.components.get('filter_by_condition2').getState();
const byValueState = this.components.get('filter_by_value').getState();
const operation = this.getOperationBasedOnArguments(this.components.get('filter_operators').getActiveOperationId(), byConditionState1, byConditionState2, byValueState);
this.conditionUpdateObserver.groupChanges();
let columnStackPosition = this.conditionCollection.getColumnStackPosition(physicalIndex);
if (columnStackPosition === -1) {
columnStackPosition = undefined;
}
this.conditionCollection.removeConditions(physicalIndex);
if (byConditionState1.command.key !== CONDITION_NONE) {
this.conditionCollection.addCondition(physicalIndex, byConditionState1, operation, columnStackPosition);
if (byConditionState2.command.key !== CONDITION_NONE) {
this.conditionCollection.addCondition(physicalIndex, byConditionState2, operation, columnStackPosition);
}
}
if (byValueState.command.key !== CONDITION_NONE) {
this.conditionCollection.addCondition(physicalIndex, byValueState, operation, columnStackPosition);
}
this.conditionUpdateObserver.flush();
this.components.forEach(component => component.saveState(physicalIndex));
this.filter();
}
(_this$dropdownMenuPlu3 = this.dropdownMenuPlugin) === null || _this$dropdownMenuPlu3 === void 0 || _this$dropdownMenuPlu3.close();
}
/**
* On component change listener.
*
* @param {BaseComponent} component Component inheriting BaseComponent.
* @param {object} command Menu item object (command).
*/
function _onComponentChange(component, command) {
const menu = this.dropdownMenuPlugin.menu;
this.updateDependentComponentsVisibility();
if (component.constructor === ConditionComponent && !command.inputsCount) {
this.setListeningDropdownMenu();
}
menu.updateMenuDimensions();
}
/**
* On component SelectUI closed listener.
*/
function _onSelectUIClosed() {
this.setListeningDropdownMenu();
}
function _onAfterGetColHeader(col, TH, headerLevel) {
const physicalColumn = this.hot.toPhysicalColumn(col);
if (this.enabled && this.conditionCollection.hasConditions(physicalColumn) && headerLevel === this.hot.view.getColumnHeadersCount() - 1) {
addClass(TH, 'htFiltersActive');
} else {
removeClass(TH, 'htFiltersActive');
}
}
function _updateComponents(conditionsState) {
var _this$dropdownMenuPlu4;
if (!((_this$dropdownMenuPlu4 = this.dropdownMenuPlugin) !== null && _this$dropdownMenuPlu4 !== void 0 && _this$dropdownMenuPlu4.enabled)) {
return;
}
const {
editedConditionStack: {
conditions,
column
},
conditionArgsChange
} = conditionsState;
if (Array.isArray(conditionArgsChange)) {
// update the previous condition stack (only for 'by_value' condition) on each dataset
// change to make the undo/redo work properly
_classPrivateFieldSet(_previousConditionStack, this, _classPrivateFieldGet(_previousConditionStack, this).map(stack => {
if (stack.column === column && conditions.length > 0) {
stack.conditions.forEach(condition => {
if (condition.name === 'by_value') {
condition.args = [[...conditionArgsChange]];
}
});
}
return stack;
}));
}
const conditionsByValue = conditions.filter(condition => condition.name === CONDITION_BY_VALUE);
const conditionsWithoutByValue = conditions.filter(condition => condition.name !== CONDITION_BY_VALUE);
if (conditionsByValue.length >= 2 || conditionsWithoutByValue.length >= 3) {
warn(toSingleLine`The filter conditions have been applied properly, but couldn’t be displayed visually.\x20
The overall amount of conditions exceed the capability of the dropdown menu.\x20
For more details see the documentation.`);
} else {
const operationType = this.conditionCollection.getOperation(column);
this.components.get('filter_by_condition').updateState(conditionsWithoutByValue[0], column);
this.components.get('filter_by_condition2').updateState(conditionsWithoutByValue[1], column);
this.components.get('filter_operators').updateState(operationType, column);
this.components.get('filter_by_value').updateState(conditionsState);
}
}