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.

GreasyThug – Greasemonkey, Gears and jQuery

I’ve been called a greasy thug, too. It never stops hurting. So here’s what we’re gonna do: We’re gonna grease ourselves up real good and trash that place with a baseball bat. – Homer

Presenting: GreasyThug

Here’s my problem, I want to develop Greasemonkey scripts. This doesn’t sound like a problem, but JavaScript has a certain terribleness to it, at least in its current browser implementations, and I can never go back to raw JS, NEVER. I’ve also grown accustom to having an interactive console for development and debugging, but Firebug doesn’t have access to Greasemonkey code. And another thing, shouldn’t I be able to make changes to a page, on the fly, and have them persist, without having to dig out my scripts and modify them? Shouldn’t every website be using Gears by now? Wouldn’t it be great to be able to use jQuery in your browser console on every website you go to?

Fact: GreasyThug will make all of your wildest dreams come true.

GreasyThug – Interactive JavaScript Console Features

  • Built in jQuery functionality.
  • Google Gears included.
  • A persistent command history across page reloads and browser restarts.
  • Drag and drop – remembers where you put it for each site.
  • Ability to persist micro-scripts and apply them automatically everytime you visit the page.

Warning! GreasyThug is slick (it’s the grease) and dangerous (it’s a thug). An interactive console is essentially a pipe straight into eval(). So… BE CAREFUL! If your thug becomes compromised it will be your house that gets trashed with a baseball bat. Remember, this is eval in the elevated Greasemonkey privilages context, its strength for development is also its weakness for security.

Prerequisites

Demonstration

Let’s spruce up the google search page. Maybe we should make a whole Greasemonkey user script? Nah, that’s a huge hassle now that we already have GreasyThug.

  1. Go to Google.com
  2. A “The website below wants to store information on your computer using Gears” security warning will pop up, as it will do for everydomain that you have GreasyThug enabled for. It’s not really the website using Gears, though some might eventually. Click “Allow”. (This is how the command history and micro-scripts are saved).
  3. Now let’s get cracking! Drag the interactive console to a comfortable location. (It will begin in the top left by default)greasy_thug-1
  4. Execute some JavaScript statements to get a feel for it. No need for semicolons, we’re not chumps.
  5. Now on to the cool stuff: that white background is a little bland for Valentine’s Day, let’s spice it up. Pop this into your console:
    $('body').css('background-color', '#F8A')

    greasy_thug-03 It’s beautiful! See how I can use jQuery? Neat! Also, the up arrow populates the input with my previous command.

  6. Maybe it’s not quite as good looking as I thought, probably best to stick with white… let’s just refresh and forget about this debacle. greasy_thug-04 Back to normal… but the history remembered my command in case I want to try it again.
  7. It is my strong belief that there should be a link to STRd6 right next to everyone’s email address on the Google search page. Obviously this should only be for logged in users… I can only change it for myself though…
    $('#gb nobr').prepend($('STRd6'))
  8. But what about when I refresh… it’ll disappear and all that hard work will be gone?!? Not so good friend:
    savePrevious()

    This will store whatever command you last executed to be executed again when you return. You can save many commands. These are those micro-scripts that you’ve been hearing so much about and they are the future.greasy_thug-06

So is this the end? It is for today. Now imagine sharing micro-scripts with your friends. It’s our internet now. It just takes some elbow grease and a little thuggery.

Feature requests go in the comments.

What a drag: cancelling with onStart

So in this awesome new web application that I am writing I’ve got this totally sweet window/widget/bazfoo system where users can drag stuff around and it will remember the positions. This is all with Ruby on Rails and Prototype and Scriptaculous, and although the specifics are pretty Scriptaculous specific the generalities can apply to you favorite framework, unless it is … well they could probably apply.

Generally the UI doesn’t care what users want to drag when, but sometimes it does care. Say for example players can move stuff around in other players’ houses (to make it look like a ghost was there or something). This is very cool. Problem: what about not dragging things when not in ‘move-stuff-around-like-a-ghost’ mode? Perhaps there should only be one mode and things can always be moved around like a ghost, but even then, based on context, it seems that you might just not want the player to move stuff sometimes. I didn’t. Even though ‘rampant-ghost’ was the only mode I had so far I could envision wanting to drag an area-of-effect-spell-deployment or power meter or anything else without worrying about dragging the furniture.

So I had to sometimes cancel dragging like some companies sometimes cancel bonuses, but unlike those companies I decided to cancel during the onStart callback, not the somewhereInTheMiddleGodKnowsWhy callback. Scriptaculous provides a convenient hook to onStart (but surprisingly lacks one to halfwayThereButSimultaneoslyCutYourPay. They don’t have it? I know, it’s silly!). So lets just throw some code down to keep it real:

function drag_start(draggable, event) {
  draggable.element.should_revert = true;

  if($current_action == null || $current_action.id != "ghost_party_action") {
    draggable.finishDrag(event, false);
  }
}

This is a pretty simple function, really standard, we have all our draggables sign it. First tell the element that it should revert unless it hears otherwise. Then check the $current_action (the $ lets me know I’m using it as a global, just like Ruby). If the current action doesn’t exist or it’s not the one where the ghosts party, then finish it off like Houchen. Great. Except, it still kind of drags and gets all weird. I could have sworn I was using the onStart callback, not the getWeirdAnyway callback… too bad.

So time to dive into Scriptaculous, source code that is! The file is dragdrop.js, the year 2008, film Noir has lost popularity in recent years but is still present in the minds of… The functions of interest are:

  updateDrag: function(event, pointer) {
    if(!this.dragging) this.startDrag(event);
    // ... Lots more omitted
  }

  startDrag: function(event) {
    this.dragging = true;
    // ... Lots of setup, initialization ...

    // Bingo!
    Draggables.notify('onStart', this, event);

    if(this.options.starteffect) this.options.starteffect(this.element);
  },

updateDrag gets called first when the user wiggles that mouse over the element. updateDrag then calls startDrag, which then calls your callback, which then ends the drag, but updateDrag is still in the dark, so let’s enlighten it:

  updateDrag: function(event, pointer) {
    if(!this.dragging) this.startDrag(event);
    // Added part
    if(!this.dragging) {
      Event.stop(event);
      return;
    }

    // Lots more stuff stays the same
  }

See what happened there? updateDrag wasn’t expecting the drag to end so soon after it got started! Now it knows, let that be a lesson. This keeps it from going all loosey goosey everywhere.

Now that is how you cancel a drag with onStart. Leave a comment, it will probably be at least as cool as the UK lottery comment. Also subscribe to my RSS and tell a friend, it’s like twice a month that I publish anything and then you can just scroll past it in Google Reader to get back to your 300 unread TechCrunch posts. Peace!