diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/views/search_panel.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/views/search_panel.js')
| -rw-r--r-- | addons/web/static/src/js/views/search_panel.js | 214 |
1 files changed, 214 insertions, 0 deletions
diff --git a/addons/web/static/src/js/views/search_panel.js b/addons/web/static/src/js/views/search_panel.js new file mode 100644 index 00000000..71537847 --- /dev/null +++ b/addons/web/static/src/js/views/search_panel.js @@ -0,0 +1,214 @@ +odoo.define("web/static/src/js/views/search_panel.js", function (require) { + "use strict"; + + const { Model, useModel } = require("web/static/src/js/model.js"); + const patchMixin = require("web.patchMixin"); + + const { Component, hooks } = owl; + const { useState, useSubEnv } = hooks; + + /** + * Search panel + * + * Represent an extension of the search interface located on the left side of + * the view. It is divided in sections defined in a "<searchpanel>" node located + * inside of a "<search>" arch. Each section is represented by a list of different + * values (categories or ungrouped filters) or groups of values (grouped filters). + * Its state is directly affected by its model (@see SearchPanelModelExtension). + * @extends Component + */ + class SearchPanel extends Component { + constructor() { + super(...arguments); + + useSubEnv({ searchModel: this.props.searchModel }); + + this.state = useState({ + active: {}, + expanded: {}, + }); + this.model = useModel("searchModel"); + this.scrollTop = 0; + this.hasImportedState = false; + + this.importState(this.props.importedState); + } + + async willStart() { + this._expandDefaultValue(); + this._updateActiveValues(); + } + + mounted() { + this._updateGroupHeadersChecked(); + if (this.hasImportedState) { + this.el.scroll({ top: this.scrollTop }); + } + } + + async willUpdateProps() { + this._updateActiveValues(); + } + + //--------------------------------------------------------------------- + // Public + //--------------------------------------------------------------------- + + exportState() { + const exported = { + expanded: this.state.expanded, + scrollTop: this.el.scrollTop, + }; + return JSON.stringify(exported); + } + + importState(stringifiedState) { + this.hasImportedState = Boolean(stringifiedState); + if (this.hasImportedState) { + const state = JSON.parse(stringifiedState); + this.state.expanded = state.expanded; + this.scrollTop = state.scrollTop; + } + } + + //--------------------------------------------------------------------- + // Private + //--------------------------------------------------------------------- + + /** + * Expands category values holding the default value of a category. + * @private + */ + _expandDefaultValue() { + if (this.hasImportedState) { + return; + } + const categories = this.model.get("sections", s => s.type === "category"); + for (const category of categories) { + this.state.expanded[category.id] = {}; + if (category.activeValueId) { + const ancestorIds = this._getAncestorValueIds(category, category.activeValueId); + for (const ancestorId of ancestorIds) { + this.state.expanded[category.id][ancestorId] = true; + } + } + } + } + + /** + * @private + * @param {Object} category + * @param {number} categoryValueId + * @returns {number[]} list of ids of the ancestors of the given value in + * the given category. + */ + _getAncestorValueIds(category, categoryValueId) { + const { parentId } = category.values.get(categoryValueId); + return parentId ? [...this._getAncestorValueIds(category, parentId), parentId] : []; + } + + /** + * Prevent unnecessary calls to the model by ensuring a different category + * is clicked. + * @private + * @param {Object} category + * @param {Object} value + */ + async _toggleCategory(category, value) { + if (value.childrenIds.length) { + const categoryState = this.state.expanded[category.id]; + if (categoryState[value.id] && category.activeValueId === value.id) { + delete categoryState[value.id]; + } else { + categoryState[value.id] = true; + } + } + if (category.activeValueId !== value.id) { + this.state.active[category.id] = value.id; + this.model.dispatch("toggleCategoryValue", category.id, value.id); + } + } + + /** + * @private + * @param {number} filterId + * @param {{ values: Map<Object> }} group + */ + _toggleFilterGroup(filterId, { values }) { + const valueIds = []; + const checked = [...values.values()].every( + (value) => this.state.active[filterId][value.id] + ); + values.forEach(({ id }) => { + valueIds.push(id); + this.state.active[filterId][id] = !checked; + }); + this.model.dispatch("toggleFilterValues", filterId, valueIds, !checked); + } + + /** + * @private + * @param {number} filterId + * @param {Object} [group] + * @param {number} valueId + * @param {MouseEvent} ev + */ + _toggleFilterValue(filterId, valueId, { currentTarget }) { + this.state.active[filterId][valueId] = currentTarget.checked; + this._updateGroupHeadersChecked(); + this.model.dispatch("toggleFilterValues", filterId, [valueId]); + } + + _updateActiveValues() { + for (const section of this.model.get("sections")) { + if (section.type === "category") { + this.state.active[section.id] = section.activeValueId; + } else { + this.state.active[section.id] = {}; + if (section.groups) { + for (const group of section.groups.values()) { + for (const value of group.values.values()) { + this.state.active[section.id][value.id] = value.checked; + } + } + } + if (section && section.values) { + for (const value of section.values.values()) { + this.state.active[section.id][value.id] = value.checked; + } + } + } + } + } + + /** + * Updates the "checked" or "indeterminate" state of each of the group + * headers according to the state of their values. + * @private + */ + _updateGroupHeadersChecked() { + const groups = this.el.querySelectorAll(":scope .o_search_panel_filter_group"); + for (const group of groups) { + const header = group.querySelector(":scope .o_search_panel_group_header input"); + const vals = [...group.querySelectorAll(":scope .o_search_panel_filter_value input")]; + header.checked = false; + header.indeterminate = false; + if (vals.every((v) => v.checked)) { + header.checked = true; + } else if (vals.some((v) => v.checked)) { + header.indeterminate = true; + } + } + } + } + SearchPanel.modelExtension = "SearchPanel"; + + SearchPanel.props = { + className: { type: String, optional: 1 }, + importedState: { type: String, optional: 1 }, + searchModel: Model, + }; + SearchPanel.template = "web.SearchPanel"; + + return patchMixin(SearchPanel); +}); |
