summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js/views/search_panel.js
diff options
context:
space:
mode:
authorstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
committerstephanchrst <stephanchrst@gmail.com>2022-05-10 21:51:50 +0700
commit3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch)
treea44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/views/search_panel.js
parent0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (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.js214
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);
+});