summaryrefslogtreecommitdiff
path: root/misc
diff options
context:
space:
mode:
authorDries Buytaert <dries@buytaert.net>2005-08-31 18:37:30 +0000
committerDries Buytaert <dries@buytaert.net>2005-08-31 18:37:30 +0000
commite03ce2f99670b75a7f3e0709dd8705a2a3f4625e (patch)
treea7678fa8a55ccd2927cbbc8d0e30bbacf630ad3d /misc
parent3029da00d62c20a4d97d668931bf9f0c918d1b09 (diff)
downloadbrdo-e03ce2f99670b75a7f3e0709dd8705a2a3f4625e.tar.gz
brdo-e03ce2f99670b75a7f3e0709dd8705a2a3f4625e.tar.bz2
- Patch #28483 by Steven: JavaScript enabled uploading.
Comment from Steven: It does this by redirecting the submission of the form to a hidden <iframe> when you click "Attach" (we cannot submit data through Ajax directly because you cannot read file contents from JS for security reasons). Once the file is submitted, the upload-section of the form is updated. Things to note: * The feature degrades back to the current behaviour without JS. * If there are errors with the uploaded file (disallowed type, too big, ...), they are displayed at the top of the file attachments fieldset. * Though the hidden-iframe method sounds dirty, it's quite compact and is 100% implemented in .js files. The drupal.js api makes it a snap to use. * I included some minor improvements to the Drupal JS API and code. * I added an API drupal_call_js() to bridge the PHP/JS gap: it takes a function name and arguments, and outputs a <script> tag. The kicker is that it preserves the structure and type of arguments, so e.g. PHP associative arrays end up as objects in JS. * I also included a progressbar widget that I wrote for drumm's ongoing update.php work. It includes Ajax status updating/monitoring, but it is only used as a pure throbber in this patch. But as the code was already written and is going to be used in the near future, I left that part in. It's pretty small ;). If PHP supports ad-hoc upload info in the future like Ruby on Rails, we can implement that in 5 minutes.
Diffstat (limited to 'misc')
-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
6 files changed, 222 insertions, 4 deletions
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