summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--includes/common.inc55
-rw-r--r--misc/autocomplete.js5
-rw-r--r--misc/drupal.css22
-rw-r--r--misc/drupal.js60
-rw-r--r--misc/progress.gifbin0 -> 1254 bytes
-rw-r--r--misc/progress.js80
-rw-r--r--misc/upload.js59
-rw-r--r--modules/upload.module38
-rw-r--r--modules/upload/upload.module38
9 files changed, 344 insertions, 13 deletions
diff --git a/includes/common.inc b/includes/common.inc
index 7b6fe29da..2a098aa7c 100644
--- a/includes/common.inc
+++ b/includes/common.inc
@@ -1461,6 +1461,10 @@ function form_file($title, $name, $size, $description = NULL, $required = FALSE)
* 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.
*
@@ -1468,8 +1472,8 @@ function form_file($title, $name, $size, $description = NULL, $required = FALSE)
* 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";
}
/**
@@ -1488,7 +1492,7 @@ function form_hidden($name, $value, $edit = 'edit') {
* 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";
}
/**
@@ -1806,6 +1810,51 @@ function drupal_add_js($file) {
}
/**
+ * 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
diff --git a/misc/autocomplete.js b/misc/autocomplete.js
index 52702909d..0f4eee475 100644
--- a/misc/autocomplete.js
+++ b/misc/autocomplete.js
@@ -17,10 +17,9 @@ function autocompleteAutoAttach() {
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]);
}
}
diff --git a/misc/drupal.css b/misc/drupal.css
index 258fd8791..a8366dd37 100644
--- a/misc/drupal.css
+++ b/misc/drupal.css
@@ -579,6 +579,28 @@ input.throbbing {
}
/*
+** 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 {
diff --git a/misc/drupal.js b/misc/drupal.js
index 80414afb0..4007817c4 100644
--- a/misc/drupal.js
+++ b/misc/drupal.js
@@ -102,6 +102,42 @@ function HTTPPost(uri, object, callbackFunction, callbackParameter) {
}
/**
+ * 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 @@ function addLoadEvent(func) {
}
/**
+ * 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 eregReplace(search, replace, subject) {
*/
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 @@ function removeNode(node) {
return false;
}
}
+
+/**
+ * Wrapper around document.getElementById().
+ */
+function $(id) {
+ return document.getElementById(id);
+}
diff --git a/misc/progress.gif b/misc/progress.gif
new file mode 100644
index 000000000..3564e7b06
--- /dev/null
+++ b/misc/progress.gif
Binary files differ
diff --git a/misc/progress.js b/misc/progress.js
new file mode 100644
index 000000000..b4e4c9039
--- /dev/null
+++ b/misc/progress.js
@@ -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);
+}
diff --git a/misc/upload.js b/misc/upload.js
new file mode 100644
index 000000000..48f403448
--- /dev/null
+++ b/misc/upload.js
@@ -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
diff --git a/modules/upload.module b/modules/upload.module
index 3cf56d16c..335fd8dfc 100644
--- a/modules/upload.module
+++ b/modules/upload.module
@@ -60,6 +60,12 @@ function upload_menu($may_cache) {
'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.
@@ -378,8 +384,18 @@ function upload_delete($node) {
}
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, empty($node->files), 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) {
@@ -393,15 +409,19 @@ function upload_form($node) {
}
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, empty($node->files), 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) {
@@ -438,4 +458,16 @@ function _upload_image($file) {
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;
+}
diff --git a/modules/upload/upload.module b/modules/upload/upload.module
index 3cf56d16c..335fd8dfc 100644
--- a/modules/upload/upload.module
+++ b/modules/upload/upload.module
@@ -60,6 +60,12 @@ function upload_menu($may_cache) {
'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.
@@ -378,8 +384,18 @@ function upload_delete($node) {
}
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, empty($node->files), 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) {
@@ -393,15 +409,19 @@ function upload_form($node) {
}
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, empty($node->files), 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) {
@@ -438,4 +458,16 @@ function _upload_image($file) {
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;
+}