[MIG] web_responsive: Migrate to v12 and refactor

This migration includes a full refactoring to make this module more
maintainable. Some things that have changed:

- Removed external libraries.
- Change Less for Scss.
- Reduce ES and XML to the minimal required needs.
- Implement as much features as possible with just Scss.
- Remove copyright from `__init__.py` files.
- Trigger the new hotkeys system from Odoo v12 with `Shift+Alt` instead
  of just `Alt`, and restore some good old hotkeys (`E` for "Edit",
  `D` for "Discard", and `A` for "Apps menu").
  See https://github.com/odoo/odoo/issues/30068 on the matter.
- Control panel breadcrumbs are collapsed into a single backwards icon.
- Add FA icons to most common buttons in control panel.
- Hide text in XS for those buttons, to have a slicker phone experience.
- Lots of gifs in the README!
This commit is contained in:
Jairo Llopis
2018-10-04 10:24:36 +02:00
committed by Sergey Shebanin
parent 5122c2be11
commit 0709b3396c
33 changed files with 1247 additions and 5737 deletions

View File

@@ -0,0 +1,427 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
@mixin full-screen-dropdown {
border: none;
box-shadow: none;
display: flex;
flex-direction: column;
height: calc(100vh - #{$o-navbar-height});
max-height: calc(100vh - #{$o-navbar-height});
position: fixed;
width: 100vw;
z-index: 100;
// Inline style will override our `top`, so we need !important here
top: $o-navbar-height !important;
transform: none !important;
}
// Main navbar (with apps menu, user menu, debug menu...)
@include media-breakpoint-down(sm) {
.o_main_navbar {
// This allows to have a sane layout for mobiles
display: flex;
// Remove clutter to only have small icons that fit in a small screen
> .dropdown {
display: flex;
.navbar-toggler {
color: white;
}
.o_menu_sections,
.o_menu_systray,
{
padding: 0;
}
}
// Whitespace before systray icons
.o_menu_systray {
margin-left: auto;
}
// Hide big things
.o_menu_brand,
.o_menu_sections,
.oe_topbar_name,
{
display: none;
}
// Fix toggler button padding
.o-menu-toggle {
cursor: pointer;
padding: 0 $o-horizontal-padding;
}
// Custom fullscreen layout when showing submenus
.o_menu_sections.show {
@include full-screen-dropdown();
background-color: $dropdown-bg;
.show {
display: flex;
flex-direction: column;
.dropdown-menu {
margin-left: 1rem;
min-width: auto;
width: calc(100vw - 2rem);
}
}
> li,
.o_menu_entry_lvl_1,
.o_menu_header_lvl_1,
{
// Homogeneous background color
background-color: $dropdown-bg;
color: $dropdown-link-color;
&:hover {
background-color: $dropdown-link-hover-bg;
color: $dropdown-link-hover-color;
}
// Disable the .o-no-caret class effect, to display the caret
&.o-no-caret::after {
content: "";
}
// Fix a strange glitch leaving headers invisible
.dropdown-header {
color: $dropdown-header-color;
}
}
}
// Custom fullscreen layout for systray items
.o_mail_systray_dropdown.show {
@include full-screen-dropdown();
// Fix stretchy images
.o_mail_preview_image {
align-items: center;
display: flex;
flex-direction: row;
img {
display: block;
height: auto;
}
}
}
// Higher height for dropdown items, for those with sausage fingers
.dropdown-menu .dropdown-item {
padding: {
bottom: 0.5rem;
top: 0.5rem;
}
}
}
}
// Iconized full screen apps menu
.o_menu_apps {
.search-input:focus {
border-color: $o-brand-primary;
}
.dropdown-menu.show {
@include full-screen-dropdown();
// Display apps in a grid
align-content: flex-start;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: flex-start;
@include media-breakpoint-up(lg) {
padding: {
left: 20vw;
right: 20vw;
}
}
.o_app {
align-items: center;
display: flex;
flex-direction: column;
justify-content: flex-start;
// 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: 7rem;
width: 100%;
}
// Search input for menus
.form-row {
width: 100%;
}
.o-menu-search-result {
align-items: center;
background-position: left;
background-repeat: no-repeat;
background-size: contain;
cursor: pointer;
display: flex;
padding-left: 3rem;
white-space: normal;
}
// Allow to scroll only on results, keeping static search box above
.search-container.has-results {
height: 100%;
.search-input {
height: 3em;
}
.search-results {
height: calc(100% - 3em);
overflow: auto;
}
}
}
}
// Scroll all but top bar
html .o_web_client .o_main .o_main_content {
@include media-breakpoint-down(sm) {
overflow: auto;
.o_content {
overflow: initial;
}
}
}
// Control panel (breadcrumbs, search box, buttons...)
@include media-breakpoint-down(sm) {
.o_control_panel {
// Arrange buttons to use space better
.breadcrumb,
.o_cp_buttons,
.o_cp_left,
.o_cp_right,
.o_cp_searchview,
{
flex: 1 1 100%;
@include media-breakpoint-up(md) {
flex-basis: 50%;
}
}
.breadcrumb {
flex-basis: 80%;
}
.o_cp_searchview,
.o_cp_right,
{
flex-basis: 10%;
}
.o_cp_left {
flex-basis: 50%;
white-space: nowrap;
}
.o_cp_pager {
white-space: nowrap;
}
// Hide all but 2 last breadcrumbs, and render 2nd-to-last as arrow
.breadcrumb-item {
&:not(.active) {
padding-left: 0;
}
&::before {
content: none;
padding-right: 0;
}
&:nth-last-of-type(1n+3) {
display: none;
}
&:nth-last-of-type(2) {
&::before {
color: var(--primary);
content: "\f048"; // .fa-step-backward
cursor: pointer;
font-family: FontAwesome;
}
a {
display: none;
}
}
}
// Ellipsize long breadcrumbs
.breadcrumb {
max-width: 100%;
text-overflow: ellipsis;
}
// Empty sidebar should not break layout
.o_cp_sidebar:blank {
display: none;
}
// In case you install `mail`, there is a mess on BS vs inline styles
// we need to fix
.o_cp_buttons .btn.d-block:not(.d-none) {
display: inline-block !important;
}
// Dropdown with buttons to switch the view type
.o_cp_switch_buttons.show {
.dropdown-menu {
align-content: center;
display: flex;
flex-direction: row;
justify-content: space-around;
padding: 0;
.btn {
border: {
bottom: 0;
radius: 0;
top: 0;
}
}
}
}
}
}
// Normal views
.o_content, .modal-content {
max-width: 100%;
// Form views
.o_form_view {
.o_form_sheet {
max-width: calc(100% - 32px);
}
@include media-breakpoint-down(sm) {
min-width: auto;
// Avoid overflow on forms with title and/or button box
.oe_button_box,
.oe_title,
{
max-width: 100%;
}
// Avoid overflow on modals
.o_form_sheet {
min-width: auto;
}
// Render website inputs properly in phones
.o_group .o_field_widget.o_text_overflow {
// Overrides another !important
width: auto !important;
}
// Make all input groups vertical
.o_group_col_6 {
width: 100%;
}
// Statusbar buttons dropdown for mobiles
.o_statusbar_buttons_dropdown {
border: {
bottom: 0;
radius: 0;
top: 0;
}
height: 100%;
}
.o_statusbar_buttons > .btn {
border-radius: 0;
border: 0;
width: 100%;
margin-bottom: 0.2rem;
&:last-child {
margin-bottom: 0;
}
}
.o_statusbar_status {
// Arrow from rightmost button exceeds allowed width
.o_arrow_button:first-child::before {
content: none;
display: none;
}
}
// Full width in form sheets
.o_form_sheet,
.oe_chatter,
{
min-width: auto;
max-width: 98%;
}
// Settings pages
.app_settings_block {
.row {
margin: 0;
}
}
}
}
// Sided chatter, if user wants
.o_chatter_position_sided & {
@include media-breakpoint-up(md) {
.o_form_view:not(.o_form_nosheet) {
display: flex;
flex-flow: row nowrap;
height: 100%;
.o_form_sheet_bg {
flex: 1 1 60%;
overflow: auto;
}
.o_chatter {
border-left: 1px solid gray('400');
flex: 1 1 40%;
max-width: 50%;
min-width: 30%;
overflow: auto;
}
}
}
}
}

View File

@@ -1,532 +1,409 @@
/* Copyright 2016 LasLabs Inc.
/* Copyright 2018 Tecnativa - Jairo Llopis
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define('web_responsive', function(require) {
odoo.define('web_responsive', function (require) {
'use strict';
var Menu = require('web.Menu');
var rpc = require('web.rpc');
var SearchView = require('web.SearchView');
var core = require('web.core');
var config = require('web.config');
var session = require('web.session');
var ViewManager = require('web.ViewManager');
var RelationalFields = require('web.relational_fields');
var AbstractWebClient = require("web.AbstractWebClient");
var AppsMenu = require("web.AppsMenu");
var config = require("web.config");
var core = require("web.core");
var FormRenderer = require('web.FormRenderer');
var Widget = require('web.Widget');
var Menu = require("web.Menu");
var RelationalFields = require('web.relational_fields');
var qweb = core.qweb;
Menu.include({
// Force all_outside to prevent app icons from going into more menu
reflow: function() {
this._super('all_outside');
},
/* Overload to collapse unwanted visible submenus
* @param allow_open bool Switch to allow submenus to be opened
*/
open_menu: function(id, allowOpen) {
this._super(id);
if (allowOpen) {
return;
}
var $clicked_menu = this.$secondary_menus.find('a[data-menu=' + id + ']');
$clicked_menu.parents('.oe_secondary_submenu').css('display', '');
/**
* Reduce menu data to a searchable format understandable by fuzzy.js
*
* `AppsMenu.init()` gets `menuData` in a format similar to this (only
* relevant data is shown):
*
* ```js
* {
* [...],
* children: [
* // This is a menu entry:
* {
* action: "ir.actions.client,94", // Or `false`
* children: [... similar to above "children" key],
* name: "Actions",
* parent_id: [146, "Settings/Technical/Actions"], // Or `false`
* },
* ...
* ]
* }
* ```
*
* 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.action) {
var key = menu.parent_id ? menu.parent_id[1] + "/" : "";
memo[key + menu.name] = menu;
}
});
SearchView.include({
// Prevent focus of search field on mobile devices
toggle_visibility: function(is_visible) {
$('div.oe_searchview_input').last().one(
'focus', $.proxy(this.preventMobileFocus, this));
return this._super(is_visible);
},
// It prevents focusing of search el on mobile
preventMobileFocus: function(event) {
if (this.isMobile()) {
event.preventDefault();
}
},
// For lack of Modernizr, TouchEvent will do
isMobile: function() {
try {
document.createEvent('TouchEvent');
return true;
} catch (ex) {
return false;
}
if (menu.children.length) {
_.reduce(menu.children, findNames, memo);
}
});
return memo;
}
var AppDrawer = Widget.extend({
AppsMenu.include({
events: _.extend({
"keydown .search-input input": "_searchResultsNavigate",
"click .o-menu-search-result": "_searchResultChosen",
"shown.bs.dropdown": "_searchFocus",
"hidden.bs.dropdown": "_searchReset",
}, AppsMenu.prototype.events),
/* Provides all features inside of the application drawer navigation.
Attributes:
directionCodes (str): Canonical key name to direction mappings.
deleteCodes
/**
* Rescue some menu data stripped out in original method.
*
* @override
*/
LEFT: 'left',
RIGHT: 'right',
UP: 'up',
DOWN: 'down',
// These keys are ignored when presented as single input
MODIFIERS: [
'Alt',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'ArrowUp',
'Control',
'Enter',
'Escape',
'Meta',
'Shift',
'Tab',
],
isOpen: false,
keyBuffer: '',
keyBufferTime: 500,
keyBufferTimeoutEvent: false,
dropdownHeightFactor: 0.90,
initialized: false,
searching: false,
init: function() {
init: function (parent, menuData) {
this._super.apply(this, arguments);
this.directionCodes = {
'left': this.LEFT,
'right': this.RIGHT,
'up': this.UP,
'pageup': this.UP,
'down': this.DOWN,
'pagedown': this.DOWN,
'+': this.RIGHT,
'-': this.LEFT
};
this.$searchAction = $('.app-drawer-search-action');
this.$searchAction.hide();
this.$searchResultsContainer = $('#appDrawerSearchResults');
this.$searchInput = $('#appDrawerSearchInput');
this.initDrawer();
this.handleWindowResize();
var $clickZones = $('.odoo_webclient_container, ' +
'a.oe_menu_leaf, ' +
'a.oe_menu_toggler, ' +
'a.oe_logo, ' +
'i.oe_logo_edit'
// Keep base64 icon for main menus
for (var n in this._apps) {
this._apps[n].web_icon_data =
menuData.children[n].web_icon_data;
}
// Store menu data in a format searchable by fuzzy.js
this._searchableMenus = _.reduce(
menuData.children,
findNames,
{}
);
$clickZones.click($.proxy(this.handleClickZones, this));
this.$searchResultsContainer.click($.proxy(this.searchMenus, this));
this.$el.find('.drawer-search-open').click(
$.proxy(this.searchMenus, this)
);
this.$el.find('.drawer-search-close').hide().click(
$.proxy(this.closeSearchMenus, this)
);
this.filter_timeout = $.Deferred();
core.bus.on('resize', this, this.handleWindowResize);
core.bus.on('keydown', this, this.handleKeyDown);
core.bus.on('keyup', this, this.redirectKeyPresses);
core.bus.on('keypress', this, this.redirectKeyPresses);
// Search only after timeout, for fast typers
this._search_def = $.Deferred();
},
// Provides initialization handlers for Drawer
initDrawer: function() {
this.$el = $('.drawer');
this.$el.drawer();
this.$el.one('drawer.opened', $.proxy(this.onDrawerOpen, this));
// Setup the iScroll options.
// You should be able to pass these to ``.drawer``, but scroll freezes.
this.$el.on(
'drawer.opened',
function setIScrollProbes(){
var onIScroll = $.proxy(
function() {
this.iScroll.refresh();
},
this
);
// Scroll probe aggressiveness level
// 2 == always executes the scroll event except during momentum and bounce.
this.iScroll.options.probeType = 2;
this.iScroll.on('scroll', onIScroll);
// Initialize Scrollbars manually
this.iScroll.options.scrollbars = true;
this.iScroll.options.fadeScrollbars = true;
this.iScroll._initIndicators();
}
);
this.initialized = true;
},
// Provides handlers to hide drawer when "unfocused"
handleClickZones: function() {
this.$el.drawer('close');
$('.o_sub_menu_content')
.parent()
.collapse('hide');
$('.navbar-collapse').collapse('hide');
},
// Resizes bootstrap dropdowns for screen
handleWindowResize: function() {
$('.dropdown-scrollable').css(
'max-height', $(window).height() * this.dropdownHeightFactor
);
},
/* Provide keyboard shortcuts for app drawer nav.
*
* It is required to perform this functionality only on the ``keydown``
* event in order to prevent duplication of the arrow events.
*
* @param e The ``keydown`` event triggered by ``core.bus``.
/**
* @override
*/
handleKeyDown: function(e) {
if (!this.isOpen){
return;
}
var directionCode = $.hotkeys.specialKeys[e.keyCode.toString()];
if (Object.keys(this.directionCodes).indexOf(directionCode) !== -1) {
var $link = false;
if (this.searching) {
var $collection = this.$el.find('#appDrawerMenuSearch a');
$link = this.findAdjacentLink(
this.$el.find('#appDrawerMenuSearch a:first, #appDrawerMenuSearch a.web-responsive-focus').last(),
this.directionCodes[directionCode],
$collection,
true
);
} else {
$link = this.findAdjacentLink(
this.$el.find('#appDrawerApps a:first, #appDrawerApps a.web-responsive-focus').last(),
this.directionCodes[directionCode]
);
}
this.selectLink($link);
} else if ($.hotkeys.specialKeys[e.keyCode.toString()] === 'esc') {
// We either back out of the search, or close the app drawer.
if (this.searching) {
this.closeSearchMenus();
} else {
this.handleClickZones();
}
} else {
this.redirectKeyPresses(e);
}
start: function () {
this.$search_container = this.$(".search-container");
this.$search_input = this.$(".search-input input");
this.$search_results = this.$(".search-results");
return this._super.apply(this, arguments);
},
/* Provide centralized key event redirects for the App Drawer.
/**
* Get all info for a given menu.
*
* This method is for all key events not related to arrow navigation.
* @param {String} key
* Full path to requested menu.
*
* @param e The key event that was triggered by ``core.bus``.
* @returns {Object}
* Menu definition, plus extra needed keys.
*/
redirectKeyPresses: function(e) {
if ( !this.isOpen ) {
// Drawer isn't open; Ignore.
return;
}
// Trigger navigation to pseudo-focused link
// & fake a click (in case of anchor link).
if (e.key === 'Enter') {
var href = $('.web-responsive-focus').attr('href');
if (!_.isUndefined(href)) {
window.location.href = href;
this.handleClickZones();
}
return;
}
// Ignore any other modifier keys.
if (this.MODIFIERS.indexOf(e.key) !== -1) {
return;
}
// Event is already targeting the search input.
// Perform search, then stop processing.
if ( e.target === this.$searchInput[0] ) {
this.searchMenus();
return;
}
// Prevent default event,
// redirect it to the search input,
// and search.
e.preventDefault();
this.$searchInput.trigger({
type: e.type,
key: e.key,
keyCode: e.keyCode,
which: e.which,
});
this.searchMenus();
_menuInfo: function (key) {
var original = this._searchableMenus[key];
return _.extend({
action_id: parseInt(original.action.split(',')[1], 10),
}, original);
},
/* Performs close actions
* @fires ``drawer.closed`` to the ``core.bus``
* @listens ``drawer.opened`` and sends to onDrawerOpen
/**
* Autofocus on search field on big screens.
*/
onDrawerClose: function() {
core.bus.trigger('drawer.closed');
this.closeSearchMenus();
this.$el.one('drawer.opened', $.proxy(this.onDrawerOpen, this));
this.isOpen = false;
// Remove inline style inserted by drawer.js
this.$el.css("overflow", "");
},
/* Finds app links and register event handlers
* @fires ``drawer.opened`` to the ``core.bus``
* @listens ``drawer.closed`` and sends to :meth:``onDrawerClose``
*/
onDrawerOpen: function() {
this.closeSearchMenus();
this.$appLinks = $('.app-drawer-icon-app').parent();
this.selectLink($(this.$appLinks[0]));
this.$el.one('drawer.closed', $.proxy(this.onDrawerClose, this));
core.bus.trigger('drawer.opened');
this.isOpen = true;
this.$searchInput.val("");
},
// Selects a link visibly & deselects others.
selectLink: function($link) {
$('.web-responsive-focus').removeClass('web-responsive-focus');
if ($link) {
$link.addClass('web-responsive-focus');
_searchFocus: function () {
if (!config.device.isMobile) {
this.$search_input.focus();
}
},
/**
* Search matching menus immediately
* Reset search input and results
*/
_searchReset: function () {
this.$search_container.removeClass("has-results");
this.$search_results.empty();
this.$search_input.val("");
},
/**
* Schedule a search on current menu items.
*/
_searchMenusSchedule: function () {
this._search_def.reject();
this._search_def = $.Deferred();
setTimeout(this._search_def.resolve.bind(this._search_def), 50);
this._search_def.done(this._searchMenus.bind(this));
},
/**
* Search among available menu items, and render that search.
*/
_searchMenus: function () {
rpc.query({
model: 'ir.ui.menu',
method: 'search_read',
kwargs: {
fields: ['action', 'display_name', 'id'],
domain: [
['name', 'ilike', this.$searchInput.val()],
['action', '!=', false],
],
context: session.user_context,
},
}).then(this.showFoundMenus.bind(this));
var query = this.$search_input.val();
if (query === "") {
this.$search_container.removeClass("has-results");
this.$search_results.empty();
return;
}
var results = fuzzy.filter(
query,
_.keys(this._searchableMenus),
{
pre: "<b>",
post: "</b>",
}
);
this.$search_container.toggleClass(
"has-results",
Boolean(results.length)
);
this.$search_results.html(
core.qweb.render(
"web_responsive.MenuSearchResults",
{
results: results,
widget: this,
}
)
);
},
/**
* Queue the next menu search for the search input
* Use chooses a search result, so we navigate to that menu
*
* @param {jQuery.Event} event
*/
searchMenus: function() {
// Stop current search, if any
this.filter_timeout.reject();
this.filter_timeout = $.Deferred();
// Schedule a new search
this.filter_timeout.done(this._searchMenus.bind(this));
setTimeout(
this.filter_timeout.resolve.bind(this.filter_timeout),
200
);
// Focus search input
this.$searchInput = $('#appDrawerSearchInput').focus();
_searchResultChosen: function (event) {
event.preventDefault();
var $result = $(event.currentTarget),
text = $result.text().trim(),
data = $result.data(),
suffix = ~text.indexOf("/") ? "/" : "";
// Load the menu view
this.trigger_up("menu_clicked", {
action_id: data.actionId,
id: data.menuId,
previous_menu_id: data.parentId,
});
// Find app that owns the chosen menu
var app = _.find(this._apps, function (_app) {
return text.indexOf(_app.name + suffix) === 0;
});
// Update navbar menus
core.bus.trigger("change_menu_section", app.menuID);
},
/* Display the menus that are provided as input.
/**
* Navigate among search results
*
* @param {jQuery.Event} event
*/
showFoundMenus: function(menus) {
this.searching = true;
this.$el.find('#appDrawerApps').hide();
this.$searchAction.hide();
this.$el.find('.drawer-search-close').show();
this.$el.find('.drawer-search-open').hide();
this.$searchResultsContainer
// Render the results
.html(
core.qweb.render(
'AppDrawerMenuSearchResults',
{menus: menus}
)
)
// Get the parent container and show it.
.closest('#appDrawerMenuSearch')
.show()
// Find the input, set focus.
.find('.menu-search-query')
.focus()
;
var $menuLinks = this.$searchResultsContainer.find('a');
$menuLinks.click($.proxy(this.handleClickZones, this));
this.selectLink($menuLinks.first());
},
/* Close search menu and switch back to app menu.
*/
closeSearchMenus: function() {
this.searching = false;
this.$el.find('#appDrawerApps').show();
this.$el.find('.drawer-search-close').hide();
this.$el.find('.drawer-search-open').show();
this.$searchResultsContainer.closest('#appDrawerMenuSearch').hide();
this.$searchAction.show();
},
/* Returns the link adjacent to $link in provided direction.
* It also handles edge cases in the following ways:
* * Moves to last link if LEFT on first
* * Moves to first link if PREV on last
* * Moves to first link of following row if RIGHT on last in row
* * Moves to last link of previous row if LEFT on first in row
* * Moves to top link in same column if DOWN on bottom row
* * Moves to bottom link in same column if UP on top row
* @param $link jQuery obj of App icon link
* @param direction str of direction to go (constants LEFT, UP, etc.)
* @param $objs jQuery obj representing the collection of links. Defaults
* to `this.$appLinks`.
* @param restrictHorizontal bool Set to true if the collection consists
* only of vertical elements.
* @return jQuery obj for adjacent link
*/
findAdjacentLink: function($link, direction, $objs, restrictHorizontal) {
if (_.isUndefined($objs)) {
$objs = this.$appLinks;
_searchResultsNavigate: function (event) {
// Exit soon when not navigating results
if (this.$search_results.is(":empty")) {
// Just in case it is the 1st search
this._searchMenusSchedule();
return;
}
var obj = [];
var $rows = restrictHorizontal ? $objs : this.getRowObjs($link, this.$appLinks);
switch (direction) {
case this.LEFT:
obj = $objs[$objs.index($link) - 1];
if (!obj) {
obj = $objs[$objs.length - 1];
}
break;
case this.RIGHT:
obj = $objs[$objs.index($link) + 1];
if (!obj) {
obj = $objs[0];
}
break;
case this.UP:
obj = $rows[$rows.index($link) - 1];
if (!obj) {
obj = $rows[$rows.length - 1];
}
break;
case this.DOWN:
obj = $rows[$rows.index($link) + 1];
if (!obj) {
obj = $rows[0];
}
break;
}
if (obj.length) {
// Find current results and active element (1st by default)
var all = this.$search_results.find(".o-menu-search-result"),
pre_focused = all.filter(".active") || $(all[0]),
offset = all.index(pre_focused),
key = event.key;
// Transform tab presses in arrow presses
if (key === "Tab") {
event.preventDefault();
key = event.shiftKey ? "ArrowUp" : "ArrowDown";
}
switch (key) {
// Pressing enter is the same as clicking on the active element
case "Enter":
pre_focused.click();
break;
// Navigate up or down
case "ArrowUp":
offset--;
break;
case "ArrowDown":
offset++;
break;
// Other keys trigger a search
default:
this._searchMenusSchedule();
return;
}
// Allow looping on results
if (offset < 0) {
offset = all.length + offset;
} else if (offset >= all.length) {
offset -= all.length;
}
// Switch active element
var new_focused = $(all[offset]);
pre_focused.removeClass("active");
new_focused.addClass("active");
this.$search_results.scrollTo(new_focused, {
offset: {
top: this.$search_results.height() * -0.5,
},
});
},
});
return $(obj);
Menu.include({
events: _.extend({
// Clicking a hamburger menu item should close the hamburger
"click .o_menu_sections [role=menuitem]": "_hideMobileSubmenus",
// Opening any dropdown in the navbar should hide the hamburger
"show.bs.dropdown .o_menu_systray, .o_menu_apps":
"_hideMobileSubmenus",
}, Menu.prototype.events),
start: function () {
this.$menu_toggle = this.$(".o-menu-toggle");
return this._super.apply(this, arguments);
},
/* Returns els in the same row
* @param @obj jQuery object to get row for
* @param $grid jQuery objects representing grid
* @return $objs jQuery objects of row
/**
* Hide menus for current app if you're in mobile
*/
getRowObjs: function($obj, $grid) {
// Filter by object which middle lies within left/right bounds
function filterWithin(left, right) {
return function() {
var $this = $(this),
thisMiddle = $this.offset().left + $this.width() / 2;
return thisMiddle >= left && thisMiddle <= right;
};
_hideMobileSubmenus: function () {
if (
this.$menu_toggle.is(":visible") &&
this.$section_placeholder.is(":visible")
) {
this.$section_placeholder.collapse("hide");
}
var left = $obj.offset().left,
right = left + $obj.outerWidth();
return $grid.filter(filterWithin(left, right));
}
},
});
// Init a new AppDrawer when the web client is ready
core.bus.on('web_client_ready', null, function () {
new AppDrawer();
});
// if we are in small screen change default view to kanban if exists
ViewManager.include({
get_default_view: function() {
var default_view = this._super();
if (config.device.size_class <= config.device.SIZES.XS &&
default_view.type !== 'kanban' &&
this.views.kanban) {
default_view.type = 'kanban';
/**
* No menu brand in mobiles
*
* @override
*/
_updateMenuBrand: function () {
if (!config.device.isMobile) {
return this._super.apply(this, arguments);
}
return default_view;
},
});
// FieldStatus (responsive fold)
RelationalFields.FieldStatus.include({
_renderQWebValues: function () {
return {
selections: this.status_information, // Needed to preserve order
has_folded: _.filter(this.status_information, {'selected': false}).length > 0,
clickable: !!this.attrs.clickable,
};
},
_render: function () {
// FIXME: Odoo framework creates view values & render qweb in the
// same method. This cause a "double render" process to use
// new custom values.
/**
* Fold all on mobiles.
*
* @override
*/
_setState: function () {
this._super.apply(this, arguments);
this.$el.html(qweb.render("FieldStatus.content", this._renderQWebValues()));
}
if (config.device.isMobile) {
_.map(this.status_information, function (value) {
value.fold = true;
});
}
},
});
// Responsive view "action" buttons
FormRenderer.include({
_renderHeaderButtons: function (node) {
var self = this;
var $buttons = this._super(node);
var $container = $(qweb.render('web_responsive.MenuStatusbarButtons'));
$container.find('.o_statusbar_buttons_base').append($buttons);
/**
* In mobiles, put all statusbar buttons in a dropdown.
*
* @override
*/
_renderHeaderButtons: function () {
var $buttons = this._super.apply(this, arguments);
if (
!config.device.isMobile ||
!$buttons.is(":has(>:not(.o_invisible_modifier))")
) {
return $buttons;
}
var $dropdownMenu = $container.find('.dropdown-menu');
_.each(node.children, function (child) {
if (child.tag === 'button') {
$dropdownMenu.append($('<LI>').append(self._renderHeaderButton(child)));
}
});
return $container;
}
// $buttons must be appended by JS because all events are bound
$buttons.addClass("dropdown-menu");
var $dropdown = $(core.qweb.render(
'web_responsive.MenuStatusbarButtons'
));
$buttons.addClass("dropdown-menu").appendTo($dropdown);
return $dropdown;
},
});
/**
* Use ALT+SHIFT instead of ALT as hotkey triggerer.
*
* HACK https://github.com/odoo/odoo/issues/30068 - See it to know why.
*
* Cannot patch in `KeyboardNavigationMixin` directly because it's a mixin,
* not a `Class`, and altering a mixin's `prototype` doesn't alter it where
* it has already been used.
*
* Instead, we provide an additional mixin to be used wherever you need to
* enable this behavior.
*/
var KeyboardNavigationShiftAltMixin = {
return {
'AppDrawer': AppDrawer,
/**
* Alter the key event to require pressing Shift.
*
* This will produce a mocked event object where it will seem that
* `Alt` is not pressed if `Shift` is not pressed.
*
* The reason for this is that original upstream code, found in
* `KeyboardNavigationMixin` is very hardcoded against the `Alt` key,
* so it is more maintainable to mock its input than to rewrite it
* completely.
*
* @param {keyEvent} keyEvent
* Original event object
*
* @returns {keyEvent}
* Altered event object
*/
_shiftPressed: function (keyEvent) {
var alt = keyEvent.altKey || keyEvent.key === "Alt",
newEvent = _.extend({}, keyEvent),
shift = keyEvent.shiftKey || keyEvent.key === "Shift";
// Mock event to make it seem like Alt is not pressed
if (alt && !shift) {
newEvent.altKey = false;
if (newEvent.key === "Alt") {
newEvent.key = "Shift";
}
}
return newEvent;
},
_onKeyDown: function (keyDownEvent) {
return this._super(this._shiftPressed(keyDownEvent));
},
_onKeyUp: function (keyUpEvent) {
return this._super(this._shiftPressed(keyUpEvent));
},
};
// Include the SHIFT+ALT mixin wherever
// `KeyboardNavigationMixin` is used upstream
AbstractWebClient.include(KeyboardNavigationShiftAltMixin);
});

View File

@@ -1,129 +0,0 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
.app-drawer-nav {
border-color: @dropdown-border;
background-color: @dropdown-bg;
border: 1px solid @dropdown-fallback-border; // IE8 fallback
border: 1px solid @dropdown-border;
-webkit-border-radius: @border-radius-base;
-moz-border-radius: @border-radius-base;
border-radius: @border-radius-base;
.box-shadow(0 6px 12px rgba(0, 0, 0, .175));
background-clip: padding-box;
z-index: 10000;
.o_tooltip {
z-index: 1051;
}
.oe_logo {
margin-top: -11px;
position: relative;
img {
height: @app-drawer-title-height;
}
.oe_logo_edit {
position: absolute;
bottom: 0px;
width: 100%;
padding: 4px;
display: none;
color: @odoo-list-footer-bg-color;
background: rgba(37,37,37,0.9);
}
&:hover .oe_logo_edit_admin {
display: block;
}
}
.navbar-left {
width: 100%;
li {
padding: 0;
}
}
.app-drawer-title {
float: none;
font-weight: bold; // Bold titles for apps in the app-drawer
}
.app-drawer-panel-title {
line-height: 16px;
> .drawer-toggle {
padding-top: 17px;
padding-bottom: 17px;
cursor: pointer;
}
}
.app-drawer-icon-app {
height: 100%;
width: 100%;
max-width: @app-drawer-icon-size;
max-height: @app-drawer-icon-size;
object-fit: contain;
object-position: center;
}
.panel-body {
padding-top: @app-drawer-title-height;
}
#appDrawerAppPanelHead {
position: absolute;
height: @app-drawer-title-height;
width: 100%;
}
.app-drawer-search-panel {
.panel-body {
padding-top: @padding-base-vertical;
}
}
}
.drawer-nav {
width: @app-drawer-width;
}
.drawer--left .drawer-nav {
left: -@app-drawer-width;
}
.drawer--left.drawer-open .drawer-hamburger {
left: @app-drawer-width;
}
.drawer--right .drawer-nav {
right: -@app-drawer-width;
}
.drawer-open .oe-right-toolbar {
display: none;
}
.drawer-closed .oe-right-toolbar {
display: block;
}
/* App Drawer Toggle */
.app-drawer-toggle {
background-color: transparent;
}
.app-drawer-toggle.navbar-toggle {
margin-left: 1em;
}
/* Icon Focusing */
.web-responsive-focus {
.tab-focus();
}

View File

@@ -1,212 +0,0 @@
/* Copyright 2016 Ponto Suprimentos Ltda.
Copyright 2018 Alexandre Díaz
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
@sheet-margin: @sheet-padding;
@chatter-side-width: 30%;
// Sided Chatter
@media (min-width: @screen-md) {
.o_chatter_position_sided {
.o_form_view:not(.o_form_nosheet) {
display: flex;
height: 100%;
.o_form_sheet_bg {
border-right: 1px solid @table-border-color;
overflow: auto;
flex: 1 1 auto;
}
.oe_chatter {
overflow: auto;
flex: 0 0 @chatter-side-width;
.o_chatter_topbar {
height: auto;
flex-wrap: wrap;
button:last-of-type {
flex: 1 0 auto;
text-align: left;
}
.o_followers {
order: -10;
flex: 0 1 100%;
}
}
&:empty {
display: none;
}
}
}
}
}
// Normal Chatter
.o_chatter_position_normal {
.oe_chatter {
max-width: initial;
}
}
// Sticky Header & Footer in List View
.o_view_manager_content {
>div {
>.table-responsive {
>.o_list_view {
thead {
position: sticky;
top: 0;
}
tfoot {
position: sticky;
bottom: 0;
}
}
}
}
}
.o_form_view {
// Form must fill 100% width in any size
.o_form_sheet_bg {
.o_form_sheet {
min-width: auto;
max-width: 100%;
margin: @sheet-margin;
}
@media (max-width: @screen-sm-max) {
padding: 0;
.o_form_sheet {
border: none;
}
}
.o_form_statusbar {
position: sticky;
top: 0;
z-index: 1;
.o-status-more > li > button {
border: 0;
}
.o_statusbar_buttons_container {
.o_statusbar_buttons_dropdown {
height: 100%;
>#dropdownMenuHeader {
height: 100%;
border-top: 0;
border-bottom: 0;
border-radius: 0;
}
>.dropdown-menu > li > button {
width: 100%;
border-radius: 0;
border: 0;
}
}
.o_statusbar_buttons_base > .o_statusbar_buttons {
.o-flex-flow(row, wrap);
>.btn {
@o-statusbar-buttons-vmargin: 4px;
min-height: @odoo-statusbar-height - 2 * @o-statusbar-buttons-vmargin;
margin: @o-statusbar-buttons-vmargin 3px @o-statusbar-buttons-vmargin 0;
padding-top: 2px;
padding-bottom: 2px;
}
}
}
}
}
// No overflowing buttons or titles
.oe_button_box, .oe_title {
max-width: 100%;
}
@media (max-width: @screen-xs) {
.o_form_field > .o_form_input_dropdown {
width: 80%;
}
}
.o_group {
&.o_inner_group > tbody > tr > td {
.note-editor > .note-toolbar {
// prevent wysiwyg editor buttons from expanding the screen
white-space: initial;
}
}
// reduce form maximum columns for smaller screens
@media (max-width: @screen-xs-max) {
.o-generate-groups(12);
.o-generate-groups(@n, @i: 1) when (@i =< @n) {
.o_group_col_@{i} {
width: 100%;
}
.o-generate-groups(@n, @i + 1);
}
}
// break field label into a separate line from field on small screens
@media (max-width: @screen-xs) {
&.o_inner_group {
display: block;
> tbody {
display: block;
> tr {
margin-top: 8px;
.o-flex-display();
.o-flex-flow(row, wrap);
> td {
.o-flex(1, 0, auto);
padding: 0;
display: block;
padding: 0;
// odoo adds a `style="width: 100%"` by javascript
// directly on the tag so we need `!important` here:
width: auto!important;
max-width: 100%;
&.o_td_label {
border-right: 0;
// keep 6% space on line to fit checkboxes
// see above about `!important`
width: 94%!important;
}
}
}
}
}
}
}
// Make image editing controls always available, instead of depending on resolution or hover
.o_form_field_image > .o_form_image_controls {
position: initial;
opacity: 1;
> .fa {
width: 50%;
padding: 6px;
margin: 0px;
text-align: center;
}
> .fa.o_select_file_button {
background: @odoo-brand-primary;
}
> .fa.o_clear_file_button {
background: @brand-danger;
}
}
// Adapt chatter widget to small viewports
.oe_chatter {
min-width: inherit;
}
}

View File

@@ -1,102 +0,0 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
body {
width: 100%;
height: 100%;
// Do not fix the search part, it's too big for small screens
@media (max-width: @screen-sm-max) {
overflow: inherit;
.odoo {
.oe-view-manager {
overflow: inherit;
}
}
}
}
main {
width: 100%;
height: 100%;
overflow: hidden;
}
.navbar {
z-index: 10;
}
.o_cp_switch_buttons {
.active {
z-index: 10;
}
}
.o_sub_menu {
.o_sub_menu_logo {
display: none;
}
.o_sub_menu_footer {
display: none;
}
}
.o_tooltip.active {
z-index: 1051;
}
.o_web_client {
>.o_main {
overflow: auto;
> .o_main_content {
overflow: initial;
> .o_content {
@media (max-width: @screen-xs-max) {
overflow: initial;
}
@media (min-width: @screen-sm-min) {
// .o_content is the one to display horizontal scrolling in
// case of wide tables
.table-responsive {
overflow-x: visible;
}
}
}
}
}
}
@media (max-width: @screen-sm-max) {
.o_control_panel {
// Remove z-index from CP buttons so it doesn't overlap the menu
.btn-group > .btn.active {
z-index: initial;
}
// Better horizontal space usage for buttons
justify-content: space-between;
.o_cp_left, .o_cp_right {
width: inherit;
}
.o_search_options > .o_dropdown {
&.hidden-xs {
// No other way to display "Group By" button :(
display: inline-block !important;
}
// Hack to hide text and display larger icons
> .btn {
font-size: 0;
> .fa {
font-size: @odoo-font-size-base * 1.4;
}
}
}
}
}
.o_chat_window {
z-index: 1000;
}

View File

@@ -1,195 +0,0 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
header {
margin: 0;
padding: 0;
@media print {
display: none;
}
> .main-nav {
display: block;
white-space: nowrap;
.navbar-systray {
white-space: nowrap;
@media (max-width: @screen-xs-max) {
position: absolute;
top: 0;
right: 56px;
}
> .oe_user_menu_placeholder > li > a {
> .oe_topbar_avatar {
border-radius: 50%;
margin-top: -8px;
max-height: 36px;
height: 36px;
width: 36px;
}
.oe_topbar_name {
position: relative;
top: -3px;
@media (max-width: @screen-xs-max) {
display: none;
}
}
.caret {
position: relative;
top: -3.5px;
}
}
.o_switch_company_menu {
.oe_topbar_name {
@media (max-width: @screen-xs-max) {
display: none;
}
}
}
> .oe_systray > li > a {
.fa {
position: relative;
top: 3px;
font-size: 16px;
}
.caret {
position: relative;
top: 0.5px;
}
}
}
.navbar-right {
float: right;
> li {
float: left;
}
@media (max-width: @screen-xs-max) {
.navbar-nav .open .dropdown-menu {
position: fixed;
top: 46px;
bottom: 0;
left: 0;
right: 0;
overflow: auto;
float: left;
background-color: @odoo-view-background-color;
border: 1px solid rgba(0, 0, 0, 0.15);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
}
}
}
.container-fluid:before, .container-fluid:after, .navbar-collapse:before, .navbar-collapse:after {
display: inline;
}
> .container-fluid {
margin: 0;
padding: 0;
@media (max-width: @screen-xs-max) {
> .navbar-collapse {
margin: 0;
padding: 0;
overflow: auto;
&.collapsing {
overflow: hidden;
}
}
}
> .navbar-header {
margin: 0;
padding: 0;
> .drawer-toggle, .navbar-toggle {
margin: 0;
padding: 0;
border: 0px;
border-radius: 0px;
> i.fa, div.fa {
padding: 17px 14px 16px;
}
}
}
.o_sub_menu > .o_sub_menu_content > .oe_secondary_menu {
ul.dropdown-menu > li.dropdown-header {
color: @odoo-view-background-color;
text-decoration: none;
background-color: @odoo-main-color-muted;
font-weight: bold;
}
@media (min-width: @screen-sm-min) {
height: @navbar-height;
}
margin: 0;
padding: 0;
> li {
@media (min-width: @screen-sm-min) {
height: @navbar-height;
}
margin: 0;
padding: 0;
&.app-name {
display: block;
padding: 7px 8px;
> .oe_menu_text {
font-size: 20px;
}
@media (min-width: @screen-sm-min) {
padding: 8.5px 12px;
}
}
> a {
margin: 0;
@media (min-width: @screen-sm-min) {
height: @navbar-height;
padding: 14px 8px;
}
}
}
}
}
> .navbar-right.o_menu_systray {
display: inline;
margin: 0;
padding: 0;
> ul {
margin: 0;
padding: 0;
> li > a {
margin: 0;
padding: 13px 8px;
height: @navbar-height;
}
}
}
.badge {
position: absolute;
top: 3px;
right: @navbar-padding-horizontal / 2;
}
ul.nav > li > a {
padding: @app-drawer-navbar-padding-vertical @app-drawer-padding-horizontal;
}
.o_planner_systray > .progress {
margin-top: 15px;
}
}
}
a.navbar-collapse.collapse {
@media (min-width: @screen-sm-min) {
padding-bottom: @app-drawer-navbar-padding-vertical;
padding-top: @app-drawer-navbar-padding-vertical;
}
}
.dropdown-scrollable {
overflow-x: hidden;
}

View File

@@ -1,18 +0,0 @@
/* Copyright 2016 LasLabs Inc.
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
// App Drawer / Icons
@app-drawer-icon-size: 6em;
@app-drawer-icon-margin: 1em;
@app-drawer-width: 80%;
@app-drawer-title-height: 54px;
// Navbar
@navbar-height: 46px;
@app-drawer-navbar-height: @navbar-height / 2;
@app-drawer-navbar-padding-vertical: @navbar-padding-vertical / 2;
@app-drawer-padding-horizontal: @navbar-padding-horizontal / 2;
// Drawer Toggle
@drawer-toggle-height: @navbar-height;
@drawer-toggle-width: @navbar-height;

View File

@@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 LasLabs Inc.
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<templates xml:space="preserve">
<t t-name="AppDrawerMenuSearchResults">
<li class="menu-search-element" t-foreach="menus" t-as="menu">
<a t-att-id="menu.id"
t-attf-href="#action={{ menu.action and menu.action.split(',')[1] or ''}}&amp;menu_id={{ menu.id }}">
<h2 class="text-center">
<t t-esc="menu.display_name" />
</h2>
</a>
</li>
</t>
</templates>

View File

@@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2018 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t t-extend="AppsMenu">
<!-- App icons should be more than a text -->
<t t-jquery=".o_app &gt; t" t-operation="replace">
<t t-call="web_responsive.AppIcon"/>
</t>
<!-- Same hotkey as in EE -->
<t t-jquery=".full" t-operation="attributes">
<attribute name="accesskey">a</attribute>
</t>
<!-- Search bar -->
<t t-jquery="[t-as=app]" t-operation="before">
<div class="search-container form-row align-items-center mb-4 col-12">
<div class="search-input col-md-10 ml-auto mr-auto mb-2">
<div class="input-group">
<div class="input-group-prepend">
<div class="input-group-text">
<i class="fa fa-search"/>
</div>
</div>
<input type="search"
placeholder="Search menus..."
class="form-control"/>
</div>
</div>
<div class="search-results col-md-10 ml-auto mr-auto"/>
</div>
</t>
</t>
<!-- Separate app icon template, for easier inheritance -->
<t t-name="web_responsive.AppIcon">
<img class="o-app-icon"
t-attf-src="data:image/png;base64,#{app.web_icon_data}"/>
<span class="o-app-name">
<t t-esc="app.name"/>
</span>
</t>
<!-- A search result -->
<t t-name="web_responsive.MenuSearchResults">
<t t-foreach="results" t-as="result">
<t t-set="menu" t-value="widget._menuInfo(result.original)"/>
<div t-attf-class="o-menu-search-result dropdown-item col-12 ml-auto mr-auto #{result_first ? 'active' : ''}"
t-attf-style="background-image:url('data:image/png;base64,#{menu.web_icon_data}')"
t-att-data-menu-id="menu.id"
t-att-data-action-id="menu.action_id"
t-att-data-parent-id="menu.parent_id[0]"
t-raw="result.string"/>
</t>
</t>
</template>

View File

@@ -2,63 +2,140 @@
<!--
Copyright 2017 LasLabs Inc.
Copyright 2018 Alexandre Díaz
Copyright 2018 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl.html).
-->
<templates id="form_view" xml:space="preserve">
<t t-extend="FormView.buttons">
<t t-jquery="button[accesskey='a']" t-operation="attributes">
<attribute name="accesskey">e</attribute>
</t>
</t>
<t t-extend="FieldStatus.content.button">
<t t-jquery="button" t-operation="replace">
<button type="button" t-att-data-value="i.id" t-att-disabled="disabled ? 'disabled' : undefined"
t-attf-class="btn btn-sm o_arrow_button btn-#{i.selected ? 'primary' : 'default'}#{disabled ? ' disabled' : ''} #{button_css or ''}">
<t t-esc="i.display_name"/>
</button>
</t>
</t>
<t t-extend="FieldStatus.content">
<t t-jquery="[t-if='selection_folded.length']" t-operation="replace">
<t t-if="selections &amp;&amp; has_folded">
<ul class="dropdown-menu o-status-more hidden-lg hidden-md" role="menu">
<t t-set="button_css" t-value="'hidden-lg hidden-md'" />
<li t-foreach="selections" t-as="i">
<t t-if="!i.selected" t-call="FieldStatus.content.button"/>
</li>
</ul>
<button type="button" class="btn btn-sm o_arrow_button btn-default dropdown-toggle hidden-lg hidden-md" data-toggle="dropdown" aria-expanded="false">More <span class="caret"/></button>
</t>
</t>
<t t-jquery="[t-foreach='selection_unfolded.reverse()']" t-operation="replace">
<t t-if="selections">
<t t-foreach="selections.reverse()" t-as="i">
<t t-set="button_css" t-value="i.selected?'':'hidden-xs hidden-sm'" />
<t t-call="FieldStatus.content.button"/>
</t>
</t>
</t>
<!-- Template for buttons that display only the icon in xs -->
<t t-name="web_responsive.icon_button">
<i t-attf-class="fa fa-#{icon}"
t-att-title="label"/>
<span class="d-none d-sm-inline" t-esc="label"/>
</t>
<t t-name="web_responsive.MenuStatusbarButtons">
<div class="o_statusbar_buttons_container">
<div class="o_statusbar_buttons_base hidden-xs hidden-sm">
<!-- Normal Buttons Zone -->
</div>
<div class="dropdown o_statusbar_buttons_dropdown hidden-lg hidden-md">
<button class="btn btn-default dropdown-toggle" type="button" id="dropdownMenuHeader" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
Task
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="dropdownMenuHeader">
</ul>
</div>
<div class="dropdown">
<button class="o_statusbar_buttons_dropdown btn btn-secondary dropdown-toggle"
type="button"
data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'cogs'"/>
<t t-set="label">Quick actions</t>
</t>
</button>
<!-- A div.o_statusbar_buttons.dropdown-menu
is appended here from JS -->
</div>
</t>
<t t-extend="FormView.buttons">
<!-- Change "Edit" button hotkey to "E" -->
<t t-jquery=".o_form_button_edit" t-operation="attributes">
<attribute name="accesskey">e</attribute>
</t>
<!-- Change "Discard" button hotkey to "D" -->
<t t-jquery=".o_form_button_cancel" t-operation="attributes">
<attribute name="accesskey">d</attribute>
</t>
<!-- Add responsive icons to buttons -->
<t t-jquery=".o_form_button_edit" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'pencil'"/>
<t t-set="label">Edit</t>
</t>
</t>
<t t-jquery=".o_form_button_create" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'plus'"/>
<t t-set="label">Create</t>
</t>
</t>
<t t-jquery=".o_form_button_save" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'check'"/>
<t t-set="label">Save</t>
</t>
</t>
<t t-jquery=".o_form_button_cancel" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'times'"/>
<t t-set="label">Discard</t>
</t>
</t>
</t>
<t t-extend="KanbanView.buttons">
<!-- Add responsive icons to buttons -->
<t t-jquery="button" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'plus'"/>
<t t-set="label" t-value="create_text || _t('Create')"/>
</t>
</t>
</t>
<t t-extend="ListView.buttons">
<!-- Change "Discard" button hotkey to "D" -->
<t t-jquery=".o_list_button_discard" t-operation="attributes">
<attribute name="accesskey">d</attribute>
</t>
<!-- Add responsive icons to buttons -->
<t t-jquery=".o_list_button_add" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'plus'"/>
<t t-set="label">Create</t>
</t>
</t>
<t t-jquery=".o_list_button_save" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'check'"/>
<t t-set="label">Save</t>
</t>
</t>
<t t-jquery=".o_list_button_discard" t-operation="inner">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'times'"/>
<t t-set="label">Discard</t>
</t>
</t>
</t>
<t t-extend="Sidebar">
<!-- Replace some common sections by icons in mobile -->
<t t-jquery=".o_dropdown_toggler_btn t[t-esc='section.label']"
t-operation="replace">
<t t-set="label" t-value="section.label"/>
<t t-if="section.name == 'files'">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'paperclip'"/>
</t>
</t>
<t t-elif="section.name == 'print'">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'print'"/>
</t>
</t>
<t t-elif="section.name == 'other'">
<t t-call="web_responsive.icon_button">
<t t-set="icon" t-value="'wrench'"/>
</t>
</t>
<t t-else="">
<span t-esc="label"/>
</t>
</t>
</t>
</templates>

View File

@@ -1,11 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017 Jairo Llopis <jairo.llopis@tecnativa.com>
<!-- Copyright 2017-2018 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t t-extend="SwitchCompanyMenu">
<t t-jquery=".oe_topbar_name" t-operation="before">
<i class="fa fa-building visible-xs-inline-block"/>
<t t-extend="Menu">
<t t-jquery=".o_menu_apps" t-operation="after">
<!-- Hamburger button to show submenus in sm screens -->
<button class="o-menu-toggle d-md-none"
data-toggle="collapse"
data-target=".o_main_navbar .o_menu_sections">
<i class="fa fa-bars"/>
</button>
</t>
</t>
</template>