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;
 
  $('<div />')
    .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!

Leave a Reply

Your email address will not be published. Required fields are marked *