@ -1,26 +1,28 @@
odoo . define ( 'web_keyboard_navigation.KeyboardNavigationMixin' , function ( require ) {
odoo . define ( 'web_keyboard_navigation.KeyboardNavigationMixin' , function (
require ) {
"use strict" ;
var BrowserDetection = require ( 'web.BrowserDetection' ) ;
/ * *
* 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
* 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
'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
'A' ,
'C' ,
'H' ,
'J' ,
'K' ,
'L' ,
'N' ,
'P' ,
'S' ,
'Q' ,
'E' ,
'F' ,
'D' ,
'0' , '1' , '2' , '3' , '4' , '5' , '6' , '7' , '8' , '9'
] ;
var KeyboardNavigationMixin = {
@ -35,38 +37,26 @@ odoo.define('web_keyboard_navigation.KeyboardNavigationMixin', function (require
this . BrowserDetection = new BrowserDetection ( ) ;
} ,
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/ * *
* @ private
* /
_addAccessKeyOverlays : function ( ) {
var accesskeyElements = $ ( document ) . find ( '[accesskey]' ) . filter ( ':visible' ) ;
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 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 ) {
@ -74,13 +64,7 @@ odoo.define('web_keyboard_navigation.KeyboardNavigationMixin', function (require
} ) ;
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' ) ;
@ -88,140 +72,159 @@ odoo.define('web_keyboard_navigation.KeyboardNavigationMixin', function (require
return overlays . remove ( ) ;
}
} ,
/ * *
* @ private
* /
_setAccessKeyOnTopNavigation : function ( ) {
this . $el . find ( '.o_menu_sections>li>a' ) . each ( function ( number , item ) {
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 ;
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 ( ) ;
var buttonsWithoutAccessKey = this . $el . find ( 'button.btn:visible' )
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 ) ) {
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]' ) ;
var elementsWithoutAriaKeyshortcut = this . $el . find (
'[accesskey]' ) . not ( '[aria-keyshortcuts]' ) ;
_ . each ( elementsWithoutAriaKeyshortcut , function ( elem ) {
elem . setAttribute ( 'aria-keyshortcuts' , 'Alt+Shift+' + elem . accessKey ) ;
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
if ( keyDownEvent . altKey &&
! keyDownEvent . ctrlKey &&
keyDownEvent . key . length === 1 ) {
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 ( keyDownEvent . keyCode >= 65 &&
keyDownEvent . keyCode <= 90 ||
keyDownEvent . keyCode >= 97 &&
keyDownEvent . keyCode <= 122 ) {
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
! this . BrowserDetection . isBrowserChrome ( ) ) {
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 ;
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 ] ;
if ( keyDownEvent . originalEvent . code &&
keyDownEvent . originalEvent . code . indexOf (
'Digit' ) === 0 ) {
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
} else if (
keyDownEvent . keyCode >= 48 &&
keyDownEvent . keyCode <= 57 ) {
numberKey = keyDownEvent . keyCode - 48 ;
}
if ( numberKey >= '0' && numberKey <= '9' ) {
elementWithAccessKey = document . querySelectorAll ( '[accesskey="' + numberKey + '"]' ) ;
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 ;
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 ) {
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 ;
if ( keyUpEvent . preventDefault ) {
keyUpEvent . preventDefault ( ) ;
}
else {
keyUpEvent . returnValue = false ;
}
if ( keyUpEvent . stopPropagation ) {
keyUpEvent . stopPropagation ( ) ;
}
if ( keyUpEvent . cancelBubble ) {
keyUpEvent . cancelBubble = true ;
}
return false ;
}
} ,