Commit e03ce2f9 authored by Dries's avatar Dries
Browse files

- 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.
parent 3029da00
......@@ -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";
}
/**
......@@ -1805,6 +1809,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.
*
......
......@@ -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]);
}
}
......
......@@ -578,6 +578,28 @@ input.throbbing {
background-position: 100% -18px;
}
/*
** 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
*/
......
......@@ -101,6 +101,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
*/
......@@ -117,6 +153,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
*/
......@@ -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);
}
/**
* 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);
}
// 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
......@@ -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;
}
......@@ -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;
}
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment