mirror of https://github.com/OCA/web.git
233 lines
6.8 KiB
JavaScript
233 lines
6.8 KiB
JavaScript
/** @odoo-module **/
|
|
/* Copyright 2018 Tecnativa - Jairo Llopis
|
|
* Copyright 2021 ITerra - Sergey Shebanin
|
|
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
|
|
|
import {Dropdown} from "@web/core/dropdown/dropdown";
|
|
import {NavBar} from "@web/webclient/navbar/navbar";
|
|
import {useAutofocus, useBus, useService} from "@web/core/utils/hooks";
|
|
import {useHotkey} from "@web/core/hotkeys/hotkey_hook";
|
|
import {scrollTo} from "@web/core/utils/scrolling";
|
|
import {debounce} from "@web/core/utils/timing";
|
|
import {fuzzyLookup} from "@web/core/utils/search";
|
|
import {WebClient} from "@web/webclient/webclient";
|
|
import {patch} from "web.utils";
|
|
|
|
const {Component} = owl;
|
|
const {useState, useRef} = owl.hooks;
|
|
|
|
// Patch WebClient to show AppsMenu instead of default app
|
|
patch(WebClient.prototype, "web_responsive.DefaultAppsMenu", {
|
|
setup() {
|
|
this._super();
|
|
useBus(Dropdown.bus, "state-changed", (payload) => {
|
|
if (payload.emitter.el.classList.contains("o_navbar_apps_menu")) {
|
|
this.el.classList.toggle("o_apps_menu_opened", payload.newState.open);
|
|
this.el.classList.toggle("o_first_app", false);
|
|
}
|
|
});
|
|
},
|
|
_loadDefaultApp() {
|
|
var menu_apps_dropdown = document.querySelector(
|
|
".o_navbar_apps_menu .dropdown-toggle"
|
|
);
|
|
menu_apps_dropdown.click();
|
|
this.el.classList.toggle("o_apps_menu_opened", true);
|
|
this.el.classList.toggle("o_first_app", true);
|
|
return true;
|
|
},
|
|
});
|
|
|
|
/**
|
|
* @extends Dropdown
|
|
*/
|
|
export class AppsMenu extends Dropdown {
|
|
setup() {
|
|
super.setup();
|
|
useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", () => this.close());
|
|
useBus(this.env.bus, "APPS_MENU:CLOSE", () => this.close());
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Reduce menu data to a searchable format understandable by fuzzyLookup
|
|
*
|
|
* `menuService.getMenuAsTree()` returns array in a format similar to this (only
|
|
* relevant data is shown):
|
|
*
|
|
* ```js
|
|
* // This is a menu entry:
|
|
* {
|
|
* actionID: 12, // Or `false`
|
|
* name: "Actions",
|
|
* childrenTree: {0: {...}, 1: {...}}}, // List of inner menu entries
|
|
* // in the same format or `undefined`
|
|
* }
|
|
* ```
|
|
*
|
|
* This format is very hard to process to search matches, and it would
|
|
* slow down the search algorithm, so we reduce it with this method to be
|
|
* able to later implement a simpler search.
|
|
*
|
|
* @param {Object} memo
|
|
* Reference to current result object, passed on recursive calls.
|
|
*
|
|
* @param {Object} menu
|
|
* A menu entry, as described above.
|
|
*
|
|
* @returns {Object}
|
|
* Reduced object, without entries that have no action, and with a
|
|
* format like this:
|
|
*
|
|
* ```js
|
|
* {
|
|
* "Discuss": {Menu entry Object},
|
|
* "Settings": {Menu entry Object},
|
|
* "Settings/Technical/Actions/Actions": {Menu entry Object},
|
|
* ...
|
|
* }
|
|
* ```
|
|
*/
|
|
function findNames(memo, menu) {
|
|
if (menu.actionID) {
|
|
memo[menu.name.trim()] = menu;
|
|
}
|
|
if (menu.childrenTree) {
|
|
const innerMemo = _.reduce(menu.childrenTree, findNames, {});
|
|
for (const innerKey in innerMemo) {
|
|
memo[menu.name.trim() + " / " + innerKey] = innerMemo[innerKey];
|
|
}
|
|
}
|
|
return memo;
|
|
}
|
|
|
|
/**
|
|
* @extends Component
|
|
*/
|
|
export class AppsMenuSearchBar extends Component {
|
|
setup() {
|
|
super.setup();
|
|
this.state = useState({
|
|
results: [],
|
|
offset: 0,
|
|
});
|
|
useAutofocus({selector: "input"});
|
|
this.searchBarInput = useRef("SearchBarInput");
|
|
this._searchMenus = debounce(this._searchMenus, 100);
|
|
// Store menu data in a format searchable by fuzzy.js
|
|
this._searchableMenus = [];
|
|
this.menuService = useService("menu");
|
|
for (const menu of this.menuService.getApps()) {
|
|
Object.assign(
|
|
this._searchableMenus,
|
|
_.reduce([this.menuService.getMenuAsTree(menu.id)], findNames, {})
|
|
);
|
|
}
|
|
// Set up key navigation
|
|
this._setupKeyNavigation();
|
|
}
|
|
|
|
willPatch() {
|
|
// Allow looping on results
|
|
if (this.state.offset < 0) {
|
|
this.state.offset = this.state.results.length + this.state.offset;
|
|
} else if (this.state.offset >= this.state.results.length) {
|
|
this.state.offset -= this.state.results.length;
|
|
}
|
|
}
|
|
|
|
patched() {
|
|
// Scroll to selected element on keyboard navigation
|
|
if (this.state.results.length) {
|
|
const listElement = this.el.querySelector(".search-results");
|
|
const activeElement = this.el.querySelector(".highlight");
|
|
if (activeElement) {
|
|
scrollTo(activeElement, listElement);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Search among available menu items, and render that search.
|
|
*/
|
|
_searchMenus() {
|
|
const query = this.searchBarInput.el.value;
|
|
this.state.results =
|
|
query === ""
|
|
? []
|
|
: fuzzyLookup(query, _.keys(this._searchableMenus), (k) => k);
|
|
}
|
|
|
|
/**
|
|
* Get menu object for a given key.
|
|
* @param {String} key Full path to requested menu.
|
|
* @returns {Object} Menu object.
|
|
*/
|
|
_menuInfo(key) {
|
|
return this._searchableMenus[key];
|
|
}
|
|
|
|
/**
|
|
* Setup navigation among search results
|
|
*/
|
|
_setupKeyNavigation() {
|
|
const repeatable = {
|
|
allowRepeat: true,
|
|
};
|
|
useHotkey(
|
|
"ArrowDown",
|
|
() => {
|
|
this.state.offset++;
|
|
},
|
|
repeatable
|
|
);
|
|
useHotkey(
|
|
"ArrowUp",
|
|
() => {
|
|
this.state.offset--;
|
|
},
|
|
repeatable
|
|
);
|
|
useHotkey(
|
|
"Tab",
|
|
() => {
|
|
this.state.offset++;
|
|
},
|
|
repeatable
|
|
);
|
|
useHotkey(
|
|
"Shift+Tab",
|
|
() => {
|
|
this.state.offset--;
|
|
},
|
|
repeatable
|
|
);
|
|
useHotkey("Home", () => {
|
|
this.state.offset = 0;
|
|
});
|
|
useHotkey("End", () => {
|
|
this.state.offset = this.state.results.length - 1;
|
|
});
|
|
useHotkey("Enter", () => {
|
|
if (this.state.results.length) {
|
|
this.el.querySelector(".highlight").click();
|
|
}
|
|
});
|
|
}
|
|
|
|
_onKeyDown(ev) {
|
|
if (ev.code === "Escape") {
|
|
ev.stopPropagation();
|
|
ev.preventDefault();
|
|
const query = this.searchBarInput.el.value;
|
|
if (query) {
|
|
this.searchBarInput.el.value = "";
|
|
} else {
|
|
this.env.bus.trigger("APPS_MENU:CLOSE");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
AppsMenuSearchBar.template = "web_responsive.AppsMenuSearchResults";
|
|
Object.assign(NavBar.components, {AppsMenu, AppsMenuSearchBar});
|