Index: dreditor.user.js
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/dreditor/dreditor.user.js,v
retrieving revision 1.63
diff -u -p -r1.63 dreditor.user.js
--- dreditor.user.js	18 Dec 2010 19:44:04 -0000	1.63
+++ dreditor.user.js	18 Dec 2010 23:28:20 -0000
@@ -8,12 +8,62 @@
 // @include        https://drupal.org/*
 // ==/UserScript==
 
-// Initialize window objects.
-$ = window.$ = window.jQuery = unsafeWindow.jQuery;
-Drupal = window.Drupal = unsafeWindow.Drupal;
-// Bail out in (the unlikely) case that JS has been disabled.
-if (Drupal === undefined) {
-  return false;
+/**
+ * User script environment negotiation and initialization.
+ *
+ * Support and available features for user scripts highly varies across browser
+ * vendors. Some browsers (e.g., Firefox) require to install a browser extension
+ * (GreaseMonkey) in order to install and execute user scripts. Some others
+ * have built-in support for user scripts, but do not support all features of
+ * GreaseMonkey (variable storage, cross-domain XHR, etc). In the special case
+ * of Chrome, user scripts are executed before the DOM has been fully loaded and
+ * initialized; they can only access and manipulate the plain DOM document as
+ * is, but none of the scripts on the actual page are loaded yet.
+ *
+ * Therefore, different environments need to be negotiated:
+ * - A fully-fledged GreaseMonkey environment: Two execution environments exist,
+ *   unsafeWindow and window (scope-limited to the user script). Initialization
+ *   has to ensure that the required objects of unsafeWindow (jQuery and Drupal)
+ *   are available in the scope-limited window.
+ * - A pre-execution environment: None of the scripts on the actual page have
+ *   been loaded or executed yet, so this entire user script cannot do anything.
+ *   The initialization injects a SCRIPT tag into the document and ends. The
+ *   page will load a hosted copy of the entire user script, which will be
+ *   executed within the normal window environment of the actual page.
+ * - A regular window environment (derived from previous bullet): The script is
+ *   executed as if it would have been loaded by the actual page itself.
+ */
+
+// If unsafeWindow is defined, then we are in a user script environment.
+if (typeof unsafeWindow != 'undefined') {
+  // If unsafeWindow does not contain the jQuery object, then we are in a
+  // pre-execution environment and this script will break, since jQuery and
+  // Drupal objects are missing. We inject a SCRIPT tag that loads a hosted
+  // version of the script.
+  if (typeof unsafeWindow.jQuery == 'undefined') {
+    var script = document.createElement("script");
+    script.setAttribute('type', 'text/javascript');
+    // ViewVC on drupalcode.org would involve server-side processing on every
+    // single access, so we use a manual mirror.
+    script.setAttribute('src', 'http://drupal.unleashedmind.com/dreditor/dreditor.user.js');
+    document.getElementsByTagName('head')[0].appendChild(script);
+
+    // End execution.
+    return;
+  }
+
+  // Otherwise, ensure that jQuery and Drupal objects of the unsafeWindow are
+  // available in the scope-limited window environment.
+  // @todo Implement usual closure to provide jQuery in $.
+  $ = window.$ = window.jQuery = unsafeWindow.jQuery;
+  Drupal = window.Drupal = unsafeWindow.Drupal;
+}
+// If we are in a GreaseMonkey environment and JavaScript is disabled, user
+// scripts are executed nevertheless and can still act on the DOM, but none of
+// the scripts on the actual page are executed. Cancel processing in this case.
+// Drupal is also undefined when drupal.org is down.
+if (typeof Drupal == 'undefined') {
+  return;
 }
 
 /**
@@ -30,8 +80,15 @@ if (Drupal === undefined) {
  */
 jQuery.extend({
   debug: function () {
-    // Setup debug storage in global window. We want to look into it.
-    window.debug = unsafeWindow.debug = window.debug || [];
+    // Initialize window.debug storage, to make debug data accessible later
+    // (e.g., via browser console). Although we are going to possibly store
+    // named keys, this needs to be an Array, so we can determine its length.
+    window.debug = window.debug || [];
+    // Keep unsafeWindow.debug and window.debug in sync in user script
+    // environments.
+    if (typeof unsafeWindow != 'undefined') {
+      unsafeWindow.debug = window.debug;
+    }
 
     args = jQuery.makeArray(arguments);
     // Determine data source; this is an object for $variable.debug().
@@ -305,7 +362,13 @@ Drupal.dreditor = {
     //window.scrollTo(0, $(selector).offset().top);
     // Gecko-only method to scroll DOM elements into view.
     // @see https://developer.mozilla.org/en/DOM/element.scrollIntoView
-    $(selector).get(0).scrollIntoView();
+    var $target = $(selector);
+    if ($target.length) {
+      $target.get(0).scrollIntoView();
+    }
+    else if (typeof console.warn == 'function') {
+      console.warn(selector + ' does not exist.');
+    }
   },
 
   /**
@@ -493,38 +556,26 @@ Drupal.storage.unserialize = function (v
 };
 
 /**
- * @defgroup form_api JavaScript port of Form API
+ * @defgroup form_api JavaScript port of Drupal Form API
  * @{
  */
 
-/**
- * Dreditor JavaScript form API.
- *
- * Due to Greasemonkey limitations, we have to instantiate new form objects in
- * a wrapper object to pass Drupal.dreditor.form as context to the prototype.
- */
 Drupal.dreditor.form = {
   forms: [],
 
-  create: function (id) {
-    // We must pass this as wrapping object.
-    return new this.form(this, id);
+  create: function (form_id) {
+    return new this.form(form_id);
   }
 };
 
-Drupal.dreditor.form.form = function (o, id) {
+Drupal.dreditor.form.form = function (form_id) {
   var self = this;
 
   // Turn this object into a jQuery object, being a form. :)
-  $.extend(true, self, $('<form id="' + id + '"></form>'));
+  $.extend(true, self, $('<form id="' + form_id + '"></form>'));
 
   // Override the default submit handler.
   self.submit(function (e) {
-    // Invoke the submit handler of the clicked button.
-    var op = e.originalEvent.explicitOriginalTarget.value;
-    if (self.submitHandlers[op]) {
-      self.submitHandlers[op](this, self);
-    }
     // Unless proven wrong, we remove the form after submission.
     self.remove();
     // We never really submit.
@@ -536,9 +587,14 @@ Drupal.dreditor.form.form.prototype = {
   submitHandlers: {},
 
   addButton: function (op, onSubmit) {
-    this.submitHandlers[op] = onSubmit;
-    this.append('<input name="op" class="dreditor-button" type="submit" value="' + op + '" />');
-    // Return the jQurey form object to allow for chaining.
+    var self = this;
+    self.submitHandlers[op] = onSubmit;
+    var $button = $('<input name="op" class="dreditor-button" type="submit" value="' + op + '" />');
+    $button.bind('click.form', function () {
+      self.submitHandlers[op].call(self, $button);
+    });
+    this.append($button);
+    // Return the jQuery form object to allow for chaining.
     return this;
   }
 };
@@ -669,10 +725,10 @@ Drupal.dreditor.patchReview = {
       // Add comment textarea.
       self.$form.append('<textarea name="comment" class="form-textarea resizable" rows="10"></textarea>');
       // Add comment save button.
-      self.$form.addButton((self.data.id !== undefined ? 'Update' : 'Save'), function (form, $form) {
+      self.$form.addButton((self.data.id !== undefined ? 'Update' : 'Save'), function ($button) {
         // @todo For any reason, FF 3.5 breaks when trying to access
         //   form.comment.value. Works in FF 3.0.x. WTF?
-        var value = $form.find('textarea').val();
+        var value = this.find('textarea').val();
         // Store new comment, if non-empty.
         if ($.trim(value).length) {
           self.comment.save({
@@ -685,13 +741,13 @@ Drupal.dreditor.patchReview = {
         self.reset();
       });
       // Add comment cancel button.
-      self.$form.addButton('Cancel', function (form, $form) {
+      self.$form.addButton('Cancel', function ($button) {
         // Reset pastie.
         self.reset();
       });
       // Add comment delete button for existing comments.
       if (self.data.id !== undefined) {
-        self.$form.addButton('Delete', function (form, $form) {
+        self.$form.addButton('Delete', function ($button) {
           self.comment.delete(self.data.id);
           // Reset pastie.
           self.reset();
