[ticket/11915] Add the new uploader.

PHPBB3-11915
This commit is contained in:
Cesar G 2013-11-10 18:54:07 -08:00
parent 2719a18f7f
commit 688ca7fade
3 changed files with 576 additions and 263 deletions

View file

@ -1,38 +1,73 @@
plupload.addI18n(phpbb.plupload.i18n); plupload.addI18n(phpbb.plupload.i18n);
plupload.attachment_data = []; phpbb.plupload.data = phpbb.plupload.ids = [];
(function($) { // Avoid conflicts with other libraries
"use strict";
/** /**
* Returns the index of the plupload.attachment_data array where the given * Set up the uploader.
* attach id appears
* *
* @param int id The attachment id of the file * @return undefined
*
* @return bool Returns false if the id cannot be found
* @return int Returns the index in the main array where the attachment id
* was found
*/ */
function phpbb_plupload_find_attachment_idx(id) { phpbb.plupload.initialize = function () {
var data = plupload.attachment_data; phpbb.plupload.form = $(phpbb.plupload.config.form_hook)[0],
for (var i = 0; i < data.length; i++) { phpbb.plupload.rowTpl = $('#attach-row-tpl')[0].outerHTML;
if (data[i].attach_id == id) {
return i;
}
}
return false; // Hide the basic upload panel and remove the attach row template.
} $('#attach-row-tpl, #attach-panel-basic').remove();
// Show multi-file upload options.
$('#attach-panel-multi').show();
// Gather existing attachment data from HTML.
var data = phpbb.plupload.getDataFromHtml();
phpbb.plupload.setData(data);
phpbb.plupload.updateMultipartParams(phpbb.plupload.getSerializedData());
// Initialize the Plupload uploader.
uploader.init();
};
/** /**
* Converts an array of objects into an object that PHP would expect as POST * Unsets all elements in the object uploader.settings.multipart_params whose keys
* data * begin with 'attachment_data['
*
* @return undefined
*/
phpbb.plupload.clearParams = function() {
var obj = uploader.settings.multipart_params;
for (var key in obj) {
if (!obj.hasOwnProperty(key) || key.indexOf('attachment_data[') !== 0) {
continue;
}
delete uploader.settings.multipart_params[key];
}
};
/**
* Update uploader.settings.multipart_params object with new data.
*
* @param object obj
* @return undefined
*/
phpbb.plupload.updateMultipartParams = function (obj) {
uploader.settings.multipart_params = $.extend(
uploader.settings.multipart_params,
obj
);
};
/**
* Convert the array of attachment objects into an object that PHP would expect as POST data.
* *
* @return object An object in the form 'attachment_data[i][key]': value as * @return object An object in the form 'attachment_data[i][key]': value as
* expected by the server * expected by the server
*/ */
function phpbb_plupload_attachment_data_serialize() { phpbb.plupload.getSerializedData = function () {
var obj = {}; var obj = {};
for (var i = 0; i < plupload.attachment_data.length; i++) { for (var i = 0; i < phpbb.plupload.data.length; i++) {
var datum = plupload.attachment_data[i]; var datum = phpbb.plupload.data[i];
for (var key in datum) { for (var key in datum) {
if (!datum.hasOwnProperty(key)) { if (!datum.hasOwnProperty(key)) {
continue; continue;
@ -41,93 +76,376 @@ function phpbb_plupload_attachment_data_serialize() {
obj['attachment_data[' + i + '][' + key + ']'] = datum[key]; obj['attachment_data[' + i + '][' + key + ']'] = datum[key];
} }
} }
return obj; return obj;
} };
/** /**
* Unsets all elements in an object whose keys begin with 'attachment_data[' * Grab all attachment data present in the HTML. This assumes
* that all revelant data is present in form elements that have a name that follows
* the naming convention of attachment_data[index][property]
* *
* @param object The object to be cleared * @return array An array containing all gathered data in the form of
* * array(index => object(property: value))
* @return undefined
*/ */
function phpbb_plupload_clear_params(obj) { phpbb.plupload.getDataFromHtml = function () {
for (var key in obj) { var data = [],
if (!obj.hasOwnProperty(key) || key.indexOf('attachment_data[') !== 0) { form = phpbb.plupload.form;
continue;
}
delete obj[key];
}
}
/**
* Update hidden attachment inputs in posting form
* Pre-existing hidden inputs will be removed by comparing the old attachment
* data (old_data) to the new attachment data (data) that has been sent back
* by plupload.
*
* @param object form Posting form
* @param object data Current attachment_data
* @param object old_date Previous attachment_data (before submission)
*
* @return void
*/
phpbb.update_hidden_attachment_inputs = function(form, data, old_data) {
// Update already existing hidden inputs
for (var i = 0; i < form.length; i++) {
if (data.hasOwnProperty(form[i].name)) {
form[i].value = data[form[i].name];
delete data[form[i].name];
} else if (typeof old_data !== 'undefined' && old_data.hasOwnProperty(form[i].name)) {
var inputRegex = /\b^[a-z_]+[+[0-9]+]/;
var inputName = inputRegex.exec(form[i].name);
if (typeof inputName !== 'undefined' && inputName[0] !== '') {
$("input[type='hidden'][name^='" + inputName[0] + "']").remove();
}
}
}
// Append new inputs
for (var key in data) {
if (!data.hasOwnProperty(key)) {
continue;
}
var input = $('<input />')
.attr('type', 'hidden')
.attr('name', key)
.attr('value', data[key]);
$(form).append(input);
}
}
jQuery(function($) {
$(phpbb.plupload.config.element_hook).pluploadQueue(phpbb.plupload.config);
var uploader = $(phpbb.plupload.config.element_hook).pluploadQueue();
// Check the page for already-existing attachment data and add it to the
// array
var form = $(phpbb.plupload.config.form_hook)[0];
for (var i = 0; i < form.length; i++) { for (var i = 0; i < form.length; i++) {
if (form[i].name.indexOf('attachment_data[') !== 0) { if (form[i].name.indexOf('attachment_data[') !== 0) {
continue; continue;
} }
var matches = form[i].name.match(/\[(\d+)\]\[([^\]]+)\]/); var matches = form[i].name.match(/attachment_data\[(\d+)\]\[([^\]]+)\]/);
var index = matches[1]; var index = matches[1];
var property = matches[2]; var property = matches[2];
if (!plupload.attachment_data[index]) { if (typeof data[index] === 'undefined') {
plupload.attachment_data[index] = {}; data[index] = {};
}
data[index][property] = form[i].value;
}
return data;
};
/**
* Get the index from the phpbb.plupload.data array where the given
* attachment id appears.
*
* @param int attach_id The attachment id of the file.
* @return bool Returns false if the id cannot be found.
* @return int Returns the index of the file if it exists.
*/
phpbb.plupload.getIndex = function(attach_id) {
var index = phpbb.plupload.ids.indexOf(Number(attach_id));
return (index !== -1) ? index : false;
};
/**
* Set the data in phpbb.plupload.data and phpbb.plupload.ids arrays.
*
* @param array data Array containing the new data to use. In the form of
* array(index => object(property: value). Requires attach_id to be one of the object properties.
*
* @return undefined
*/
phpbb.plupload.setData = function(data) {
// Make sure that the array keys are reset.
phpbb.plupload.ids = phpbb.plupload.data = [];
phpbb.plupload.data = data;
for (var i = 0; i < data.length; i++) {
phpbb.plupload.ids.push(Number(data[i].attach_id));
}
};
/**
* Update the attachment data in the HTML and the phpbb & phpbb.plupload objects.
*
* @param array data Array containing the new data to use.
* @param string action The action that required the update. Used to update the inline attachment bbcodes.
* @param int index The index from phpbb.plupload_ids that was affected by the action.
* @return undefined
*/
phpbb.plupload.update = function (data, action, index) {
phpbb.plupload.setData(data);
phpbb.plupload.updateRows();
phpbb.plupload.clearParams();
phpbb.plupload.updateMultipartParams(phpbb.plupload.getSerializedData());
};
/**
* Update the relevant elements and hidden data for all attachments.
*
* @return undefined
*/
phpbb.plupload.updateRows = function () {
for (var i = 0; i < phpbb.plupload.ids.length; i++) {
phpbb.plupload.updateRow(i);
}
};
/**
* Insert a row for a new attachment. This expects an HTML snippet in the HTML
* using the id "attach-row-tpl" to be present. This snippet is cloned and the
* data for the file inserted into it. The row is then appended or prepended to
* #file-list based on the attach_order setting.
*
* @param object file Plupload file object for the new attachment.
* @return undefined
*/
phpbb.plupload.insertRow = function(file) {
var row = $(phpbb.plupload.rowTpl);
row.attr('id', file.id);
row.find('.file-name').html(file.name);
row.find('.file-size').html(plupload.formatSize(file.size));
if (phpbb.plupload.order == 'desc') {
$('#file-list').prepend(row);
} else {
$('#file-list').append(row);
}
};
/**
* Update the relevant elements and hidden data for an attachment.
*
* @param int index The index from phpbb.plupload.ids of the attachment to edit.
* @return undefined
*/
phpbb.plupload.updateRow = function (index) {
var attach = phpbb.plupload.data[index];
var row = $('[data-attach-id="' + attach.attach_id + '"]');
row.find('textarea').attr('name', 'comment_list[' + index + ']');
row.find('.file-inline-bbcode').attr('onclick', 'attach_inline(' + index + ',\'' + attach.real_filename + '\');');
phpbb.plupload.updateHiddenData(row, attach, index);
};
/**
* Update hidden input data for an attachment.
*
* @param object row jQuery object for the attachment row.
* @param object attach Attachment data object from phpbb.plupload.data
* @param int index Attachment index from phpbb.plupload.ids
* @return undefined
*/
phpbb.plupload.updateHiddenData = function (row, attach, index) {
row.find('input[type="hidden"]').remove();
for (var key in attach) {
var input = $('<input />')
.attr('type', 'hidden')
.attr('name', 'attachment_data[' + index + '][' + key +']')
.attr('value', attach[key]);
$('textarea', row).after(input);
}
};
/**
* Deleting a file removes it from the queue and fires an AJAX event to the
* server to tell it to remove the temporary attachment. The server
* responds with the updated attachment data list so that any future
* uploads can maintain state with the server
*
* @param object row jQuery object for the attachment row.
* @param int attachId Attachment id of the file to be removed.
*
* @return undefined
*/
phpbb.plupload.deleteFile = function (row, attachId) {
// If there's no attach id, then the file hasn't been uploaded. Simply delete the row.
if (typeof attachId === 'undefined') {
phpbb.plupload.hideEmptyList();
row.slideUp(100).delay(100).remove();
} }
plupload.attachment_data[index][property] = form[i].value; var index = phpbb.plupload.getIndex(attachId);
uploader.settings.multipart_params[form[i].name] = form[i].value; row.find('.file-status').toggleClass('file-uploaded file-working');
if (index === false) {
return;
}
var fields = {};
fields['delete_file[' + index + ']'] = 1;
var always = function() {
row.find('.file-status').removeClass('file-working');
};
var done = function(response) {
var json = {};
try {
json = $.parseJSON(response);
} catch (e) {
return;
} }
/** // trigger_error() was called which likely means a permission error was encountered.
if (typeof response.title !== 'undefined') {
uploader.trigger('Error', {message: response.message});
// We will have to assume that the deletion failed. So leave the file status as uploaded.
row.find('.file-status').toggleClass('file-uploaded');
return;
}
phpbb.plupload.update(response, 'removal', index);
// Check if the user can upload files now if he had reached the max files limit.
phpbb.plupload.handleMaxFilesReached();
if (row.attr('id')) {
var file = uploader.getFile(row.attr('id'));
uploader.removeFile(file);
}
row.slideUp(100, function() {
row.remove();
// Hide the file list if it's empty now.
phpbb.plupload.hideEmptyList();
});
uploader.trigger('FilesRemoved');
};
$.ajax(phpbb.plupload.config.url, {
type: 'POST',
data: $.extend(fields, phpbb.plupload.getSerializedData()),
headers: {'X-PHPBB-USING-PLUPLOAD': '1', 'X-Requested-With': 'XMLHttpRequest'}
})
.always(always)
.done(done);
};
/**
* Check the attachment list and hide its container if it's empty.
*
* @return undefined
*/
phpbb.plupload.hideEmptyList = function() {
if (!$('#file-list').children().length) {
$('#file-list-container').slideUp(100);
}
}
/**
* Get Plupload file objects based on their upload status.
*
* @param int status Plupload status - plupload.DONE, plupload.FAILED, plupload.QUEUED,
* plupload.STARTED, plupload.STOPPED
*
* @return Returns an array of the Plupload file objects matching the status.
*/
phpbb.plupload.getFilesByStatus = function (status) {
var files = [];
$.each(uploader.files, function(i, file) {
if (file.status === status) {
files.push(file);
}
});
return files;
}
/**
* Check whether the user has reached the maximun number of files that he's allowed
* to upload. If so, disables the uploader and marks the queued files as failed. Otherwise
* makes sure that the uploader is enabled.
*
* @return bool Returns true if the limit has been reached. False if otherwise.
*/
phpbb.plupload.handleMaxFilesReached = function () {
// If there is no limit, the user is an admin or moderator.
if (!phpbb.plupload.maxFiles) {
return false;
}
if (phpbb.plupload.maxFiles <= phpbb.plupload.ids.length) {
// Fail the rest of the queue.
phpbb.plupload.markQueuedFailed(phpbb.plupload.lang.TOO_MANY_ATTACHMENTS);
// Disable the uploader.
phpbb.plupload.disableUploader();
uploader.trigger('Error', {message: phpbb.plupload.lang.TOO_MANY_ATTACHMENTS});
return true;
} else if(phpbb.plupload.maxFiles > phpbb.plupload.ids.length) {
// Enable the uploader if the user is under the limit
phpbb.plupload.enableUploader();
}
return false;
}
/**
* Disable the uploader
*
* @return undefined
*/
phpbb.plupload.disableUploader = function () {
$('#add_files').addClass('disabled');
uploader.disableBrowse();
}
/**
* Enable the uploader
*
* @return undefined
*/
phpbb.plupload.enableUploader = function () {
$('#add_files').removeClass('disabled');
uploader.disableBrowse(false);
}
/**
* Mark all queued files as failed.
*
* @param string error Error message to present to the user.
* @return undefined
*/
phpbb.plupload.markQueuedFailed = function (error) {
var files = phpbb.plupload.getFilesByStatus(plupload.QUEUED);
$.each(files, function(i, file) {
$('#' + file.id).find('.file-progress').hide();
phpbb.plupload.fileError(file, error);
});
}
/**
* Marks a file as failed and sets the error message for it.
*
* @param object file Plupload file object that failed.
* @param string error Error message to present to the user.
* @return undefined
*/
phpbb.plupload.fileError = function (file, error) {
file.status = plupload.FAILED;
file.error = error;
$('#' + file.id).find('.file-status').addClass('file-error').attr({'data-error-title': phpbb.plupload.lang.ERROR, 'data-error-message': error});
}
/**
* Set up the Plupload object and get some basic data.
*/
var uploader = new plupload.Uploader(phpbb.plupload.config);
phpbb.plupload.initialize();
/**
* Delete a file.
*/
$('#file-list').on('click', '.file-delete', function(e) {
var row = $(this).parents('.attach-row'),
attachId = row.attr('data-attach-id');
phpbb.plupload.deleteFile(row, attachId);
e.preventDefault();
});
/**
* Display the error message for a particular file when the error icon is clicked.
*/
$('#file-list').on('click', '.file-error', function(e) {
phpbb.alert($(this).attr('data-error-title'), $(this).attr('data-error-message'));
e.preventDefault();
});
/**
* Fires when an error occurs.
*/
uploader.bind('Error', function(up, error) {
// The error message that Plupload provides for these is vague, so we'll be more specific.
if (error.code === plupload.FILE_EXTENSION_ERROR) {
error.message = plupload.translate('Invalid file extension: ') + error.file.name;
} else if (error.code === plupload.FILE_SIZE_ERROR) {
error.message = plupload.translate('File too large: ') + error.file.name;
}
phpbb.alert(phpbb.plupload.lang.ERROR, error.message);
});
/**
* Fires before a given file is about to be uploaded. This allows us to * Fires before a given file is about to be uploaded. This allows us to
* send the real filename along with the chunk. This is necessary because * send the real filename along with the chunk. This is necessary because
* for some reason the filename is set to 'blob' whenever a file is chunked * for some reason the filename is set to 'blob' whenever a file is chunked
@ -138,14 +456,15 @@ jQuery(function($) {
* *
* @return undefined * @return undefined
*/ */
uploader.bind('BeforeUpload', function(up, file) { uploader.bind('BeforeUpload', function(up, file) {
up.settings.multipart_params = $.extend( if (phpbb.plupload.handleMaxFilesReached()) {
up.settings.multipart_params, return;
{'real_filename': file.name} }
);
});
/** phpbb.plupload.updateMultipartParams({'real_filename': file.name});
});
/**
* Fired when a single chunk of any given file is uploaded. This parses the * Fired when a single chunk of any given file is uploaded. This parses the
* response from the server and checks for an error. If an error occurs it * response from the server and checks for an error. If an error occurs it
* is reported to the user and the upload of this particular file is halted * is reported to the user and the upload of this particular file is halted
@ -157,7 +476,7 @@ jQuery(function($) {
* *
* @return undefined * @return undefined
*/ */
uploader.bind('ChunkUploaded', function(up, file, response) { uploader.bind('ChunkUploaded', function(up, file, response) {
if (response.chunk >= response.chunks - 1) { if (response.chunk >= response.chunks - 1) {
return; return;
} }
@ -186,9 +505,40 @@ jQuery(function($) {
}) })
}); });
} }
});
/**
* Fires when files are added to the queue.
*
* @return undefined
*/
uploader.bind('FilesAdded', function(up, files) {
// Prevent unnecessary requests to the server if the user already uploaded
// the maximum number of files allowed.
if (phpbb.plupload.handleMaxFilesReached()) {
return;
}
// Show the file list if there aren't any files currently.
if (!$('#file-list-container').is(':visible')) {
$('#file-list-container').show(100);
}
$.each(files, function(i, file) {
phpbb.plupload.insertRow(file);
}); });
/** up.bind('UploadProgress', function(up, file) {
$('#' + file.id + " .file-progress-bar").css('width', file.percent + '%');
$('#file-total-progress-bar').css('width', up.total.percent + '%');
});
// Start uploading the files once the user has selected them.
up.start();
});
/**
* Fires when an entire file has been uploaded. It checks for errors * Fires when an entire file has been uploaded. It checks for errors
* returned by the server otherwise parses the list of attachment data and * returned by the server otherwise parses the list of attachment data and
* appends it to the next file upload so that the server can maintain state * appends it to the next file upload so that the server can maintain state
@ -201,38 +551,45 @@ jQuery(function($) {
* *
* @return undefined * @return undefined
*/ */
uploader.bind('FileUploaded', function(up, file, response) { uploader.bind('FileUploaded', function(up, file, response) {
var json = {}; var json = {},
row = $('#' + file.id),
error;
// Hide the progress indicator.
row.find('.file-progress').hide();
try { try {
json = $.parseJSON(response.response); json = $.parseJSON(response.response);
} catch (e) { } catch (e) {
file.status = plupload.FAILED; error = 'Error parsing server response.';
file.error = 'Error parsing server response.'
} }
if (json.error) { // If trigger_error() was called, then a permission error likely occurred.
file.status = plupload.FAILED; if (typeof json.title !== 'undefined') {
file.error = json.error.message; error = json.message;
up.trigger('Error', {message: error});
// The rest of the queue will fail.
phpbb.plupload.markQueuedFailed(error);
} else if (json.error) {
error = json.error.message;
}
if (typeof error !== 'undefined') {
phpbb.plupload.fileError(file, error);
} else if (file.status === plupload.DONE) { } else if (file.status === plupload.DONE) {
plupload.attachment_data = json;
file.attachment_data = json[0]; file.attachment_data = json[0];
up.settings.multipart_params = $.extend(
up.settings.multipart_params,
phpbb_plupload_attachment_data_serialize()
);
}
});
/** row.attr('data-attach-id', file.attachment_data.attach_id);
* Fires when the entire queue of files have been uploaded. It resets the row.find('.file-inline-bbcode').show();
* 'add files' button to allow more files to be uploaded and also attaches row.find('.file-status').addClass('file-uploaded');
* several events to each row of the currently-uploaded files to facilitate phpbb.plupload.update(json, 'addition', 0);
* deleting any one of the files. }
* });
* Deleting a file removes it from the queue and fires an ajax event to the
* server to tell it to remove the temporary attachment. The server /**
* responds with the updated attachment data list so that any future * Fires when the entire queue of files have been uploaded.
* uploads can maintain state with the server
* *
* @param object up The plupload.Uploader object * @param object up The plupload.Uploader object
* @param array files An array of plupload.File objects that have just * @param array files An array of plupload.File objects that have just
@ -240,65 +597,13 @@ jQuery(function($) {
* *
* @return undefined * @return undefined
*/ */
uploader.bind('UploadComplete', function(up, files) { uploader.bind('UploadComplete', function(up, files) {
$('.plupload_upload_status').css('display', 'none'); // Hide the progress bar
$('.plupload_buttons').css('display', 'block'); setTimeout(function() {
$('#file-total-progress-bar').fadeOut(500, function() {
// Insert a bunch of hidden input elements containing the attachment $(this).css('width', 0).show();
// data so that the save/preview/submit buttons work as expected.
var form = $(phpbb.plupload.config.form_hook)[0];
var data = phpbb_plupload_attachment_data_serialize();
phpbb.update_hidden_attachment_inputs(form, data);
files.forEach(function(file) {
if (file.status !== plupload.DONE) {
var click = function(evt) {
alert(file.error);
}
$('#' + file.id).attr('title', file.error);
$('#' + file.id).click(click);
return;
}
var click = function(evt) {
$(evt.target).find('a').addClass('working');
// The index is always found because file.attachment_data is
// just an element of plupload.attachment_data
var idx = phpbb_plupload_find_attachment_idx(file.attachment_data.attach_id);
var fields = {};
fields['delete_file[' + idx + ']'] = 1;
var always = function() {
$(evt.target).find('a').removeClass('working');
};
var done = function(response) {
up.removeFile(file);
plupload.attachment_data = response;
phpbb.update_hidden_attachment_inputs(form, phpbb_plupload_attachment_data_serialize(), data);
phpbb_plupload_clear_params(up.settings.multipart_params);
up.settings.multipart_params = $.extend(
up.settings.multipart_params,
phpbb_plupload_attachment_data_serialize()
);
};
$.ajax(phpbb.plupload.config.url, {
type: 'POST',
data: $.extend(fields, phpbb_plupload_attachment_data_serialize()),
headers: {'X-PHPBB-USING-PLUPLOAD': '1'}
})
.always(always)
.done(done);
};
$('#' + file.id)
.addClass('can_delete')
.click(click);
});
}); });
}, 2000);
}); });
})(jQuery); // Avoid conflicts with other libraries

View file

@ -56,16 +56,16 @@ phpbb.plupload = {
multipart_params: {'add_file': '{LA_ADD_FILE}'}, multipart_params: {'add_file': '{LA_ADD_FILE}'},
img_path: '{T_ASSETS_PATH}/plupload/jquery.plupload.queue/img', img_path: '{T_ASSETS_PATH}/plupload/jquery.plupload.queue/img',
element_hook: '#attach-panel .inner', element_hook: '#attach-panel .inner',
form_hook: '#postform' form_hook: '#postform',
} browse_button: 'add_files',
}; drop_element : 'message',
}, },
lang: { lang: {
ERROR: '{LA_ERROR}', ERROR: '{LA_ERROR}',
TOO_MANY_ATTACHMENTS: '{LA_TOO_MANY_ATTACHMENTS}', TOO_MANY_ATTACHMENTS: '{LA_TOO_MANY_ATTACHMENTS}',
}, },
order: '{ATTACH_ORDER}', order: '{ATTACH_ORDER}',
max_files: {MAX_ATTACHMENTS}, maxFiles: {MAX_ATTACHMENTS},
} }
//]]> //]]>
</script> </script>

View file

@ -170,7 +170,15 @@
<div id="tabs" class="sub-panels" data-show-panel="options-panel"> <div id="tabs" class="sub-panels" data-show-panel="options-panel">
<ul> <ul>
<li id="options-panel-tab" class="activetab"><a href="#tabs" data-subpanel="options-panel"><span>{L_OPTIONS}</span></a></li> <li id="options-panel-tab" class="activetab"><a href="#tabs" data-subpanel="options-panel"><span>{L_OPTIONS}</span></a></li>
<!-- IF S_SHOW_ATTACH_BOX --><li id="attach-panel-tab"><a href="#tabs" data-subpanel="attach-panel"><span>{L_ATTACHMENTS}</span></a></li><!-- ENDIF --> <!-- IF S_SHOW_ATTACH_BOX -->
<li id="attach-panel-tab">
<a href="#tabs" data-subpanel="attach-panel">
<span>
{L_ATTACHMENTS} <strong id="file-total-progress"><strong id="file-total-progress-bar"></strong></strong>
</span>
</a>
</li>
<!-- ENDIF -->
<!-- IF S_SHOW_POLL_BOX || S_POLL_DELETE --><li id="poll-panel-tab"><a href="#tabs" data-subpanel="poll-panel"><span>{L_ADD_POLL}</span></a></li><!-- ENDIF --> <!-- IF S_SHOW_POLL_BOX || S_POLL_DELETE --><li id="poll-panel-tab"><a href="#tabs" data-subpanel="poll-panel"><span>{L_ADD_POLL}</span></a></li><!-- ENDIF -->
</ul> </ul>
</div> </div>