Shortcut Binder

By ionel.mc Last update Feb 9, 2009 — Installed 328 times. Daily Installs: 0, 0, 0, 0, 0, 2, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 3, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 0, 0, 0

There are 13 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name            ShortcutBinder
// @namespace       webmonkey
// @description     Tool to bind keyboard shortcuts to clickable elements.
// @include         *
// @revision        $Revision: 45 $
// @id              $Id: shortcutbinder.user.js 45 2009-02-09 09:56:50Z ionel.mc $
// @date            $Date: 2009-02-09 11:56:50 +0200 (Mon, 09 Feb 2009) $
// @source          $URL: https://webmonkey-userscripts.googlecode.com/svn/trunk/shortcut_binder/shortcutbinder.user.js $
// @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<xpaths.length; i++) {
                var xpath = xpaths[i];
                var match;
                if (DEBUG) GM_log('Trying << '+ xpath + ' >>');
                    
                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<computeds.length; i++) 
            computeds[i].style.MozOutline = '';
        serialize("bindDialog-posX", form.style.left);
        serialize("bindDialog-posY", form.style.top);
        binddialog_opened = false;
    }
    function save(event) {
        if (id) {
            binding_store.set(id, {
                xpath:path_input.value, 
                bind:binding, 
                include:include_input.value, 
                exclude:exclude_input.value 
            });
        } else {    
            binding_store.add({
                xpath:path_input.value, 
                bind:binding, 
                include:include_input.value, 
                exclude:exclude_input.value 
            });
        }
        cleanup();
    }
    function remove(event) {
        if (event.charCode == 0 && event.keyCode == 27) {
            event.preventDefault();
            event.stopPropagation();
            cleanup();
        }
    }
    
    function element_mouseOver(event) {
        if (DEBUG) GM_log('mouseOver'+ event.target.innerHTML);
    }
    function element_mouseMove(event) {
        if (dialog_selected) {
            form.style.left = (event.clientX-offsetx)+'px';
            form.style.top = (event.clientY-offsety)+'px';
        } else {            
            var element = event.target;
            if (element) {
                var owner = element.ownerDocument;
                if (owner) {
                    if (element!=form && !isAncestor(element, form)) {
                        if (outlined_element) 
                            if (!computeds.some(function(el) {return el==outlined_element})) 
                                outlined_element.style.MozOutline = '';
                        if (!computeds.some(function(el) {return el==element})) {
                            outlined_element = element;
                            outlined_element.style.MozOutline = '1px solid blue';
                        }
                    }
                }
            }
        }
        event.preventDefault();
        event.stopPropagation();
        if (DEBUG) GM_log('mouseMove', event.target.innerHTML);
    }   
    function element_click(event) {
        var element = event.target;
        if (element && element!=form && !isAncestor(element, form)) {
            var val = computeXPath(element);
            path_input.value = val;
            path_changed(null, val);
            binding_input.focus();
            event.preventDefault();
            event.stopPropagation();
        }
        if (DEBUG) GM_log('elementClick', event.target.innerHTML);
    }
    function path_changed(event, value) {
        for (var i=0; i<computeds.length; i++) {
            if (computeds[i].style)
                computeds[i].style.MozOutline = '';
        }
        var computed;
        try { 
            if (DEBUG) GM_log(value||path_input.value);
            computed = $x(value||path_input.value).filter(function(el) {
                return el!=form && !isAncestor(el, form);
            });
        } catch (exc) {
            suggestions.textContent = "ERROR:"+exc;
            path_input.style.background = 'red';
            computeds = [];
            return;
        }
        computeds = computed;
        if (computed.length == 1) {
            suggestions.textContent = "Excelent, we've matched 1 node !";
            if (computed[0]) 
                computed[0].style.MozOutline = '2px dashed green';
            path_input.style.background = 'lightgreen';
        }
        if (computed.length > 1) {
            suggestions.textContent = "We've matched "+computed.length+" nodes ! Only the first element will be used.";
            for (var i=0; i<computed.length; i++) {
                if (computeds[i].style) 
                    computed[i].style.MozOutline = '2px dashed red';
            }
            path_input.style.background = 'yellow';
        }
        if (computed.length == 0) {
            suggestions.textContent = "Sorry, we've matched 0 nodes ! You'll have to edit or rewrite the xpath expression yourself :(";
            path_input.style.background = 'red';
        }
    }
    var val_include, val_exclude, val_bind, val_xpath;
    if (id) {
        var obj = binding_store.get(id);
        val_include = obj.include;
        val_exclude = obj.exclude;
        val_bind = shortcutToString(obj.bind);
        binding = obj.bind;
        val_xpath = obj.xpath;
    } else {
        val_include = /(.*?:[\/]{0,2}.*?\/).*/.exec(window.location)[1]+"*";
        val_exclude = '';
        val_bind = '';
        val_xpath = '';
    }        
    document.body.appendChild(
     form=EL('div', { id:"ShortcutBinderAddDialog", 
                      style:'top:'+deserialize("bindDialog-posY", "'50px'")+
                           ';left:'+deserialize("bindDialog-posX", "'50px'")},
      header=EL('h2', {}, 'Add new binding'),
      EL('label', {}, "XPath to element:", path_input=EL('textarea', {wrap:'hard'}, val_xpath)),  
      suggestions=EL('div', {'class':'suggestions'}, ''),
      EL('label', {}, "Shortcut:", binding_input=EL('input', {type: 'input',value:val_bind})),  
      EL('label', {}, "Run on pages matching:", include_input=EL('input', {type: 'input', value:val_include})),  
      EL('label', {}, "Exclude pages matching:", exclude_input=EL('input', {type: 'input', value:val_exclude})),  
      close_button=EL('input', {type: 'button', value:'Save', 'onclick':save})
     )
    )
    
    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);
    path_input.addEventListener('change', path_changed, true);
    path_input.addEventListener('keyup',  path_changed, true);
    
    document.addEventListener("click", element_click, true);
    document.addEventListener("mousemove", element_mouseMove, false);
    document.addEventListener("mouseover", element_mouseOver, false);
    function update() {
        binding_input.value = shortcutToString(binding);
        close_button.focus();
    }
    HandleKeypress(binding, binding_input, update);    
}
function computePath(element) {
    var path = []
    while (element.parentNode) {
        path.push({
            name:element.tagName,
            'class':element.className,
            id:element.id,
            element:element
        })
        element = element.parentNode;
    }
    return path;
}
function computeXPath(element) {
    var path = computePath(element);
    var xpath = [];
    for (var i=0; i<path.length; i++) {
        var tok = path[i];
        var expr = [];
        var p = tok.element.firstChild;
                    /*Node.ELEMENT_NODE*/
        while (p && (p.nodeType != 1 || !p.textContent)) {
            p = p.nextSibling;
        }
        if (tok.element.textContent && !p && i==0) {
            expr.push("(text()='"+element.textContent+"')");
        } 
        if (tok.element.nodeName=="INPUT" && tok.element.value) {
            expr.push("(@value='"+tok.element.value+"')");
        }
        if (tok.name) {
            if (tok['class'])
                expr.push("(@class='" + tok['class'] + "')");
            if (tok.id)
                expr.push("(@id='" + tok.id + "')");
            xpath.push(expr.length?(tok.name.toLowerCase()+'['+expr.join(' and ')+']'):'');
        }
    }
    xpath.push([]);
    xpath = xpath.reduceRight(function(prev, curr, index, array){
        if (!prev.length || prev[prev.length-1] || curr)
            prev.push(curr);
        return prev;
    })
    if (!xpath[0])
        xpath.unshift('');
    if (!xpath[xpath.length-1])
        xpath.pop();
    if (DEBUG) GM_log(uneval(xpath));
    return xpath.join('/');
}

function HandleKeypress(shortcut, shortcut_input, callback) {
    function listener(event) {
        if (!event.charCode) return;
        if (DEBUG) GM_log(shortcut+':'+typeof shortcut);
        var out = []
        for (var key in KEYS) {
            shortcut[key] = event[key];
        }
        
        if (callback) callback();
        event.preventDefault();
        event.stopPropagation();
    }
    shortcut_input.addEventListener('keypress', listener, true);
    return listener;
}
function SetOptions() {
    var close_button, form, 
        bind_shortcut_input, bind_shortcut=deserialize("bindDialogShortcut"),
        manage_shortcut_input, manage_shortcut=deserialize("manageDialogShortcut");
    function save(event) {
        serialize("bindDialogShortcut", bind_shortcut);
        serialize("manageDialogShortcut", manage_shortcut);
        document.body.removeChild(form);
        window.removeEventListener('keypress', remove, true);
    }
    function update_bind() {
        bind_shortcut_input.value = shortcutToString(bind_shortcut);
        manage_shortcut_input.focus();
    }
    function update_manage() {
        manage_shortcut_input.value = shortcutToString(manage_shortcut);
        close_button.focus();
    }
    function remove(event) {
        if (event.charCode == 0 && event.keyCode == 27) {
            event.preventDefault();
            event.stopPropagation();
            document.body.removeChild(form);
            window.removeEventListener('keypress', remove, true);
        }
    }
        
    document.body.appendChild(
     form=EL('div', {id: 'ShortcutBinderGlobalDialog'}, 
      EL('h2', {}, "Shortcuts for add/manage dialogs"), EL('br'),
      EL('label', {}, "Add shortcut:", bind_shortcut_input=EL('input', {
          type: 'input', value:shortcutToString(bind_shortcut)
      })), EL('br'),
      EL('label', {}, "Manage shortcut:", manage_shortcut_input=EL('input', {
          type: 'input', value:shortcutToString(manage_shortcut)
      })), EL('br'),
      close_button=EL('input', {type: 'button', value:'Save', 'onclick':save})
     )
    );
    HandleKeypress(bind_shortcut, bind_shortcut_input, update_bind);
    HandleKeypress(manage_shortcut, manage_shortcut_input, update_manage);
    form.addEventListener('keypress', remove, true);
    window.addEventListener('keypress', remove, true);
    bind_shortcut_input.focus();
}
    
    
    
    
    
////////////////////////////////////////////////////////////////////////////////
// utility functions

function triggerEvent(element, eventType, canBubble, controlKeyDown, altKeyDown, shiftKeyDown, metaKeyDown) {
    canBubble = (typeof(canBubble) == undefined) ? true : canBubble;
    var evt = document.createEvent('HTMLEvents');
    
    try {
        evt.shiftKey = shiftKeyDown;
        evt.metaKey = metaKeyDown;
        evt.altKey = altKeyDown;
        evt.ctrlKey = controlKeyDown;
    } catch (e) {
        // On Firefox 1.0, you can only set these during initMouseEvent or initKeyEvent
        // we'll have to ignore them here
        GM_log(e);
    }
    
    evt.initEvent(eventType, canBubble, true);
    element.dispatchEvent(evt);
}

function getElementPosition(element, xPosition) { 
    var position = 0;

    // If the element is set
    if(element)
    {
        var elementOffsetParent = element.offsetParent;

        // If the element has an offset parent
        if(elementOffsetParent)
        {
            // While there is an offset parent
            while((elementOffsetParent = element.offsetParent) != null)
            {
                // If getting the x position
                if(xPosition)
                {
                    position += element.offsetLeft;
                }
                else
                {
                    position += element.offsetTop;
                }

                element = elementOffsetParent;
            }
        }
        else
        {
            // If getting the x position
            if(xPosition)
            {
                position = element.offsetLeft;
            }
            else
            {
                position = element.offsetTop;
            }
        }
    }

    return position;
}


function shortcutToString(shortcut) {
    var out = [];
    for (var key in KEYS) if (shortcut[key])
        out.push(KEYS[key] || String.fromCharCode(shortcut[key]).toUpperCase());
    return out.join(" + ");
}


function keyHashEq(hash1, hash2) {
    for (var key in KEYS) {
        if (hash1[key] != hash2[key]) 
            return false;
    }
    return true;
}

function deserialize(name, def) {
    var ret;
    try {
        ret = eval(GM_getValue(name) || def || '({})');
        if (DEBUG) GM_log("Deserializing '"+name+"' => '"+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<arguments.length; i++) {
        var child=arguments[i];
        if (child) { 
            if (typeof(child)=='string') {
                node.appendChild(TEXT(child));
            } else {
                node.appendChild(child);
            }
        }
    }
    for (var attr in attributes) if (attributes.hasOwnProperty(attr)){
        if (attr.slice(0,2) == 'on') {
            node.addEventListener(attr.slice(2), attributes[attr], false);
        } else {
            node.setAttribute(attr, attributes[attr]);
        }
    }
    return node;
} 


// Gets all the documents from the current page
function getDocuments(frame)
{
    var documents = new Array();

    // If the frame is set
    if(frame)
    {
        var frames       = frame.frames;
        var framesLength = frames.length;

        // If the frame document exists
        if(frame.document)
        {
            documents.push(frame.document);
        }

        // Loop through the frames
        for(var i = 0; i < framesLength; i++)
        {
            documents = documents.concat(getDocuments(frames[i]));
        }
    }

    return documents;
}
function isAncestor(element, ancestorElement) {
    // If the element and ancestor element are set
    if(element && ancestorElement)
    {
        var parentElement = null;

        // Loop through the parent elements
        while((parentElement = element.parentNode) != null)
        {
            // If the parent element is the ancestor element
            if(parentElement == ancestorElement)
            {
                return true;
            }
            else
            {
                element = parentElement;
            }
        }
    }

    return false;
}

// Converts a pattern in this programs simple notation to a regular expression.
// thanks AdBlock! http://www.mozdev.org/source/browse/adblock/adblock/
function convert2RegExp( pattern ) {
  var s = new String(pattern);
  var res = new String("^");

  for (var i = 0 ; i < s.length ; i++) {
    switch(s[i]) {
      case "*" :
        res += ".*";
        break;

      case "." :
      case "?" :
      case "^" :
      case "$" :
      case "+" :
      case "{" :
      case "[" :
      case "|" :
      case "(" :
      case ")" :
      case "]" :
        res += "\\" + s[i];
        break;

      case "\\" :
        res += "\\\\";
        break;

      case " " :
        // Remove spaces from URLs.
        break;

      default :
        res += s[i];
        break;
    }
  }

  var tldRegExp = new RegExp("^(\\^(?:[^/]*)(?://)?(?:[^/]*))(\\\\\\.tld)((?:/.*)?)$")
  var tldRes = res.match(tldRegExp);
  if (tldRes) {
    // build the mighty TLD RegExp
    var tldStr = "\.(?:demon\\.co\\.uk|esc\\.edu\\.ar|(?:c[oi]\\.)?[^\\.]\\.(?:vt|ne|ks|il|hi|sc|nh|ia|wy|or|ma|vi|tn|in|az|id|nc|co|dc|nd|me|al|ak|de|wv|nm|mo|pr|nj|sd|md|va|ri|ut|ct|pa|ok|ky|mt|ga|la|oh|ms|wi|wa|gu|mi|tx|fl|ca|ar|mn|ny|nv)\\.us|[^\\.]\\.(?:(?:pvt\\.)?k12|cc|tec|lib|state|gen)\\.(?:vt|ne|ks|il|hi|sc|nh|ia|wy|or|ma|vi|tn|in|az|id|nc|co|dc|nd|me|al|ak|de|wv|nm|mo|pr|nj|sd|md|va|ri|ut|ct|pa|ok|ky|mt|ga|la|oh|ms|wi|wa|gu|mi|tx|fl|ca|ar|mn|ny|nv)\\.us|[^\\.]\\.vt|ne|ks|il|hi|sc|nh|ia|wy|or|ma|vi|tn|in|az|id|nc|co|dc|nd|me|al|ak|de|wv|nm|mo|pr|nj|sd|md|va|ri|ut|ct|pa|ok|ky|mt|ga|la|oh|ms|wi|wa|gu|mi|tx|fl|ca|ar|mn|ny|nvus|ne|gg|tr|mm|ki|biz|sj|my|hn|gl|ro|tn|co|br|coop|cy|bo|ck|tc|bv|ke|aero|cs|dm|km|bf|af|mv|ls|tm|jm|pg|ky|ga|pn|sv|mq|hu|za|se|uy|iq|ai|com|ve|na|ba|ph|xxx|no|lv|tf|kz|ma|in|id|si|re|om|by|fi|gs|ir|li|tz|td|cg|pa|am|tv|jo|bi|ee|cd|pk|mn|gd|nz|as|lc|ae|cn|ag|mx|sy|cx|cr|vi|sg|bm|kh|nr|bz|vu|kw|gf|al|uz|eh|int|ht|mw|gm|bg|gu|info|aw|gy|ac|ca|museum|sk|ax|es|kp|bb|sa|et|ie|tl|org|tj|cf|im|mk|de|pro|md|fm|cl|jp|bn|vn|gp|sm|ar|dj|bd|mc|ug|nu|ci|dk|nc|rw|aq|name|st|hm|mo|gq|ps|ge|ao|gr|va|is|mt|gi|la|bh|ms|bt|gb|it|wf|sb|ly|ng|gt|lu|il|pt|mh|eg|kg|pf|um|fr|sr|vg|fj|py|pm|sn|sd|au|sl|gh|us|mr|dz|ye|kn|cm|arpa|bw|lk|mg|tk|su|sc|ru|travel|az|ec|mz|lb|ml|bj|edu|pr|fk|lr|nf|np|do|mp|bs|to|cu|ch|yu|eu|mu|ni|pw|pl|gov|pe|an|ua|uk|gw|tp|kr|je|tt|net|fo|jobs|yt|cc|sh|io|zm|hk|th|so|er|cz|lt|mil|hr|gn|be|qa|cv|vc|tw|ws|ad|sz|at|tg|zw|nl|info\\.tn|org\\.sd|med\\.sd|com\\.hk|org\\.ai|edu\\.sg|at\\.tt|mail\\.pl|net\\.ni|pol\\.dz|hiroshima\\.jp|org\\.bh|edu\\.vu|net\\.im|ernet\\.in|nic\\.tt|com\\.tn|go\\.cr|jersey\\.je|bc\\.ca|com\\.la|go\\.jp|com\\.uy|tourism\\.tn|com\\.ec|conf\\.au|dk\\.org|shizuoka\\.jp|ac\\.vn|matsuyama\\.jp|agro\\.pl|yamaguchi\\.jp|edu\\.vn|yamanashi\\.jp|mil\\.in|sos\\.pl|bj\\.cn|net\\.au|ac\\.ae|psi\\.br|sch\\.ng|org\\.mt|edu\\.ai|edu\\.ck|ac\\.yu|org\\.ws|org\\.ng|rel\\.pl|uk\\.tt|com\\.py|aomori\\.jp|co\\.ug|video\\.hu|net\\.gg|org\\.pk|id\\.au|gov\\.zw|mil\\.tr|net\\.tn|org\\.ly|re\\.kr|mil\\.ye|mil\\.do|com\\.bb|net\\.vi|edu\\.na|co\\.za|asso\\.re|nom\\.pe|edu\\.tw|name\\.et|jl\\.cn|gov\\.ye|ehime\\.jp|miyazaki\\.jp|kanagawa\\.jp|gov\\.au|nm\\.cn|he\\.cn|edu\\.sd|mod\\.om|web\\.ve|edu\\.hk|medecin\\.fr|org\\.cu|info\\.au|edu\\.ve|nx\\.cn|alderney\\.gg|net\\.cu|org\\.za|mb\\.ca|com\\.ye|edu\\.pa|fed\\.us|ac\\.pa|alt\\.na|mil\\.lv|fukuoka\\.jp|gen\\.in|gr\\.jp|gov\\.br|gov\\.ac|id\\.fj|fukui\\.jp|hu\\.com|org\\.gu|net\\.ae|mil\\.ph|ltd\\.je|alt\\.za|gov\\.np|edu\\.jo|net\\.gu|g12\\.br|org\\.tn|store\\.co|fin\\.tn|ac\\.nz|gouv\\.fr|gov\\.il|org\\.ua|org\\.do|org\\.fj|sci\\.eg|gov\\.tt|cci\\.fr|tokyo\\.jp|net\\.lv|gov\\.lc|ind\\.br|ca\\.tt|gos\\.pk|hi\\.cn|net\\.do|co\\.tv|web\\.co|com\\.pa|com\\.ng|ac\\.ma|gov\\.bh|org\\.zw|csiro\\.au|lakas\\.hu|gob\\.ni|gov\\.fk|org\\.sy|gov\\.lb|gov\\.je|ed\\.cr|nb\\.ca|net\\.uy|com\\.ua|media\\.hu|com\\.lb|nom\\.pl|org\\.br|hk\\.cn|co\\.hu|org\\.my|gov\\.dz|sld\\.pa|gob\\.pk|net\\.uk|guernsey\\.gg|nara\\.jp|telememo\\.au|k12\\.tr|org\\.nz|pub\\.sa|edu\\.ac|com\\.dz|edu\\.lv|edu\\.pk|com\\.ph|net\\.na|net\\.et|id\\.lv|au\\.com|ac\\.ng|com\\.my|net\\.cy|unam\\.na|nom\\.za|net\\.np|info\\.pl|priv\\.hu|rec\\.ve|ac\\.uk|edu\\.mm|go\\.ug|ac\\.ug|co\\.dk|net\\.tt|oita\\.jp|fi\\.cr|org\\.ac|aichi\\.jp|org\\.tt|edu\\.bh|us\\.com|ac\\.kr|js\\.cn|edu\\.ni|com\\.mt|fam\\.pk|experts-comptables\\.fr|or\\.kr|org\\.au|web\\.pk|mil\\.jo|biz\\.pl|org\\.np|city\\.hu|org\\.uy|auto\\.pl|aid\\.pl|bib\\.ve|mo\\.cn|br\\.com|dns\\.be|sh\\.cn|org\\.mo|com\\.sg|me\\.uk|gov\\.kw|eun\\.eg|kagoshima\\.jp|ln\\.cn|seoul\\.kr|school\\.fj|com\\.mk|e164\\.arpa|rnu\\.tn|pro\\.ae|org\\.om|gov\\.my|net\\.ye|gov\\.do|co\\.im|org\\.lb|plc\\.co\\.im|net\\.jp|go\\.id|net\\.tw|gov\\.ai|tlf\\.nr|ac\\.im|com\\.do|net\\.py|tozsde\\.hu|com\\.na|tottori\\.jp|net\\.ge|gov\\.cn|org\\.bb|net\\.bs|ac\\.za|rns\\.tn|biz\\.pk|gov\\.ge|org\\.uk|org\\.fk|nhs\\.uk|net\\.bh|tm\\.za|co\\.nz|gov\\.jp|jogasz\\.hu|shop\\.pl|media\\.pl|chiba\\.jp|city\\.za|org\\.ck|net\\.id|com\\.ar|gon\\.pk|gov\\.om|idf\\.il|net\\.cn|prd\\.fr|co\\.in|or\\.ug|red\\.sv|edu\\.lb|k12\\.ec|gx\\.cn|net\\.nz|info\\.hu|ac\\.zw|info\\.tt|com\\.ws|org\\.gg|com\\.et|ac\\.jp|ac\\.at|avocat\\.fr|org\\.ph|sark\\.gg|org\\.ve|tm\\.pl|net\\.pg|gov\\.co|com\\.lc|film\\.hu|ishikawa\\.jp|hotel\\.hu|hl\\.cn|edu\\.ge|com\\.bm|ac\\.om|tec\\.ve|edu\\.tr|cq\\.cn|com\\.pk|firm\\.in|inf\\.br|gunma\\.jp|gov\\.tn|oz\\.au|nf\\.ca|akita\\.jp|net\\.sd|tourism\\.pl|net\\.bb|or\\.at|idv\\.tw|dni\\.us|org\\.mx|conf\\.lv|net\\.jo|nic\\.in|info\\.vn|pe\\.kr|tw\\.cn|org\\.eg|ad\\.jp|hb\\.cn|kyonggi\\.kr|bourse\\.za|org\\.sb|gov\\.gg|net\\.br|mil\\.pe|kobe\\.jp|net\\.sa|edu\\.mt|org\\.vn|yokohama\\.jp|net\\.il|ac\\.cr|edu\\.sb|nagano\\.jp|travel\\.pl|gov\\.tr|com\\.sv|co\\.il|rec\\.br|biz\\.om|com\\.mm|com\\.az|org\\.vu|edu\\.ng|com\\.mx|info\\.co|realestate\\.pl|mil\\.sh|yamagata\\.jp|or\\.id|org\\.ae|greta\\.fr|k12\\.il|com\\.tw|gov\\.ve|arts\\.ve|cul\\.na|gov\\.kh|org\\.bm|etc\\.br|or\\.th|ch\\.vu|de\\.tt|ind\\.je|org\\.tw|nom\\.fr|co\\.tt|net\\.lc|intl\\.tn|shiga\\.jp|pvt\\.ge|gov\\.ua|org\\.pe|net\\.kh|co\\.vi|iwi\\.nz|biz\\.vn|gov\\.ck|edu\\.eg|zj\\.cn|press\\.ma|ac\\.in|eu\\.tt|art\\.do|med\\.ec|bbs\\.tr|gov\\.uk|edu\\.ua|eu\\.com|web\\.do|szex\\.hu|mil\\.kh|gen\\.nz|okinawa\\.jp|mob\\.nr|edu\\.ws|edu\\.sv|xj\\.cn|net\\.ru|dk\\.tt|erotika\\.hu|com\\.sh|cn\\.com|edu\\.pl|com\\.nc|org\\.il|arts\\.co|chirurgiens-dentistes\\.fr|net\\.pa|takamatsu\\.jp|net\\.ng|org\\.hu|net\\.in|net\\.vu|gen\\.tr|shop\\.hu|com\\.ae|tokushima\\.jp|za\\.com|gov\\.eg|co\\.jp|uba\\.ar|net\\.my|biz\\.et|art\\.br|ac\\.fk|gob\\.pe|com\\.bs|co\\.ae|de\\.net|net\\.eg|hyogo\\.jp|edunet\\.tn|museum\\.om|nom\\.ve|rnrt\\.tn|hn\\.cn|com\\.fk|edu\\.dz|ne\\.kr|co\\.je|sch\\.uk|priv\\.pl|sp\\.br|net\\.hk|name\\.vn|com\\.sa|edu\\.bm|qc\\.ca|bolt\\.hu|per\\.kh|sn\\.cn|mil\\.id|kagawa\\.jp|utsunomiya\\.jp|erotica\\.hu|gd\\.cn|net\\.tr|edu\\.np|asn\\.au|com\\.gu|ind\\.tn|mil\\.br|net\\.lb|nom\\.co|org\\.la|mil\\.pl|ac\\.il|gov\\.jo|com\\.kw|edu\\.sh|otc\\.au|gmina\\.pl|per\\.sg|gov\\.mo|int\\.ve|news\\.hu|sec\\.ps|ac\\.pg|health\\.vn|sex\\.pl|net\\.nc|qc\\.com|idv\\.hk|org\\.hk|gok\\.pk|com\\.ac|tochigi\\.jp|gsm\\.pl|law\\.za|pro\\.vn|edu\\.pe|info\\.et|sch\\.gg|com\\.vn|gov\\.bm|com\\.cn|mod\\.uk|gov\\.ps|toyama\\.jp|gv\\.at|yk\\.ca|org\\.et|suli\\.hu|edu\\.my|org\\.mm|co\\.yu|int\\.ar|pe\\.ca|tm\\.hu|net\\.sb|org\\.yu|com\\.ru|com\\.pe|edu\\.kh|edu\\.kw|org\\.qa|med\\.om|net\\.ws|org\\.in|turystyka\\.pl|store\\.ve|org\\.bs|mil\\.uy|net\\.ar|iwate\\.jp|org\\.nc|us\\.tt|gov\\.sh|nom\\.fk|go\\.th|gov\\.ec|com\\.br|edu\\.do|gov\\.ng|pro\\.tt|sapporo\\.jp|net\\.ua|tm\\.fr|com\\.lv|com\\.mo|edu\\.uk|fin\\.ec|edu\\.ps|ru\\.com|edu\\.ec|ac\\.fj|net\\.mm|veterinaire\\.fr|nom\\.re|ingatlan\\.hu|fr\\.vu|ne\\.jp|int\\.co|gov\\.cy|org\\.lv|de\\.com|nagasaki\\.jp|com\\.sb|gov\\.za|org\\.lc|com\\.fj|ind\\.in|or\\.cr|sc\\.cn|chambagri\\.fr|or\\.jp|forum\\.hu|tmp\\.br|reklam\\.hu|gob\\.sv|com\\.pl|saitama\\.jp|name\\.tt|niigata\\.jp|sklep\\.pl|nom\\.ni|co\\.ma|net\\.la|co\\.om|pharmacien\\.fr|port\\.fr|mil\\.gu|au\\.tt|edu\\.gu|ngo\\.ph|com\\.ve|ac\\.th|gov\\.fj|barreau\\.fr|net\\.ac|ac\\.je|org\\.kw|sport\\.hu|ac\\.cn|net\\.bm|ibaraki\\.jp|tel\\.no|org\\.cy|edu\\.mo|gb\\.net|kyoto\\.jp|sch\\.sa|com\\.au|edu\\.lc|fax\\.nr|gov\\.mm|it\\.tt|org\\.jo|nat\\.tn|mil\\.ve|be\\.tt|org\\.az|rec\\.co|co\\.ve|gifu\\.jp|net\\.th|hokkaido\\.jp|ac\\.gg|go\\.kr|edu\\.ye|qh\\.cn|ab\\.ca|org\\.cn|no\\.com|co\\.uk|gov\\.gu|de\\.vu|miasta\\.pl|kawasaki\\.jp|co\\.cr|miyagi\\.jp|org\\.jp|osaka\\.jp|web\\.za|net\\.za|gov\\.pk|gov\\.vn|agrar\\.hu|asn\\.lv|org\\.sv|net\\.sh|org\\.sa|org\\.dz|assedic\\.fr|com\\.sy|net\\.ph|mil\\.ge|es\\.tt|mobile\\.nr|co\\.kr|ltd\\.uk|ac\\.be|fgov\\.be|geek\\.nz|ind\\.gg|net\\.mt|maori\\.nz|ens\\.tn|edu\\.py|gov\\.sd|gov\\.qa|nt\\.ca|com\\.pg|org\\.kh|pc\\.pl|com\\.eg|net\\.ly|se\\.com|gb\\.com|edu\\.ar|sch\\.je|mil\\.ac|mil\\.ar|okayama\\.jp|gov\\.sg|ac\\.id|co\\.id|com\\.ly|huissier-justice\\.fr|nic\\.im|gov\\.lv|nu\\.ca|org\\.sg|com\\.kh|org\\.vi|sa\\.cr|lg\\.jp|ns\\.ca|edu\\.co|gov\\.im|edu\\.om|net\\.dz|org\\.pl|pp\\.ru|tm\\.mt|org\\.ar|co\\.gg|org\\.im|edu\\.qa|org\\.py|edu\\.uy|targi\\.pl|com\\.ge|gub\\.uy|gov\\.ar|ltd\\.gg|fr\\.tt|net\\.qa|com\\.np|ass\\.dz|se\\.tt|com\\.ai|org\\.ma|plo\\.ps|co\\.at|med\\.sa|net\\.sg|kanazawa\\.jp|com\\.fr|school\\.za|net\\.pl|ngo\\.za|net\\.sy|ed\\.jp|org\\.na|net\\.ma|asso\\.fr|police\\.uk|powiat\\.pl|govt\\.nz|sk\\.ca|tj\\.cn|mil\\.ec|com\\.jo|net\\.mo|notaires\\.fr|avoues\\.fr|aeroport\\.fr|yn\\.cn|gov\\.et|gov\\.sa|gov\\.ae|com\\.tt|art\\.dz|firm\\.ve|com\\.sd|school\\.nz|edu\\.et|gob\\.pa|telecom\\.na|ac\\.cy|gz\\.cn|net\\.kw|mobil\\.nr|nic\\.uk|co\\.th|com\\.vu|com\\.re|belgie\\.be|nl\\.ca|uk\\.com|com\\.om|utazas\\.hu|presse\\.fr|co\\.ck|xz\\.cn|org\\.tr|mil\\.co|edu\\.cn|net\\.ec|on\\.ca|konyvelo\\.hu|gop\\.pk|net\\.om|info\\.ve|com\\.ni|sa\\.com|com\\.tr|sch\\.sd|fukushima\\.jp|tel\\.nr|atm\\.pl|kitakyushu\\.jp|com\\.qa|firm\\.co|edu\\.tt|games\\.hu|mil\\.nz|cri\\.nz|net\\.az|org\\.ge|mie\\.jp|net\\.mx|sch\\.ae|nieruchomosci\\.pl|int\\.vn|edu\\.za|com\\.cy|wakayama\\.jp|gov\\.hk|org\\.pa|edu\\.au|gov\\.in|pro\\.om|2000\\.hu|szkola\\.pl|shimane\\.jp|co\\.zw|gove\\.tw|com\\.co|net\\.ck|net\\.pk|net\\.ve|org\\.ru|uk\\.net|org\\.co|uu\\.mt|com\\.cu|mil\\.za|plc\\.uk|lkd\\.co\\.im|gs\\.cn|sex\\.hu|net\\.je|kumamoto\\.jp|mil\\.lb|edu\\.yu|gov\\.ws|sendai\\.jp|eu\\.org|ah\\.cn|net\\.vn|gov\\.sb|net\\.pe|nagoya\\.jp|geometre-expert\\.fr|net\\.fk|biz\\.tt|org\\.sh|edu\\.sa|saga\\.jp|sx\\.cn|org\\.je|org\\.ye|muni\\.il|kochi\\.jp|com\\.bh|org\\.ec|priv\\.at|gov\\.sy|org\\.ni|casino\\.hu|res\\.in|uy\\.com)"

    // insert it
    res = tldRes[1] + tldStr + tldRes[3];
  }
  return new RegExp(res + "$", "i");
}

  
////////////////////////////////////////////////////////////////////////////////

GM_addStyle(
    "#ShortcutBinderGlobalDialog { "+
    "   color:black; background-color:white;"+
    "   position: fixed; top: 50px; left: 50%;"+
    "   border: 1px dashed black; margin: 0 0 0 -200px; padding: 5px;"+
    "   width: 400px; "+
    "   text-align: center;"+
    "   z-index: 99999999;"+
    "}"+

    "#ShortcutBinderGlobalDialog input { "+
    "   margin: 2px 10px;"+
    "}"+

    "#ShortcutBinderAddDialog { "+
    "   color:black; background-color:white;"+
    "   position: fixed; z-index: 99999998;"+
    "   border: 1px solid gray; margin: 0; padding: 5px;"+
    "   width: 400px;"+
    "   text-align: left;"+
    "}"+
    "#ShortcutBinderAddDialog h2, #ShortcutBinderManageDialog h2 { "+
    "   cursor: move;"+
    "}"+
    "#ShortcutBinderGlobalDialog h2, #ShortcutBinderAddDialog h2, #ShortcutBinderManageDialog h2{ "+
    "   padding: 5px; margin: 0;"+
    "   color: white; background: gray;"+
    "}"+

    "#ShortcutBinderAddDialog label { "+
    "   display: block; margin: 5px;"+
    "}"+
    "#ShortcutBinderAddDialog input {"+
    "   width: 100%; margin: 0; border: "+
    "}"+
    "#ShortcutBinderAddDialog .suggestions { "+
    "   margin: 5px 5px 5px 15px;"+
    "}"+
    "#ShortcutBinderAddDialog textarea {"+
    "   width: 100%; margin: 0;"+
    "   height: 100px;"+
    "}"+
    
    "#ShortcutBinderManageDialog { "+
    "   color:black; background-color:white;"+
    "   position: absolute; z-index: 99999997;"+
    "   border: 1px solid gray; margin: 0; padding: 5px;"+
    "   width: 80%;"+
    "   text-align: center;"+
    "}"+
    "#ShortcutBinderManageDialog table {"+
    "   width: 100%; empty-cells: show;"+
    "}"+
    "#ShortcutBinderManageDialog td {"+
    "   text-align: center;"+
    "   border: 1px solid gray; -moz-border-radius: 3px"+
    "}"+
    "#ShortcutBinderManageDialog input {"+
    "   width: 60px;"+
    "}"+
    
    ""
);