diff options
| author | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
|---|---|---|
| committer | stephanchrst <stephanchrst@gmail.com> | 2022-05-10 21:51:50 +0700 |
| commit | 3751379f1e9a4c215fb6eb898b4ccc67659b9ace (patch) | |
| tree | a44932296ef4a9b71d5f010906253d8c53727726 /addons/web/static/src/js/chrome/keyboard_navigation_mixin.js | |
| parent | 0a15094050bfde69a06d6eff798e9a8ddf2b8c21 (diff) | |
initial commit 2
Diffstat (limited to 'addons/web/static/src/js/chrome/keyboard_navigation_mixin.js')
| -rw-r--r-- | addons/web/static/src/js/chrome/keyboard_navigation_mixin.js | 261 |
1 files changed, 261 insertions, 0 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 new file mode 100644 index 00000000..c67cb98b --- /dev/null +++ b/addons/web/static/src/js/chrome/keyboard_navigation_mixin.js @@ -0,0 +1,261 @@ +odoo.define('web.KeyboardNavigationMixin', function (require) { + "use strict"; + var BrowserDetection = require('web.BrowserDetection'); + const core = require('web.core'); + + /** + * list of the key that should not be used as accesskeys. Either because we want to reserve them for a specific behavior in Odoo or + * because they will not work in certain browser/OS + */ + var knownUnusableAccessKeys = [' ', + 'A', // reserved for Odoo Edit + 'B', // reserved for Odoo Previous Breadcrumb (Back) + 'C', // reserved for Odoo Create + 'H', // reserved for Odoo Home + 'J', // reserved for Odoo Discard + 'K', // reserved for Odoo Kanban view + 'L', // reserved for Odoo List view + 'N', // reserved for Odoo pager Next + 'P', // reserved for Odoo pager Previous + 'S', // reserved for Odoo Save + 'Q', // reserved for Odoo Search + 'E', // chrome does not support 'E' access key --> go to address bar to search google + 'F', // chrome does not support 'F' access key --> go to menu + 'D', // chrome does not support 'D' access key --> go to address bar + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' // reserved for Odoo menus + ]; + + var KeyboardNavigationMixin = { + events: { + 'keydown': '_onKeyDown', + 'keyup': '_onKeyUp', + }, + + /** + * @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(); + }, + /** + * @override + */ + start: function () { + const temp = this._hideAccessKeyOverlay.bind(this); + this._hideAccessKeyOverlay = () => temp(); + window.addEventListener('blur', this._hideAccessKeyOverlay); + core.bus.on('click', null, this._hideAccessKeyOverlay); + }, + /** + * @destructor + */ + destroy: function () { + window.removeEventListener('blur', this._hideAccessKeyOverlay); + core.bus.off('click', null, this._hideAccessKeyOverlay); + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @private + */ + _addAccessKeyOverlays: function () { + var accesskeyElements = $(document).find('[accesskey]').filter(':visible'); + _.each(accesskeyElements, function (elem) { + var overlay = $(_.str.sprintf("<div class='o_web_accesskey_overlay'>%s</div>", $(elem).attr('accesskey').toUpperCase())); + + var $overlayParent; + if (elem.tagName.toUpperCase() === "INPUT") { + // special case for the search input that has an access key + // defined. We cannot set the overlay on the input itself, + // only on its parent. + $overlayParent = $(elem).parent(); + } else { + $overlayParent = $(elem); + } + + if ($overlayParent.css('position') !== 'absolute') { + $overlayParent.css('position', 'relative'); + } + overlay.appendTo($overlayParent); + }); + }, + /** + * @private + * @return {jQuery[]} + */ + _getAllUsedAccessKeys: function () { + var usedAccessKeys = knownUnusableAccessKeys.slice(); + this.$el.find('[accesskey]').each(function (_, elem) { + usedAccessKeys.push(elem.accessKey.toUpperCase()); + }); + return usedAccessKeys; + }, + /** + * hides the overlay that shows the access keys. + * + * @private + * @param $parent {jQueryElemen} the parent of the DOM element to which shorcuts overlay have been added + * @return {undefined|jQuery} + */ + _hideAccessKeyOverlay: function () { + this._areAccessKeyVisible = false; + var overlays = this.$el.find('.o_web_accesskey_overlay'); + if (overlays.length) { + return overlays.remove(); + } + }, + /** + * @private + */ + _setAccessKeyOnTopNavigation: function () { + this.$el.find('.o_menu_sections>li>a').each(function (number, item) { + item.accessKey = number + 1; + }); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * Assign access keys to all buttons inside $el and sets an overlay to show the access key + * The access keys will be assigned using first the name of the button, letter by letter until we find one available, + * after that we will assign any available letters. + * Not all letters should be used as access keys, some of the should be reserved for standard odoo behavior or browser behavior + * + * @private + * @param keyDownEvent {jQueryKeyboardEvent} the keyboard event triggered + * return {undefined|false} + */ + _onKeyDown: function (keyDownEvent) { + if ($('body.o_ui_blocked').length && + (keyDownEvent.altKey || keyDownEvent.key === 'Alt') && + !keyDownEvent.ctrlKey) { + if (keyDownEvent.preventDefault) keyDownEvent.preventDefault(); else keyDownEvent.returnValue = false; + if (keyDownEvent.stopPropagation) keyDownEvent.stopPropagation(); + if (keyDownEvent.cancelBubble) keyDownEvent.cancelBubble = true; + return false; + } + if (!this._areAccessKeyVisible && + (keyDownEvent.altKey || keyDownEvent.key === 'Alt') && + !keyDownEvent.ctrlKey) { + + this._areAccessKeyVisible = true; + + this._setAccessKeyOnTopNavigation(); + + var usedAccessKey = this._getAllUsedAccessKeys(); + + 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) { + elem.setAttribute('aria-keyshortcuts', 'Alt+Shift+' + elem.accessKey); + }); + this._addAccessKeyOverlays(); + } + // on mac, there are a number of keys that are only accessible though the usage of + // the ALT key (like the @ sign in most keyboards) + // for them we do not facilitate the access keys, so they will need to be activated classically + // though Control + Alt + key (case sensitive), see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey + if (this.BrowserDetection.isOsMac()) + return; + + if (keyDownEvent.altKey && !keyDownEvent.ctrlKey && keyDownEvent.key.length === 1) { // we don't want to catch the Alt key down, only the characters A to Z and number keys + var elementWithAccessKey = []; + if (keyDownEvent.keyCode >= 65 && keyDownEvent.keyCode <= 90 || keyDownEvent.keyCode >= 97 && keyDownEvent.keyCode <= 122) { + // 65 = A, 90 = Z, 97 = a, 122 = z + elementWithAccessKey = document.querySelectorAll('[accesskey="' + String.fromCharCode(keyDownEvent.keyCode).toLowerCase() + + '"], [accesskey="' + String.fromCharCode(keyDownEvent.keyCode).toUpperCase() + '"]'); + if (elementWithAccessKey.length) { + if (this.BrowserDetection.isOsMac() || + !this.BrowserDetection.isBrowserChrome()) { // on windows and linux, chrome does not prevent the default of the accesskeys + elementWithAccessKey[0].focus(); + elementWithAccessKey[0].click(); + if (keyDownEvent.preventDefault) keyDownEvent.preventDefault(); else keyDownEvent.returnValue = false; + if (keyDownEvent.stopPropagation) keyDownEvent.stopPropagation(); + if (keyDownEvent.cancelBubble) keyDownEvent.cancelBubble = true; + return false; + } + } + } + else { + // identify if the user has tapped on the number keys above the text keys. + // this is not trivial because alt is a modifier and will not input the actual number in most keyboard layouts + var numberKey; + if (keyDownEvent.originalEvent.code && keyDownEvent.originalEvent.code.indexOf('Digit') === 0) { + //chrome & FF have the key Digit set correctly for the numbers + numberKey = keyDownEvent.originalEvent.code[keyDownEvent.originalEvent.code.length - 1]; + } else if (keyDownEvent.originalEvent.key && + keyDownEvent.originalEvent.key.length === 1 && + keyDownEvent.originalEvent.key >= '0' && + keyDownEvent.originalEvent.key <= '9') { + //edge does not use 'code' on the original event, but the 'key' is set correctly + numberKey = keyDownEvent.originalEvent.key; + } else if (keyDownEvent.keyCode >= 48 && keyDownEvent.keyCode <= 57) { + //fallback on keyCode if both code and key are either not set or not digits + numberKey = keyDownEvent.keyCode - 48; + } + + if (numberKey >= '0' && numberKey <= '9') { + elementWithAccessKey = document.querySelectorAll('[accesskey="' + numberKey + '"]'); + if (elementWithAccessKey.length) { + elementWithAccessKey[0].click(); + if (keyDownEvent.preventDefault) keyDownEvent.preventDefault(); else keyDownEvent.returnValue = false; + if (keyDownEvent.stopPropagation) keyDownEvent.stopPropagation(); + if (keyDownEvent.cancelBubble) keyDownEvent.cancelBubble = true; + return false; + } + } + } + } + }, + /** + * hides the shortcut overlays when keyup event is triggered on the ALT key + * + * @private + * @param keyUpEvent {jQueryKeyboardEvent} the keyboard event triggered + * @return {undefined|false} + */ + _onKeyUp: function (keyUpEvent) { + if ((keyUpEvent.altKey || keyUpEvent.key === 'Alt') && !keyUpEvent.ctrlKey) { + this._hideAccessKeyOverlay(); + if (keyUpEvent.preventDefault) keyUpEvent.preventDefault(); else keyUpEvent.returnValue = false; + if (keyUpEvent.stopPropagation) keyUpEvent.stopPropagation(); + if (keyUpEvent.cancelBubble) keyUpEvent.cancelBubble = true; + return false; + } + }, + }; + + return KeyboardNavigationMixin; + +}); |
