add backend_theme from openworx.nl

This commit is contained in:
santiky 2021-08-07 14:02:32 +02:00
parent 17ecc2e4a0
commit 46d66821ec
Signed by: snt
GPG key ID: A9FD34930EADBE71
70 changed files with 5413 additions and 0 deletions

View file

@ -0,0 +1,763 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
$chatter_zone_width: 35%;
@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;
}
// Support for long titles
@include media-breakpoint-up(md) {
.o_form_view .oe_button_box + .oe_title,
.o_form_view .oe_button_box + .oe_avatar + .oe_title {
/* Button-box has a hardcoded width of 132px per button and have three columns */
width: calc(100% - 450px);
}
}
// 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;
overflow: auto;
.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;
}
// 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;
}
}
max-width: 100%;
}
// Control panel (breadcrumbs, search box, buttons...)
.o_control_panel {
.o_search_options {
flex-grow: 1;
> div {
width: 100%;
> .btn-group {
max-width: 33%;
.o_dropdown_toggler_btn {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.o_filters_menu {
.o_menu_item {
@include o-search-options-dropdown-custom-item;
a {
overflow: hidden;
text-overflow: ellipsis;
}
}
}
}
.o_pager_counter {
white-space: nowrap;
}
@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);
overflow-x: auto;
.oe_button_box {
.o_dropdown_more {
padding: 0.1em;
width: min-content;
}
}
}
// Sticky statusbar
.o_form_statusbar {
position: sticky;
top: 0;
z-index: 2;
}
// Support for long title (with ellipsis)
.oe_title {
span.o_field_widget {
&:not(.oe_inline) {
max-width: 100%;
text-overflow: ellipsis;
white-space: nowrap;
overflow: hidden;
width: initial;
&:active {
white-space: normal;
}
}
}
}
@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%;
}
.oe_button_box + .oe_title,
.oe_button_box + .oe_avatar + .oe_title {
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;
}
}
.o_chatter {
// Display send button on small screens
.o_thread_composer {
padding-left: $o-mail-thread-avatar-size*0.5;
.o_composer_button_send {
display: initial !important; // Forced in core
}
.o_chatter_avatar {
display: none;
}
}
.o_chatter_topbar {
height: auto;
flex-wrap: wrap-reverse;
> .o_topbar_right_area {
flex: 1 0 auto;
flex-wrap: wrap;
max-width: 100%;
// Display followers on small screens
.o_followers {
display: initial !important; // Forced in core
@include media-breakpoint-down(xs) {
padding-bottom: 50px;
}
}
}
}
}
}
}
// Sided chatter, if user wants
.o_chatter_position_sided & {
@include media-breakpoint-up(lg) {
.o_form_view:not(.o_form_nosheet) {
display: flex;
flex-flow: row nowrap;
height: 100%;
.o_form_sheet_bg {
flex: 1 1 auto;
overflow: auto;
> .o_form_sheet {
min-width: unset;
.oe_subtotal_footer .popover {
/* This is an inaccurate workaround for a problem with how Odoo places the popover
* See: https://github.com/odoo/odoo/blob/12.0/addons/account/static/src/js/account_payment_field.js#L83
*/
position: fixed !important;
top: 11.4em !important;
}
}
}
.o_chatter {
border-left: 1px solid gray('400');
flex: 0 0 $chatter_zone_width;
max-width: initial;
min-width: initial;
overflow: auto;
.o_chatter_header_container {
padding-top: $grid-gutter-width * 0.5;
top: 0;
position: sticky;
background-color: $o-view-background-color;
z-index: 1;
.o_chatter_topbar {
margin-top: 0;
flex-wrap: wrap;
height: auto;
button:last-of-type {
flex: 1 0 auto;
text-align: left;
}
.o_topbar_right_area {
order: -10;
flex: 0 1 100%;
border-bottom-color: transparent;
}
}
// Scrollable input text to avoid hide conversation & buttons
.o_composer_text_field {
max-height: 120px;
overflow-y: auto !important; /* Forced because Odoo uses inline style */
}
.o_attachments_list {
overflow: auto;
max-height: $o-mail-attachment-image-size * 3;
margin-top: 0.4em;
}
.o_attachments_previews {
overflow: auto;
max-height: $o-mail-attachment-image-size * 6;
}
}
}
}
}
}
}
// Sticky Header & Footer in List View
.table-responsive {
overflow-x: initial;
.o_list_view {
// th & td are here for compatibility with chrome
thead, thead tr:nth-child(1) th {
position: sticky;
top: 0;
z-index: 1;
}
thead tr:nth-child(1) th {
background-color: $o-list-footer-bg-color;
}
tfoot, tfoot tr:nth-child(1) td {
position: sticky;
bottom: 0;
}
tfoot tr:nth-child(1) td {
background-color: $o-list-footer-bg-color;
}
}
}
// Big checkboxes
.o_list_view {
.o_list_record_selector {
.custom-checkbox {
margin-right: 10px;
.custom-control-label {
top: -19px;
}
}
}
.o_data_cell:not(.o_boolean_toggle_cell) {
.custom-checkbox .custom-control-label {
top: -6px;
}
}
.custom-checkbox {
margin-right: 10px;
.custom-control-label {
&::after {
width: 24px;
height: 24px;
}
&::before {
outline: none !important;
border: 1px solid #4c4c4c;
width: 24px;
height: 24px;
}
}
}
}
// Waiting Cursor
.oe_wait {
cursor: progress;
}
// Document Viewer
.o_web_client.o_chatter_position_sided {
.o_modal_fullscreen.o_document_viewer {
// On-top of navbar
z-index: 10;
&.o_responsive_document_viewer {
/* Show sided viewer on large screens */
@include media-breakpoint-up(lg) {
width: $chatter_zone_width;
margin-left: auto;
/* Show/Hide control buttons (next, prev, etc..) */
&:hover .arrow, &:hover .o_viewer_toolbar {
display: unset;
}
.arrow, .o_viewer_toolbar {
display: none;
}
.o_viewer_img_wrapper {
position: relative;
.o_viewer_pdf {
width: 95%;
}
}
}
.o_minimize_btn {
display: none;
}
}
&:not(.o_responsive_document_viewer) {
.o_maximize_btn {
display: none;
}
}
@include media-breakpoint-down(lg) {
.o_minimize_btn, .o_maximize_btn {
display: none;
}
}
}
}
/* Max/Min buttons only are usefull in sided mode */
.o_web_client:not(.o_chatter_position_sided) {
.o_minimize_btn, .o_maximize_btn {
display: none;
}
}
// Apply improvements for Document Viewer on all modes
.o_modal_fullscreen .o_viewer_content {
.o_viewer-header {
.o_image_caption {
display: contents;
.o_split_pdf_area {
padding: 16px;
margin-bottom: 0;
position: relative;
top: -5px;
label {
margin-bottom: unset;
}
.o_page_number_input {
border: 1px solid white;
border-radius: 3px;
}
}
/* Hide 'split pdf' feature on small screens */
@include media-breakpoint-down(xs) {
.o_split_pdf_area {
display: none;
}
}
}
// Now uses a container to have more buttons
.o_buttons {
min-width: 35px;
text-align: right;
// Now close button ('X') it's a fa-icon
> .o_close_btn {
top: unset;
left: unset;
bottom: unset;
right: unset;
font-size: unset;
font-weight: unset;
}
}
}
.o_viewer_toolbar {
width: max-content;
}
}

View file

@ -0,0 +1,561 @@
/* Copyright 2018 Tecnativa - Jairo Llopis
* License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). */
odoo.define('web_responsive', function (require) {
'use strict';
var ActionManager = require('web.ActionManager');
var AbstractWebClient = require("web.AbstractWebClient");
var AppsMenu = require("web.AppsMenu");
var BasicController = require('web.BasicController');
var config = require("web.config");
var core = require("web.core");
var FormRenderer = require('web.FormRenderer');
var Menu = require("web.Menu");
var RelationalFields = require('web.relational_fields');
var Chatter = require('mail.Chatter');
var DocumentViewer = require('mail.DocumentViewer');
var ListRenderer = require('web.ListRenderer');
/* Hide AppDrawer in desktop and mobile modes.
* To avoid delays in pages with a lot of DOM nodes we make
* sub-groups' with 'querySelector' to improve the performance.
*/
function closeAppDrawer () {
_.defer(function () {
// Need close AppDrawer?
var menu_apps_dropdown = document.querySelector(
'.o_menu_apps .dropdown');
$(menu_apps_dropdown).has('.dropdown-menu.show')
.find('> a').dropdown('toggle');
// Need close Sections Menu?
// TODO: Change to 'hide' in modern Bootstrap >4.1
var menu_sections = document.querySelector(
'.o_menu_sections li.show .dropdown-toggle');
$(menu_sections).dropdown('toggle');
// Need close Mobile?
var menu_sections_mobile = document.querySelector(
'.o_menu_sections.show');
$(menu_sections_mobile).collapse('hide');
});
}
/**
* 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;
}
if (menu.children.length) {
_.reduce(menu.children, findNames, memo);
}
return memo;
}
AppsMenu.include({
events: _.extend({
"keydown .search-input input": "_searchResultsNavigate",
"input .search-input input": "_searchMenusSchedule",
"click .o-menu-search-result": "_searchResultChosen",
"shown.bs.dropdown": "_searchFocus",
"hidden.bs.dropdown": "_searchReset",
"hide.bs.dropdown": "_hideAppsMenu",
}, AppsMenu.prototype.events),
/**
* Rescue some menu data stripped out in original method.
*
* @override
*/
init: function (parent, menuData) {
this._super.apply(this, arguments);
// 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,
{}
);
// Search only after timeout, for fast typers
this._search_def = $.Deferred();
},
/**
* @override
*/
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);
},
/**
* Prevent the menu from being opened twice
*
* @override
*/
_onAppsMenuItemClicked: function (ev) {
this._super.apply(this, arguments);
ev.preventDefault();
ev.stopPropagation();
},
/**
* Get all info for a given menu.
*
* @param {String} key
* Full path to requested menu.
*
* @returns {Object}
* Menu definition, plus extra needed keys.
*/
_menuInfo: function (key) {
var original = this._searchableMenus[key];
return _.extend({
action_id: parseInt(original.action.split(',')[1], 10),
}, original);
},
/**
* Autofocus on search field on big screens.
*/
_searchFocus: function () {
if (!config.device.isMobile) {
this.$search_input.focus();
}
},
/**
* 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 () {
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,
}
)
);
},
/**
* Use chooses a search result, so we navigate to that menu
*
* @param {jQuery.Event} event
*/
_searchResultChosen: function (event) {
event.preventDefault();
event.stopPropagation();
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);
},
/**
* Navigate among search results
*
* @param {jQuery.Event} event
*/
_searchResultsNavigate: function (event) {
// 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;
// Keyboard navigation only supports search results
if (!all.length) {
return;
}
// 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;
default:
// Other keys are useless in this event
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,
},
});
},
/*
* Control if AppDrawer can be closed
*/
_hideAppsMenu: function () {
return !this.$('input').is(':focus');
},
});
BasicController.include({
/**
* Close the AppDrawer if the data set is dirty and a discard dialog
* is opened
*
* @override
*/
canBeDiscarded: function (recordID) {
if (this.model.isDirty(recordID || this.handle)) {
closeAppDrawer();
}
return this._super.apply(this, arguments);
},
});
Menu.include({
events: _.extend({
// Clicking a hamburger menu item should close the hamburger
"click .o_menu_sections [role=menuitem]": "_onClickMenuItem",
// 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);
},
/**
* Hide menus for current app if you're in mobile
*/
_hideMobileSubmenus: function () {
if (
config.device.isMobile &&
this.$menu_toggle.is(":visible") &&
this.$section_placeholder.is(":visible")
) {
this.$section_placeholder.collapse("hide");
}
},
/**
* Prevent hide the menu (should be closed when action is loaded)
*
* @param {ClickEvent} ev
*/
_onClickMenuItem: function (ev) {
ev.stopPropagation();
},
/**
* No menu brand in mobiles
*
* @override
*/
_updateMenuBrand: function () {
if (!config.device.isMobile) {
return this._super.apply(this, arguments);
}
},
});
RelationalFields.FieldStatus.include({
/**
* Fold all on mobiles.
*
* @override
*/
_setState: function () {
this._super.apply(this, arguments);
if (config.device.isMobile) {
_.map(this.status_information, function (value) {
value.fold = true;
});
}
},
});
// Responsive view "action" buttons
FormRenderer.include({
/**
* 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;
}
// $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;
},
});
// Chatter Hide Composer
Chatter.include({
_openComposer: function (options) {
if (this._composer &&
options.isLog === this._composer.options.isLog &&
this._composer.$el.is(':visible')) {
this._closeComposer(false);
} else {
this._super.apply(this, arguments);
}
},
});
// Hide AppDrawer or Menu when the action has been completed
ActionManager.include({
/**
* @override
*/
_appendController: function () {
this._super.apply(this, arguments);
closeAppDrawer();
},
});
/**
* 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 = {
/**
* 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);
// DocumentViewer: Add support to maximize/minimize
DocumentViewer.include({
// Widget 'keydown' and 'keyup' events are only dispatched when
// this.$el is active, but now the modal have buttons that can obtain
// the focus. For this reason we now listen core events, that are
// dispatched every time.
events: _.extend(_.omit(DocumentViewer.prototype.events, [
'keydown',
'keyup',
]), {
'click .o_maximize_btn': '_onClickMaximize',
'click .o_minimize_btn': '_onClickMinimize',
'shown.bs.modal': '_onShownModal',
}),
start: function () {
core.bus.on('keydown', this, this._onKeydown);
core.bus.on('keyup', this, this._onKeyUp);
return this._super.apply(this, arguments);
},
destroy: function () {
core.bus.off('keydown', this, this._onKeydown);
core.bus.off('keyup', this, this._onKeyUp);
this._super.apply(this, arguments);
},
_onShownModal: function () {
// Disable auto-focus to allow to use controls in edit mode.
// This only affects the active modal.
// More info: https://stackoverflow.com/a/14795256
$(document).off('focusin.modal');
},
_onClickMaximize: function () {
this.$el.removeClass('o_responsive_document_viewer');
},
_onClickMinimize: function () {
this.$el.addClass('o_responsive_document_viewer');
},
});
// List Renderer: Big checkboxes
ListRenderer.include({
/**
* @override
*/
_renderSelector: function () {
var $elm = this._super.apply(this, arguments);
$elm.find(".custom-control-label").html('');
return $elm;
},
});
});

View file

@ -0,0 +1,65 @@
<?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 must be clickable -->
<t t-jquery=".o_app" t-operation="attributes">
<attribute name="t-attf-href">#menu_id=#{app.menuID}&amp;action_id=#{app.actionID}</attribute>
</t>
<!-- 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"
autocomplete="off"
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)"/>
<a 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-attf-href="#menu_id=#{menu.id}&amp;action_id=#{menu.action_id}"
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

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2019 Tecnativa - Alexandre Díaz
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<t t-extend="DocumentViewer">
<t t-jquery=".o_modal_fullscreen" t-operation="attributes">
<attribute name="class">modal o_modal_fullscreen o_document_viewer o_responsive_document_viewer</attribute>
<attribute name="data-backdrop">false</attribute>
</t>
</t>
<t t-extend="DocumentViewer.Content">
<t t-jquery=".o_close_btn" t-operation="replace">
<div class="o_buttons float-right mr8">
<a role="button" class="mr8 o_maximize_btn" tabindex="0" aria-label="Maximize" title="Maximize"><i class="fa fa-window-maximize"></i></a>
<a role="button" class="mr8 o_minimize_btn" tabindex="0" aria-label="Minimize" title="Minimize"><i class="fa fa-window-minimize"></i></a>
<a role="button" class="o_close_btn" tabindex="0" aria-label="Close" title="Close"><i class="fa fa-close"></i></a>
</div>
</t>
</t>
</template>

View file

@ -0,0 +1,151 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
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">
<!-- 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="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>
<t t-extend="mail.Chatter">
<t t-jquery=".o_chatter_topbar" t-operation="replace">
<div class="o_chatter_header_container">
<div class="o_chatter_topbar">
<div class="o_topbar_right_area"/>
</div>
</div>
</t>
</t>
</templates>

View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-extend="UserMenu">
<t t-jquery=".dropdown-toggle" t-operation="attributes">
<attribute name="accesskey">u</attribute>
</t>
</t>
<div t-extend="UserMenu.shortcuts">
<!-- Replace "a" shortcut by "e" (for 'Edit a Record') -->
<t t-jquery="table.o_shortcut_table > tbody > tr > td:nth-child(2) > span:nth-child(2).o_key:contains('a')" t-operation="replace">
<span class="o_key">e</span>
</t>
<t t-jquery="table.o_shortcut_table > tbody > tr > td:nth-child(3) > span:nth-child(3).o_key:contains('a')" t-operation="replace">
<span class="o_key">e</span>
</t>
<!-- Replace "j" shortcut by "d" (for 'Discard a record modification') -->
<t t-jquery="table.o_shortcut_table > tbody > tr > td:nth-child(2) > span:nth-child(2).o_key:contains('j')" t-operation="replace">
<span class="o_key">d</span>
</t>
<t t-jquery="table.o_shortcut_table > tbody > tr > td:nth-child(3) > span:nth-child(3).o_key:contains('j')" t-operation="replace">
<span class="o_key">d</span>
</t>
<!-- Add Shift for all windows / linux shortcuts -->
<t t-jquery="table.o_shortcut_table > tbody > tr > td:nth-child(2) > span:first-child" t-operation="after">
+ <span class="o_key">Shift</span>
</t>
<!-- Add new accesskeys -->
<t t-jquery=".o_shortcut_table tbody" t-operation="append">
<tr>
<td align="left">Select menu</td>
<td>
<span class="o_key">Alt</span> + <span class="o_key">Shift</span> + <span class="o_key">a</span>
</td>
<td>
<span class="o_key">Control</span> + <span class="o_key">Alt</span> + <span class="o_key">a</span>
</td>
</tr>
</t>
<t t-jquery=".o_shortcut_table tbody" t-operation="append">
<tr>
<td align="left">User menu</td>
<td>
<span class="o_key">Alt</span> + <span class="o_key">Shift</span> + <span class="o_key">u</span>
</td>
<td>
<span class="o_key">Control</span> + <span class="o_key">Alt</span> + <span class="o_key">u</span>
</td>
</tr>
</t>
</div>
</templates>

View file

@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Copyright 2017-2018 Tecnativa - Jairo Llopis
License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl). -->
<template>
<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>