Index: includes/common.inc
===================================================================
RCS file: /cvs/drupal/drupal/includes/common.inc,v
retrieving revision 1.467
diff -u -r1.467 common.inc
--- includes/common.inc	16 Aug 2005 18:06:18 -0000	1.467
+++ includes/common.inc	17 Aug 2005 00:39:59 -0000
@@ -1464,6 +1464,10 @@
  *   The internal name used to refer to the field.
  * @param $value
  *   The stored data.
+ * @param $edit
+ *   The array name to prefix to the $name.
+ * @param $attributes
+ *   An array of HTML attributes for the input tag.
  * @return
  *   A themed HTML string representing the hidden field.
  *
@@ -1471,8 +1475,8 @@
  * but be sure to validate the data on the receiving page as it is possible for
  * an attacker to change the value before it is submitted.
  */
-function form_hidden($name, $value, $edit = 'edit') {
-  return '<input type="hidden" name="'. $edit .'['. $name .']" value="'. check_plain($value) ."\" />\n";
+function form_hidden($name, $value, $edit = 'edit', $attributes = NULL) {
+  return '<input type="hidden" name="'. $edit .'['. $name .']" id="'. form_clean_id($edit .'-'. $name) .'" value="'. check_plain($value) .'"'. drupal_attributes($attributes) ." />\n";
 }
 
 /**
@@ -1491,7 +1495,7 @@
  *   A themed HTML string representing the button.
  */
 function form_button($value, $name = 'op', $type = 'submit', $attributes = NULL) {
-  return '<input type="'. $type .'" class="form-'. $type .'" name="'. $name .'" value="'. check_plain($value) .'" '. drupal_attributes($attributes) ." />\n";
+  return '<input type="'. $type .'" class="form-'. $type .'" name="'. $name .'" id="'. form_clean_id($name) .'" value="'. check_plain($value) .'" '. drupal_attributes($attributes) ." />\n";
 }
 
 /**
@@ -1809,6 +1813,51 @@
 }
 
 /**
+ * Generates a Javascript call, while importing the arguments as is.
+ * PHP arrays are turned into JS objects to preserve keys. This means the array
+ * keys must conform to JS's member naming rules.
+ *
+ * @param $function
+ *   The name of the function to call.
+ * @param $arguments
+ *   An array of arguments.
+ */
+function drupal_call_js($function) {
+  $arguments = func_get_args();
+  array_shift($arguments);
+  $args = array();
+  foreach ($arguments as $arg) {
+    $args[] = drupal_to_js($arg);
+  }
+  $output = '<script type="text/javascript">'. $function .'('. implode(', ', $args) .');</script>';
+  return $output;
+}
+
+/**
+ * Converts a PHP variable into its Javascript equivalent.
+ */
+function drupal_to_js($var) {
+  switch (gettype($var)) {
+    case 'boolean':
+    case 'integer':
+    case 'double':
+      return $var;
+    case 'resource':
+    case 'string':
+      return '"'. str_replace(array("\r", "\n"), array('\r', '\n'), addslashes($var)) .'"';
+    case 'array':
+    case 'object':
+      $output = array();
+      foreach ($var as $k => $v) {
+        $output[] = $k .': '. drupal_to_js($v);
+      }
+      return '{ '. implode(', ', $output) .' }';
+    default:
+      return 'null';
+  }
+}
+
+/**
  * Implode a PHP array into a string that can be decoded by the autocomplete JS routines.
  *
  * Items are separated by double pipes. Each item consists of a key-value pair
cvs diff: Diffing misc
Index: misc/autocomplete.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/autocomplete.js,v
retrieving revision 1.4
diff -u -r1.4 autocomplete.js
--- misc/autocomplete.js	11 Aug 2005 13:00:17 -0000	1.4
+++ misc/autocomplete.js	17 Aug 2005 00:39:59 -0000
@@ -17,10 +17,9 @@
       if (!acdb[uri]) {
         acdb[uri] = new ACDB(uri);
       }
-      id = input.id.substr(0, input.id.length - 13);
-      input = document.getElementById(id);
+      input = $(input.id.substr(0, input.id.length - 13));
       input.setAttribute('autocomplete', 'OFF');
-      input.form.onsubmit = autocompleteSubmit;
+      addSubmitEvent(input.form, autocompleteSubmit);
       new jsAC(input, acdb[uri]);
     }
   }
Index: misc/drupal.css
===================================================================
RCS file: /cvs/drupal/drupal/misc/drupal.css,v
retrieving revision 1.112
diff -u -r1.112 drupal.css
--- misc/drupal.css	16 Aug 2005 18:06:18 -0000	1.112
+++ misc/drupal.css	17 Aug 2005 00:39:59 -0000
@@ -597,6 +597,28 @@
 }
 
 /*
+** Progressbar styles
+*/
+.progress {
+  font-weight: bold;
+}
+.progress .bar {
+  background: #fff url('progress.gif');
+  border: 1px solid #00375a;
+  height: 1.5em;
+  margin-top: 0.2em;  
+}
+.progress .filled {
+  background: #0072b9;
+  height: 1.33em;
+  border-bottom: 0.67em solid #004a73;
+  width: 0%;
+}
+.progress .percentage {
+  float: right;
+}
+
+/*
 ** Collapsing fieldsets
 */
 html.js fieldset.collapsed {
Index: misc/drupal.js
===================================================================
RCS file: /cvs/drupal/drupal/misc/drupal.js,v
retrieving revision 1.5
diff -u -r1.5 drupal.js
--- misc/drupal.js	11 Aug 2005 13:00:17 -0000	1.5
+++ misc/drupal.js	17 Aug 2005 00:39:59 -0000
@@ -102,6 +102,42 @@
 }
 
 /**
+ * Redirects a button's form submission to a hidden iframe and displays the result
+ * in a given wrapper. The iframe should contain a call to
+ * window.parent.iframeHandler() after submission.
+ */
+function redirectFormButton(uri, button, handler) {
+  // Insert the iframe
+  var div = document.createElement('div');
+  div.innerHTML = '<iframe name="redirect-target" id="redirect-target" src="" style="width:0px;height:0px;border:0;"></iframe>';
+  button.parentNode.appendChild(div);
+
+  // Trap the button
+  button.onfocus = function() {
+    button.onclick = function() {
+      // Prepare vars for use in anonymous function.
+      var button = this;
+      var action = button.form.action;
+      var target = button.form.target;
+      // Redirect form submission
+      this.form.action = uri;
+      this.form.target = 'redirect-target';
+      handler.onsubmit();
+      // Set iframe handler for later
+      window.iframeHandler = function (data) {
+        // Restore form submission
+        button.form.action = action;
+        button.form.target = target;
+        handler.oncomplete(data);
+      }
+    }
+  }
+  button.onblur = function() {
+    button.onclick = null;
+  }
+}
+
+/**
  * Adds a function to the window onload event
  */
 function addLoadEvent(func) {
@@ -118,6 +154,21 @@
 }
 
 /**
+ * Adds a function to the window onload event
+ */
+function addSubmitEvent(form, func) {
+  var oldSubmit = form.onsubmit;
+  if (typeof oldSubmit != 'function') {
+    form.onsubmit = func;
+  }
+  else {
+    form.onsubmit = function() {
+      return oldSubmit() && func();
+    }
+  }
+}
+
+/**
  * Retrieves the absolute position of an element on the screen
  */
 function absolutePosition(el) {
@@ -196,7 +247,7 @@
  */
 function removeNode(node) {
   if (typeof node == 'string') {
-    node = document.getElementById(node);
+    node = $(node);
   }
   if (node && node.parentNode) {
     return node.parentNode.removeChild(node);
@@ -205,3 +256,10 @@
     return false;
   }
 }
+
+/**
+ * Wrapper around document.getElementById().
+ */
+function $(id) {
+  return document.getElementById(id);
+}
Index: misc/progress.gif
===================================================================
RCS file: misc/progress.gif
diff -N misc/progress.gif
Binary files /dev/null and progress.gif differ
Index: misc/progress.js
===================================================================
RCS file: misc/progress.js
diff -N misc/progress.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ misc/progress.js	17 Aug 2005 00:40:00 -0000
@@ -0,0 +1,80 @@
+/**
+ * A progressbar object. Initialized with the given id. Must be inserted into
+ * the DOM afterwards through progressBar.element.
+ *
+ * e.g. pb = new progressBar('myProgressBar');
+ *      some_element.appendChild(pb.element);
+ */
+function progressBar(id) {
+  var pb = this;
+  this.id = id;
+
+  this.element = document.createElement('div');
+  this.element.id = id;
+  this.element.className = 'progress';
+  this.element.innerHTML = '<div class="percentage"></div>'+
+                           '<div class="status">&nbsp;</div>'+
+                           '<div class="bar"><div class="filled"></div></div>';
+}
+
+/**
+ * Set the percentage and status message for the progressbar.
+ */
+progressBar.prototype.setProgress = function (percentage, status) {
+  var divs = this.element.getElementsByTagName('div');
+  for (i in divs) {
+    if (percentage >= 0) {
+      if (hasClass(divs[i], 'filled')) {
+        divs[i].style.width = percentage + '%';
+      }
+      if (hasClass(divs[i], 'percentage')) {
+        divs[i].innerHTML = percentage + '%';
+      }
+    }
+    if (hasClass(divs[i], 'status')) {
+      divs[i].innerHTML = status;
+    }
+  }
+}
+
+/**
+ * Start monitoring progress via Ajax.
+ */
+progressBar.prototype.startMonitoring = function (uri, delay) {
+  this.delay = delay;
+  this.uri = uri;
+  this.sendPing();
+}
+
+/**
+ * Stop monitoring progress via Ajax.
+ */
+progressBar.prototype.stopMonitoring = function () {
+  clearTimeout(this.timer);
+}
+
+/**
+ * Request progress data from server.
+ */
+progressBar.prototype.sendPing = function () {
+  if (this.timer) {
+    clearTimeout(this.timer);
+  }
+  HTTPGet(this.uri, this.receivePing, this);
+}
+
+/**
+ * HTTP callback function. Passes data back to the progressbar and sets a new
+ * timer for the next ping.
+ */
+progressBar.prototype.receivePing = function(string, xmlhttp, pb) {
+  if (xmlhttp.status != 200) {
+    return alert('An HTTP error '+ xmlhttp.status +' occured.\n'+ pb.uri);
+  }
+  // Split into values
+  var matches = string.length > 0 ? string.split('|') : [];
+  if (matches.length >= 2) {
+    pb.setProgress(matches[0], matches[1]);
+  }
+  pb.timer = setTimeout(function() { pb.sendPing(); }, pb.delay);
+}
Index: misc/upload.js
===================================================================
RCS file: misc/upload.js
diff -N misc/upload.js
--- /dev/null	1 Jan 1970 00:00:00 -0000
+++ misc/upload.js	17 Aug 2005 00:40:00 -0000
@@ -0,0 +1,59 @@
+// Global Killswitch
+if (isJsEnabled()) {
+  addLoadEvent(uploadAutoAttach);
+}
+
+/**
+ * Attaches the upload behaviour to the upload form.
+ */
+function uploadAutoAttach() {
+  var acdb = [];
+  var inputs = document.getElementsByTagName('input');
+  for (i = 0; input = inputs[i]; i++) {
+    if (input && hasClass(input, 'upload')) {
+      var uri = input.value;
+      var button = input.id.substr(5);
+      var wrapper = button + '-wrapper';
+      var hide = button + '-hide';
+      var upload = new jsUpload(uri, button, wrapper, hide);
+    }
+  }
+}
+
+/**
+ * JS Upload object
+ */
+function jsUpload(uri, button, wrapper, hide) {
+  var upload = this;
+  this.button = button;
+  this.wrapper = wrapper;
+  this.hide = hide;
+  redirectFormButton(uri, $(button), this);
+}
+
+/**
+ * Handler for the form redirection submission.
+ */
+jsUpload.prototype.onsubmit = function () {
+  var hide = $(this.hide);
+  // Insert progressbar and stretch to take the same space.
+  this.progress = new progressBar('uploadprogress');
+  this.progress.setProgress(-1, 'Uploading file...');
+  this.progress.element.style.width = '28em';
+  this.progress.element.style.height = hide.offsetHeight +'px';
+  hide.parentNode.insertBefore(this.progress.element, hide);
+  // Hide file form
+  hide.style.display = 'none';
+}
+
+/**
+ * Handler for the form redirection completion.
+ */
+jsUpload.prototype.oncomplete = function (data) {
+  // Remove progressbar
+  removeNode(this.progress);
+  this.progress = null;
+  // Replace form and re-attach behaviour
+  $(this.wrapper).innerHTML = data;
+  uploadAutoAttach();
+}
\ No newline at end of file
cvs diff: Diffing modules
Index: modules/upload.module
===================================================================
RCS file: /cvs/drupal/drupal/modules/upload.module,v
retrieving revision 1.45
diff -u -r1.45 upload.module
--- modules/upload.module	11 Aug 2005 13:02:08 -0000	1.45
+++ modules/upload.module	17 Aug 2005 00:40:02 -0000
@@ -60,6 +60,12 @@
       'access' => user_access('administer site configuration'),
       'type' => MENU_NORMAL_ITEM
     );
+    $items[] = array(
+      'path' => 'upload/js',
+      'callback' => 'upload_js',
+      'access' => user_access('upload files'),
+      'type' => MENU_CALLBACK
+    );
   }
   else {
     // Add handlers for previewing new uploads.
@@ -363,8 +369,18 @@
 }
 
 function upload_form($node) {
+  drupal_add_js('misc/progress.js');
+  drupal_add_js('misc/upload.js');
+
+  $output = '<div id="fileop-wrapper">'. _upload_form($node) .'</div>';
+
+  return '<div class="attachments">'. form_group_collapsible(t('File attachments'), $output, TRUE, t('Changes made to the attachments are not permanent until you save this post. The first "listed" file will be included in RSS feeds.')) .'</div>';
+}
+
+function _upload_form($node) {
   $header = array(t('Delete'), t('List'), t('Url'), t('Size'));
   $rows = array();
+  $output = '';
 
   if (is_array($node->files)) {
     foreach ($node->files as $key => $file) {
@@ -378,14 +394,18 @@
   }
 
   if (count($node->files)) {
-    $output = theme('table', $header, $rows);
+    $output .= theme('table', $header, $rows);
   }
   if (user_access('upload files')) {
+    $output .= '<div id="fileop-hide">';
     $output .= form_file(t('Attach new file'), "upload", 40);
     $output .= form_button(t('Attach'), 'fileop');
+    // The class triggers the js upload behaviour.
+    $output .= form_hidden('fileop', url('upload/js', NULL, NULL, TRUE), 'edit', array('class' => 'upload'));
+    $output .= '</div>';
   }
 
-  return '<div class="attachments">'. form_group_collapsible(t('File attachments'), $output, TRUE, t('Changes made to the attachments are not permanent until you save this post.  The first "listed" file will be included in RSS feeds.')) .'</div>';
+  return $output;
 }
 
 function upload_load($node) {
@@ -422,4 +442,18 @@
   return $file;
 }
 
+/**
+ * Menu-callback for JavaScript-based uploads.
+ */
+function upload_js() {
+  // We only do the upload.module part of the node validation process.
+  $node = array2object($_POST['edit']);
+  upload_nodeapi(&$node, 'validate', NULL);
+  $output = theme('status_messages') . _upload_form($node);
+
+  // We send the updated file attachments form.
+  print drupal_call_js('window.parent.iframeHandler', $output);
+  exit;
+}
+
 ?>
