📜 ⬆️ ⬇️

Google Chrome Extensions: Making Hot Keys

Hello, dear habravchane.
I continue a series of posts with personal experience and tips on developing Google Chrome browser extensions.
In this release - “global” hotkeys at the browser level.

So, the task is to implement in the expansion support for global keyboard shortcuts to perform some expansion functions with the ability to customize each user.
To begin with, the browser does not have native support for global hotkeys.
Refinement
So it was at the time of writing my extensions, so I thought until the writing of the article. Now the API has expanded, added support for commands. But they are rigidly prescribed in the manifest and do not provide sufficient flexibility. Also, in the settings of extensions in the browser, hotkeys are configured to activate the extension windows.
This is beyond the scope of the post, so I send it to Google and the documentation .

But the task can be solved by adding keystroke event handling to all browser windows. This option is not fully functional, as it depends on the presence of focus in the body of the document, but in most cases it works.
Important. It should be borne in mind that handlers embedded in this way override most of the standard browser hotkeys. For example, if the document contains the processing of a click of Ctrl + digit with preventDefault, then a similar browser hot key for switching between tabs will not work if there is focus in the document with such a handler. The same should be warned the user if he is given the opportunity to customize the combinations.

For implementation, you need to create a content-script that will be embedded in the pages, register it in the manifest, register processing in the background-page of the extension, and implement the ability to customize keyboard shortcuts. Unfortunately, it is not possible to arrange this in a more convenient and portable form.

It all looks something like this.
')
mainfest.json
{ ... "content_scripts": [ { "all_frames": true, //,       ,     iframe "js": ["js/hotkeys.js" ], //,     "matches": [ "http://*/*", "https://*/*" ], //    "run_at": "document_end" } ], ... 

It's all clear. It is prescribed to add a handler to all opened pages.

The general structure of hotkeys.js
 if(!EXT_HOTKEY_JS_INSERTED){ var EXT_HOTKEY_JS_INSERTED = true; var hotkeys = ''; var messages = ''; var timeout = ''; chrome.extension.sendRequest({operation: "hotkeys"}, function(response) { //       if (response) if (response.hotkeys){ hotkeys = JSON.parse(response.hotkeys); var d = document.createElement('DIV'); //       d.id = "hotkeyresponder"; //+: , ,  z-index document.body.appendChild(d); } }); document.addEventListener('keydown', event_handleKeyDownEvent, true); //,        function event_handleKeyDownEvent(e){ if (!hotkeys) return true; var c=0; var a=0; var s=0; var k = e.keyCode; if (e.shiftKey) s = 1; if (e.altKey) a = 1; if (e.ctrlKey) c = 1; if (k==27 && !c && !a && !s){ if (document.getElementById('hotkeyresponder').style.display!='none'){ //    Esc document.getElementById('hotkeyresponder').style.display="none"; e.preventDefault(); e.cancelBubble = true; e.bubbles = false; return false; } } for (var name in hotkeys){ if (hotkeys.hasOwnProperty(name)){ if (hotkeys[name].c == c && hotkeys[name].a == a && hotkeys[name].s == s){ // ,   ,  if (name == 'selectProfile' && k > 48 && k < 58 && (c || a || s)){ //   <+> chrome.extension.sendRequest({operation: "hotkey", key:name, id:(k-48)}, function(response) { responseHotkey(name,response.status,response.message); }); e.preventDefault(); e.cancelBubble = true; e.bubbles = false; return false; } else if (hotkeys[name].k==k){ //       .     ,     if (name=='toggleBodyi'){ document.getElementById('togglebody').onclick(); } //  ,       (toggle - ,  DOM, etc.) else{ // ,      chrome.extension.sendRequest({operation: "hotkey", key:name}, function(response) { responseHotkey('pp',response.status,response.message); }); } e.preventDefault(); e.cancelBubble = true; e.bubbles = false; return false; } else{ continue; } } } } } function responseHotkey(type,status,message){ //     if (status == 'OK' && message){ document.getElementById('hotkeyresponder').innerHTML = message; document.getElementById('hotkeyresponder').style.display = 'block'; if (timeout) clearTimeout(timeout); timeout = setTimeout(function(){document.getElementById('hotkeyresponder').style.display='none';},2000); } } } 

The above script is a synthesis of slightly different hotkey scripts from two extensions. Clearly, when reused, you need to modify to fit your needs. For example, if all keys are specified uniquely (without universal modifiers), you only need to make one if block, where you can compare all 4 parameters of a hot key. If your extension does not perform any actions in the DOM - it is clear that conditional blocks, depending on the name of the hot key, must be abolished before a simple request to the extension.

Fragment background.js
 chrome.extension.onRequest.addListener(function(request, sender, sendResponse) { loadSettings(); //  if (request.operation == 'hotkeys'){ //     sendResponse({hotkeys:localStorage.hotkeys}); } else if (request.operation == 'hotkey'){ //   if (request.key == 'selectProfile'){ //  } //    } else{ //sendResponse({}); } }); 

It is said that sendRequest and onRequest deprecated in favor of sendMessage with similar syntax and functionality. At the time of writing this post, I didn’t test this completely, but my extensions still work without any visible problems.

In this form (if you manually drive the settings into the extension), the keys are already operational.



Left settings. Here, in general, there are many options, everything is up to you. If there are a lot of keys, it makes sense to automate this business as much as possible.
In case someone is going to repeat in his creations, I’ll give you code snippets.
The settings page itself is made by adding a parameter in the manifest:
 { ... "options_page": "options.html", ... } 

There is nothing special in HTML, just placeholder. Everything is formed in JavaScript.
options.html
 <html> <head> <title>Hotkeys</title> <script type="text/javascript" src="js/tools.js"></script> <script type="text/javascript" src="js/options.js"></script> </head> <body> <!--  header'  . --> <div id="tab1"> <p style="margin-top:0;"> -   </p> </div> </body> </html> 

options.js
The required data: the names of the keys, the current settings of the hot keys, and just an array of internal names of supported combinations.
 ... var keycodes = ['','','','','','','','','','Tab','','','','Enter','','','','','','','','','','','','','','Escape','','','','','Space','Page Up','Page Down','End','Home','Left','Up','Right','Down','','','','','Insert','Delete','','0','1','2','3','4','5','6','7','8','9','','','','','','','','A','B','C','D','E','F','G','H','I','J','K','L','M','N','O','P','Q','R','S','T','U','V','W','X','Y','Z','MainM Left','MainM Right','Menu','','','NumPad 0','NumPad 1','NumPad 2','NumPad 3','NumPad 4','NumPad 5','NumPad 6','NumPad 7','NumPad 8','NumPad 9','NumPad *','NumPad +','','NumPad -','NumPad ,','NumPad /','F1','F2','F3','F4','F5','F6','F7','F8','F9','F10','F11','F12','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','','=','','-','','','~','','','','','','','','','','','','','','','','','','','','','','','','','','','','\\']; var hotkeys = JSON.parse(localStorage.hotkeys); var phk = ['prevProfile', 'nextProfile', 'selectProfile', 'openOpts', 'toggleHideWidget', 'toggleWidget', 'toggleInCT', 'toggleBody', 'openWidget', 'toggleBodyi', 'actMC', 'actMG']; 

Page initialization function: filling in HTML, hanging handlers, displaying current settings.
 initt = function(){ ... for (var i = 0; i < phk.length;i++){ var d = document.createElement('DIV'); d.innerHTML = '<h4 style="margin:0;">'+_getMessage(phk[i]+'_title')+'</h4><input type="checkbox" id="'+phk[i]+'_c"><label for="'+phk[i]+'_c">Ctrl <b>+</b></label><input type="checkbox" id="'+phk[i]+'_a"><label for="'+phk[i]+'_a">Alt <b>+</b></label><input type="checkbox" id="'+phk[i]+'_s"><label for="'+phk[i]+'_s">Shift <b>+</b></label>'+((phk[i]=='selectProfile')?('<key 1-9><input type="hidden" id="'+phk[i]+'_k" value="$"> '):('<input type="text" id="'+phk[i]+'_k">'))+'<img src="img/delete.gif" id="'+phk[i]+'_imgerr" class="err_disp" style="height:16px;display:none;" /><img src="img/ok.png" id="'+phk[i]+'_imgok" class="err_disp" style="height:16px;display:none;" /><br/><span id="result_'+phk[i]+'" class="err_disp" style="color:red"></span>'; document.getElementById('tab1').appendChild(d); } document.getElementById('tab1').innerHTML+='<button>'+_getMessage('opts_13')+'</button>'; for (var i = 0; i < phk.length;i++){ if (hotkeys[phk[i]]){ document.getElementById(phk[i]+'_c').checked = (hotkeys[phk[i]]['c']?true:false); document.getElementById(phk[i]+'_a').checked = (hotkeys[phk[i]]['a']?true:false); document.getElementById(phk[i]+'_s').checked = (hotkeys[phk[i]]['s']?true:false); if (phk[i]!='selectProfile') document.getElementById(phk[i]+'_k').value = keycodes[hotkeys[phk[i]]['k']]; } } ... var m = document.querySelectorAll('input[type=text]') for (var i = 0; i < m.length; i++){ m[i].addEventListener('keydown',function(event){this.value=(keycodes[event.keyCode]?keycodes[event.keyCode]:'');prevDef(event);return false},false); } ... } 

Save function It also implemented a check for validity and conflicts. And the correct combinations are immediately saved, and the wrong ones are marked accordingly.
 save = function(){ //     var err_els = document.getElementsByClassName('err_disp'); for (var i = 0; i < err_els.length; i++){ if (i%3==2) err_els[i].innerHTML = ''; else err_els.style.display = 'none'; } var hktp = {}; var hkts = {}; //      for (var i = 0; i < phk.length; i++){ if ((document.getElementById(phk[i]+'_c').checked || document.getElementById(phk[i]+'_a').checked || document.getElementById(phk[i]+'_s').checked) && document.getElementById(phk[i]+'_k').value){ //  hktp[phk[i]] = { c:document.getElementById(phk[i]+'_c').checked, a:document.getElementById(phk[i]+'_a').checked, s:document.getElementById(phk[i]+'_s').checked, k:document.getElementById(phk[i]+'_k').value, }; } else{ //   -  if (document.getElementById(phk[i]+'_c').checked || document.getElementById(phk[i]+'_a').checked || document.getElementById(phk[i]+'_s').checked || (document.getElementById(phk[i]+'_k').value && document.getElementById(phk[i]+'_k').value!='$')){ document.getElementById('result_'+phk[i]).innerHTML = _getMessage('opts_12'); document.getElementById(phk[i]+'_imgerr').style.display = 'inline'; } } } //  for (var i = 0; i < phk.length-1; i++){ for (var j = i+1; j < phk.length; j++){ if (hktp[phk[i]] && hktp[phk[j]]) if (hktp[phk[i]].c == hktp[phk[j]].c && hktp[phk[i]].a == hktp[phk[j]].a && hktp[phk[i]].s == hktp[phk[j]].s && (hktp[phk[i]].k == hktp[phk[j]].k || ((hktp[phk[i]].k == '$' || hktp[phk[j]].k == '$') && ((hktp[phk[i]].k*1>0 && hktp[phk[i]].k*1<=9) || (hktp[phk[j]].k*1>0 && hktp[phk[j]].k*1<=9))))){ document.getElementById('result_'+phk[i]).innerHTML = _getMessage('opts_11'); document.getElementById(phk[i]+'_imgerr').style.display = 'inline'; document.getElementById('result_'+phk[j]).innerHTML = _getMessage('opts_11'); document.getElementById(phk[j]+'_imgerr').style.display = 'inline'; hktp[phk[i]] = false; hktp[phk[j]] = false; } } } // ""    for (var name in hktp){ if (hktp.hasOwnProperty(name) && hktp[name]){ document.getElementById(phk[i]+'_imgok').style.display = 'inline'; hkts[name] = { c:(document.getElementById(name+'_c').checked?1:0), a:(document.getElementById(name+'_a').checked?1:0), s:(document.getElementById(name+'_s').checked?1:0), k:keycodes.in_array(document.getElementById(name+'_k').value), } } } localStorage.hotkeys = JSON.stringify(hkts); // } 

If you are confused by functions like _getMessage, these are wrappers for functions i18n (see my previous post )
Here is how it looks in work.


That's all. From these sources it is quite possible to make any functional decisions. In such a way, the hot key support systems work with honor in my extensions, users do not complain. The only thing that the option I proposed is not able to do is to invoke a popup window. I did not find the appropriate API methods, but my extensions are not one of those where a combination of keys to display a pop-up window is absolutely necessary, and the proposed arsenal fully covers the functionality of the extension.

Source: https://habr.com/ru/post/171565/


All Articles