summaryrefslogtreecommitdiff
path: root/addons
diff options
context:
space:
mode:
authorSamuel Degueldre <sad@odoo.com>2020-03-05 10:47:48 +0000
committerqsm-odoo <qsm@odoo.com>2020-04-16 09:45:58 +0000
commit0b0690a3934cedcab0d5e34373e8d40c2ab25660 (patch)
tree42065ab203bc1805229fc2868459271b6cf4b34d /addons
parentd6893ae7277ba626c3383fbc3dbf4355b75386fc (diff)
[IMP] website, web: add fullscreen mode and frontend keyboard shortcuts
While the edit mode interface is full of useful things, it also adds a lot of clutter, and alters the page's width and thus, its layout. This commit adds keyboard shortcut for website editors to use, and allows them to enter a "fullscreen" mode that hides the editor interface while remaining in edit mode, allowing the user to have fewer distractions while editing text content, and a closer rendition of the page's final layout while editing. For this, keyboard shortcuts were required, and a few other useful shortcuts have been added, such as alt-1,2,3 for the navbar, just like in the backed. task-2126915 closes odoo/odoo#46953 Related: odoo/enterprise#9295 Signed-off-by: Quentin Smetz (qsm) <qsm@odoo.com>
Diffstat (limited to 'addons')
-rw-r--r--addons/web/static/src/js/chrome/keyboard_navigation_mixin.js45
-rw-r--r--addons/web/static/src/scss/ui.scss18
-rw-r--r--addons/web/static/src/scss/webclient.scss10
-rw-r--r--addons/web_editor/views/snippets.xml2
-rw-r--r--addons/website/static/src/js/content/website_root.js65
-rw-r--r--addons/website/static/src/js/menu/mobile_view.js5
-rw-r--r--addons/website/static/src/js/menu/new_content.js6
-rw-r--r--addons/website/static/src/scss/website.ui.scss20
-rw-r--r--addons/website/static/src/xml/website.editor.xml4
-rw-r--r--addons/website/views/assets.xml3
-rw-r--r--addons/website/views/snippets/snippets.xml2
-rw-r--r--addons/website/views/website_navbar_templates.xml13
12 files changed, 153 insertions, 40 deletions
diff --git a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js
index 4318bca879a..707dab1f8f9 100644
--- a/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js
+++ b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js
@@ -30,7 +30,17 @@ odoo.define('web.KeyboardNavigationMixin', function (require) {
'keyup': '_onKeyUp',
},
- init: function () {
+ /**
+ * @constructor
+ * @param {object} [options]
+ * @param {boolean} [options.autoAccessKeys=true]
+ * Whether accesskeys should be created automatically for buttons
+ * without them in the page.
+ */
+ init: function (options) {
+ this.options = Object.assign({
+ autoAccessKeys: true,
+ }, options);
this._areAccessKeyVisible = false;
this.BrowserDetection = new BrowserDetection();
},
@@ -130,22 +140,24 @@ odoo.define('web.KeyboardNavigationMixin', function (require) {
var usedAccessKey = this._getAllUsedAccessKeys();
- var buttonsWithoutAccessKey = this.$el.find('button.btn:visible')
- .not('[accesskey]')
- .not('[disabled]')
- .not('[tabindex="-1"]');
- _.each(buttonsWithoutAccessKey, function (elem) {
- var buttonString = [elem.innerText, elem.title, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"].join('');
- for (var letterIndex = 0; letterIndex < buttonString.length; letterIndex++) {
- var candidateAccessKey = buttonString[letterIndex].toUpperCase();
- if (candidateAccessKey >= 'A' && candidateAccessKey <= 'Z' &&
- !_.includes(usedAccessKey, candidateAccessKey)) {
- elem.accessKey = candidateAccessKey;
- usedAccessKey.push(candidateAccessKey);
- break;
+ if (this.options.autoAccessKeys) {
+ var buttonsWithoutAccessKey = this.$el.find('button.btn:visible')
+ .not('[accesskey]')
+ .not('[disabled]')
+ .not('[tabindex="-1"]');
+ _.each(buttonsWithoutAccessKey, function (elem) {
+ var buttonString = [elem.innerText, elem.title, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"].join('');
+ for (var letterIndex = 0; letterIndex < buttonString.length; letterIndex++) {
+ var candidateAccessKey = buttonString[letterIndex].toUpperCase();
+ if (candidateAccessKey >= 'A' && candidateAccessKey <= 'Z' &&
+ !_.includes(usedAccessKey, candidateAccessKey)) {
+ elem.accessKey = candidateAccessKey;
+ usedAccessKey.push(candidateAccessKey);
+ break;
+ }
}
- }
- });
+ });
+ }
var elementsWithoutAriaKeyshortcut = this.$el.find('[accesskey]').not('[aria-keyshortcuts]');
_.each(elementsWithoutAriaKeyshortcut, function (elem) {
@@ -230,4 +242,3 @@ odoo.define('web.KeyboardNavigationMixin', function (require) {
return KeyboardNavigationMixin;
});
-
diff --git a/addons/web/static/src/scss/ui.scss b/addons/web/static/src/scss/ui.scss
index 0a318c57d13..61854a8d304 100644
--- a/addons/web/static/src/scss/ui.scss
+++ b/addons/web/static/src/scss/ui.scss
@@ -143,3 +143,21 @@ span.o_force_ltr {
.o_width_128 {
width: 128px;
}
+
+.o_web_accesskey_overlay {
+ @include o-position-absolute(0, 0);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: 100%;
+ background-color: rgba(0, 0, 0, .6);
+ color: #FFFFFF;
+ font-size: 1rem;
+
+ // Needed as the parent button could be using font-awesome.
+ // Can't be put to a BS variable here as we are in assets common (will be
+ // overridden for backend but left with 'sans-serif' for frontend to not
+ // have an useless frontend rule.
+ font-family: sans-serif;
+}
diff --git a/addons/web/static/src/scss/webclient.scss b/addons/web/static/src/scss/webclient.scss
index d41d1fb0ba3..9bc77f1b29d 100644
--- a/addons/web/static/src/scss/webclient.scss
+++ b/addons/web/static/src/scss/webclient.scss
@@ -84,16 +84,6 @@ div.o_boolean_toggle.custom-control.custom-checkbox {
.o_web_accesskey_overlay {
font-family: $font-family-sans-serif;
- position: absolute;
- width: 100%;
- height: 100%;
- left: 0;
- top: 0;
- background-color: rgba(0,0,0,.6);
- color: #FFFFFF;
- justify-content: center;
- display: flex;
- align-items: center;
}
// Decorations
diff --git a/addons/web_editor/views/snippets.xml b/addons/web_editor/views/snippets.xml
index 3b08d7d5a52..5ba6583ecf1 100644
--- a/addons/web_editor/views/snippets.xml
+++ b/addons/web_editor/views/snippets.xml
@@ -2,7 +2,7 @@
<odoo>
<template id="snippets">
<div id="snippets_menu">
- <button type="button" class="o_we_add_snippet_btn active">
+ <button type="button" class="o_we_add_snippet_btn active" accesskey="1">
Blocks
</button>
<t t-set="customize_title">Click in the page to customize</t>
diff --git a/addons/website/static/src/js/content/website_root.js b/addons/website/static/src/js/content/website_root.js
index bf1a28fd5c2..dd2f54b72c1 100644
--- a/addons/website/static/src/js/content/website_root.js
+++ b/addons/website/static/src/js/content/website_root.js
@@ -3,6 +3,8 @@ odoo.define('website.root', function (require) {
var core = require('web.core');
var Dialog = require('web.Dialog');
+const KeyboardNavigationMixin = require('web.KeyboardNavigationMixin');
+const session = require('web.session');
var publicRootData = require('web.public.root');
require("web.zoomodoo");
@@ -10,8 +12,8 @@ var _t = core._t;
var websiteRootRegistry = publicRootData.publicRootRegistry;
-var WebsiteRoot = publicRootData.PublicRoot.extend({
- events: _.extend({}, publicRootData.PublicRoot.prototype.events || {}, {
+var WebsiteRoot = publicRootData.PublicRoot.extend(KeyboardNavigationMixin, {
+ events: _.extend({}, KeyboardNavigationMixin.events, publicRootData.PublicRoot.prototype.events || {}, {
'click .js_change_lang': '_onLangChangeClick',
'click .js_publish_management .js_publish_btn': '_onPublishBtnClick',
'click .js_multi_website_switch': '_onWebsiteSwitch',
@@ -25,6 +27,16 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({
/**
* @override
*/
+ init() {
+ this.isFullscreen = false;
+ KeyboardNavigationMixin.init.call(this, {
+ autoAccessKeys: false,
+ });
+ return this._super(...arguments);
+ },
+ /**
+ * @override
+ */
start: function () {
// Compatibility lang change ?
if (!this.$('.js_change_lang').length) {
@@ -82,6 +94,38 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({
}
return registry;
},
+ /**
+ * Toggles the fullscreen mode.
+ *
+ * @private
+ * @param {boolean} state toggle fullscreen on/off (true/false)
+ */
+ _toggleFullscreen(state) {
+ this.isFullscreen = state;
+ document.body.classList.toggle('o_fullscreen', this.isFullscreen);
+ document.body.style.overflowX = 'hidden';
+ let resizing = true;
+ window.requestAnimationFrame(function resizeFunction() {
+ window.dispatchEvent(new Event('resize'));
+ if (resizing) {
+ window.requestAnimationFrame(resizeFunction);
+ }
+ });
+ let stopResizing;
+ const onTransitionEnd = ev => {
+ if (ev.target === document.body && ev.propertyName === 'padding-top') {
+ stopResizing();
+ }
+ };
+ stopResizing = () => {
+ resizing = false;
+ document.body.style.overflowX = '';
+ document.body.removeEventListener('transitionend', onTransitionEnd);
+ };
+ document.body.addEventListener('transitionend', onTransitionEnd);
+ // Safeguard in case the transitionend event doesn't trigger for whatever reason.
+ window.setTimeout(() => stopResizing(), 500);
+ },
//--------------------------------------------------------------------------
// Handlers
@@ -147,6 +191,9 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({
*/
_onPublishBtnClick: function (ev) {
ev.preventDefault();
+ if (document.body.classList.contains('editor_enable')) {
+ return;
+ }
var self = this;
var $data = $(ev.currentTarget).parents(".js_publish_management:first");
@@ -207,6 +254,20 @@ var WebsiteRoot = publicRootData.PublicRoot.extend({
_onModalShown: function (ev) {
$(ev.target).addClass('modal_shown');
},
+ /**
+ * @override
+ */
+ _onKeyDown(ev) {
+ if (!session.user_id) {
+ return;
+ }
+ // If document.body doesn't contain the element, it was probably removed as a consequence of pressing Esc.
+ // we don't want to toggle fullscreen as the removal (eg, closing a modal) is the intended action.
+ if (ev.keyCode !== $.ui.keyCode.ESCAPE || !document.body.contains(ev.target) || ev.target.closest('.modal')) {
+ return KeyboardNavigationMixin._onKeyDown.apply(this, arguments);
+ }
+ this._toggleFullscreen(!this.isFullscreen);
+ },
});
return {
diff --git a/addons/website/static/src/js/menu/mobile_view.js b/addons/website/static/src/js/menu/mobile_view.js
index acfd28042c9..668962c8a10 100644
--- a/addons/website/static/src/js/menu/mobile_view.js
+++ b/addons/website/static/src/js/menu/mobile_view.js
@@ -50,7 +50,10 @@ var MobileMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
* @private
*/
_onMobilePreviewClick: function () {
- new MobilePreviewDialog(this, {
+ if (this.mobilePreview && !this.mobilePreview.isDestroyed()) {
+ return this.mobilePreview.close();
+ }
+ this.mobilePreview = new MobilePreviewDialog(this, {
title: _t('Mobile preview') + ' <span class="fa fa-refresh"/>',
}).open();
},
diff --git a/addons/website/static/src/js/menu/new_content.js b/addons/website/static/src/js/menu/new_content.js
index 918a840561d..efb1b428134 100644
--- a/addons/website/static/src/js/menu/new_content.js
+++ b/addons/website/static/src/js/menu/new_content.js
@@ -132,6 +132,7 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
* @private
*/
_hideMenu: function () {
+ this.shown = false;
this.$newContentMenuChoices.addClass('o_hidden');
$('body').removeClass('o_new_content_open');
},
@@ -168,6 +169,7 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
});
}).then(function () {
self.firstTab = true;
+ self.shown = true;
self.$newContentMenuChoices.removeClass('o_hidden');
$('body').addClass('o_new_content_open');
self.$('> a').focus();
@@ -203,9 +205,13 @@ var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
* @param {Event} ev
*/
_onBackgroundKeydown: function (ev) {
+ if (!this.shown) {
+ return;
+ }
switch (ev.which) {
case $.ui.keyCode.ESCAPE:
this._hideMenu();
+ ev.stopPropagation();
break;
case $.ui.keyCode.TAB:
if (ev.shiftKey) {
diff --git a/addons/website/static/src/scss/website.ui.scss b/addons/website/static/src/scss/website.ui.scss
index 8ac07bc6ade..f57cfb8ea84 100644
--- a/addons/website/static/src/scss/website.ui.scss
+++ b/addons/website/static/src/scss/website.ui.scss
@@ -8,9 +8,29 @@ body {
// Set frontend direction that will be flipped with
// rtlcss for right-to-left text direction.
direction: ltr;
+ transition: padding 0.35s;
}
body.o_connected_user {
padding-top: $o-navbar-height!important;
+
+ &.o_fullscreen {
+ padding-top: 0 !important;
+
+ &.editor_enable.editor_has_snippets {
+ padding-left: 0 !important;
+ }
+
+ #oe_main_menu_navbar, #web_editor-top-edit {
+ transform: translateY(-100%);
+ }
+
+ #oe_snippets {
+ transform: translateX(-100%);
+ }
+ }
+}
+#oe_main_menu_navbar, #web_editor-top-edit, #oe_snippets {
+ transition: transform 0.35s !important;
}
// MAIN MENU STYLE (added above navbar.scss)
diff --git a/addons/website/static/src/xml/website.editor.xml b/addons/website/static/src/xml/website.editor.xml
index 9c8791d8fe7..fecb8626102 100644
--- a/addons/website/static/src/xml/website.editor.xml
+++ b/addons/website/static/src/xml/website.editor.xml
@@ -14,8 +14,8 @@
<t t-name="website.editorbar">
<div id="web_editor-top-edit">
<form class="navbar-form text-muted">
- <button type="button" class="btn btn-secondary" data-action="cancel"><i class="fa fa-times"/> Discard</button>
- <button type="button" class="btn btn-primary" data-action="save"><i class="fa fa-floppy-o"/> Save</button>
+ <button type="button" class="btn btn-secondary" data-action="cancel" accesskey="j"><i class="fa fa-times"/> Discard</button>
+ <button type="button" class="btn btn-primary" data-action="save" accesskey="s"><i class="fa fa-floppy-o"/> Save</button>
</form>
</div>
</t>
diff --git a/addons/website/views/assets.xml b/addons/website/views/assets.xml
index 15b5f068ec8..029c17ab43a 100644
--- a/addons/website/views/assets.xml
+++ b/addons/website/views/assets.xml
@@ -77,6 +77,9 @@
<script type="text/javascript" src="/website/static/lib/jstz.min.js"/>
<script type="text/javascript" src="/website/static/src/js/utils.js"/>
+ <script type="text/javascript" src="/web/static/src/js/core/browser_detection.js"/>
+ <script type="text/javascript" src="/web/static/src/js/chrome/keyboard_navigation_mixin.js"/>
+
<script type="text/javascript" src="/website/static/src/js/content/website_root.js"/>
<script type="text/javascript" src="/website/static/src/js/content/compatibility.js"/>
<script type="text/javascript" src="/website/static/src/js/content/lazy_template_call.js"/>
diff --git a/addons/website/views/snippets/snippets.xml b/addons/website/views/snippets/snippets.xml
index bb30daf38a1..2c02a2d1c50 100644
--- a/addons/website/views/snippets/snippets.xml
+++ b/addons/website/views/snippets/snippets.xml
@@ -6,7 +6,7 @@
<xpath expr="//div[@id='snippets_menu']" position="inside">
<button type="button" class="o_we_customize_theme_btn"
data-title="Customize your theme"
- groups="website.group_website_designer">
+ groups="website.group_website_designer" accesskey="2">
Options
</button>
</xpath>
diff --git a/addons/website/views/website_navbar_templates.xml b/addons/website/views/website_navbar_templates.xml
index df717dce76d..f8b0149e57e 100644
--- a/addons/website/views/website_navbar_templates.xml
+++ b/addons/website/views/website_navbar_templates.xml
@@ -4,8 +4,8 @@
<template id="website_publisher" name="Website Publisher">
<t groups="website.group_website_publisher" t-if="'website_published' in main_object.fields_get() and ('can_publish' not in main_object.fields_get() or main_object.can_publish)">
<li t-attf-class="js_publish_management #{extra_classes} #{main_object.website_published and 'css_published' or 'css_unpublished'}" t-att-data-description="env['ir.model']._get(main_object._name).display_name" t-att-data-id="main_object.id" t-att-data-object="main_object._name" t-att-data-controller="publish_controller">
- <label class="o_switch o_switch_danger js_publish_btn" for="id">
- <input type="checkbox" t-att-checked="main_object.website_published" id="id"/>
+ <label class="o_switch o_switch_danger js_publish_btn" t-att-accesskey="desktop_navbar and 'p'">
+ <input type="checkbox" t-att-checked="main_object.website_published"/>
<span/>
<span class="css_publish">Unpublished</span>
<span class="css_unpublish">Published</span>
@@ -68,13 +68,14 @@
<ul class="o_menu_systray d-none d-md-block" groups="website.group_website_publisher">
<t t-call="website.website_publisher">
<t t-set="extra_classes" t-value=""/>
+ <t t-set="desktop_navbar" t-value="true"/>
</t>
<!-- Mobile preview -->
<li class="o_mobile_preview" id="mobile-menu">
- <a data-action="show-mobile-preview" href="#"><span title="Mobile preview" role="img" aria-label="Mobile preview" class="fa fa-mobile"/></a>
+ <a data-action="show-mobile-preview" href="#" accesskey="v"><span title="Mobile preview" role="img" aria-label="Mobile preview" class="fa fa-mobile"/></a>
</li>
<li groups="website.group_multi_website" t-if="multi_website_websites" id="website_switcher">
- <a class="dropdown-toggle" data-toggle="dropdown" href="#">
+ <a class="dropdown-toggle" data-toggle="dropdown" href="#" accesskey="w">
<i class="fa fa-globe d-lg-none"/>
<span class="d-none d-lg-inline-block">
<t t-esc="multi_website_websites_current['name']"/>
@@ -98,7 +99,7 @@
<!-- Page Edition -->
<li class="o_new_content_menu" id="new-content-menu">
- <a href="#"><span class="fa fa-plus mr-2"/>New</a>
+ <a href="#" accesskey="c"><span class="fa fa-plus mr-2"/>New</a>
<div id="o_new_content_menu_choices" class="o_hidden">
<div class="container pt32 pb32">
<div class="row">
@@ -161,7 +162,7 @@
</div>
</li>
<li t-if="not translatable" id="edit-page-menu">
- <a data-action="edit" href="#"><span class="fa fa-pencil mr-2"/>Edit</a>
+ <a data-action="edit" href="#" accesskey="a"><span class="fa fa-pencil mr-2"/>Edit</a>
</li>
<li t-if="edit_in_backend or ('website_published' in main_object.fields_get() and main_object._name != 'website.page')">
<a role="button" class="btn btn-primary btn-sm dropdown-toggle css_edit_dynamic" data-toggle="dropdown">