mirror of
https://github.com/OCA/web.git
synced 2025-02-22 13:21:25 +02:00
[MIG] web_responsive: Migration to 15.0
This commit is contained in:
committed by
anjeel.haria
parent
cfea8e7762
commit
cd321a6ef1
208
web_responsive/static/src/components/apps_menu/apps_menu.esm.js
Normal file
208
web_responsive/static/src/components/apps_menu/apps_menu.esm.js
Normal file
@@ -0,0 +1,208 @@
|
||||
/** @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";
|
||||
|
||||
const {Component} = owl;
|
||||
const {useState, useRef} = owl.hooks;
|
||||
|
||||
/**
|
||||
* @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});
|
||||
188
web_responsive/static/src/components/apps_menu/apps_menu.scss
Normal file
188
web_responsive/static/src/components/apps_menu/apps_menu.scss
Normal file
@@ -0,0 +1,188 @@
|
||||
/* Copyright 2018 Tecnativa - Jairo Llopis
|
||||
* Copyright 2021 ITerra - Sergey Shebanin
|
||||
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
|
||||
|
||||
@mixin full-screen-dropdown {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
min-height: calc(100vh - #{$o-navbar-height});
|
||||
min-height: calc(var(--vh100, 100vh) - #{$o-navbar-height});
|
||||
position: fixed;
|
||||
margin: 0;
|
||||
width: 100vw;
|
||||
z-index: 200;
|
||||
left: 0 !important;
|
||||
}
|
||||
|
||||
// Iconized full screen apps menu
|
||||
.o_navbar_apps_menu {
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 100ms ease;
|
||||
}
|
||||
.fade-enter,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
.dropdown-menu {
|
||||
@include full-screen-dropdown();
|
||||
cursor: pointer;
|
||||
background: url("../../img/home-menu-bg-overlay.svg"),
|
||||
linear-gradient(
|
||||
to bottom,
|
||||
$o-brand-odoo,
|
||||
desaturate(lighten($o-brand-odoo, 20%), 15)
|
||||
);
|
||||
background-size: cover;
|
||||
border-radius: 0;
|
||||
// Display apps in a grid
|
||||
align-content: flex-start;
|
||||
display: flex !important;
|
||||
flex-direction: row;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
padding: {
|
||||
left: calc((100vw - 850px) / 2);
|
||||
right: calc((100vw - 850px) / 2);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.o_app {
|
||||
background: none;
|
||||
img {
|
||||
box-shadow: none;
|
||||
margin-bottom: 5px;
|
||||
transition: 300ms ease;
|
||||
transition-property: box-shadow, transform;
|
||||
}
|
||||
|
||||
a {
|
||||
outline: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
flex-direction: column;
|
||||
justify-content: flex-start;
|
||||
white-space: normal;
|
||||
color: gray("white") !important;
|
||||
padding: 15px 0 10px;
|
||||
font-size: 1.25rem;
|
||||
text-shadow: 1px 1px 1px rgba(gray("black"), 0.4);
|
||||
border-radius: 4px;
|
||||
transition: 300ms ease;
|
||||
transition-property: background-color;
|
||||
background: none;
|
||||
&:focus {
|
||||
background-color: rgba(gray("white"), 0.05);
|
||||
}
|
||||
}
|
||||
&:hover img,
|
||||
a:focus img {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 9px 12px -4px rgba(gray("black"), 0.3);
|
||||
}
|
||||
|
||||
// Size depends on screen
|
||||
width: 33.33333333%;
|
||||
@include media-breakpoint-up(sm) {
|
||||
width: 25%;
|
||||
}
|
||||
@include media-breakpoint-up(md) {
|
||||
width: 16.6666666%;
|
||||
}
|
||||
}
|
||||
|
||||
// Hide app icons when searching
|
||||
.has-results ~ .o_app {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.o-app-icon {
|
||||
height: auto;
|
||||
max-width: 6rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// Search input for menus
|
||||
.form-row {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
width: 100%;
|
||||
margin: 1rem 1.5rem 0;
|
||||
|
||||
.search-input {
|
||||
display: flex;
|
||||
justify-items: middle;
|
||||
box-shadow: inset 0 1px 0 rgba(gray("white"), 0.1),
|
||||
0 1px 0 rgba(gray("black"), 0.1);
|
||||
text-shadow: 0 1px 0 rgba(gray("black"), 0.5);
|
||||
border-radius: 4px;
|
||||
padding: 0.4rem 0.8rem;
|
||||
margin-bottom: 1rem;
|
||||
background-color: rgba(gray("white"), 0.1);
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
padding: 0.8rem 1.2rem;
|
||||
}
|
||||
|
||||
.search-icon {
|
||||
color: gray("white");
|
||||
font-size: 1.5rem;
|
||||
margin-right: 1rem;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
height: 2rem;
|
||||
background: none;
|
||||
border: none;
|
||||
color: gray("white");
|
||||
display: block;
|
||||
padding: 1px 2px 2px 2px;
|
||||
box-shadow: none;
|
||||
|
||||
&::placeholder {
|
||||
color: gray("white");
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Allow to scroll only on results, keeping static search box above
|
||||
.search-results {
|
||||
margin-top: 1rem;
|
||||
max-height: calc(100vh - #{$o-navbar-height} - 8rem) !important;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
}
|
||||
.search-result {
|
||||
display: block;
|
||||
align-items: center;
|
||||
background-position: left;
|
||||
background-repeat: no-repeat;
|
||||
background-size: contain;
|
||||
color: gray("white");
|
||||
cursor: pointer;
|
||||
line-height: 2.5rem;
|
||||
padding-left: 3.5rem;
|
||||
white-space: normal;
|
||||
font-weight: 100;
|
||||
&.highlight,
|
||||
&:hover {
|
||||
background-color: rgba(gray("black"), 0.11);
|
||||
}
|
||||
b {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
80
web_responsive/static/src/components/apps_menu/apps_menu.xml
Normal file
80
web_responsive/static/src/components/apps_menu/apps_menu.xml
Normal file
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<!-- Copyright 2018 Tecnativa - Jairo Llopis
|
||||
Copyright 2021 ITerra - Sergey Shebanin
|
||||
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
|
||||
<templates>
|
||||
<t t-inherit="web.NavBar.AppsMenu" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//Dropdown" position="replace">
|
||||
<!-- Same hotkey as in EE -->
|
||||
<AppsMenu
|
||||
hotkey="'a'"
|
||||
title="'Home Menu'"
|
||||
manualOnly="true"
|
||||
class="o_navbar_apps_menu"
|
||||
>
|
||||
<t t-set-slot="toggler">
|
||||
<i class="fa fa-th-large" />
|
||||
</t>
|
||||
<t t-transition="o_notification_fade">
|
||||
<AppsMenuSearchBar />
|
||||
<MenuItem
|
||||
t-foreach="apps"
|
||||
t-as="app"
|
||||
t-key="app.id"
|
||||
class="o_app"
|
||||
t-att-class="{ o_dropdown_active: menuService.getCurrentApp() === app }"
|
||||
payload="app"
|
||||
>
|
||||
<a t-att-href="getMenuItemHref(app)">
|
||||
<img
|
||||
class="o-app-icon"
|
||||
draggable="false"
|
||||
t-attf-src="data:image/png;base64,{{app.webIconData}}"
|
||||
/>
|
||||
<div t-esc="app.name" />
|
||||
</a>
|
||||
</MenuItem>
|
||||
</t>
|
||||
</AppsMenu>
|
||||
</xpath>
|
||||
</t>
|
||||
<!-- Search bar -->
|
||||
<t t-name="web_responsive.AppsMenuSearchResults" owl="1">
|
||||
<div
|
||||
class="search-container"
|
||||
t-att-class="state.results.length ? 'has-results' : ''"
|
||||
>
|
||||
<div class="search-input">
|
||||
<span class="fa fa-search search-icon" />
|
||||
<input
|
||||
type="search"
|
||||
t-ref="SearchBarInput"
|
||||
t-on-input="_searchMenus"
|
||||
t-on-keydown="_onKeyDown"
|
||||
autocomplete="off"
|
||||
placeholder="Search menus..."
|
||||
class="form-control"
|
||||
/>
|
||||
</div>
|
||||
<div t-if="state.results.length" class="search-results">
|
||||
<t t-foreach="state.results" t-as="result">
|
||||
<t t-set="menu" t-value="_menuInfo(result)" />
|
||||
<a
|
||||
t-attf-class="search-result {{result_index == state.offset ? 'highlight' : ''}}"
|
||||
t-att-style="menu.webIconData ? "background-image:url('data:image/png;base64," + menu.webIconData + "')" : ''"
|
||||
t-attf-href="#menu_id={{menu.id}}&action_id={{menu.actionID}}"
|
||||
t-att-data-menu-id="menu.id"
|
||||
t-att-data-action-id="menu.actionID"
|
||||
draggable="false"
|
||||
t-esc="result"
|
||||
/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
<t t-inherit="web.Dropdown" t-inherit-mode="extension" owl="1">
|
||||
<xpath expr="//div[hasclass('dropdown-menu')]" position="attributes">
|
||||
<attribute name="t-transition">fade</attribute>
|
||||
</xpath>
|
||||
</t>
|
||||
</templates>
|
||||
Reference in New Issue
Block a user