// ==UserScript== // @name ShortcutBinder // @namespace webmonkey // @description Tool to bind keyboard shortcuts to clickable elements. // @include * // @revision $Revision$ // @id $Id$ // @date $Date$ // @source $URL$ // @author Maries Ionel Cristian // @version 1.2.3 // ==/UserScript== //TODO //- fix shortcut mashing (keeping the shortcut pressed floods the browser // with key/location change events, add some timeouts //- fix previous shortcut eating the keypress event from the add shortcut dialog GM_registerMenuCommand("Set shortcut for bind dialog", SetOptions, "k"); GM_registerMenuCommand("Add manual bind", BindDialog, "b"); GM_registerMenuCommand("Manage bindings", ManageDialog, "m"); var KEYS = { altKey:'Alt', ctrlKey:'Ctrl', metaKey:'Meta', shiftKey:'Shift', charCode:'' }; var DEBUG = deserialize("debug_log", "(false)");; function Bindings() { this.load(); this.make_cache(); } Bindings.prototype.load = function() { this.bindingsCount = deserialize("bindingsCount", "(1)"); this.bindings = deserialize("bindings", "({})"); } Bindings.prototype.make_cache = function () { this.cache = {}; for (var id in this.bindings) { var bindObj = this.bindings[id]; var bindHash = bindObj.bind; var includeex = convert2RegExp(bindObj.include); var excludeex = convert2RegExp(bindObj.exclude); if (includeex.test(window.location.href) && !excludeex.test(window.location.href)) { var charCodeObj = this.cache[bindHash.charCode] || {}; var shiftKeyObj = charCodeObj[bindHash.shiftKey] || {}; var altKeyObj = shiftKeyObj[bindHash.altKey] || {}; var ctrlKeyObj = altKeyObj[bindHash.ctrlKey] || {}; var xpathsObj = ctrlKeyObj[bindHash.metaKey] || []; this.cache[bindHash.charCode] = charCodeObj; charCodeObj[bindHash.shiftKey] = shiftKeyObj; shiftKeyObj[bindHash.altKey] = altKeyObj; altKeyObj[bindHash.ctrlKey] = ctrlKeyObj; ctrlKeyObj[bindHash.metaKey] = xpathsObj; xpathsObj.push(bindObj.xpath); } } } Bindings.prototype.match = function(shortcutHash) { var alt = shortcutHash.altKey; var chr = shortcutHash.charCode; var ctrl = shortcutHash.ctrlKey; var meta = shortcutHash.metaKey; var shift = shortcutHash.shiftKey; var a,b,c,d,e; if ((a=this.cache[chr]) && (b=a[shift]) && (c=b[alt]) && (d=c[ctrl]) && (e=d[meta])) return e; } Bindings.prototype.save = function () { serialize("bindings", this.bindings); serialize("bindingsCount", this.bindingsCount); } Bindings.prototype.add = function (new_binding) { this.load(); var confirmed = false; for (var id in this.bindings) { var bindObj = this.bindings[id]; // check for bindings with the same key/include/exclude if ( keyHashEq(bindObj.bind, new_binding.bind) && bindObj.include == new_binding.include && bindObj.exclude == new_binding.exclude) { if (confirm('There is an existing binding with the same shortcut and include/exclude patterns with xpath: "'+bindObj.xpath+'". Replace (OK) or add anyway (Cancel) ?')) { delete this.bindings[id]; } } } this.bindings[++this.bindingsCount] = new_binding; this.save(); this.make_cache(); } Bindings.prototype.set = function (id, binding) { this.load(); this.bindings[parseInt(id)] = binding; this.save(); this.make_cache(); } Bindings.prototype.get = function (id) { return this.bindings[parseInt(id)]; } Bindings.prototype.remove = function (id) { this.load(); delete this.bindings[parseInt(id)]; this.save(); this.make_cache(); } Bindings.prototype.log = function () { GM_log("ID Count:"+this.bindingsCount+"("+typeof this.bindingsCount+")"); for (var id in this.bindings) { GM_log("Binding_"+id+": "+this.bindings[id]); } for (var chr in this.cache) { GM_log("Cache: char:"+chr); if (this.cache[chr]) for (var shift in this.cache[chr]) { GM_log("Cache: shift:"+shift); if (this.cache[chr][shift]) for (var alt in this.cache[chr][shift]) { GM_log("Cache: alt:"+alt); if (this.cache[chr][shift][alt]) for (var ctrl in this.cache[chr][shift][alt]) { GM_log("Cache: ctrl:"+ctrl); if (this.cache[chr][shift][alt][ctrl]) for (var meta in this.cache[chr][shift][alt][ctrl]) { GM_log("Cache: meta:"+meta); if (this.cache[chr][shift][alt][ctrl][meta]) GM_log("Cache: XPATH:"+this.cache[chr][shift][alt][ctrl][meta]); } } } } } } var binding_store = new Bindings(); if (DEBUG) binding_store.log(); var binddialog_opened = false; var managedialog_opened = false; HandlePageCombo(); // add listeners on the window and check // for keypresses matching the bindings function HandlePageCombo() { var combo = {}, bind_shortcut = deserialize("bindDialogShortcut"), manage_shortcut = deserialize("manageDialogShortcut"); function listener(event) { for (var key in KEYS) { combo[key] = event[key]; } if (!(combo.altKey || combo.ctrlKey || combo.metaKey || combo.shiftKey) && event && event.target && event.target.nodeName && ( (event.target.nodeName == 'INPUT' && (event.target.type == 'password' || event.target.type == 'text') ) || event.target.nodeName == 'TEXTAREA' )) { if (DEBUG) GM_log('input or textarea has focus. will not trigger bindings.'); return; }; if (DEBUG) GM_log(shortcutToString(combo) + " was pressed."); if (keyHashEq(combo, bind_shortcut)) { event.preventDefault(); event.stopPropagation(); if (!binddialog_opened) BindDialog(); return; } if (keyHashEq(combo, manage_shortcut)) { event.preventDefault(); event.stopPropagation(); if (!managedialog_opened) ManageDialog(); return; } var xpaths = binding_store.match(combo); if (xpaths && xpaths.length) { for (var i=0; i>'); try { match = $x(xpath) } catch(exc) { if (DEBUG) GM_log("Match expression << "+xpath+" >> failed with: "+exc); return; } if (DEBUG) GM_log("Matched "+match.length+" elements."); if (match.length > 1) if (DEBUG) GM_log("We've matched "+match.length+" elements. We'll use the first one."); if (match.length >= 1) { var m = match[0]; if (m.focus) { if (DEBUG) GM_log("Focusing."); m.focus(); } else { triggerEvent(m, 'focus'); } if (m.click) { if (DEBUG) GM_log("Clicking."); m.click(); } else { if (DEBUG) GM_log("Match didn't had a click method ! Creating event..."); //try the click event var savedEvent = null; m.addEventListener('click', function(evt) { savedEvent = evt; }, false); var evt = document.createEvent('MouseEvents'); evt.initMouseEvent( 'click', true, true, document.defaultView, 1, getElementPosition(m), getElementPosition(m, true), getElementPosition(m), getElementPosition(m, true), false, false, false, false, 0, m ); evt.initEvent('click', false, true); m.dispatchEvent(evt); if (savedEvent != null && !savedEvent.getPreventDefault()) { while (!m.href && m.parentNode) { m = m.parentNode; } if (m.href) { window.location.href = m.href; } else { if (DEBUG) GM_log("Matched element didn't have a href !"); } } else { if (DEBUG) GM_log("Matched element canceled the click event."); } } event.preventDefault(); event.stopPropagation(); } else { if (DEBUG) GM_log("Match expression << "+xpath+" >> matched: "+match.length+" elements (should match only 1)."); } } } } document.addEventListener('keypress', listener, true); } function ManageDialog() { managedialog_opened = true; var form, header, table, close_button, dialog_selected, offsetx, offsety; function cleanup() { window.removeEventListener('keypress', remove, true); document.removeEventListener("mousemove", handle_move, false); document.body.removeChild(form); serialize("manageDialog-posX", form.style.left); serialize("manageDialog-posY", form.style.top); managedialog_opened = false; } document.body.appendChild( form=EL('div', { id:"ShortcutBinderManageDialog", style:'top:'+deserialize("manageDialog-posY", "'15px'")+ ';left:'+deserialize("manageDialog-posX", "'15px'")}, header=EL('h2', {}, 'Manage bindings'), table=EL('table', {}, EL('tr', {}, EL('th', {}, ''), EL('th', {}, 'xpath'), EL('th', {}, 'binding'), EL('th', {}, 'include'), EL('th', {}, 'exclude') ) ), close_button=EL('input', {type: 'button', value:'Close', 'onclick':cleanup}) ) ) for (var id in binding_store.bindings) { (function (id) { var binding = binding_store.bindings[id]; var bind_cell; var row; table.appendChild(row=EL('tr',{}, EL('td', {width:"60px"}, EL('input', {type:'button', value:'Delete', onclick:function(){ binding_store.remove(id); row.parentNode.removeChild(row); }}), EL('input', {type:'button', value:'Edit', onclick:function(){ BindDialog(id); }}) ), EL('td', {}, binding.xpath), bind_cell=EL('td', {}, shortcutToString(binding.bind)), EL('td', {}, binding.include), EL('td', {}, binding.exclude) )); })(id); } function handle_move(event) { if (dialog_selected) { form.style.left = (event.clientX-offsetx)+'px'; form.style.top = (event.clientY-offsety)+'px'; } } function remove(event) { if (!binddialog_opened && event.charCode == 0 && event.keyCode == 27) { event.preventDefault(); event.stopPropagation(); cleanup(); } } form.addEventListener('keypress', remove, true); form.focus(); window.addEventListener('keypress', remove, true); header.addEventListener('mousedown', function(event) { dialog_selected = true; offsetx = event.clientX-getElementPosition(event.target, true); offsety = event.clientY-getElementPosition(event.target, false); event.preventDefault(); event.stopPropagation(); }, true); header.addEventListener('mouseup', function(event) { dialog_selected = false; event.preventDefault(); event.stopPropagation(); }, true); document.addEventListener("mousemove", handle_move, false); } //~ ManageDialog(); function BindDialog(id) { binddialog_opened = true; var form, path_input, binding_input, include_input, exclude_input, close_button, dialog_selected=false, outlined_element, header, offsetx, offsety, suggestions, matched_element, computeds = [], binding = {}; function cleanup() { document.body.removeChild(form); window.removeEventListener('keypress', remove, true); document.removeEventListener("click", element_click, true); document.removeEventListener("mousemove", element_mouseMove, false); document.removeEventListener("mouseover", element_mouseOver, false); if (outlined_element) outlined_element.style.MozOutline = ''; for (var i=0; i 1) { suggestions.textContent = "We've matched "+computed.length+" nodes ! Only the first element will be used."; for (var i=0; i '"+ret+"'"); } catch (exc) { if (DEBUG) GM_log("Deserializing error for '"+name+"': '"+exc+"'"); return; } return ret; } function serialize(name, val) { var saved = uneval(val); if (DEBUG) GM_log("Serializing '"+name+"'='"+val+"' => '"+saved+"'."); GM_setValue(name, saved); } function $(id) { return document.getElementById(id); } function $x(p, context) { if (!context) context = document; var i, arr = [], xpr = document.evaluate(p, context, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); for (i = 0; item = xpr.snapshotItem(i); i++) arr.push(item); return arr; } function TEXT(str){ return document.createTextNode(str); } function EL(type, attributes){ var node = document.createElement(type); for (var i=2; i