      1 /*
      2  * JBoss, Home of Professional Open Source
      3  * Copyright ${year}, Red Hat, Inc. and individual contributors
      4  * by the @authors tag. See the copyright.txt in the distribution for a
      5  * full listing of individual contributors.
      6  *
      7  * This is free software; you can redistribute it and/or modify it
      8  * under the terms of the GNU Lesser General Public License as
      9  * published by the Free Software Foundation; either version 2.1 of
     10  * the License, or (at your option) any later version.
     11  *
     12  * This software is distributed in the hope that it will be useful,
     13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
     14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
     15  * Lesser General Public License for more details.
     16  *
     17  * You should have received a copy of the GNU Lesser General Public
     18  * License along with this software; if not, write to the Free
     19  * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
     20  * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
     21  */
     22 (function($, rf) {
     23
     24     rf.ui = rf.ui || {};
     25
     26     rf.ui.FileUpload = function(id, options) {
     27         this.id = id;
     28         this.items = [];
     29         this.submitedItems = [];
     30
     31         $.extend(this, options);
     32         if (this.acceptedTypes) {
     33             this.acceptedTypes = $.trim(this.acceptedTypes).toUpperCase().split(/\s*,\s*/);
     34         }
     35         if (this.maxFilesQuantity) {
     36             this.maxFilesQuantity = parseInt($.trim(this.maxFilesQuantity));
     37         }
     38         this.element = $(this.attachToDom());
     39         this.form = this.element.parents("form:first");
     40         var header = this.element.children(".rf-fu-hdr:first");
     41         var leftButtons = header.children(".rf-fu-btns-lft:first");
     42         this.addButton = leftButtons.children(".rf-fu-btn-add:first");
     43         this.uploadButton = this.addButton.next();
     44         this.clearButton = leftButtons.next().children(".rf-fu-btn-clr:first");
     45         this.inputContainer = this.addButton.find(".rf-fu-inp-cntr:first");
     46         this.input = this.inputContainer.children("input");
     47         this.list = header.next();
     48         this.element.bind('dragenter', function(e) {e.stopPropagation(); e.preventDefault();});
     49         this.element.bind('dragover', function(e) {e.stopPropagation(); e.preventDefault();});
     50         this.element.bind('drop', $.proxy(this.__addItemsFromDrop, this));
     51
     52         this.hiddenContainer = this.list.next();
     53         this.cleanInput = this.input.clone();
     54         this.addProxy = $.proxy(this.__addItems, this);
     55         this.input.change(this.addProxy);
     56         this.addButton.mousedown(pressButton).mouseup(unpressButton).mouseout(unpressButton);
     57         this.uploadButton.click($.proxy(this.__startUpload, this)).mousedown(pressButton)
     58             .mouseup(unpressButton).mouseout(unpressButton);
     59         this.clearButton.click($.proxy(this.__removeAllItems, this)).mousedown(pressButton)
     60             .mouseup(unpressButton).mouseout(unpressButton);
     61         if (this.onfilesubmit) {
     62             rf.Event.bind(this.element, "onfilesubmit", new Function("event", this.onfilesubmit));
     63         }
     64         if (this.ontyperejected) {
     65             rf.Event.bind(this.element, "ontyperejected", new Function("event", this.ontyperejected));
     66         }
     67         if (this.onuploadcomplete) {
     68             rf.Event.bind(this.element, "onuploadcomplete", new Function("event", this.onuploadcomplete));
     69         }
     70         if (this.onclear) {
     71             rf.Event.bind(this.element, "onclear", new Function("event", this.onclear));
     72         }
     73         if (this.onfileselect) {
     74             rf.Event.bind(this.element, "onfileselect", new Function("event", this.onfileselect));
     75         }
     76     };
     77
     78     var UID = "rf_fu_uid";
     79     var UID_ALT = "rf_fu_uid_alt";
     80     var FAKE_PATH = "C:\\fakepath\\";
     81     var ITEM_HTML = '<div class="rf-fu-itm">'
     82         + '<span class="rf-fu-itm-lft"><span class="rf-fu-itm-lbl"/><span class="rf-fu-itm-st" />'
     83         + '<div class="progress progress-striped active">'
     84         + '<div class="progress-bar" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">'
     85         + '<span></span></div></div></span>'
     86         + '<span class="rf-fu-itm-rgh"><a href="javascript:void(0)" class="rf-fu-itm-lnk"/></span></div>';
     87
     88     var ITEM_STATE = {
     89         NEW: "new",
     90         UPLOADING: "uploading",
     91         DONE: "done",
     92         SIZE_EXCEEDED: "sizeExceeded",
     93         STOPPED: "stopped",
     94         SERVER_ERROR: "serverError"
     95     };
     96
     97     var pressButton = function(event) {
     98         $(this).children(":first").css("background-position", "3px 3px").css("padding", "4px 4px 2px 22px");
     99     };
    100
    101     var unpressButton = function(event) {
    102         $(this).children(":first").css("background-position", "2px 2px").css("padding", "3px 5px 3px 21px");
    103     };
    104
    105     rf.BaseComponent.extend(rf.ui.FileUpload);
    106
    107     function TypeRejectedException(fileName) {
    108         this.name = "TypeRejectedException";
    109         this.message = "The type of file " + fileName + " is not accepted";
    110         this.fileName = fileName;
    111     }
    112
    113     $.extend(rf.ui.FileUpload.prototype, (function () {
    114
    115         return {
    116             name: "FileUpload",
    117
    118             doneLabel: "Done",
    119             sizeExceededLabel: "File size is exceeded",
    120             stoppedLabel: "",
    121             serverErrorLabel: "Server error",
    122             clearLabel: "Clear",
    123             deleteLabel: "Delete",
    124
    125             __addFiles : function(files) {
    126                 var context = {
    127                     acceptedFileNames: [],
    128                     rejectedFileNames: []
    129                 };
    130
    131                 if (files) {
    132                     for (var i = 0 ; i < files.length; i++) {
    133                         this.__tryAddItem(context, files[i]);
    134
    135                         if (this.maxFilesQuantity && this.__getTotalItemCount() >= this.maxFilesQuantity) {
    136                             this.addButton.hide();
    137                             break;
    138                         }
    139                     }
    140                 } else {
    141                     var fileName = this.input.val();
    142                     this.__tryAddItem(context, fileName);
    143                 }
    144
    145                 if (context.rejectedFileNames.length > 0) {
    146                     rf.Event.fire(this.element, "ontyperejected", context.rejectedFileNames.join(','));
    147                 }
    148
    149                 if (this.immediateUpload) {
    150                     this.__startUpload();
    151                 }
    152             },
    153
    154             __addItems : function() {
    155                 this.__addFiles(this.input.prop("files"));
    156             },
    157
    158             __addItemsFromDrop: function(dropEvent) {
    159                 dropEvent.stopPropagation();
    160                 dropEvent.preventDefault();
    161
    162                 if (this.maxFilesQuantity && this.__getTotalItemCount() >= this.maxFilesQuantity) {
    163                     return;
    164                 }
    165
    166                 this.__addFiles(dropEvent.originalEvent.dataTransfer.files);
    167             },
    168
    169             __tryAddItem: function(context, file) {
    170                 try {
    171                     if (this.__addItem(file)) {
    172                         context.acceptedFileNames.push(file.name);
    173                     }
    174                 } catch (e) {
    175                     if (e instanceof TypeRejectedException) {
    176                         context.rejectedFileNames.push(file.name);
    177                     } else {
    178                         throw e;
    179                     }
    180                 }
    181             },
    182
    183             __addItem: function(file) {
    184                 var fileName = file.name;
    185                 if (!navigator.platform.indexOf("Win")) {
    186                     fileName = fileName.match(/[^\\]*$/)[0];
    187                 } else {
    188                     if (!fileName.indexOf(FAKE_PATH)) {
    189                         fileName = fileName.substr(FAKE_PATH.length);
    190                     } else {
    191                         fileName = fileName.match(/[^\/]*$/)[0];
    192                     }
    193                 }
    194                 if (this.__accept(fileName) && (!this.noDuplicate || !this.__isFileAlreadyAdded(fileName))) {
    195                     this.input.remove();
    196                     this.input.unbind("change", this.addProxy);
    197                     var item = new Item(this, file);
    198                     this.list.append(item.getJQuery());
    199                     this.items.push(item);
    200                     this.input = this.cleanInput.clone();
    201                     this.inputContainer.append(this.input);
    202                     this.input.change(this.addProxy);
    203                     this.__updateButtons();
    204                     rf.Event.fire(this.element, "onfileselect", fileName);
    205                     return true;
    206                 }
    207
    208                 return false;
    209             },
    210
    211             __removeItem: function(item) {
    212                 this.items.splice($.inArray(item, this.items), 1);
    213                 this.submitedItems.splice($.inArray(item, this.submitedItems), 1);
    214                 this.__updateButtons();
    215                 rf.Event.fire(this.element, "onclear", [item.model]);
    216             },
    217
    218             __removeAllItems: function(item) {
    219                 var itemsRemoved = [];
    220                 for (var i in this.submitedItems) {
    221                     itemsRemoved.push(this.submitedItems[i].model);
    222                 }
    223                 for (var i in this.items) {
    224                     itemsRemoved.push(this.items[i].model);
    225                 }
    226                 this.list.empty();
    227                 this.items.splice(0, this.items.length);
    228                 this.submitedItems.splice(0, this.submitedItems.length);
    229                 this.__updateButtons();
    230                 rf.Event.fire(this.element, "onclear", itemsRemoved);
    231             },
    232
    233             __updateButtons: function() {
    234                 if (!this.loadableItem && this.list.children(".rf-fu-itm").size()) {
    235                     if (this.items.length) {
    236                         this.uploadButton.css("display", "inline-block");
    237                     } else {
    238                         this.uploadButton.hide();
    239                     }
    240                     this.clearButton.css("display", "inline-block");
    241                 } else {
    242                     this.uploadButton.hide();
    243                     this.clearButton.hide();
    244                 }
    245                 if (this.maxFilesQuantity && this.__getTotalItemCount() >= this.maxFilesQuantity) {
    246                     this.addButton.hide();
    247                 } else {
    248                     this.addButton.css("display", "inline-block");
    249                 }
    250             },
    251
    252             __startUpload: function() {
    253                 if (!this.items.length) {
    254                     this.__finishUpload();
    255                     return;
    256                 }
    257                 this.loadableItem = this.items.shift();
    258                 this.__updateButtons();
    259                 this.loadableItem.startUploading();
    260             },
    261
    262             __accept: function(fileName) {
    263                 fileName = fileName.toUpperCase();
    264                 var result = !this.acceptedTypes;
    265                 for (var i = 0; !result && i < this.acceptedTypes.length; i++) {
    266                     var extension = "." + this.acceptedTypes[i];
    267
    268                     if (extension === "." && fileName.indexOf(".") < 0) {
    269                         // no extension
    270                         result = true;
    271                     } else {
    272                         result = fileName.indexOf(extension, fileName.length - extension.length) !== -1;
    273                     }
    274                 }
    275                 if (!result) {
    276                     throw new TypeRejectedException(fileName);
    277                 }
    278                 return result;
    279             },
    280
    281             __isFileAlreadyAdded: function(fileName) {
    282                 var result = false;
    283                 for (var i = 0; !result && i < this.items.length; i++) {
    284                     result = this.items[i].model.name == fileName;
    285                 }
    286                 result = result || (this.loadableItem && this.loadableItem.model.name == fileName);
    287                 for (var i = 0; !result && i < this.submitedItems.length; i++) {
    288                     result = this.submitedItems[i].model.name == fileName;
    289                 }
    290                 return result;
    291             },
    292
    293
    294             __getTotalItemCount : function() {
    295                 return this.__getItemCountByState(this.items, ITEM_STATE.NEW)
    296                     + this.__getItemCountByState(this.submitedItems, ITEM_STATE.DONE);
    297             },
    298
    299             __getItemCountByState : function(items) {
    300                 var statuses = {};
    301                 var s = 0;
    302                 for ( var i = 1; i < arguments.length; i++) {
    303                     statuses[arguments[i]] = true;
    304                 }
    305                 for ( var i = 0; i < items.length; i++) {
    306                     if (statuses[items[i].model.state]) {
    307                         s++;
    308                     }
    309                 }
    310                 return s;
    311             },
    312
    313             __finishUpload : function() {
    314                 this.loadableItem = null;
    315                 this.__updateButtons();
    316                 var items = [];
    317                 for (var i in this.submitedItems) {
    318                     items.push(this.submitedItems[i].model);
    319                 }
    320                 for (var i in this.items) {
    321                     items.push(this.items[i].model);
    322                 }
    323                 rf.Event.fire(this.element, "onuploadcomplete", items);
    324             }
    325         };
    326     })());
    327
    328
    329     var Item = function(fileUpload, file) {
    330         this.fileUpload = fileUpload;
    331         this.model = {name: file.name, state: ITEM_STATE.NEW, file: file};
    332     };
    333
    334     $.extend(Item.prototype, {
    335             getJQuery: function() {
    336                 this.element = $(ITEM_HTML);
    337                 var leftArea = this.element.children(".rf-fu-itm-lft:first");
    338                 this.label = leftArea.children(".rf-fu-itm-lbl:first");
    339                 this.state = this.label.nextAll(".rf-fu-itm-st:first");
    340                 this.progressBar = leftArea.find(".progress-bar");
    341                 this.progressBar.parent().hide();
    342                 this.progressLabel = this.progressBar.find('span');
    343                 this.link = leftArea.next().children("a");
    344                 this.label.html(this.model.name);
    345                 this.link.html(this.fileUpload["deleteLabel"]);
    346                 this.link.click($.proxy(this.removeOrStop, this));
    347                 return this.element;
    348             },
    349
    350             removeOrStop: function() {
    351                 this.element.remove();
    352                 this.fileUpload.__removeItem(this);
    353             },
    354
    355             startUploading: function() {
    356                 this.state.css("display", "block");
    357                 this.progressBar.parent().show();
    358                 this.progressLabel.html("0 %");
    359                 this.link.html("");
    360                 this.model.state = ITEM_STATE.UPLOADING;
    361                 this.uid = Math.random();
    362
    363                 var formData = new FormData(this.fileUpload.form[0]);
    364                     fileName = this.model.file.name;
    365
    366                 formData.append(this.fileUpload.id, this.model.file);
    367
    368                 var originalAction = this.fileUpload.form.attr("action"),
    369                     delimiter = originalAction.indexOf("?") == -1 ? "?" : "&",
    370                     newAction =  originalAction + delimiter + UID + "=" + this.uid +
    371                     "&javax.faces.partial.ajax=true" +
    372                     "&javax.faces.source="           + this.fileUpload.id +
    373                     "&javax.faces.partial.execute="  + this.fileUpload.id +
    374                     "&org.richfaces.ajax.component=" + this.fileUpload.id +
    375                     "&javax.faces.ViewState="        + this.fileUpload.form.find('input[name="javax.faces.ViewState"]').val();
    376
    377                 if (jsf.getClientWindow && jsf.getClientWindow()) {
    378                     newAction += "&javax.faces.ClientWindow=" + jsf.getClientWindow();
    379                 };
    380
    381                 this.xhr = new XMLHttpRequest();
    382
    383                 this.xhr.open('POST', newAction, true);
    384                 this.xhr.setRequestHeader('Faces-Request', 'partial/ajax');
    385
    386                 this.xhr.upload.onprogress = $.proxy(function(e) {
    387                         if (e.lengthComputable) {
    388                             var progress = Math.floor((e.loaded / e.total) * 100);
    389                             this.progressLabel.html( progress + " %" );
    390                             this.progressBar.attr("aria-valuenow", progress);
    391                             this.progressBar.css("width", progress + "%");
    392                         }
    393                     }, this);
    394
    395                 this.xhr.upload.onerror = $.proxy(function (e) {
    396                         this.finishUploading(ITEM_STATE.SERVER_ERROR);
    397                     }, this);
    398
    399                 this.xhr.onload = $.proxy(function (e) {
    400                     switch (e.target.status) {
    401                         case 413:
    402                             responseStatus = ITEM_STATE.SIZE_EXCEEDED;
    403                             break;
    404                         case 200:
    405                             responseStatus = ITEM_STATE.DONE;
    406                             break;
    407                         default: // 500 - error in processing parts
    408                             responseStatus = ITEM_STATE.SERVER_ERROR;
    409                     }
    410
    411                     var responseContext = {
    412                             source: this.fileUpload.element[0],
    413                             element: this.fileUpload.element[0],
    414                             /* hack for MyFaces *