JavaScript Hashtable for Objects

Gerta Rauss: So, we all agree that a closure is the best decision for all involved?

Juno MacGuff: SSHHIT! YES! Closure it up!

JavaScript…

there comes a time in every developer’s life when he wishes to be able to use a Hash in JavaScript like he can use a Hash in Ruby, you know, easily. This usually comes up when implementing A* or some other crazy graph search algorithm.

The problem: JavaScript objects make great Hashes for String and Number keys, but not for Object keys. Some QUnit tests highlight the problem readily.

    test("Hash sux?", function() {
      var x = {};
      var y = {name: "My Cool Object"};
      var barbazfoo = {name: "My Uncool Object"};

      x['a'] = true;
      x['b'] = true;

      equals(x['a'], true);
      equals(x['b'], true);

      x[y] = y.name;
      x[barbazfoo] = barbazfoo.name;

      equals(x[y], y.name); // Failed! y.toString == "[object Object]"
      equals(x[barbazfoo], barbazfoo.name);
    });

Now we have two options as I see it: download (or hand roll) our own Hashtable class that has even more akward syntax, worse performance and clocks in at 5k minified… or implement a toString() method that generates unique strings for each object (of types that we care about).

I opted for the second choice, after coming very close to choosing the first. Now if only there were some magical way to implement a unique toString method for all my objects that might want to end up as hash keys.

(function() {
    var id = 0;

    /*global GameObject */
    GameObject = function(game) {
      var self = {
        // Empty update by default
        update: function() {},
        click: function() {},
        objectId: '#Object:' + (id++),
        toString: function() {
          return self.objectId;
        }
      };
      return self;
    };
  })();

Gerta Rauss: So, we all agree that a closure is the best decision for all involved?

Juno MacGuff: SSHHIT! YES! Closure it up!

Now any GameObject can be used as a key to a hash, and as long as you aren’t going nuts and mixing and matching crazy strings as your keys you should have no collisions, or at least way fewer than 100%. Thanks a heap ‘[object Object]’!.

Pixie – A JavaScript Pixel Editor

As part of this amazing new game that we are working on we have an online pixel editor component. Right now it is a rough proof of concept. The primary goal is to create a simple online editor that provides the right blend of tools to easily create 32×32 pixel images which can then be saved locally or uploaded to the server. You can even load previously uploaded images, sweet!

The inspiration is Pixen, but instead of being Mac-only it will be available online for all platforms. Pixen provides a strong tool set and a great interface, but why be so exclusive about it? I know, right? The browser is the future anyway, and doesn’t crash as often.

Check out Pixie here. The idea is to keep it simple, there are only a few features missing from fullfilling every one of my wildest dreams (undo, selection, semi-transparency). For fun check out the JS code, it was designed with clarity and extensibility in mind. It’s a purely JS/CSS based editor, built using Prototype.js.

Editing images is fun!
Editing images is fun!

To get the images to the server they undergo an amazing (and probably inefficient) journey. Each pixel is read from the “canvas” and put into an ancient (late 90s) JS PNG encoder. That PNG encoder then spits out the image as a png data file which is then base64 encoded and sent to the server. The server base64 decodes the file and saves it to the local file system. Loading images from the server makes it even crazier! Since this ancient PNG encoder doesn’t load PNG data (to my knowledge) the server uses RMagick to read the uploaded image’s pixels and convert them to a JS array of hex color values, then passes that array into a load method back on the client. It’s amazing!

Have fun editing all those little images, and you can even check out the game that goes with it!

Using GreasyThug to Answer a Greasemonkey Question

On StackOverflow a user asked: How can I create an object of a class which defined in the remote page?

The page includes code like this (which I entered into Firebug):

function foo(){ this.bar = 0; }

Then I verified that it could be read from Greasemonkey with the GreasyThug console by the following expression:

_foo = unsafeWindow.foo;
x = new _foo();
Debugging with GreasyThug
Debugging with GreasyThug

This caused a “Not enough arguments” error, whatever the hell that is. Not quite the poster’s actual error. What if we added an argument? “Illegal Value” Bingo! Replicated the issue. Now to solve it.

Let’s try and migrate the function over into the Greasemonkey script zone.

_foo = eval('(' + unsafeWindow.foo.toSource() + ')')
=> function foo(){ this.bar = 0; }

That’s the ticket! Now to instantiate and verify:

The magic of a debugging thug
The magic of a debugging thug

Ship it! Holla!

Two Column Google Greasemonkey Script

I remember installing a userscript that would display Google search results in two columns in days of yore. Then one day it stopped working. All the other Google userscripts were massive customize everything about Google ever. I just want two columns homie, and favicons, but I’ve already got the FF plugin for that.

So here it is, the amazing remake that is as good as the original… Two Column Google, nothing fancy, just two columns.

Display Google search results in two columns
Display Google search results in two columns

jQuery Selector Tester

The latest release in an ongoing bender of Greasemonkey scripts involving jQuery: the jQuery Selector Tester!

Highlighting all li elements
Highlighting all li elements

This little guy will hang out on your internet and highlight all the elements that match the selectors you type in. Indispensible for development! It doesn’t remember where you leave it on the page yet so it’s probably easiest to turn on only when needed.

Highlighting all anchors that are descendants of h3
Highlighting all anchors that are descendants of h3

Find some more of my jQuery Greasemonkey scripts here. They are sometimes ahead and sometimes behind what I post on the blog.

Introducing: Speakeasy.js – It's kind of like ActiveResource

During this Greasemonkey bender I’m on I wanted to get a cleaner interface for working with GM_xmlhttpRequest. I’m using a resourceful style Rails app to serve up my data so I jimmied up this little library to handle my data storage, update, and retrieval needs. This requires jQuery and is designed to be included in a Greasemonkey script. Example to follow.

/*
  Speakeasy.js
  Version: 0.1.0
  It's kind of like ActiveResource
  Copyright (c) 2009, STRd6 (http://strd6.com)
  Liscensed under the MIT License

  Prerequisites:
    Greasemonkey Environment
    jQuery
*/

/**
  Speakeasy abstracts the GM_xmlhttprequest and handles communication with the remote script server.
  It's kind of like ActiveResource
 */
Speakeasy = function($) {
  var baseUrl = 'http://localhost:3000/';
  var apiKey = 0;

  function generateArrayDataTransfer(objectType, callback) {
    return function(responseData) {
      var dataArray = eval('(' + responseData + ')');
      var elements = $.map(dataArray, function(element) {
        return element[objectType];
      });
      callback(elements);
    };
  }

  function generateDataTransfer(objectType, callback) {
    return function(responseData) {
      var data = eval('(' + responseData + ')');
      callback(data[objectType]);
    };
  }

  function loadOptionsData(type, dataObject) {
    var optionsData = {
      api_key: apiKey
    };

    $.each(dataObject, function(field, value) {
      log(field + ': ' + value)
      optionsData[type + '[' + field +']'] = value;
    });
    return optionsData;
  }

  function makeRequest(resource, options) {
    var method = options.method || 'GET';
    var url = baseUrl + resource + '.js';
    var headers = {
      'User-agent': 'Mozilla/4.0 (compatible) Greasemonkey',
      'Accept': 'application/json,application/atom+xml,application/xml,text/xml'
    };
    var data = $.param(options.data || '');
    var onSuccess = options.onSuccess || (function(){});

    if(method == 'POST') {
      headers['Content-type'] = 'application/x-www-form-urlencoded';
    } else if(method == 'GET') {
      if(data) {
        url += '?' + data;
      }
    }

    GM_xmlhttpRequest({
      method: method,
      url: url,
      headers: headers,
      data: data,

      onload: function(responseDetails) {
        if(responseDetails.status == 200) {
          onSuccess(responseDetails.responseText);
        } else {
          console.warn(url + ' - ' + responseDetails.status + ':nn' + responseDetails.responseText);
        }
      }
    });
  }

  function generateResource(type) {
    var pluralType = type + 's';

    var all = function() {
      return function(options, callback) {
        var dataTransfer = generateArrayDataTransfer(type, callback);
        options.onSuccess = dataTransfer;
        makeRequest(pluralType, options);
      };
    }();

    var create = function() {
      return function(dataObject, callback) {
        var options = {
          method: 'POST'
        };

        options.data = loadOptionsData(type, dataObject);
        makeRequest(pluralType, options);
      };
    }();

    var find = function() {
      return function(options, callback) {
        var dataTransfer = generateDataTransfer(type, callback);
        if(typeof(options) == 'number') {
          options.onSuccess = dataTransfer;
          makeRequest(pluralType + '/' + options, dataTransfer);
        } else {
          log("TODO: Non-integer find not currently supported!");
        }
      };
    }();

    var update = function() {
      return function(dataObject, callback) {
        var id = dataObject.id;
        var options = {
          method: 'POST'
        };

        options.data = loadOptionsData(type, dataObject);
        makeRequest(pluralType + '/update/' + id, options);
      };
    }();

    var resource = {
      all: all,
      create: create,
      find: find,
      update: update
    };

    return resource;
  }

  var self = {
    annotation: generateResource('annotation'),
    script: generateResource('script')
  };

  return self;
}(jQuery);

Example uses:

// ==UserScript==
// @name           Speakeasy Demo
// @namespace      http://strd6.com
// @description    Super-simple website annotations shared with all!
// @include        *
//
// @require     http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js
// @require     http://strd6.com/stuff/jqui/speakeasy.js
// ==/UserScript==

function display(annotation) {
  var id = annotation.id;

  $('
') .text(annotation.text) .addClass('annotation') .css({ top: annotation.top, left: annotation.left }) .bind('drag', function( event ) { $( this ).css({ top: event.offsetY, left: event.offsetX }); }) .bind('dragend', function( event ) { Speakeasy.annotation.update({id: id, top: $(this).css('top'), left: $(this).css('left')}); }) .fadeTo('fast', 0.75) .appendTo('body'); } Speakeasy.annotation.all({data: {url: currentUrl}}, function(annotations) { $.each(annotations, function(index, annotation) { display(annotation); }); });

Enjoy!

How to load jQuery UI CSS In Greasemonkey

// ==UserScript==
// @name           Test
// @namespace      http://strd6.com
// @description    jquery-ui-1.6rc6 Resource Include Test
// @include        *
//
// @resource       jQuery               http://ajax.googleapis.com/ajax/libs/jquery/1.3.1/jquery.min.js
// @resource       jQueryUI             http://strd6.com/stuff/jqui/jquery-ui-personalized-1.6rc6.min.js
//
// @resource       jQueryUICSS          http://strd6.com/stuff/jqui/theme/ui.all.css
//
// @resource    ui-bg_diagonals-thick_18_b81900_40x40.png       http://strd6.com/stuff/jqui/theme/images/ui-bg_diagonals-thick_18_b81900_40x40.png
// @resource    ui-bg_glass_100_f6f6f6_1x400.png                http://strd6.com/stuff/jqui/theme/images/ui-bg_glass_100_f6f6f6_1x400.png
// @resource    ui-bg_diagonals-thick_20_666666_40x40.png       http://strd6.com/stuff/jqui/theme/images/ui-bg_diagonals-thick_20_666666_40x40.png
// @resource    ui-bg_glass_65_ffffff_1x400.png                 http://strd6.com/stuff/jqui/theme/images/ui-bg_glass_65_ffffff_1x400.png
// @resource    ui-bg_gloss-wave_35_f6a828_500x100.png          http://strd6.com/stuff/jqui/theme/images/ui-bg_gloss-wave_35_f6a828_500x100.png
// @resource    ui-icons_222222_256x240.png                     http://strd6.com/stuff/jqui/theme/images/ui-icons_222222_256x240.png
// @resource    ui-bg_flat_10_000000_40x100.png                 http://strd6.com/stuff/jqui/theme/images/ui-bg_flat_10_000000_40x100.png
// @resource    ui-icons_ef8c08_256x240.png                     http://strd6.com/stuff/jqui/theme/images/ui-icons_ef8c08_256x240.png
// @resource    ui-icons_ffd27a_256x240.png                     http://strd6.com/stuff/jqui/theme/images/ui-icons_ffd27a_256x240.png
// @resource    ui-bg_glass_100_fdf5ce_1x400.png                http://strd6.com/stuff/jqui/theme/images/ui-bg_glass_100_fdf5ce_1x400.png
// @resource    ui-icons_228ef1_256x240.png                     http://strd6.com/stuff/jqui/theme/images/ui-icons_228ef1_256x240.png
// @resource    ui-icons_ffffff_256x240.png                     http://strd6.com/stuff/jqui/theme/images/ui-icons_ffffff_256x240.png
// @resource    ui-bg_highlight-soft_75_ffe45c_1x100.png        http://strd6.com/stuff/jqui/theme/images/ui-bg_highlight-soft_75_ffe45c_1x100.png
// @resource    ui-bg_highlight-soft_100_eeeeee_1x100.png       http://strd6.com/stuff/jqui/theme/images/ui-bg_highlight-soft_100_eeeeee_1x100.png
// ==/UserScript==

// Inject jQuery into page... gross hack... for now...
(function() {
  var head = document.getElementsByTagName('head')[0];

  var script = document.createElement('script');
  script.type = 'text/javascript';

  var jQuery = GM_getResourceText('jQuery');
  var jQueryUI = GM_getResourceText('jQueryUI');

  script.innerHTML = jQuery + jQueryUI;
  head.appendChild(script);

  $ = unsafeWindow.$;
})();

// Load UI Styles
(function() {
    var resources = {
      'ui-bg_diagonals-thick_18_b81900_40x40.png': GM_getResourceURL('ui-bg_diagonals-thick_18_b81900_40x40.png'),
      'ui-bg_glass_100_f6f6f6_1x400.png': GM_getResourceURL('ui-bg_glass_100_f6f6f6_1x400.png'),
      'ui-bg_diagonals-thick_20_666666_40x40.png': GM_getResourceURL('ui-bg_diagonals-thick_20_666666_40x40.png'),
      'ui-bg_glass_65_ffffff_1x400.png': GM_getResourceURL('ui-bg_glass_65_ffffff_1x400.png'),
      'ui-bg_gloss-wave_35_f6a828_500x100.png': GM_getResourceURL('ui-bg_gloss-wave_35_f6a828_500x100.png'),
      'ui-icons_222222_256x240.png': GM_getResourceURL('ui-icons_222222_256x240.png'),
      'ui-bg_flat_10_000000_40x100.png': GM_getResourceURL('ui-bg_flat_10_000000_40x100.png'),
      'ui-icons_ef8c08_256x240.png': GM_getResourceURL('ui-icons_ef8c08_256x240.png'),
      'ui-icons_ffd27a_256x240.png': GM_getResourceURL('ui-icons_ffd27a_256x240.png'),
      'ui-bg_glass_100_fdf5ce_1x400.png': GM_getResourceURL('ui-bg_glass_100_fdf5ce_1x400.png'),
      'ui-icons_228ef1_256x240.png': GM_getResourceURL('ui-icons_228ef1_256x240.png'),
      'ui-icons_ffffff_256x240.png': GM_getResourceURL('ui-icons_ffffff_256x240.png'),
      'ui-bg_highlight-soft_75_ffe45c_1x100.png': GM_getResourceURL('ui-bg_highlight-soft_75_ffe45c_1x100.png'),
      'ui-bg_highlight-soft_100_eeeeee_1x100.png': GM_getResourceURL('ui-bg_highlight-soft_100_eeeeee_1x100.png')
    };

    var head = document.getElementsByTagName('head')[0];

    var style = document.createElement('style');
    style.type = 'text/css';

    var css = GM_getResourceText ('jQueryUICSS');
    $.each(resources, function(resourceName, resourceUrl) {
      console.log(resourceName + ': ' + resourceUrl);
      css = css.replace( 'images/' + resourceName, resourceUrl);
    });

    style.innerHTML = css;
    head.appendChild(style);
})();

This technique works whether or not you inject or @require the jQuery js libraries.

The drawback to injecting jQuery is that it is forced to run in the unsafe window context which doesn’t allow you to use GM_* methods in callbacks (this make $.each pretty weak). Also, it breaks pages that define the $ function, but I believe this can be avoided by telling jQuery not to interfere and you can set $ as the local alias within your GM script zone (if that makes any sense).

The drawback of using require is that the UI classes throw exceptions (at least on version 1.6rc6). The dialogs display ok, but you need to catch the exceptions they throw. Also, they throw an exception when you try and drag. I’m pretty sure that it has to do with XPCNativeWrapper. One day UI will be easy in Greasemonkey… one day… Until then this should get you part way there.

The @resource technique works for more than just jQueryUI, use it for your own css and images, at least until jQuery UI gets fixed.

I can't stop thinking about these little yellow boxes…

I just created an Annotations spin-off of GreasyThug. This doesn’t have an interactive console, or use Gears. It does let you add comments to any web page in any location, for all the world (who have installed the script) to see.

Take a look at this page after installing it, and leave some annotations of your own.