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_PROCESS: "serverErrorProc",
 95         SERVER_ERROR_UPLOAD: "serverErrorUp"
 96     };
 97 
 98     var pressButton = function(event) {
 99         $(this).children(":first").css("background-position", "3px 3px").css("padding", "4px 4px 2px 22px");
100     };
101 
102     var unpressButton = function(event) {
103         $(this).children(":first").css("background-position", "2px 2px").css("padding", "3px 5px 3px 21px");
104     };
105 
106     rf.BaseComponent.extend(rf.ui.FileUpload);
107 
108     function TypeRejectedException(fileName) {
109         this.name = "TypeRejectedException";
110         this.message = "The type of file " + fileName + " is not accepted";
111         this.fileName = fileName;
112     }
113 
114     $.extend(rf.ui.FileUpload.prototype, (function () {
115 
116         return {
117             name: "FileUpload",
118 
119             doneLabel: "Done",
120             sizeExceededLabel: "File size is exceeded",
121             stoppedLabel: "",
122             serverErrorProcLabel: "Server error: error in processing",
123             serverErrorUpLabel: "Server error: upload failed",
124             clearLabel: "Clear",
125             deleteLabel: "Delete",
126 
127             __addFiles : function(files) {
128                 var context = {
129                     acceptedFileNames: [],
130                     rejectedFileNames: []
131                 };
132 
133                 if (files) {
134                     for (var i = 0 ; i < files.length; i++) {
135                         this.__tryAddItem(context, files[i]);
136 
137                         if (this.maxFilesQuantity && this.__getTotalItemCount() >= this.maxFilesQuantity) {
138                             this.addButton.hide();
139                             break;
140                         }
141                     }
142                 } else {
143                     var fileName = this.input.val();
144                     this.__tryAddItem(context, fileName);
145                 }
146 
147                 if (context.rejectedFileNames.length > 0) {
148                     rf.Event.fire(this.element, "ontyperejected", context.rejectedFileNames.join(','));
149                 }
150 
151                 if (this.immediateUpload) {
152                     this.__startUpload();
153                 }
154             },
155 
156             __addItems : function() {
157                 this.__addFiles(this.input.prop("files"));
158             },
159 
160             __addItemsFromDrop: function(dropEvent) {
161                 dropEvent.stopPropagation();
162                 dropEvent.preventDefault();
163                 
164                 if (this.maxFilesQuantity && this.__getTotalItemCount() >= this.maxFilesQuantity) {
165                     return;
166                 }
167 
168                 this.__addFiles(dropEvent.originalEvent.dataTransfer.files);
169             },
170 
171             __tryAddItem: function(context, file) {
172                 try {
173                     if (this.__addItem(file)) {
174                         context.acceptedFileNames.push(file.name);
175                     }
176                 } catch (e) {
177                     if (e instanceof TypeRejectedException) {
178                         context.rejectedFileNames.push(file.name);
179                     } else {
180                         throw e;
181                     }
182                 }
183             },
184 
185             __addItem: function(file) {
186                 var fileName = file.name;
187                 if (!navigator.platform.indexOf("Win")) {
188                     fileName = fileName.match(/[^\\]*$/)[0];
189                 } else {
190                     if (!fileName.indexOf(FAKE_PATH)) {
191                         fileName = fileName.substr(FAKE_PATH.length);
192                     } else {
193                         fileName = fileName.match(/[^\/]*$/)[0];
194                     }
195                 }
196                 if (this.__accept(fileName) && (!this.noDuplicate || !this.__isFileAlreadyAdded(fileName))) {
197                     this.input.remove();
198                     this.input.unbind("change", this.addProxy);
199                     var item = new Item(this, file);
200                     this.list.append(item.getJQuery());
201                     this.items.push(item);
202                     this.input = this.cleanInput.clone();
203                     this.inputContainer.append(this.input);
204                     this.input.change(this.addProxy);
205                     this.__updateButtons();
206                     rf.Event.fire(this.element, "onfileselect", fileName);
207                     return true;
208                 }
209 
210                 return false;
211             },
212 
213             __removeItem: function(item) {
214                 this.items.splice($.inArray(item, this.items), 1);
215                 this.submitedItems.splice($.inArray(item, this.submitedItems), 1);
216                 this.__updateButtons();
217                 rf.Event.fire(this.element, "onclear", [item.model]);
218             },
219 
220             __removeAllItems: function(item) {
221                 var itemsRemoved = [];
222                 for (var i in this.submitedItems) {
223                     itemsRemoved.push(this.submitedItems[i].model);
224                 }
225                 for (var i in this.items) {
226                     itemsRemoved.push(this.items[i].model);
227                 }
228                 this.list.empty();
229                 this.items.splice(0, this.items.length);
230                 this.submitedItems.splice(0, this.submitedItems.length);
231                 this.__updateButtons();
232                 rf.Event.fire(this.element, "onclear", itemsRemoved);
233             },
234 
235             __updateButtons: function() {
236                 if (!this.loadableItem && this.list.children(".rf-fu-itm").size()) {
237                     if (this.items.length) {
238                         this.uploadButton.css("display", "inline-block");
239                     } else {
240                         this.uploadButton.hide();
241                     }
242                     this.clearButton.css("display", "inline-block");
243                 } else {
244                     this.uploadButton.hide();
245                     this.clearButton.hide();
246                 }
247                 if (this.maxFilesQuantity && this.__getTotalItemCount() >= this.maxFilesQuantity) {
248                     this.addButton.hide();
249                 } else {
250                     this.addButton.css("display", "inline-block");
251                 }
252             },
253 
254             __startUpload: function() {
255                 if (!this.items.length) {
256                     this.__finishUpload();
257                     return;
258                 }
259                 this.loadableItem = this.items.shift();
260                 this.__updateButtons();
261                 this.loadableItem.startUploading();
262             },
263 
264             __accept: function(fileName) {
265                 fileName = fileName.toUpperCase();
266                 var result = !this.acceptedTypes;
267                 for (var i = 0; !result && i < this.acceptedTypes.length; i++) {
268                     var extension = "." + this.acceptedTypes[i];
269 
270                     if (extension === "." && fileName.indexOf(".") < 0) {
271                         // no extension
272                         result = true;
273                     } else {
274                         result = fileName.indexOf(extension, fileName.length - extension.length) !== -1;
275                     }
276                 }
277                 if (!result) {
278                     throw new TypeRejectedException(fileName);
279                 }
280                 return result;
281             },
282 
283             __isFileAlreadyAdded: function(fileName) {
284                 var result = false;
285                 for (var i = 0; !result && i < this.items.length; i++) {
286                     result = this.items[i].model.name == fileName;
287                 }
288                 result = result || (this.loadableItem && this.loadableItem.model.name == fileName);
289                 for (var i = 0; !result && i < this.submitedItems.length; i++) {
290                     result = this.submitedItems[i].model.name == fileName;
291                 }
292                 return result;
293             },
294 
295 
296             __getTotalItemCount : function() {
297                 return this.__getItemCountByState(this.items, ITEM_STATE.NEW)
298                     + this.__getItemCountByState(this.submitedItems, ITEM_STATE.DONE);
299             },
300 
301             __getItemCountByState : function(items) {
302                 var statuses = {};
303                 var s = 0;
304                 for ( var i = 1; i < arguments.length; i++) {
305                     statuses[arguments[i]] = true;
306                 }
307                 for ( var i = 0; i < items.length; i++) {
308                     if (statuses[items[i].model.state]) {
309                         s++;
310                     }
311                 }
312                 return s;
313             },
314 
315             __finishUpload : function() {
316                 this.loadableItem = null;
317                 this.__updateButtons();
318                 var items = [];
319                 for (var i in this.submitedItems) {
320                     items.push(this.submitedItems[i].model);
321                 }
322                 for (var i in this.items) {
323                     items.push(this.items[i].model);
324                 }
325                 rf.Event.fire(this.element, "onuploadcomplete", items);
326             }
327         };
328     })());
329 
330 
331     var Item = function(fileUpload, file) {
332         this.fileUpload = fileUpload;
333         this.model = {name: file.name, state: ITEM_STATE.NEW, file: file};
334     };
335 
336     $.extend(Item.prototype, {
337             getJQuery: function() {
338                 this.element = $(ITEM_HTML);
339                 var leftArea = this.element.children(".rf-fu-itm-lft:first");
340                 this.label = leftArea.children(".rf-fu-itm-lbl:first");
341                 this.state = this.label.nextAll(".rf-fu-itm-st:first");
342                 this.progressBar = leftArea.find(".progress-bar");
343                 this.progressBar.parent().hide();
344                 this.progressLabel = this.progressBar.find('span');
345                 this.link = leftArea.next().children("a");
346                 this.label.html(this.model.name);
347                 this.link.html(this.fileUpload["deleteLabel"]);
348                 this.link.click($.proxy(this.removeOrStop, this));
349                 return this.element;
350             },
351 
352             removeOrStop: function() {
353                 this.element.remove();
354                 this.fileUpload.__removeItem(this);
355             },
356 
357             startUploading: function() {
358                 this.state.css("display", "block");
359                 this.progressBar.parent().show();
360                 this.progressLabel.html("0 %");
361                 this.link.html("");
362                 this.model.state = ITEM_STATE.UPLOADING;
363                 this.uid = Math.random();
364 
365                 var formData = new FormData(this.fileUpload.form[0]);
366                     fileName = this.model.file.name;
367 
368                 formData.append(this.fileUpload.id, this.model.file);
369 
370                 var originalAction = this.fileUpload.form.attr("action"),
371                     delimiter = originalAction.indexOf("?") == -1 ? "?" : "&",
372                     newAction =  originalAction + delimiter + UID + "=" + this.uid + 
373                         "&javax.faces.partial.ajax=true" + 
374                         "&javax.faces.source="           + this.fileUpload.id +
375                         "&javax.faces.partial.execute="  + this.fileUpload.id +
376                         "&org.richfaces.ajax.component=" + this.fileUpload.id + 
377                         "&" + jsf.getViewState(this.fileUpload.form[0]);
378 
379                 if (jsf.getClientWindow && jsf.getClientWindow()) {
380                     newAction += "&javax.faces.ClientWindow=" + jsf.getClientWindow();
381                 };
382 
383                 this.xhr = new XMLHttpRequest();
384 
385                 this.xhr.open('POST', newAction, true);
386                 this.xhr.setRequestHeader('Faces-Request', 'partial/ajax');
387                 
388                 this.xhr.upload.onprogress = $.proxy(function(e) {
389                         if (e.lengthComputable) {
390                             var progress = Math.floor((e.loaded / e.total) * 100);
391                             this.progressLabel.html( progress + " %" );
392                             this.progressBar.attr("aria-valuenow", progress);
393                             this.progressBar.css("width", progress + "%");
394                         }
395                     }, this);
396 
397                 this.xhr.upload.onerror = $.proxy(function (e) {
398                         this.fileUpload.loadableItem = null;
399                         this.finishUploading(ITEM_STATE.SERVER_ERROR_UPLOAD);
400                     }, this);
401                     
402                 this.xhr.onload = $.proxy(function (e) {
403                     switch (e.target.status) {
404                         case 413:
405                             responseStatus = ITEM_STATE.SIZE_EXCEEDED;
406                             break;
407                         case 200:
408                             responseStatus = ITEM_STATE.DONE;
409                             break;
410                         default: // 500 - error in processing parts
411                             responseStatus = ITEM_STATE.SERVER_ERROR_PROCESS;
412                     }
413                     
414                     var responseContext = {
415                             source: this.fileUpload.element[0],
416                             element: this.fileUpload.element[0],
417                             /* hack for MyFaces *