Code snippets: Difference between revisions
→Get elements by CSS selector: Broke up the code a bit to get rid of horizontal scroll bar. |
Alien scum (talk | contribs) →Get elements by CSS selector: better string and class suport |
||
Line 40: | Line 40: | ||
function $$(xpath,root) { | function $$(xpath,root) { | ||
xpath=xpath.replace(/((^|\|)\s*)([^/|\s]+)/g,'$2.//$3'). | xpath=xpath.replace(/((^|\|)\s*)([^/|\s]+)/g,'$2.//$3'). | ||
replace(/ | replace(/\.([\w-]+)(?!([^"]*")|([^'"]*("|')))/g,'[@class="$1" or @class$=" $1" or @class^="$1 " or @class~=" $1 "]'). | ||
replace(/#([\w-]+)/g,'[@id="$1"]'). | replace(/#([\w-]+)/g,'[@id="$1"]'). | ||
replace(/\/\[/g,'/*['); | replace(/\/\[/g,'/*['); | ||
str='(@\\w+|"[^"]*"|\'[^\']*\')' | |||
replace( | xpath=xpath.replace(new RegExp(str+'\\s*~=\\s*'+str,'g'),'contains($1,$2)'). | ||
replace( | replace(new RegExp(str+'\\s*\\^=\\s*'+str,'g'),'starts-with($1,$2)'). | ||
replace(new RegExp(str+'\\s*\\$=\\s*'+str,'g'),'substring($1,string-length($1)-string-length($2)+1)=$2'); | |||
var got=document.evaluate(xpath,root||document,null,null,null), result=[]; | var got=document.evaluate(xpath,root||document,null,null,null), result=[]; | ||
while(next=got.iterateNext()) result.push(next); | while(next=got.iterateNext()) result.push(next); | ||
return result; | return result; | ||
} | } | ||
Example usage: | Example usage: | ||
$$('#title')[0].innerHTML='Greased'; | |||
$$('a[@href $= "user.js"]').forEach(function (a) { | |||
a.innerHTML='check it out a script'; | |||
} | |||
$$('a[@href ^= "http"]').forEach(function (a) { | |||
a.innerHTML += ' (external)'; | |||
} | |||
= Conditional logging = | = Conditional logging = |
Revision as of 23:04, 4 June 2007
Shortcut to document.getElementById
function $(id) { return document.getElementById(id); }
Example usage:
$("header").innerHTML = "Halloa!";
XPath helper
Run a particular XPath expression p
against the context node context
(or the document, if not provided).
Returns the results as an array.
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; }
Example usage (with Array.forEach):
var paragraphs = $x("//p"); paragraphs.forEach(function(paragraph) { // Loop over every paragraph paragraph.innerHTML = "Halloa!"; });
Note: When you specify a context node, you need to use a relative XPath expression.
Get elements by CSS selector
Lets you run an XPath using the concise CSS style selectors such as .class and #id, it also adds a much needed method of comparing the ends of strings, $=.
function $$(xpath,root) { xpath=xpath.replace(/((^|\|)\s*)([^/|\s]+)/g,'$2.//$3'). replace(/\.([\w-]+)(?!([^"]*")|([^'"]*("|')))/g,'[@class="$1" or @class$=" $1" or @class^="$1 " or @class~=" $1 "]'). replace(/#([\w-]+)/g,'[@id="$1"]'). replace(/\/\[/g,'/*['); str='(@\\w+|"[^"]*"|\'[^\']*\')' xpath=xpath.replace(new RegExp(str+'\\s*~=\\s*'+str,'g'),'contains($1,$2)'). replace(new RegExp(str+'\\s*\\^=\\s*'+str,'g'),'starts-with($1,$2)'). replace(new RegExp(str+'\\s*\\$=\\s*'+str,'g'),'substring($1,string-length($1)-string-length($2)+1)=$2'); var got=document.evaluate(xpath,root||document,null,null,null), result=[]; while(next=got.iterateNext()) result.push(next); return result; }
Example usage:
$$('#title')[0].innerHTML='Greased'; $$('a[@href $= "user.js"]').forEach(function (a) { a.innerHTML='check it out a script'; } $$('a[@href ^= "http"]').forEach(function (a) { a.innerHTML += ' (external)'; }
Conditional logging
Used to easily toggle sending debug messages to the console. Passes all arguments on to console.log, but only if console
is defined (for backward compatibility) and DEBUG
is true
.
Code and example usage:
const DEBUG = true; var links = document.links; debug("Links: %o", links); function debug() { if (DEBUG && console) { console.log.apply(this, arguments); } }
DOM node manipulation
Use .innerHTML to create DOM structure
The non W3 standard setter function .innerHTML
can be used to concisely create DOM structure in trivial userscripts. The following is a complete branding script to show that Greasemonkey has run on a host.
host = document.location.host; dummyDiv = document.createElement('div'); dummyDiv.innerHTML = '<div><span style="color: red">Greased: ' + host + '</span></div>'; document.body.insertBefore(dummyDiv.firstChild, document.body.firstChild);
Note above that dummyDiv
is only needed as a holder for its .firstChild
.
Here is a helper function that reuses the dummyDiv
:
function firstNodeOf(html){ firstNodeOf.dummyDiv.innerHTML = html; return firstNodeOf.dummyDiv.firstChild; } firstNodeOf.dummyDiv = documentCreateElement('div');
With this helper, one can write the following trivial script:
document.body.appendChild(firstNodeOf('<div><span style="color: red">END OF PAGE</span></div>');
STRONGLY NOTE: .innerHTML
seems to be sensitive to document.contentType
. When the type is text/plain
the .innerHTML
setter does not parse its argument into DOM nodes, but instead returns #text nodes. The setter seems to work fine for types such as text/html
or application/xhtml+xml
but where and how it works is undocumented.
Advantages: Concise and and probably more efficient than any hand coded approach.
Disadvantages: Other than the text/plain
probem, .innerHTML
is not a W3 standard and many people don't like non-standard code.
Upshot: Until ECMAScript for XML (E4X) becomes availble in user scripts, this is a useful hack for non text/plain
pages.
Notes
.innerHTML
cannot be used to create parts of aTABLE
. E.g.
tab = firstNodeOf('<table><tr><td> ... </td></tr></table>'); // WORKS tr = firstNodeOf('<tr><td> ... </td></tr>'); // DOESN'T WORK td = firstNodeOf('<td> ... </td>'); // DOESN'T WORK td.innerHTML = ' ... '; // WORKS
So, if a script builds a table from a HTML string, it has to be done as a whole rather than in parts.
Build a DOM node with attributes
Creates a new DOM node with the given attributes.
function createElement(type, attributes){ var node = document.createElement(type); for (var attr in attributes){ node.setAttribute(attr, attributes[attr]); } return node; }
Example usage:
link = createElement('link', {rel: 'stylesheet', type: 'text/css', href: basedir + 'style.css'});
Remove DOM node
function remove(element) { element.parentNode.removeChild(element); }
Insert node after node
function insertAfter(newNode, node) { return node.parentNode.insertBefore(newNode, node.nextSibling); }
This works because even if node
is the last node, nextSibling
returns null
so insertBefore
puts the new node at the end.
Example usage:
var link = document.getElementById("the_link"); var icon = document.createElement("img"); icon.src = "…"; insertAfter(icon, link);
Hijacking browser properties
Sometimes you want to cook your own browser constants, for instance change the value of navigator.userAgent. Getters are good for that:
var real = window.navigator.userAgent; var lie = function() { return real + " Macintosh"; }; unsafeWindow.navigator.__defineGetter__("userAgent", lie);
Extending the DOM with missing functions
Other times you might want to emulate proprietary functionality of another browser, for instance when a site uses such features:
var getter = function() { return this.textContent; }; var setter = function(t) { return this.textContent = t; }; unsafeWindow.HTMLElement.prototype.__defineGetter__("innerText", getter); unsafeWindow.HTMLElement.prototype.__defineSetter__("innerText", setter);
Advanced createElement for creating hierarchies of elements
Creates an element with attributes as well as child elements with their own attributes and children. Function should be called with arguments in the form of the following hash (note that "child1", "child2" should be hashes of the same structure): createEl({n: nodename, a: {attr1: val, attr2: val}, c: [child1, child2], evl: {type: eventlistener_type, f: eventlistener_function, bubble: bool}}, appendTo)
function createEl(elObj, parent) { var el; if (typeof elObj == 'string') { el = document.createTextNode(elObj); } else { el = document.createElement(elObj.n); if (elObj.a) { attributes = elObj.a; for (var key in attributes) { if (key.charAt(0) == '@') el.setAttribute(key.substring(1), attributes[key]); else el[key] = attributes[key]; } } if (elObj.evl) { el.addEventListener(elObj.evl.type, elObj.evl.f, elObj.evl.bubble); } if (elObj.c) { elObj.c.forEach(function (v, i, a) { createEl(v, el); }); } } if (parent) parent.appendChild(el); return el; }
Example usage:
createEl({n: 'ol', a: {'@class': 'some_list', '@id': 'my_list'}, c: [ {n: 'li', a: {textContent: 'first point'}, evl: {type: 'click', f: function() {alert('first point');}, bubble: true}}, {n: 'li', a: {textContent: 'second point'}}, {n: 'li', a: {textContent: 'third point'}} ]}, document.body);
GET a URL with callback function
Retrieves url
using HTTP GET, then calls the function cb
with the response text as its single argument.
function get(url, cb) { GM_xmlhttpRequest({ method: "GET", url: url, onload: function(xhr) { cb(xhr.responseText); } }); }
Example usage:
function inform(text) { alert("The HTML of the page: " + text); } get("http://www.google.com", inform);
RegExp escape string
Escapes regexp meta characters in a string.
function escapeRegexp(s) { return s.replace(/([.*+?^${}()|[\]\/\\])/g, '\\$1'); }
Example usage:
var re = new RegExp("^" + escapeRegexp("fo*bar") + "$"); "fo*bar".match(re); // Matches "foobar".match(re); // Doesn't match
!important Style
Appends !important to each rule then adds the CSS to the page letting you override the default formatting.
function addStyle(css) { GM_addStyle(css.replace(/;/g,' !important;')); }
Example usage:
addStyle('a {text-decoration:none;}');
A common pattern is to register a menu command that toggles some script variable that is persisted using GM_get/setValue. This function abstracts that functionality.
function makeMenuToggle(key, defaultValue, toggleOn, toggleOff, prefix) { // Load current value into variable window[key] = GM_getValue(key, defaultValue); // Add menu toggle GM_registerMenuCommand((prefix ? prefix+": " : "") + (window[key] ? toggleOff : toggleOn), function() { GM_setValue(key, !window[key]); location.reload(); }); }
The first argument is the key used with GM_get/setValue and is also the variable which will hold the current value. The second argument is the default value.
The third and fourth arguments are the text to be displayed in the menu for toggling on and toggling off, respectively. The fifth argument is an optional prefix for those menu items.
Only one menu command is added, that will toggle the option.
Example usage:
makeMenuToggle("linkify_emails", true, "Include e-mail addresses", "Exclude e-mail addresses", "Linkify"); if (linkify_emails) process_emails_too();
Note that after changing the value, the page is reloaded so that the script runs again with the changed options, and to update the menu. This is not recommended if the user might have unsaved input on the page in question. In such a case, consider adding a prompt
or re-running some method instead. Since menu items can't be edited/removed without reloading the page, one would likely also want two separate menu items instead of a toggle.
Serialize/deserialize for GM_getValue
Used to store and retrieve multiple values (typically as a serialized hash) in a single GM_getValue slot.
function deserialize(name, def) { return eval(GM_getValue(name, (def || '({})'))); } function serialize(name, val) { GM_setValue(name, uneval(val)); }
Example usage:
var settings = {a: 1, b: 2, c: 3}; serialize('test', settings); var _settings = deserialize('test'); // now "settings == _settings" should be true
make an array persistent in globalStorage
DOM:Storage is available in Firefox 2.0 and up.
/** Makes an given Array persistent in the globalStorage Object. It will not work with arrays that get additional mutator functions after it was made persistent. @param {String} name - name of the property of this that is the Array @param {String} domain - domain parameter of globalStorage @param {Array{String, String, ...}} add_mutator - additional non standard (Javascript 1.7) functions */ function makeArrayPersistent(_name, _domain, _add_mutator){ //workaround for scripts that work on pages stored on file:// var domain = _domain || ".localdomain"; //if the array is not defined yet we define it and fill it //with values stored in globalStorage if(!this[_name]){ var evalStr = String(globalStorage[domain][_name]); this[_name] = eval(evalStr); //if it is defined allready we store it in globalStorage }else{ globalStorage[domain][_name] = uneval(this[_name]); } //Watch will intercept asignments to this[_name] and store the new array in //globalStorage. The original array in globalStorage is discarded. this.watch(_name, function(_prop, _oldVal, _newVal){ globalStorage[domain][_name] = uneval(_newVal); return _newVal; }); //see a few lines below ["push", "pop", "reverse", "shift", "sort", "splice", "unshift"].forEach(function(_f){ makeMutatorFunctionGlobal(_f); }); //you can supply additional functions that will be wrapped if(_add_mutator) _add_mutator.forEach(function(_f){ makeMutatorFunctionGlobal(_f); }); //member functions that alter the array itself are wrapped. The wrapper will //call the mutator function and store the altered array in globalStorage function makeMutatorFunctionGlobal(_f){ this[_name][_f] = function(){ var f = this[_name][_f]; return function(){ var retVal; retVal = f.apply(this, arguments); globalStorage[domain].trolls = uneval(this); return retVal; } }(); } }
Example Usage
We create an array in window.
var a = [1,4,3,2];
The following would not work:
function baz(){ var b; makeArrayPersistent("b", "somewhere.org"); }
We cant keep track of variables that that are not kept track of by Javascript itself.
makeArrayPersistent("a", "somewhere.org");
We have to supply the variables name due to watch. this["a"] equals this.a in our case. If you want to store an object in a different scope you can use makeArrayPersistent.call(some.object, "a", "somewhere.org"); . The domain has to match the domain of the site where your script is injected too. Read up on globalObject for details.
this.a.push(5);
Now a equals [1,4,3,2,5]. It is stored in globalObject that way.
this.a.sort();
And now it's [1,2,3,4,5], stored again.
delete this.a;
Now the array does not exist in this anymore. But it is kept in globalObject. In the next line a will be defined in this and filled from globalObject.
makeArrayPersistent("a", "dexhome.homelinux.org");
And this.a equals [1,2,3,4,5] again.
If the user of your script got the same page open in two tabs the two scripts will start to fight over globalStorage and you will lose data. You have to alter makeMutatorFunctionGlobal with some meaningfull logic to counter this. The idea is to retrieve the array from globalStorage, combine it with the local copy and run the wrapped mutator function. Afterward it's stored in globalStorage again.
Waiting for something
Sometimes a script has to wait for some AJAX to finish before it can run this lets you do that
function wait(c,f){ if (c()) f() else window.setTimeout(function (){wait(c,f)},300,false); }
Example usage:
wait( function(){return count==0}, function(){alert('allfound')} );
Add commas to numbers
Numbers look more readable with commas, the following RegExp will add them to every integer in a string. It has issues with decimals though.
s.replace(/(\d)(?=(\d{3})+\b)/g,'$1,')
Examples:
"1234 -12345 -1.1234 1.234 -12345678.1234465 12345678".replace(/(\d)(?=(\d{3})+\b)/g,'$1,'); //gives: "1,234 -12,345 -1.1,234 1.234 -12,345,678.1,234,465 12,345,678"