Code snippets: Difference between revisions

From GreaseSpot Wiki
Jump to navigationJump to search
No edit summary
(revert Page broken, Reverting to previous version.)
Line 1: Line 1:
//
__TOC__
// version 0.6.6
// 2008-04-05
// Copyright (c) 2008
// Original Script by: ImmortalNights
// Special Enhancements: wphilipw, ecamanaut
// Released under the GPL license
// http://www.gnu.org/copyleft/gpl.html
//
// --------------------------------------------------------------------
//
// This is a Greasemonkey user script.
//
// To install, you need Greasemonkey: http://greasemonkey.mozdev.org/
// Then restart Firefox and revisit this script.
// Under Tools, there will be a new menu item to "Install User Script".
// Accept the default configuration and install.
//
// To uninstall, go to Tools/Manage User Scripts,
// select "IkariamScoreLinker", and click Uninstall.
//
// --------------------------------------------------------------------
//
// Version History:
// 0.3.0: Original Public Release
// ==================================================
// 0.4.0: Island View & Bugfixes
// 0.4.1: Description change, Generals Version update
// 0.4.5: Bugfixes, Script combination (versions)
// 0.4.6: Changed the image sizes to 16x16
// 0.4.7: Implemented 'checkAlreadyShown' to prevent the icon displaying multiple times
// ==================================================
// 0.5.0: Inline Score Display Option (AJAX)
// 0.5.2: Defaults of text inline now set. Icons and text have headers. Options link always shown
// 0.5.3: Code clean up, Money Score option & Options dialog
// 0.5.4: Previous score details are saved, so that they are not updated if the same town is viewed.
// 0.5.5: BugFix for multiple scores + timeout for saved scores (10min)
// 0.5.6: BugFix: "undefined" scores (timestamp too long, now stored in string)
// 0.5.7: Options on Options page, no longer inline
// ==================================================
// 0.6.0: Saves scores in the page after loading them once. Code cleanup. Does not try to run on the forums.
// 0.6.1: Shows max lootable gold, according to a formula by Lirave. Keyboard selection via (shift)tab, or j/k.
// 0.6.2: Removed the 'Change Options' link and the 'loot:' prefix on lootable gold.
// 0.6.3: Made the 'Change Options' link configurable. Bugfixed non-inline links. (And loot is back; sorry. ;-)
// 0.6.4: Bugfix for confusion when Kronos Utils adds info to the town size field. Also made sure Gold Scores don't wrap even if their contents get long.
// 0.6.5: Paints demilitarized (but neither allied nor your own) cities yellow.
// 0.6.6: Added some more key bindings: d/t/p/b/s, for all(?) the city actions.
//
// --------------------------------------------------------------------
//
// This script places an icon to the right of a players
// name after selecting their town on the Island overview,
// or when viewing their Town. This icon links the user to
// the scoreboard, where you can see the players score.
//
// Feel free to have a go yourself; as long as you leave
// a little credit, and of course publish for the players
// of Ikariam!
//
// This script was originally created by ImmortalNights,
// and further edited and enhanced by wphilipw and ecmanaut.
//
// --------------------------------------------------------------------
//
// ==UserScript==
// @name          IkariamScoreLinker
// @namespace      ikariamScript
// @description    Adds a link to the Scoreboard besides a players name after selecting their town on the Island Overview or when viewing their Town.
// @include        http://*.ikariam.*/*
// @exclude        http://board.ikariam.*/*
// ==/UserScript==


/*
== Shortcut to document.getElementById ==
The startup functions and global variables.
Original Author: ImmortalNights & wphilipw
For version: 0.3.0
Last changed: 0.6.0
*/


var show = { gold: 4, military: 2, total: 1 };
function $(id) {
var post = {
  return document.getElementById(id);
    total: "score",
  }
  military: "army_score_main",
    gold: "trader_score_secondary"
};


var saving;
Example usage:
var gameServer = location.host;
var valueCache = eval(GM_getValue(gameServer, "({})"));
var changeLink = GM_getValue("link", true);
var whatToShow = GM_getValue("show", "7");
var inlineScore = GM_getValue("inline", true);


if ($("options_changePass"))
$("header").innerHTML = "Halloa!";
  displayOnOptions_fn();
else
  init();


function init() {
  function maybeLookup(e) {
    var n = $X('.//span[@class="textLabel"]', e.target);
    var ul = $X('ancestor-or-self::li[1]/ul[@class="cityinfo"]', e.target);
    if ($X('li[contains(@class," name")]', ul)) return; // already fetched!
    var who = $X('li[@class="owner"]/text()[preceding::*[1]/self::span]', ul);
    var name = trim(who.textContent);
    fetchScoresFor(name, ul, n, number(n.parentNode.id));
  }
  function lookupOnClick(a) {
    onClick(a, function(e) { setTimeout(maybeLookup, 10, e); });
  }
  function peek(e) {
    var on = e.target;
    cities.map(click);
    if (/^a$/i.test(on.nodeName))
      click(on);
  }


  if ("island" == document.body.id) {
== XPath helper ==
    GM_addStyle(<><![CDATA[#island #information .messageSend img {
      position: absolute;
      margin: -3px 0 0 4px;
    }]]></>.toXMLString());
    var id = location.href.match(/[&?]id=(\d+)/);
    if (id) id = id[1];
  }
  var cities = getCityLinks();
  if (cities.length) {
    cities.forEach(lookupOnClick);
    var body = document.body;
    addEventListener("keypress", keyboard, true);
    return inlineScore && onClick(body, peek, 0, "dbl");
  }
  var player = itemValue("owner");
  if (player)
    fetchScoresFor(player, null, null, id);
}


function saveCache() {
Run a particular [[XPath]] expression <code>p</code> against the context node <code>context</code> (or the document, if not provided).
  //console.log("Saving cache: %x", uneval(valueCache));
  GM_setValue(gameServer, uneval(valueCache).replace(/ /g, ""));
}


function cacheValue(id, type, value) {
Returns the results as an array.
  //console.log("caching", id, type, value);
  var city = valueCache[id] || {};
  type = type.charAt();
  city[type] = number(value);
  city.T = time();
  valueCache[id] = city;
  saving && clearTimeout(saving);
  saving = setTimeout(saveCache, 1e3);
}


function focus(direction) {
function $x(p, context) {
  var all = getCityLinks();
  if (!context) context = document;
  var now = unsafeWindow.selectedCity;
  var i, arr = [], xpr = document.evaluate(p, context, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
  var cur = $X('id("cityLocation'+ now +'")/a') || all[all.length - 1];
  for (i = 0; item = xpr.snapshotItem(i); i++) arr.push(item);
  if (all.length) {
  return arr;
    now = all.map(function(a) { return a.id; }).indexOf(cur.id);
}
    click(all[(now + direction + all.length * 3) % all.length]);
  }
}


function keyboard(e) {
Example usage (with [[Coding_tips#array.forEach|Array.forEach]]):
  function invoke(a) {
    a = $X('id("actions")/ul[@class="cityactions"]/li[@class="'+ a +'"]/a');
    return function() { if (a && a.href) location.href = a.href; };
  }
  function counterClockwise() { focus(-1); }
  function clockwise() { focus(1); }
  function tab() {
    if (!e.altKey && !e.ctrlKey && !e.metaKey)
      focus(e.shiftKey ? -1 : 1);
  }


  var keys = {
var paragraphs = $x("//p");
    "\t": tab, j: counterClockwise, k: clockwise,
paragraphs.forEach(function(paragraph) {  // Loop over every paragraph
    d: invoke("diplomacy"), t: invoke("transport"),
  paragraph.innerHTML = "Halloa!";
    p: invoke("plunder"), b: invoke("blockade"), s: invoke("espionage")
});
  };


  var action = keys[String.fromCharCode(e.keyCode || e.charCode)];
'''Note:''' When you specify a context node, you need to use a [[XPath#Relative_paths|relative XPath expression]].
  if (action) {
    e.stopPropagation();
    e.preventDefault();
    action();
  }
}


function fetchScoresFor(name, ul, n, id) {
== Get elements by CSS selector ==
  function searchbutton(type) {
    var url = "url(/skin/" + ({
      total: "layout/medallie32x32_gold.gif) no-repeat -7px -9px",
    military: "layout/sword-icon2.gif) no-repeat 0 2px;",
        gold: "resources/icon_gold.gif) no-repeat 0 0; width:18px"
      })[type];
    return <input type="submit" name="highscoreType"
      value=" " title={"View player's "+ type +" score"}
      style={"border: 0; height: 23px; width: 16px; cursor: pointer; " +
            "color: transparent; background:"+ url}
      onclick={"this.value = '"+ post[type] +"'; this.form.submit();"}/>;
  }


  var scores = changeLink &&
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, $=.
    <a href="/index.php?view=options"
      title="Change score options">Change Options</a>;


  if (!inlineScore) {
function $$(xpath,root) {  
    var form = <form action="/index.php" method="post">
  xpath=xpath.replace(/((^|\|)\s*)([^/|\s]+)/g,'$2.//$3').
      <input type="hidden" name="view" value="highscore"/>
              replace(/\.([\w-]+)(?!([^\]]*]))/g,'[@class="$1" or @class$=" $1" or @class^="$1 " or @class~=" $1 "]').
      <input type="hidden" name="" id="searchfor"/>
              replace(/#([\w-]+)/g,'[@id="$1"]').
      <input type="hidden" name="searchUser" value={name}/>
              replace(/\/\[/g,'/*[');
    </form>;
  str='(@\\w+|"[^"]*"|\'[^\']*\')'
    for (var type in post)
  xpath=xpath.replace(new RegExp(str+'\\s*~=\\s*'+str,'g'),'contains($1,$2)').
      if (whatToShow & show[type])
              replace(new RegExp(str+'\\s*\\^=\\s*'+str,'g'),'starts-with($1,$2)').
        form.* += searchbutton(type);
              replace(new RegExp(str+'\\s*\\$=\\s*'+str,'g'),'substring($1,string-length($1)-string-length($2)+1)=$2');
    if (changeLink) {
  var got=document.evaluate(xpath,root||document,null,null,null), result=[];
      scores.@style = "position: relative; top: -6Px;";
  while(next=got.iterateNext()) result.push(next);
      form.* += scores;
  return result;
    }
}
    form.@style = "position: relative; "+ (changeLink ? "left:-26px; " : "") +
      "white-space: nowrap;";
    scores = form;
  }


  if (!inlineScore || changeLink)
    addItem("options", scores, ul);
  if (!inlineScore) return;


  for (type in show) {
Example usage:
    if (!(whatToShow & show[type]))
$$('#title')[0].innerHTML='Greased';
      continue;
$$('a[@href $= "user.js"]').forEach(function (a) {
    if ("gold" == type && isMyCity(ul) && viewingRightCity(ul)) {
  a.innerHTML='check it out a script';
      var gold = $("value_gold").innerHTML;
}
      updateItem(type, gold, cityinfoPanel(), null, lootable(gold));
$$('a[@href ^= "http"]').forEach(function (a) {
      continue;
  a.innerHTML += ' (external)';
    }
}
    addItem(type, "fetching...");
    requestScore(name, type, id, makeShowScoreCallback(name, type, ul, n, id));
  }
}


function isMyCity(ul, name) {
== Conditional logging ==
  if ("city" == document.body.id)
    return $X('id("position0")/a').href != "#";


  var name = getItem("owner", ul);
Used to easily toggle sending debug messages to the console. Passes all arguments on to [http://www.getfirebug.com/logging.html console.log], but only if <code>console</code> is defined (for backward compatibility) and <code>DEBUG</code> is <code>true</code>.
  var a = $X('a', name);
  if (a) {
    var id = a.search.match(/destinationCityId=(\d+)/)[1];
    return $X('id("citySelect")/option[@value="'+ id +'"]');
  }
  var city = itemValue("name", ul);
  return $X('id("citySelect")/option[.="'+ city +'"]');
}


function lootable(score, ul) {
Code and example usage:
  var amount = parseInt((score||"").replace(/\D+/g, "") || "0", 10);
  var panel = getItem("citylevel");
  var level = getItem("citylevel", ul);
  var size = itemValue(level);
  var max = Math.round(size * (size - 1) / 10000 * amount);
  if (isNaN(max)) return;
  max = node("span", "", null, "\xA0("+ fmtNumber(max) +"\)");
  max.title = "Amount of gold lootable from this town";
  return max;
}


function viewingRightCity(ul) {
const DEBUG = true;
  return itemValue("name") == itemValue("name", ul) &&
        itemValue("owner") == itemValue("owner", ul);
var links = document.links;
}
debug("Links: %o", links);
function debug() {
  if (DEBUG && console) {
    console.log.apply(this, arguments);
  }
}


function makeShowScoreCallback(name, type, ul, n, id) {
======Another way...======
  return function showScore(xhr, cached) {
...to do it would be this:
    var score = xhr;
    if ("yes" == cached) {
      score = fmtNumber(score);
    } else { // need to parse out the score
      score = $X('.//div[@class="content"]//tr[td[@class="name"]="' +
                name + '"]/td[@class="score" or @class="§"]',
                node("div", "", null, xhr.responseText));
      score = score.innerHTML;
    }
    if (score) {
      if ("yes" != cached) cacheValue(id, type, score);


      ul = ul || cityinfoPanel();
  const DEBUG = 1;
      if (n && "0" == score && "military" == type) {
 
        n.style.fontWeight = "bold"; // n.style.fontStyle = "italic";
  console =
        n = $X('../preceding-sibling::div[@class="cityimg"]', n);
  {
        if (n)
    log : function (text) { if( DEBUG ) unsafeWindow.console.log( text ); },
          n.style.backgroundImage = getComputedStyle(n,"").
    info : function (text) { if( DEBUG ) unsafeWindow.console.info( text ); },
            backgroundImage.replace("red.gif", "yellow.gif");
    warn : function (text) { if( DEBUG ) unsafeWindow.console.warn( text ); },
      }
    error : function (text) { if( DEBUG ) unsafeWindow.console.error( text ); }
  }


      // You rob gold (size * (size - 1)) % of the treasury of the city:
which allows you to just support more functions from the firebug console if you want and use it with unchanged syntax.
      if ("gold" == type)
        var max = lootable(score, ul);


      updateItem(type, score, ul, !!n, max);
== DOM node manipulation ==
    }
  };
}


function getCityLinks() {
=== Use .innerHTML to create DOM structure ===
  return $x('id("cities")/li[contains(@class,"city level")]/a');
The non W3 standard setter method [http://developer.mozilla.org/en/docs/DOM:element.innerHTML <code>.innerHTML</code>] 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.
}


function itemValue(item, ul) {
host = document.location.host;
  var li = "string" == typeof item ? getItem(item, ul) : item;
dummyDiv = document.createElement('div');
  var xpath = 'text()[preceding-sibling::*[1]/self::span[@class="textLabel"]]';
dummyDiv.innerHTML = '&lt;div>&lt;span style="color: red">Greased: ' + host + '&lt;/span>&lt;/div>';
  var text = $X(xpath, li);
document.body.insertBefore(dummyDiv.firstChild, document.body.firstChild);
  return text && trim(text.textContent || "");
}


function getItem(type, ul) {
Note above that <code>dummyDiv</code> is only needed as a holder for its <code>.firstChild</code>.
  return $X('li[contains(concat(" ",normalize-space(@class)," ")," '+
            type +' ")]', ul || cityinfoPanel());
}


function mkItem(type, value) {
Here is a helper function that reuses the <code>dummyDiv</code>:
  var li = node("li", type + " name", null, value);
  var title = (type in show) ?
    type.charAt().toUpperCase() + type.slice(1) + " Score:" : "Scores:";
  li.insertBefore(node("span", "textLabel", null, title), li.firstChild);
  return li;
}


function addItem(type, value, save) {
function firstNodeOf(html){
   var li = getItem(type);
   firstNodeOf.dummyDiv.innerHTML = html;
  if (li) {
   return firstNodeOf.dummyDiv.firstChild;
    li.lastChild.nodeValue = value;
   } else {
    var ul = cityinfoPanel();
    var next = $X('li[@class="ally"]/following-sibling::*', ul);
    ul.insertBefore(li = mkItem(type, value), next);
   }
   }
  if (save && !getItem(type, save)) {
firstNodeOf.dummyDiv = document.createElement('div');
    next = $X('li[@class="ally"]/following-sibling::*', save);
 
    save.insertBefore(li.cloneNode(true), next);
With this helper, one can write the following trivial script:
  }
  return li;
}


function updateItem(type, value, ul, islandView, append) {
document.body.appendChild(firstNodeOf('&lt;div>&lt;span style="color: red">END OF PAGE&lt;/span>&lt;/div>');
  var li = getItem(type, ul);
  if (li) {
    li.lastChild.nodeValue = value;
  } else {
    var next = $X('li[@class="ally"]/following-sibling::*', ul);
    ul.insertBefore(li = mkItem(type, value), next);
    if (viewingRightCity(ul) && islandView) // only touch panel on right focus
      updateItem(type, value, null, null, append && append.cloneNode(true));
  }
  if (append && !$X('span[@title]', li)) {
    li.style.whiteSpace = "nowrap";
    li.appendChild(append);
  }
  return li;
}


function cityinfoPanel() {
'''STRONGLY NOTE:''' <code>.innerHTML</code> seems to be sensitive to <code>document.contentType</code>. When the type is <code>text/plain</code> the <code>.innerHTML</code> setter does not parse its argument into DOM nodes, but instead returns #text nodes. The setter seems to work fine for types such as <code>text/html</code> or <code>application/xhtml+xml</code> but where and how it works is undocumented.
  return $X('id("information")//ul[@class="cityinfo"]');
}


function node(type, className, styles, content) {
'''Advantages:''' Concise and and probably more efficient than any hand coded approach.
  var n = document.createElement(type||"div");
  if (className) n.className = className;
  if (styles)
    for (var prop in styles)
      n.style[prop] = styles[prop];
  if (content)
    n.innerHTML = "string" == typeof content ? content : content.toXMLString();
  return n;
}


function click(node) {
'''Disadvantages:''' Other than the <code>text/plain</code> probem, <code>.innerHTML</code> is not a W3 standard and many people don't like non-standard code.
  var event = node.ownerDocument.createEvent("MouseEvents");
  event.initMouseEvent("click", true, true, node.ownerDocument.defaultView,
                      1, 0, 0, 0, 0, false, false, false, false, 0, node);
  node.dispatchEvent(event);
}


function fmtNumber(n) {
'''Upshot:''' Until [http://developer.mozilla.org/en/docs/E4X ECMAScript for XML (E4X)] becomes availble in user scripts, this is a useful hack for non <code>text/plain</code> pages.
  n += "";
==== Notes ====
  for (var i = n.length - 3; i > 0; i -= 3)
* <code>.innerHTML</code> [http://developer.mozilla.org/en/docs/DOM:element.innerHTML#Notes cannot be used to create parts of a <code>TABLE</code>]. E.g.
    n = n.slice(0, i) +","+ n.slice(i);
  return n;
}


function number(n) {
tab = firstNodeOf('&lt;table>&lt;tr>&lt;td> ... &lt;/td>&lt;/tr>&lt;/table>'); // WORKS
  n = { string: 1, number: 1 }[typeof n] ? n+"" : n.textContent;
tr  = firstNodeOf('&lt;tr>&lt;td> ... &lt;/td>&lt;/tr>');               // DOESN'T WORK
  return parseInt(n.replace(/\D+/g, "") || "0", 10);
td  = firstNodeOf('&lt;td> ... &lt;/td>');                         // DOESN'T WORK
}
td.innerHTML = ' ... ';                                      // WORKS


function trim(str) {
So, if a script builds a table from a HTML string, it has to be done as a whole rather than in parts.
  return str.replace(/^\s+|\s+$/g, "");
}


function onClick(node, fn, capture, e) {
=== Build a DOM node with attributes ===
  node.addEventListener((e||"") + "click", fn, !!capture);
}


function $(id) {
Creates a new DOM node with the given attributes.
  return document.getElementById(id);
}


function $x( xpath, root ) {
function createElement(type, attributes){
   var doc = root ? root.evaluate ? root : root.ownerDocument : document, next;
   var node = document.createElement(type);
  var got = doc.evaluate( xpath, root||doc, null, 0, null ), result = [];
   for (var attr in attributes) if (attributes.hasOwnProperty(attr)){
   switch (got.resultType) {
  node.setAttribute(attr, attributes[attr]);
    case got.STRING_TYPE:
      return got.stringValue;
    case got.NUMBER_TYPE:
      return got.numberValue;
    case got.BOOLEAN_TYPE:
      return got.booleanValue;
    default:
      while (next = got.iterateNext())
        result.push( next );
      return result;
   }
   }
}
  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);
}


function time(t) {
This works because even if <code>node</code> is the last node, <code>nextSibling</code> returns <code>null</code> so <code>insertBefore</code> puts the new node at the end.
  t = t || Date.now();
  return Math.floor(t / 6e4) - 2e7; // ~minute precision is enough
}


function $X( xpath, root ) {
Example usage:
  var got = $x( xpath, root );
  return got instanceof Array ? got[0] : got;
}


/*
var link = document.getElementById("the_link");
The AJAX request system so we can display the scores inline
var icon = document.createElement("img");
Original Author: wphilipw
icon.src = "…";
For version: 0.5.0
insertAfter(icon, link);
Last changed: 0.5.0
*/


function requestScore(name, type, id, onload) {
=== Hijacking browser properties ===
  var cached = id && valueCache[id], key = type.charAt();
  if (cached && cached[key] && ((time() - cached.T) < 10))
    return onload(cached[key], "yes");
  //else delete valueCache[id]; // stale -- but save for now; could be useful


  GM_xmlhttpRequest({
Sometimes you want to cook your own browser constants, for instance change the value of navigator.userAgent. Getters are good for that:
    method: "POST",
    url: "http://" + gameServer + "/index.php",
    data: "view=highscore&highscoreType="+ post[type] +"&searchUser="+ name,
    headers: {
      "User-agent": "Mozilla/4.0 (compatible) Greasemonkey",
      "Content-type": "application/x-www-form-urlencoded",
      "Accept": "application/atom+xml,application/xml,text/xml",
      "Referer": "http://" + gameServer + "/index.php"
    },
    onload: onload
  });
}


/*
var real = window.navigator.userAgent;
runs on first run to set up default values
var lie = function() { return real + " Macintosh"; };
Original Author: ImmortalNights
unsafeWindow.navigator.__defineGetter__("userAgent", lie);
For version: 0.5.4
Last changed: 0.6.0
*/


function displayOnOptions_fn() {
=== Extending the DOM with missing functions ===
  var mybox = node("div", "", { textAlign: "left" });
  var opts = <>
<h3>Score Display Options</h3>
<table border="0" cellpadding="0" cellspacing="0">
  <tr>
    <td style="width: 43%; text-align: right;">Show Total Score:</td>
    <td style="width: 57%"><input type="checkbox" id="totalScore"/></td>
  </tr>
  <tr>
    <td style="width:43%; text-align: right">Show Army Score:</td>
    <td><input type="checkbox" id="militaryScore"/></td>
  </tr>
  <tr>
    <td style="width: 43%; text-align: right">Show Gold Score:</td>
    <td><input type="checkbox" id="goldScore"/></td>
  </tr>
  <tr>
    <td style="width: 43%; text-align: right">Show Score Inline:</td>
    <td><input type="checkbox" id="inlineScore"/></td>
  </tr>
  <tr>
    <td style="width: 43%; text-align: right">Show Score Options link:</td>
    <td><input type="checkbox" id="changeScoreOptions"/></td>
  </tr>
</table></>;


  mybox.innerHTML = opts.toXMLString();
Other times you might want to emulate proprietary functionality of another browser, for instance when a site uses such features:
  var pwd = $('options_changePass');
 
  pwd.appendChild(mybox);
var getter = function() { return this.textContent; };
  var checkboxes = $x('//input[@type="checkbox" and contains(@id,"Score")]');
var setter = function(t) { return this.textContent = t; };
  for (var i = 0; i < checkboxes.length; i++) {
unsafeWindow.HTMLElement.prototype.__defineGetter__("innerText", getter);
    var input = checkboxes[i];
unsafeWindow.HTMLElement.prototype.__defineSetter__("innerText", setter);
    var id = input.id.replace("Score", "");
    if (id == "inline")
      input.checked = !!inlineScore;
    else if ("changeOptions" == id)
      input.checked = !!changeLink;
    else
      input.checked = !!(show[id] & whatToShow);
  }


  var inputs = $x('//input[@type="submit"]');
=== Advanced createElement for creating hierarchies of elements ===
  for (var e = 0; e < inputs.length; e++)
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):
    onClick(inputs[e], changeShow_fn, true);
createEl({n: nodename, a: {attr1: val, attr2: val}, c: [child1, child2], evl: {type: eventlistener_type, f: eventlistener_function, bubble: bool}}, appendTo)
}


/*
This function saves the options chosen above
Original Author: wphilipw
For version: 0.4.5
Last changed: 0.6.0
*/


function changeShow_fn(e) {
function createEl(elObj, parent) {
  GM_setValue("show", (
  var el;
                (show.total * $('totalScore').checked) |
  if (typeof elObj == 'string') {
                (show.military * $('militaryScore').checked) |
      el = document.createTextNode(elObj);
                (show.gold * $('goldScore').checked)
  }
              ) + "");
  else {
  GM_setValue("inline", $('inlineScore').checked);
      el = document.createElement(elObj.n);
  GM_setValue("link", $('changeScoreOptions').checked);
      if (el
  e.target.form.submit();
}

Revision as of 16:06, 16 May 2008

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);
  }
}
Another way...

...to do it would be this:

 const DEBUG = 1;
 
 console =
 { 
   log : function (text) { if( DEBUG ) unsafeWindow.console.log( text ); },
   info : function (text) { if( DEBUG ) unsafeWindow.console.info( text ); },
   warn : function (text) { if( DEBUG ) unsafeWindow.console.warn( text ); },
   error : function (text) { if( DEBUG ) unsafeWindow.console.error( text ); }
 }

which allows you to just support more functions from the firebug console if you want and use it with unchanged syntax.

DOM node manipulation

Use .innerHTML to create DOM structure

The non W3 standard setter method .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 = document.createElement('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

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) if (attributes.hasOwnProperty(attr)){
  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 (el