diff options
| author | Samuel Degueldre <sad@odoo.com> | 2020-03-05 10:47:48 +0000 |
|---|---|---|
| committer | qsm-odoo <qsm@odoo.com> | 2020-04-16 09:45:58 +0000 |
| commit | 0b0690a3934cedcab0d5e34373e8d40c2ab25660 (patch) | |
| tree | 42065ab203bc1805229fc2868459271b6cf4b34d /addons | |
| parent | d6893ae7277ba626c3383fbc3dbf4355b75386fc (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.js | 45 | ||||
| -rw-r--r-- | addons/web/static/src/scss/ui.scss | 18 | ||||
| -rw-r--r-- | addons/web/static/src/scss/webclient.scss | 10 | ||||
| -rw-r--r-- | addons/web_editor/views/snippets.xml | 2 | ||||
| -rw-r--r-- | addons/website/static/src/js/content/website_root.js | 65 | ||||
| -rw-r--r-- | addons/website/static/src/js/menu/mobile_view.js | 5 | ||||
| -rw-r--r-- | addons/website/static/src/js/menu/new_content.js | 6 | ||||
| -rw-r--r-- | addons/website/static/src/scss/website.ui.scss | 20 | ||||
| -rw-r--r-- | addons/website/static/src/xml/website.editor.xml | 4 | ||||
| -rw-r--r-- | addons/website/views/assets.xml | 3 | ||||
| -rw-r--r-- | addons/website/views/snippets/snippets.xml | 2 | ||||
| -rw-r--r-- | addons/website/views/website_navbar_templates.xml | 13 |
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"> |
