From 7cc4b46d7ec8b89af7d47e27e4e954b3d9c4556b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Bolvin?= <frederic@bol.vin> Date: Sat, 10 Sep 2016 17:06:33 +0200 Subject: [PATCH] Replaced fileuploader-custom with FineUploader closes #7083 --- Changelog.md | 1 + Gemfile | 1 + Gemfile.lock | 4 +- .../app/views/publisher/uploader_view.js | 45 +- .../helpers/profile_photo_uploader.js | 94 +- app/assets/javascripts/jasmine-load-all.js | 2 +- app/assets/javascripts/main.js | 2 +- app/assets/javascripts/mobile/mobile.js | 2 +- .../mobile/mobile_file_uploader.js | 129 +- features/desktop/edits_profile.feature | 6 +- features/mobile/edits_profile.feature | 4 +- features/mobile/getting_started.feature | 4 +- features/mobile/multiphoto.feature | 6 +- features/mobile/posts_from_main_page.feature | 10 +- features/support/publishing_cuke_helpers.rb | 4 +- lib/assets/javascripts/fileuploader-custom.js | 1267 ----------------- .../app/views/publisher_view_spec.js | 6 +- 17 files changed, 188 insertions(+), 1399 deletions(-) delete mode 100644 lib/assets/javascripts/fileuploader-custom.js diff --git a/Changelog.md b/Changelog.md index b025b9a14d..c4232da020 100644 --- a/Changelog.md +++ b/Changelog.md @@ -2,6 +2,7 @@ ## Refactor * Increase the spacing above and below post contents [#7267](https://github.com/diaspora/diaspora/pull/7267) +* Replace fileuploader-custom with FineUploader [#7083](https://github.com/diaspora/diaspora/pull/7083) ## Bug fixes * Fix background color of year on notifications page with dark theme [#7263](https://github.com/diaspora/diaspora/pull/7263) diff --git a/Gemfile b/Gemfile index 9e4a60e827..0c25a8dc92 100644 --- a/Gemfile +++ b/Gemfile @@ -105,6 +105,7 @@ source "https://rails-assets.org" do gem "rails-assets-highlightjs", "9.7.0" gem "rails-assets-bootstrap-markdown", "2.10.0" gem "rails-assets-corejs-typeahead", "1.0.1" + gem "rails-assets-fineuploader-dist", "5.11.0" # jQuery plugins diff --git a/Gemfile.lock b/Gemfile.lock index 70ff9e4fee..63b87bd2c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -657,6 +657,7 @@ GEM rails-assets-jquery.ui (~> 1.11.4) rails-assets-emojione (2.0.1) rails-assets-favico.js (0.3.10) + rails-assets-fineuploader-dist (5.11.0) rails-assets-highlightjs (9.7.0) rails-assets-jasmine (2.4.1) rails-assets-jasmine-ajax (3.2.0) @@ -1001,6 +1002,7 @@ DEPENDENCIES rails-assets-bootstrap-markdown (= 2.10.0)! rails-assets-corejs-typeahead (= 1.0.1)! rails-assets-diaspora_jsxc (= 0.1.5.develop.7)! + rails-assets-fineuploader-dist (= 5.11.0)! rails-assets-highlightjs (= 9.7.0)! rails-assets-jasmine-ajax (= 3.2.0)! rails-assets-jquery (= 2.2.4)! @@ -1052,4 +1054,4 @@ DEPENDENCIES will_paginate (= 3.1.5) BUNDLED WITH - 1.13.6 + 1.13.7 diff --git a/app/assets/javascripts/app/views/publisher/uploader_view.js b/app/assets/javascripts/app/views/publisher/uploader_view.js index 5f1fedc0cc..775cf69bba 100644 --- a/app/assets/javascripts/app/views/publisher/uploader_view.js +++ b/app/assets/javascripts/app/views/publisher/uploader_view.js @@ -5,32 +5,43 @@ // progress. Attaches previews of finished uploads to the publisher. app.views.PublisherUploader = Backbone.View.extend({ - allowedExtensions: ["jpg", "jpeg", "png", "gif", "tif", "tiff"], sizeLimit: 4194304, // bytes initialize: function(opts) { this.publisher = opts.publisher; - - this.uploader = new qq.FileUploaderBasic({ + this.uploader = new qq.FineUploaderBasic({ element: this.el, - button: this.el, - - //debug: true, - - action: "/photos", - params: { photo: { pending: true }}, - allowedExtensions: this.allowedExtensions, - sizeLimit: this.sizeLimit, + button: this.el, + + request: { + endpoint: Routes.photos(), + params: { + /* eslint-disable camelcase */ + authenticity_token: $("meta[name='csrf-token']").attr("content"), + /* eslint-enable camelcase */ + photo: { + pending: true + } + } + }, + validation: { + allowedExtensions: this.allowedExtensions, + sizeLimit: this.sizeLimit + }, messages: { - typeError: Diaspora.I18n.t("photo_uploader.invalid_ext"), - sizeError: Diaspora.I18n.t("photo_uploader.size_error"), + typeError: Diaspora.I18n.t("photo_uploader.invalid_ext"), + sizeError: Diaspora.I18n.t("photo_uploader.size_error"), emptyError: Diaspora.I18n.t("photo_uploader.empty") }, - onProgress: _.bind(this.progressHandler, this), - onSubmit: _.bind(this.submitHandler, this), - onComplete: _.bind(this.uploadCompleteHandler, this) - + callbacks: { + onProgress: _.bind(this.progressHandler, this), + onSubmit: _.bind(this.submitHandler, this), + onComplete: _.bind(this.uploadCompleteHandler, this), + onError: function(id, name, errorReason) { + if (app.flashMessages) { app.flashMessages.error(errorReason); } + } + } }); this.info = $("<div id=\"fileInfo\" />"); diff --git a/app/assets/javascripts/helpers/profile_photo_uploader.js b/app/assets/javascripts/helpers/profile_photo_uploader.js index c50bc50219..1b50b5bcf3 100644 --- a/app/assets/javascripts/helpers/profile_photo_uploader.js +++ b/app/assets/javascripts/helpers/profile_photo_uploader.js @@ -8,18 +8,22 @@ Diaspora.ProfilePhotoUploader.prototype = { constructor: Diaspora.ProfilePhotoUploader, initialize: function() { - new qq.FileUploaderBasic({ + new qq.FineUploaderBasic({ element: document.getElementById("file-upload"), - params: {"photo": {"pending": true, "aspect_ids": "all", "set_profile_photo": true}}, - allowedExtensions: ["jpg", "jpeg", "png"], - action: "/photos", - button: document.getElementById("file-upload"), - sizeLimit: 4194304, - - onProgress: function(id, fileName, loaded, total) { - var progress = Math.round(loaded / total * 100); - $("#fileInfo").text(fileName + " " + progress + "%"); + validation: { + allowedExtensions: ["jpg", "jpeg", "png"], + sizeLimit: 4194304 }, + request: { + endpoint: Routes.photos(), + params: { + /* eslint-disable camelcase */ + authenticity_token: $("meta[name='csrf-token']").attr("content"), + /* eslint-enable camelcase */ + photo: {"pending": true, "aspect_ids": "all", "set_profile_photo": true} + } + }, + button: document.getElementById("file-upload"), messages: { typeError: Diaspora.I18n.t("photo_uploader.invalid_ext"), @@ -27,33 +31,53 @@ Diaspora.ProfilePhotoUploader.prototype = { emptyError: Diaspora.I18n.t("photo_uploader.empty") }, - onSubmit: function() { - $("#file-upload").addClass("loading"); - $("#profile_photo_upload").find(".avatar").addClass("loading"); - $("#file-upload-spinner").removeClass("hidden"); - $("#fileInfo").show(); - }, + callbacks: { + onProgress: function(id, fileName, loaded, total) { + var progress = Math.round(loaded / total * 100); + $("#fileInfo").text(fileName + " " + progress + "%"); + }, + onSubmit: function() { + $("#file-upload").addClass("loading"); + $("#profile_photo_upload").find(".avatar").addClass("loading"); + $("#file-upload-spinner").removeClass("hidden"); + $("#fileInfo").show(); + }, + onComplete: function(id, fileName, responseJSON) { + $("#file-upload-spinner").addClass("hidden"); + $("#fileInfo").text(Diaspora.I18n.t("photo_uploader.completed", {"file": fileName})); + $("#file-upload").removeClass("loading"); - onComplete: function(_id, fileName, responseJSON) { - $("#file-upload-spinner").addClass("hidden"); - $("#fileInfo").text(Diaspora.I18n.t("photo_uploader.completed", {"file": fileName})); - $("#file-upload").removeClass("loading"); - - /* flash message prompt */ - var message = Diaspora.I18n.t("photo_uploader.looking_good"); - if (app.flashMessages) { app.flashMessages.success(message); } - - var id = responseJSON.data.photo.id; - var url = responseJSON.data.photo.unprocessed_image.url; - var oldPhoto = $("#photo_id"); - if (oldPhoto.length === 0) { - $("#update_profile_form").prepend("<input type='hidden' value='" + id + "' id='photo_id' name='photo_id'/>"); - } else { - oldPhoto.val(id); - } + if (responseJSON.data !== undefined) { + /* flash message prompt */ + var message = Diaspora.I18n.t("photo_uploader.looking_good"); + if (app.flashMessages) { + app.flashMessages.success(message); + } else { + alert(message); + } - $("#profile_photo_upload").find(".avatar").removeClass("loading"); - $("#profile_photo_upload").find(".avatar").attr("src", url); + var photoId = responseJSON.data.photo.id; + var url = responseJSON.data.photo.unprocessed_image.url; + var oldPhoto = $("#photo_id"); + if (oldPhoto.length === 0) { + $("#update_profile_form") + .prepend("<input type='hidden' value='" + photoId + "' id='photo_id' name='photo_id'/>"); + } else { + oldPhoto.val(photoId); + } + + $("#profile_photo_upload").find(".avatar").attr("src", url); + $(".avatar[data-person_id=" + gon.user.id + "]").attr("src", url); + } + $("#profile_photo_upload").find(".avatar").removeClass("loading"); + }, + onError: function(id, name, errorReason) { + if (app.flashMessages) { + app.flashMessages.error(errorReason); + } else { + alert(errorReason); + } + } } }); } diff --git a/app/assets/javascripts/jasmine-load-all.js b/app/assets/javascripts/jasmine-load-all.js index c84835fe3f..756f84ce7d 100644 --- a/app/assets/javascripts/jasmine-load-all.js +++ b/app/assets/javascripts/jasmine-load-all.js @@ -2,7 +2,7 @@ //= require handlebars.runtime //= require templates //= require main -//= require fileuploader-custom +//= require fineuploader-dist/dist/fine-uploader.core //= require mobile/mobile //= require jquery.autoSuggest.custom //= require contact-list diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 36541f536d..96acc6b469 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -18,7 +18,7 @@ //= require jquery-ui/sortable //= require keycodes //= require jquery.autoSuggest.custom -//= require fileuploader-custom +//= require fineuploader-dist/dist/fine-uploader.core //= require handlebars.runtime //= require posix-bracket-expressions //= require markdown-it diff --git a/app/assets/javascripts/mobile/mobile.js b/app/assets/javascripts/mobile/mobile.js index 5ae8717e37..70f55e694f 100644 --- a/app/assets/javascripts/mobile/mobile.js +++ b/app/assets/javascripts/mobile/mobile.js @@ -10,7 +10,7 @@ //= require autosize //= require keycodes //= require jquery.autoSuggest.custom -//= require fileuploader-custom +//= require fineuploader-dist/dist/fine-uploader.core //= require rails-timeago //= require underscore //= require bootstrap diff --git a/app/assets/javascripts/mobile/mobile_file_uploader.js b/app/assets/javascripts/mobile/mobile_file_uploader.js index 14f966eb2f..3b5f996a79 100644 --- a/app/assets/javascripts/mobile/mobile_file_uploader.js +++ b/app/assets/javascripts/mobile/mobile_file_uploader.js @@ -4,78 +4,93 @@ function createUploader(){ var aspectIds = gon.preloads.aspect_ids; - new qq.FileUploaderBasic({ - element: document.getElementById('file-upload-publisher'), - params: {'photo' : {'pending' : 'true', 'aspect_ids' : aspectIds},}, - allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'tiff'], - action: "/photos", - debug: false, - button: document.getElementById('file-upload-publisher'), - sizeLimit: 4194304, + new qq.FineUploaderBasic({ + element: document.getElementById("file-upload-publisher"), + request: { + endpoint: Routes.photos(), + params: { + /* eslint-disable camelcase */ + authenticity_token: $("meta[name='csrf-token']").attr("content"), + photo: { + aspect_ids: aspectIds, + /* eslint-enable camelcase */ + pending: true + } + } + }, + validation: { + allowedExtensions: ["jpg", "jpeg", "png", "gif", "tif", "tiff"], + sizeLimit: 4194304 + }, + button: document.getElementById("file-upload-publisher"), - onProgress: function(id, fileName, loaded, total){ - var progress = Math.round(loaded / total * 100 ); - $('#fileInfo-publisher').text(fileName + ' ' + progress + '%'); - }, - - messages: { - typeError: Diaspora.I18n.t("photo_uploader.invalid_ext"), - sizeError: Diaspora.I18n.t("photo_uploader.size_error"), - emptyError: Diaspora.I18n.t("photo_uploader.empty") - }, - - onSubmit: function(){ - $('#file-upload-publisher').addClass("loading"); - $('#publisher_textarea_wrapper').addClass("with_attachments"); - $('#photodropzone').append( + callbacks: { + onProgress: function(id, fileName, loaded, total) { + var progress = Math.round(loaded / total * 100); + $("#fileInfo-publisher").text(fileName + " " + progress + "%"); + }, + onSubmit: function() { + $("#file-upload-publisher").addClass("loading"); + $("#publisher_textarea_wrapper").addClass("with_attachments"); + $("#photodropzone").append( "<li class='publisher_photo loading' style='position:relative;'>" + - "<img alt='Ajax-loader2' src='"+ImagePaths.get('ajax-loader2.gif')+"' />" + + "<img alt='Ajax-loader2' src='" + ImagePaths.get("ajax-loader2.gif") + "' />" + "</li>" - ); - }, + ); + }, + onComplete: function(_id, fileName, responseJSON) { + if (responseJSON.data === undefined) { + return; + } - onComplete: function(_id, fileName, responseJSON) { - $('#fileInfo-publisher').text(Diaspora.I18n.t("photo_uploader.completed", {'file': fileName})); + $("#fileInfo-publisher").text(Diaspora.I18n.t("photo_uploader.completed", {"file": fileName})); var id = responseJSON.data.photo.id, url = responseJSON.data.photo.unprocessed_image.url, - currentPlaceholder = $('li.loading').first(); + currentPlaceholder = $("li.loading").first(); - $('#publisher_textarea_wrapper').addClass("with_attachments"); - $('#new_status_message').append("<input type='hidden' value='" + id + "' name='photos[]' />"); + $("#publisher_textarea_wrapper").addClass("with_attachments"); + $("#new_status_message").append("<input type='hidden' value='" + id + "' name='photos[]' />"); // replace image placeholders - var img = currentPlaceholder.find('img'); - img.attr('src', url); - img.attr('data-id', id); - currentPlaceholder.removeClass('loading'); + var img = currentPlaceholder.find("img"); + img.attr("src", url); + img.attr("data-id", id); + currentPlaceholder.removeClass("loading"); currentPlaceholder.append("<div class='x'>X</div>" + - "<div class='circle'></div>"); - //// + "<div class='circle'></div>"); - var publisher = $('#publisher'); + var publisher = $("#publisher"); - publisher.find("input[type='submit']").removeAttr('disabled'); + publisher.find("input[type='submit']").removeAttr("disabled"); - $('.x').bind('click', function(){ - var photo = $(this).closest('.publisher_photo'); + $(".x").bind("click", function() { + var photo = $(this).closest(".publisher_photo"); photo.addClass("dim"); - $.ajax({url: "/photos/" + photo.children('img').attr('data-id'), - dataType: 'json', - type: 'DELETE', - success: function() { - photo.fadeOut(400, function(){ - photo.remove(); - if ( $('.publisher_photo').length === 0){ - $('#publisher_textarea_wrapper').removeClass("with_attachments"); - } - }); - } - }); + $.ajax({ + url: "/photos/" + photo.children("img").attr("data-id"), + dataType: "json", + type: "DELETE", + success: function() { + photo.fadeOut(400, function() { + photo.remove(); + if ($(".publisher_photo").length === 0) { + $("#publisher_textarea_wrapper").removeClass("with_attachments"); + } + }); + } + }); }); - }, - - onAllComplete: function(){} - }); + }, + onError: function(id, name, errorReason) { + alert(errorReason); + } + }, + messages: { + typeError: Diaspora.I18n.t("photo_uploader.invalid_ext"), + sizeError: Diaspora.I18n.t("photo_uploader.size_error"), + emptyError: Diaspora.I18n.t("photo_uploader.empty") + } + }); } window.addEventListener("load", function() { createUploader(); diff --git a/features/desktop/edits_profile.feature b/features/desktop/edits_profile.feature index 9d07b58063..8eeb5bb9d0 100644 --- a/features/desktop/edits_profile.feature +++ b/features/desktop/edits_profile.feature @@ -42,8 +42,10 @@ Feature: editing your profile And I should see "#starwars" within "ul#as-selections-tags" And the "#profile_public_details" bootstrap-switch should be on - When I confirm the alert after I attach the file "spec/fixtures/bad_urls.txt" to "file" within "#file-upload" - And I attach the file "spec/fixtures/button.png" to hidden "file" within "#file-upload" + When I attach the file "spec/fixtures/bad_urls.txt" to "qqfile" within "#file-upload" + Then I should see a flash message indicating failure + + When I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload" Then I should see "button.png completed" And I should see a "img" within "#profile_photo_upload" diff --git a/features/mobile/edits_profile.feature b/features/mobile/edits_profile.feature index 77a41ab7e3..402545bf2b 100644 --- a/features/mobile/edits_profile.feature +++ b/features/mobile/edits_profile.feature @@ -39,8 +39,8 @@ Feature: editing the profile in the mobile view Then I should see "#kamino" within "ul#as-selections-tags" And I should see "#starwars" within "ul#as-selections-tags" - When I confirm the alert after I attach the file "spec/fixtures/bad_urls.txt" to "file" within "#file-upload" - And I attach the file "spec/fixtures/button.png" to hidden "file" within "#file-upload" + When I confirm the alert after I attach the file "spec/fixtures/bad_urls.txt" to "qqfile" within "#file-upload" + And I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload" Then I should see "button.png completed" And I should see a "img" within "#profile_photo_upload" diff --git a/features/mobile/getting_started.feature b/features/mobile/getting_started.feature index 7714f8807a..1a8b240ecf 100644 --- a/features/mobile/getting_started.feature +++ b/features/mobile/getting_started.feature @@ -17,8 +17,8 @@ Feature: editing the getting started in the mobile view And I should not see "awesome_button" Scenario: new user adds a profile photo and tags - When I confirm the alert after I attach the file "spec/fixtures/bad_urls.txt" to "file" within "#file-upload" - And I attach the file "spec/fixtures/button.png" to hidden "file" within "#file-upload" + When I confirm the alert after I attach the file "spec/fixtures/bad_urls.txt" to "qqfile" within "#file-upload" + And I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload" Then I should see a "img" within "#profile_photo_upload" When I fill in "follow_tags" with "#men" diff --git a/features/mobile/multiphoto.feature b/features/mobile/multiphoto.feature index ddc79a2c16..c56b6989e3 100644 --- a/features/mobile/multiphoto.feature +++ b/features/mobile/multiphoto.feature @@ -10,7 +10,7 @@ Feature: viewing photos on the mobile main page Scenario: view full size image Given I visit the mobile publisher page - When I attach the file "spec/fixtures/button.png" to hidden "file" within "#file-upload-publisher" + When I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload-publisher" Then I should see "button.png completed" And I should see an uploaded image within the photo drop zone @@ -23,9 +23,9 @@ Feature: viewing photos on the mobile main page Scenario: view multiphoto post Given I visit the mobile publisher page - When I attach the file "spec/fixtures/button.png" to hidden "file" within "#file-upload-publisher" + When I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload-publisher" Then I should see "button.png completed" - When I attach the file "spec/fixtures/button.gif" to hidden "file" within "#file-upload-publisher" + When I attach the file "spec/fixtures/button.gif" to hidden "qqfile" within "#file-upload-publisher" Then I should see "button.gif completed" When I press "Share" diff --git a/features/mobile/posts_from_main_page.feature b/features/mobile/posts_from_main_page.feature index d4c688d56b..22fb6195ad 100644 --- a/features/mobile/posts_from_main_page.feature +++ b/features/mobile/posts_from_main_page.feature @@ -30,7 +30,7 @@ Feature: posting from the mobile main page Scenario: post a photo without text Given I visit the mobile publisher page - When I attach the file "spec/fixtures/button.png" to hidden "file" within "#file-upload-publisher" + When I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload-publisher" Then I should see "button.png completed" And I should see an uploaded image within the photo drop zone When I press "Share" @@ -43,9 +43,9 @@ Feature: posting from the mobile main page Scenario: back out of posting a photo-only post Given I visit the mobile publisher page - When I confirm the alert after I attach the file "spec/fixtures/bad_urls.txt" to "file" within "#file-upload-publisher" + When I confirm the alert after I attach the file "spec/fixtures/bad_urls.txt" to "qqfile" within "#file-upload-publisher" Then I should not see an uploaded image within the photo drop zone - When I attach the file "spec/fixtures/button.png" to hidden "file" within "#file-upload-publisher" + When I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload-publisher" And I should see "button.png completed" And I click to delete the first uploaded photo Then I should not see an uploaded image within the photo drop zone @@ -53,8 +53,8 @@ Feature: posting from the mobile main page Scenario: back out of uploading a picture when another has been attached Given I visit the mobile publisher page And I append "I am eating yogurt" to the mobile publisher - And I attach the file "spec/fixtures/button.gif" to hidden "file" within "#file-upload-publisher" - And I attach the file "spec/fixtures/button.png" to hidden "file" within "#file-upload-publisher" + And I attach the file "spec/fixtures/button.gif" to hidden "qqfile" within "#file-upload-publisher" + And I attach the file "spec/fixtures/button.png" to hidden "qqfile" within "#file-upload-publisher" And I click to delete the first uploaded photo Then I should see an uploaded image within the photo drop zone And the text area wrapper mobile should be with attachments diff --git a/features/support/publishing_cuke_helpers.rb b/features/support/publishing_cuke_helpers.rb index 5d4ce7675a..046f8e3322 100644 --- a/features/support/publishing_cuke_helpers.rb +++ b/features/support/publishing_cuke_helpers.rb @@ -19,9 +19,9 @@ module PublishingCukeHelpers end def upload_file_with_publisher(path) - page.execute_script(%q{$("input[name='file']").css("opacity", '1');}) + page.execute_script(%q{$("input[name='qqfile']").css("opacity", '1');}) with_scope("#publisher_textarea_wrapper") do - attach_file("file", Rails.root.join(path).to_s) + attach_file("qqfile", Rails.root.join(path).to_s) # wait for the image to be ready page.assert_selector(".publisher_photo.loading", count: 0) end diff --git a/lib/assets/javascripts/fileuploader-custom.js b/lib/assets/javascripts/fileuploader-custom.js deleted file mode 100644 index 5b5879f526..0000000000 --- a/lib/assets/javascripts/fileuploader-custom.js +++ /dev/null @@ -1,1267 +0,0 @@ -// @license magnet:?xt=urn:btih:cf05388f2679ee054f2beb29a391d25f4e673ac3&dn=gpl-2.0.txt GPL-v2-or-Later -/** - * http://github.com/valums/file-uploader - * - * Multiple file upload component with progress-bar, drag-and-drop. - * © 2010 Andrew Valums ( andrew(at)valums.com ) - * - * Licensed under GNU GPL 2 or later, see license.txt. - * Modified by Diaspora - */ - -// -// Helper functions -// - -var qq = qq || {}; - -/** - * Adds all missing properties from second obj to first obj - */ -qq.extend = function(first, second){ - for (var prop in second){ - first[prop] = second[prop]; - } -}; - -/** - * Searches for a given element in the array, returns -1 if it is not present. - * @param {Number} [from] The index at which to begin the search - */ -qq.indexOf = function(arr, elt, from){ - if (arr.indexOf) { return arr.indexOf(elt, from); } - - from = from || 0; - var len = arr.length; - - if (from < 0) { from += len; } - - for (; from < len; from++){ - if (from in arr && arr[from] === elt){ - return from; - } - } - return -1; -}; - -qq.getUniqueId = (function(){ - var id = 0; - return function(){ return id++; }; -})(); - -// -// Events - -qq.attach = function(element, type, fn){ - if (element.addEventListener){ - element.addEventListener(type, fn, false); - } else if (element.attachEvent){ - element.attachEvent('on' + type, fn); - } -}; -qq.detach = function(element, type, fn){ - if (element.removeEventListener){ - element.removeEventListener(type, fn, false); - } else if (element.attachEvent){ - element.detachEvent('on' + type, fn); - } -}; - -qq.preventDefault = function(e){ - if (e.preventDefault){ - e.preventDefault(); - } else{ - e.returnValue = false; - } -}; - -// -// Node manipulations - -/** - * Insert node a before node b. - */ -qq.insertBefore = function(a, b){ - b.parentNode.insertBefore(a, b); -}; -qq.remove = function(element){ - element.parentNode.removeChild(element); -}; - -qq.contains = function(parent, descendant){ - // compareposition returns false in this case - if (parent == descendant) { return true; } - - if (parent.contains){ - return parent.contains(descendant); - } else { - return !!(descendant.compareDocumentPosition(parent) & 8); - } -}; - -/** - * Creates and returns element from html string - * Uses innerHTML to create an element - */ -qq.toElement = (function(){ - var div = document.createElement('div'); - return function(html){ - div.innerHTML = html; - var element = div.firstChild; - div.removeChild(element); - return element; - }; -})(); - -// -// Node properties and attributes - -/** - * Sets styles for an element. - * Fixes opacity in IE6-8. - */ -qq.css = function(element, styles){ - if (styles.opacity !== null){ - if (typeof element.style.opacity != 'string' && typeof(element.filters) != 'undefined'){ - styles.filter = 'alpha(opacity=' + Math.round(100 * styles.opacity) + ')'; - } - } - qq.extend(element.style, styles); -}; -qq.hasClass = function(element, name){ - var re = new RegExp('(^| )' + name + '( |$)'); - return re.test(element.className); -}; -qq.addClass = function(element, name){ - if (!qq.hasClass(element, name)){ - element.className += ' ' + name; - } -}; -qq.removeClass = function(element, name){ - var re = new RegExp('(^| )' + name + '( |$)'); - element.className = element.className.replace(re, ' ').replace(/^\s+|\s+$/g, ""); -}; -qq.setText = function(element, text){ - element.innerText = text; - element.textContent = text; -}; - -// -// Selecting elements - -qq.children = function(element){ - var children = [], - child = element.firstChild; - - while (child){ - if (child.nodeType == 1){ - children.push(child); - } - child = child.nextSibling; - } - - return children; -}; - -qq.getByClass = function(element, className){ - if (element.querySelectorAll){ - return element.querySelectorAll('.' + className); - } - - var result = []; - var candidates = element.getElementsByTagName("*"); - var len = candidates.length; - - for (var i = 0; i < len; i++){ - if (qq.hasClass(candidates[i], className)){ - result.push(candidates[i]); - } - } - return result; -}; - -/** - * obj2url() takes a json-object as argument and generates - * a querystring. pretty much like jQuery.param() - * - * how to use: - * - * `qq.obj2url({a:'b',c:'d'},'http://any.url/upload?otherParam=value');` - * - * will result in: - * - * `http://any.url/upload?otherParam=value&a=b&c=d` - * - * @param Object JSON-Object - * @param String current querystring-part - * @return String encoded querystring - */ -qq.obj2url = function(obj, temp, prefixDone){ - var uristrings = [], - prefix = '&', - add = function(nextObj, i){ - var nextTemp = temp - ? (/\[\]$/.test(temp)) // prevent double-encoding - ? temp - : temp+'['+i+']' - : i; - if ((nextTemp != 'undefined') && (i != 'undefined')) { - uristrings.push( - (typeof nextObj === 'object') - ? qq.obj2url(nextObj, nextTemp, true) - : (Object.prototype.toString.call(nextObj) === '[object Function]') - ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj()) - : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj) - ); - } - }; - - if (!prefixDone && temp) { - prefix = (/\?/.test(temp)) ? (/\?$/.test(temp)) ? '' : '&' : '?'; - uristrings.push(temp); - uristrings.push(qq.obj2url(obj)); - } else if ((Object.prototype.toString.call(obj) === '[object Array]') && (typeof obj != 'undefined') ) { - // we wont use a for-in-loop on an array (performance) - for (var i = 0, len = obj.length; i < len; ++i){ - add(obj[i], i); - } - } else if ((typeof obj != 'undefined') && (obj !== null) && (typeof obj === "object")){ - // for anything else but a scalar, we will use for-in-loop - for (var i in obj){ - add(obj[i], i); - } - } else { - uristrings.push(encodeURIComponent(temp) + '=' + encodeURIComponent(obj)); - } - - return uristrings.join(prefix) - .replace(/^&/, '') - .replace(/%20/g, '+'); -}; - -// -// -// Uploader Classes -// -// - -var qq = qq || {}; - -/** - * Creates upload button, validates upload, but doesn't create file list or dd. - */ -qq.FileUploaderBasic = function(o){ - this._options = { - // set to true to see the server response - debug: false, - action: '/server/upload', - params: {}, - button: null, - multiple: true, - maxConnections: 3, - // validation - allowedExtensions: [], - sizeLimit: 0, - minSizeLimit: 0, - // events - // return false to cancel submit - onSubmit: function(id, fileName){}, - onProgress: function(id, fileName, loaded, total){}, - onComplete: function(id, fileName, responseJSON){}, - onAllComplete: function(completed_files){}, - onCancel: function(id, fileName){}, - // messages - messages: { - typeError: "{file} has invalid extension. Only {extensions} are allowed.", - sizeError: "{file} is too large, maximum file size is {sizeLimit}.", - minSizeError: "{file} is too small, minimum file size is {minSizeLimit}.", - emptyError: "{file} is empty, please select files again without it.", - onLeave: "The files are being uploaded, if you leave now the upload will be cancelled." - }, - showMessage: function(message){ - alert(message); - } - }; - qq.extend(this._options, o); - - // number of files being uploaded - this._filesInProgress = 0; - this._handler = this._createUploadHandler(); - - if (this._options.button){ - this._button = this._createUploadButton(this._options.button); - } - - this._preventLeaveInProgress(); -}; - -qq.FileUploaderBasic.prototype = { - setParams: function(params){ - this._options.params = params; - }, - getInProgress: function(){ - return this._filesInProgress; - }, - _createUploadButton: function(element){ - var self = this; - - return new qq.UploadButton({ - element: element, - multiple: this._options.multiple && qq.UploadHandlerXhr.isSupported(), - onChange: function(input){ - self._onInputChange(input); - } - }); - }, - _createUploadHandler: function(){ - var self = this, - handlerClass; - - if(qq.UploadHandlerXhr.isSupported()){ - handlerClass = 'UploadHandlerXhr'; - } else { - handlerClass = 'UploadHandlerForm'; - } - - var handler = new qq[handlerClass]({ - debug: this._options.debug, - action: this._options.action, - maxConnections: this._options.maxConnections, - onProgress: function(id, fileName, loaded, total){ - self._onProgress(id, fileName, loaded, total); - self._options.onProgress(id, fileName, loaded, total); - }, - onComplete: function(id, fileName, result){ - self._onComplete(id, fileName, result); - self._options.onComplete(id, fileName, result); - }, - onAllComplete: function(completed_files){ - self._options.onAllComplete(completed_files); - }, - onCancel: function(id, fileName){ - self._onCancel(id, fileName); - self._options.onCancel(id, fileName); - } - }); - - return handler; - }, - _preventLeaveInProgress: function(){ - var self = this; - - qq.attach(window, 'beforeunload', function(e){ - if (!self._filesInProgress){return;} - - var e = e || window.event; - // for ie, ff - e.returnValue = self._options.messages.onLeave; - // for webkit - return self._options.messages.onLeave; - }); - }, - _onSubmit: function(id, fileName){ - this._filesInProgress++; - }, - _onProgress: function(id, fileName, loaded, total){ - }, - _onComplete: function(id, fileName, result){ - this._filesInProgress--; - if (result.error){ - this._options.showMessage(result.error); - } - }, - _onCancel: function(id, fileName){ - this._filesInProgress--; - }, - _onInputChange: function(input){ - if (this._handler instanceof qq.UploadHandlerXhr){ - this._uploadFileList(input.files); - } else { - if (this._validateFile(input)){ - this._uploadFile(input); - } - } - this._button.reset(); - }, - _uploadFileList: function(files){ - for (var i=0; i<files.length; i++){ - if ( !this._validateFile(files[i])){ - return; - } - } - - for (var i=0; i<files.length; i++){ - this._uploadFile(files[i]); - } - }, - _uploadFile: function(fileContainer){ - var id = this._handler.add(fileContainer); - var fileName = this._handler.getName(id); - - if (this._options.onSubmit(id, fileName) !== false){ - this._onSubmit(id, fileName); - this._handler.upload(id, this._options.params); - } - }, - _validateFile: function(file){ - var name, size; - - if (file.value){ - // it is a file input - // get input value and remove path to normalize - name = file.value.replace(/.*(\/|\\)/, ""); - } else { - // fix missing properties in Safari - name = file.fileName != null ? file.fileName : file.name; - size = file.fileSize != null ? file.fileSize : file.size; - } - - if (! this._isAllowedExtension(name)){ - this._error('typeError', name); - return false; - - } else if (size === 0){ - this._error('emptyError', name); - return false; - - } else if (size && this._options.sizeLimit && size > this._options.sizeLimit){ - this._error('sizeError', name); - return false; - - } else if (size && size < this._options.minSizeLimit){ - this._error('minSizeError', name); - return false; - } - - return true; - }, - _error: function(code, fileName){ - var message = this._options.messages[code]; - function r(name, replacement){ message = message.replace(name, replacement); } - - r('{file}', this._formatFileName(fileName)); - r('{extensions}', this._options.allowedExtensions.join(', ')); - r('{sizeLimit}', this._formatSize(this._options.sizeLimit)); - r('{minSizeLimit}', this._formatSize(this._options.minSizeLimit)); - - this._options.showMessage(message); - }, - _formatFileName: function(name){ - if (name.length > 33){ - name = name.slice(0, 19) + '...' + name.slice(-13); - } - return name; - }, - _isAllowedExtension: function(fileName){ - var ext = (-1 !== fileName.indexOf('.')) ? fileName.replace(/.*[.]/, '').toLowerCase() : ''; - var allowed = this._options.allowedExtensions; - - if (!allowed.length){return true;} - - for (var i=0; i<allowed.length; i++){ - if (allowed[i].toLowerCase() == ext){ return true;} - } - - return false; - }, - _formatSize: function(bytes){ - var i = -1; - do { - bytes = bytes / 1024; - i++; - } while (bytes > 99); - - return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i]; - } -}; - - -/** - * Class that creates upload widget with drag-and-drop and file list - * @inherits qq.FileUploaderBasic - */ -qq.FileUploader = function(o){ - // call parent constructor - qq.FileUploaderBasic.apply(this, arguments); - - // additional options - qq.extend(this._options, { - element: null, - // if set, will be used instead of qq-upload-list in template - listElement: null, - - template: '<div class="qq-uploader">' + - '<div class="qq-upload-drop-area"><span>Drop files here to upload</span></div>' + - '<div class="qq-upload-button">Upload a file</div>' + - '<ul class="qq-upload-list"></ul>' + - '</div>', - - // template for one item in file list - fileTemplate: '<li>' + - '<span class="qq-upload-file"></span>' + - '<span class="qq-upload-spinner"></span>' + - '<span class="qq-upload-size"></span>' + - '<a class="qq-upload-cancel" href="#">Cancel</a>' + - '<span class="qq-upload-failed-text">Failed</span>' + - '</li>', - - classes: { - // used to get elements from templates - button: 'qq-upload-button', - drop: 'qq-upload-drop-area', - dropActive: 'qq-upload-drop-area-active', - list: 'qq-upload-list', - - file: 'qq-upload-file', - spinner: 'qq-upload-spinner', - size: 'qq-upload-size', - cancel: 'qq-upload-cancel', - - // added to list item when upload completes - // used in css to hide progress spinner - success: 'qq-upload-success', - fail: 'qq-upload-fail' - } - }); - // overwrite options with user supplied - qq.extend(this._options, o); - - this._element = this._options.element; - this._element.innerHTML = this._options.template; - this._listElement = this._options.listElement || this._find(this._element, 'list'); - - this._classes = this._options.classes; - - this._button = this._createUploadButton(this._find(this._element, 'button')); - - this._bindCancelEvent(); - this._setupDragDrop(); -}; - -// inherit from Basic Uploader -qq.extend(qq.FileUploader.prototype, qq.FileUploaderBasic.prototype); - -qq.extend(qq.FileUploader.prototype, { - /** - * Gets one of the elements listed in this._options.classes - **/ - _find: function(parent, type){ - var element = qq.getByClass(parent, this._options.classes[type])[0]; - if (!element){ - throw new Error('element not found ' + type); - } - - return element; - }, - _setupDragDrop: function(){ - var self = this, - dropArea = this._find(this._element, 'drop'); - - var dz = new qq.UploadDropZone({ - element: dropArea, - onEnter: function(e){ - qq.addClass(dropArea, self._classes.dropActive); - e.stopPropagation(); - }, - onLeave: function(e){ - e.stopPropagation(); - }, - onLeaveNotDescendants: function(e){ - qq.removeClass(dropArea, self._classes.dropActive); - }, - onDrop: function(e){ - dropArea.style.display = 'none'; - qq.removeClass(dropArea, self._classes.dropActive); - self._uploadFileList(e.dataTransfer.files); - } - }); - - dropArea.style.display = 'none'; - - qq.attach(document, 'dragenter', function(e){ - if (!dz._isValidFileDrag(e)) return; - - dropArea.style.display = 'block'; - }); - qq.attach(document, 'dragleave', function(e){ - if (!dz._isValidFileDrag(e)) return; - - var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); - // only fire when leaving document out - if ( ! relatedTarget || relatedTarget.nodeName == "HTML"){ - dropArea.style.display = 'none'; - } - }); - }, - _onSubmit: function(id, fileName){ - qq.FileUploaderBasic.prototype._onSubmit.apply(this, arguments); - this._addToList(id, fileName); - }, - _onProgress: function(id, fileName, loaded, total){ - qq.FileUploaderBasic.prototype._onProgress.apply(this, arguments); - - var item = this._getItemByFileId(id); - var size = this._find(item, 'size'); - size.style.display = 'inline'; - - var text; - if (loaded != total){ - text = Math.round(loaded / total * 100) + '% from ' + this._formatSize(total); - } else { - text = this._formatSize(total); - } - - qq.setText(size, text); - }, - _onComplete: function(id, fileName, result){ - qq.FileUploaderBasic.prototype._onComplete.apply(this, arguments); - - // mark completed - var item = this._getItemByFileId(id); - qq.remove(this._find(item, 'cancel')); - qq.remove(this._find(item, 'spinner')); - - if (result.success){ - qq.addClass(item, this._classes.success); - } else { - qq.addClass(item, this._classes.fail); - } - }, - _addToList: function(id, fileName){ - var item = qq.toElement(this._options.fileTemplate); - item.qqFileId = id; - - var fileElement = this._find(item, 'file'); - qq.setText(fileElement, this._formatFileName(fileName)); - this._find(item, 'size').style.display = 'none'; - - this._listElement.appendChild(item); - }, - _getItemByFileId: function(id){ - var item = this._listElement.firstChild; - - // there can't be txt nodes in dynamically created list - // and we can use nextSibling - while (item){ - if (item.qqFileId == id) return item; - item = item.nextSibling; - } - }, - /** - * delegate click event for cancel link - **/ - _bindCancelEvent: function(){ - var self = this, - list = this._listElement; - - qq.attach(list, 'click', function(e){ - e = e || window.event; - var target = e.target || e.srcElement; - - if (qq.hasClass(target, self._classes.cancel)){ - qq.preventDefault(e); - - var item = target.parentNode; - self._handler.cancel(item.qqFileId); - qq.remove(item); - } - }); - } -}); - -qq.UploadDropZone = function(o){ - this._options = { - element: null, - onEnter: function(e){}, - onLeave: function(e){}, - // is not fired when leaving element by hovering descendants - onLeaveNotDescendants: function(e){}, - onDrop: function(e){} - }; - qq.extend(this._options, o); - - this._element = this._options.element; - - this._disableDropOutside(); - this._attachEvents(); -}; - -qq.UploadDropZone.prototype = { - _disableDropOutside: function(e){ - // run only once for all instances - if (!qq.UploadDropZone.dropOutsideDisabled ){ - - qq.attach(document, 'dragover', function(e){ - if (e.dataTransfer){ - e.dataTransfer.dropEffect = 'none'; - e.preventDefault(); - } - }); - - qq.UploadDropZone.dropOutsideDisabled = true; - } - }, - _attachEvents: function(){ - var self = this; - - qq.attach(self._element, 'dragover', function(e){ - if (!self._isValidFileDrag(e)) return; - - var effect = e.dataTransfer.effectAllowed; - if (effect == 'move' || effect == 'linkMove'){ - e.dataTransfer.dropEffect = 'move'; // for FF (only move allowed) - } else { - e.dataTransfer.dropEffect = 'copy'; // for Chrome - } - - e.stopPropagation(); - e.preventDefault(); - }); - - qq.attach(self._element, 'dragenter', function(e){ - if (!self._isValidFileDrag(e)) return; - - self._options.onEnter(e); - }); - - qq.attach(self._element, 'dragleave', function(e){ - if (!self._isValidFileDrag(e)) return; - - self._options.onLeave(e); - - var relatedTarget = document.elementFromPoint(e.clientX, e.clientY); - // do not fire when moving a mouse over a descendant - if (qq.contains(this, relatedTarget)) return; - - self._options.onLeaveNotDescendants(e); - }); - - qq.attach(self._element, 'drop', function(e){ - if (!self._isValidFileDrag(e)) return; - - e.preventDefault(); - self._options.onDrop(e); - }); - }, - _isValidFileDrag: function(e){ - var dt = e.dataTransfer, - // do not check dt.types.contains in webkit, because it crashes safari 4 - isWebkit = navigator.userAgent.indexOf("AppleWebKit") > -1; - - // dt.effectAllowed is none in Safari 5 - // dt.types.contains check is for firefox - return dt && dt.effectAllowed != 'none' && - (dt.files || (!isWebkit && dt.types.contains && dt.types.contains('Files'))); - - } -}; - -qq.UploadButton = function(o){ - this._options = { - element: null, - // if set to true adds multiple attribute to file input - multiple: false, - // name attribute of file input - name: 'file', - onChange: function(input){}, - hoverClass: 'qq-upload-button-hover', - focusClass: 'qq-upload-button-focus' - }; - - qq.extend(this._options, o); - - this._element = this._options.element; - - // make button suitable container for input - qq.css(this._element, { - position: 'relative', - overflow: 'hidden', - // Make sure browse button is in the right side - // in Internet Explorer - direction: 'ltr' - }); - - this._input = this._createInput(); -}; - -qq.UploadButton.prototype = { - /* returns file input element */ - getInput: function(){ - return this._input; - }, - /* cleans/recreates the file input */ - reset: function(){ - if (this._input.parentNode){ - qq.remove(this._input); - } - - qq.removeClass(this._element, this._options.focusClass); - this._input = this._createInput(); - }, - _createInput: function(){ - var input = document.createElement("input"); - - if (this._options.multiple){ - input.setAttribute("multiple", "multiple"); - } - - input.setAttribute("type", "file"); - input.setAttribute("name", this._options.name); - - qq.css(input, { - position: 'absolute', - // in Opera only 'browse' button - // is clickable and it is located at - // the right side of the input - right: 0, - top: 0, - //fontFamily: 'Arial', - // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118 - //fontSize: '118px', - margin: 0, - padding: 0, - opacity: 0 - }); - - this._element.appendChild(input); - - var self = this; - qq.attach(input, 'change', function(){ - self._options.onChange(input); - }); - - qq.attach(input, 'mouseover', function(){ - qq.addClass(self._element, self._options.hoverClass); - }); - qq.attach(input, 'mouseout', function(){ - qq.removeClass(self._element, self._options.hoverClass); - }); - qq.attach(input, 'focus', function(){ - qq.addClass(self._element, self._options.focusClass); - }); - qq.attach(input, 'blur', function(){ - qq.removeClass(self._element, self._options.focusClass); - }); - - // IE and Opera, unfortunately have 2 tab stops on file input - // which is unacceptable in our case, disable keyboard access - if (window.attachEvent){ - // it is IE or Opera - input.setAttribute('tabIndex', "-1"); - } - - return input; - } -}; - -/** - * Class for uploading files, uploading itself is handled by child classes - */ -qq.UploadHandlerAbstract = function(o){ - this._options = { - debug: false, - action: '/upload.php', - // maximum number of concurrent uploads - maxConnections: 999, - onProgress: function(id, fileName, loaded, total){}, - onComplete: function(id, fileName, response){}, - onAllComplete: function(completed_files){}, - onCancel: function(id, fileName){} - }; - qq.extend(this._options, o); - - this._queue = []; - // params for files in queue - this._params = []; - this._completed_files = []; -}; -qq.UploadHandlerAbstract.prototype = { - log: function(str){ - if (this._options.debug && window.console) console.log('[uploader] ' + str); - }, - /** - * Adds file or file input to the queue - * @returns id - **/ - add: function(file){}, - /** - * Sends the file identified by id and additional query params to the server - */ - upload: function(id, params){ - var len = this._queue.push(id); - - var copy = {}; - qq.extend(copy, params); - this._params[id] = copy; - - // if too many active uploads, wait... - if (len <= this._options.maxConnections){ - this._upload(id, this._params[id]); - } - }, - /** - * Cancels file upload by id - */ - cancel: function(id){ - this._cancel(id); - this._dequeue(id); - }, - /** - * Cancells all uploads - */ - cancelAll: function(){ - for (var i=0; i<this._queue.length; i++){ - this._cancel(this._queue[i]); - } - this._queue = []; - }, - /** - * Returns name of the file identified by id - */ - getName: function(id){}, - /** - * Returns size of the file identified by id - */ - getSize: function(id){}, - /** - * Returns id of files being uploaded or - * waiting for their turn - */ - getQueue: function(){ - return this._queue; - }, - /** - * Actual upload method - */ - _upload: function(id){}, - /** - * Actual cancel method - */ - _cancel: function(id){}, - /** - * Removes element from queue, starts upload of next - */ - _dequeue: function(id){ - var i = qq.indexOf(this._queue, id); - this._queue.splice(i, 1); - - var max = this._options.maxConnections; - - if (this._queue.length >= max){ - var nextId = this._queue[max-1]; - this._upload(nextId, this._params[nextId]); - } - - if (this._queue.length == 0){ - this._onAllComplete(); - } - }, - _onAllComplete: function(){ - this._options.onAllComplete(this._completed_files); - } -}; - -/** - * Class for uploading files using form and iframe - * @inherits qq.UploadHandlerAbstract - */ -qq.UploadHandlerForm = function(o){ - qq.UploadHandlerAbstract.apply(this, arguments); - - this._inputs = {}; -}; -// @inherits qq.UploadHandlerAbstract -qq.extend(qq.UploadHandlerForm.prototype, qq.UploadHandlerAbstract.prototype); - -qq.extend(qq.UploadHandlerForm.prototype, { - add: function(fileInput){ - fileInput.setAttribute('name', 'qqfile'); - var id = 'qq-upload-handler-iframe' + qq.getUniqueId(); - - this._inputs[id] = fileInput; - - // remove file input from DOM - if (fileInput.parentNode){ - qq.remove(fileInput); - } - - return id; - }, - getName: function(id){ - // get input value and remove path to normalize - return this._inputs[id].value.replace(/.*(\/|\\)/, ""); - }, - _cancel: function(id){ - this._options.onCancel(id, this.getName(id)); - - delete this._inputs[id]; - - var iframe = document.getElementById(id); - if (iframe){ - // to cancel request set src to something else - // we use src="javascript:false;" because it doesn't - // trigger ie6 prompt on https - iframe.setAttribute('src', 'javascript:false;'); - - qq.remove(iframe); - } - }, - _upload: function(id, params){ - var input = this._inputs[id]; - - if (!input){ - throw new Error('file with passed id was not added, or already uploaded or cancelled'); - } - - var fileName = this.getName(id); - - var iframe = this._createIframe(id); - var form = this._createForm(iframe, params); - form.appendChild(input); - $(form).append($('<input type="hidden" name="authenticity_token" value="'+$("meta[name='csrf-token']").attr("content")+'"/>')); - - var self = this; - this._attachLoadEvent(iframe, function(){ - self.log('iframe loaded'); - - var response = self._getIframeContentJSON(iframe); - - self._options.onComplete(id, fileName, response); - self._dequeue(id); - - delete self._inputs[id]; - // timeout added to fix busy state in FF3.6 - setTimeout(function(){ - qq.remove(iframe); - }, 1); - }); - - form.submit(); - qq.remove(form); - - return id; - }, - _attachLoadEvent: function(iframe, callback){ - qq.attach(iframe, 'load', function(){ - // when we remove iframe from dom - // the request stops, but in IE load - // event fires - if (!iframe.parentNode){ - return; - } - - // fixing Opera 10.53 - if (iframe.contentDocument && - iframe.contentDocument.body && - iframe.contentDocument.body.innerHTML == "false"){ - // In Opera event is fired second time - // when body.innerHTML changed from false - // to server response approx. after 1 sec - // when we upload file with iframe - return; - } - - callback(); - }); - }, - /** - * Returns json object received by iframe from server. - */ - _getIframeContentJSON: function(iframe){ - // iframe.contentWindow.document - for IE<7 - var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document, - response; - - this.log("converting iframe's innerHTML to JSON"); - this.log("innerHTML = " + doc.body.innerHTML); - - try { - response = eval("(" + doc.body.innerHTML + ")"); - } catch(err){ - response = {}; - } - - return response; - }, - /** - * Creates iframe with unique name - */ - _createIframe: function(id){ - // We can't use following code as the name attribute - // won't be properly registered in IE6, and new window - // on form submit will open - // var iframe = document.createElement('iframe'); - // iframe.setAttribute('name', id); - - var iframe = qq.toElement('<iframe src="javascript:false;" name="' + id + '" />'); - // src="javascript:false;" removes ie6 prompt on https - - iframe.setAttribute('id', id); - - iframe.style.display = 'none'; - document.body.appendChild(iframe); - - return iframe; - }, - /** - * Creates form, that will be submitted to iframe - */ - _createForm: function(iframe, params){ - // We can't use the following code in IE6 - // var form = document.createElement('form'); - // form.setAttribute('method', 'post'); - // form.setAttribute('enctype', 'multipart/form-data'); - // Because in this case file won't be attached to request - var form = qq.toElement('<form method="post" enctype="multipart/form-data"></form>'); - - var queryString = qq.obj2url(params, this._options.action); - - form.setAttribute('action', queryString); - form.setAttribute('target', iframe.name); - form.style.display = 'none'; - document.body.appendChild(form); - - return form; - } -}); - -/** - * Class for uploading files using xhr - * @inherits qq.UploadHandlerAbstract - */ -qq.UploadHandlerXhr = function(o){ - qq.UploadHandlerAbstract.apply(this, arguments); - - this._files = []; - this._xhrs = []; - - // current loaded size in bytes for each file - this._loaded = []; -}; - -// static method -qq.UploadHandlerXhr.isSupported = function(){ - var input = document.createElement('input'); - input.type = 'file'; - - return ( - 'multiple' in input && - typeof File != "undefined" && - typeof (new XMLHttpRequest()).upload != "undefined" ); -}; - -// @inherits qq.UploadHandlerAbstract -qq.extend(qq.UploadHandlerXhr.prototype, qq.UploadHandlerAbstract.prototype) - -qq.extend(qq.UploadHandlerXhr.prototype, { - /** - * Adds file to the queue - * Returns id to use with upload, cancel - **/ - add: function(file){ - if (!(file instanceof File)){ - throw new Error('Passed obj in not a File (in qq.UploadHandlerXhr)'); - } - - return this._files.push(file) - 1; - }, - getName: function(id){ - var file = this._files[id]; - // fix missing name in Safari 4 - return file.fileName != null ? file.fileName : file.name; - }, - getSize: function(id){ - var file = this._files[id]; - return file.fileSize != null ? file.fileSize : file.size; - }, - /** - * Returns uploaded bytes for file identified by id - */ - getLoaded: function(id){ - return this._loaded[id] || 0; - }, - /** - * Sends the file identified by id and additional query params to the server - * @param {Object} params name-value string pairs - */ - _upload: function(id, params){ - var file = this._files[id], - name = this.getName(id), - size = this.getSize(id); - - this._loaded[id] = 0; - - var xhr = this._xhrs[id] = new XMLHttpRequest(); - var self = this; - - xhr.upload.onprogress = function(e){ - if (e.lengthComputable){ - self._loaded[id] = e.loaded; - self._options.onProgress(id, name, e.loaded, e.total); - } - }; - - xhr.onreadystatechange = function(){ - if (xhr.readyState == 4){ - self._onComplete(id, xhr); - } - }; - - // build query string - params = params || {}; - params['qqfile'] = name; - var queryString = qq.obj2url(params, this._options.action); - - xhr.open("POST", queryString, true); - xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest"); - xhr.setRequestHeader("X-File-Name", encodeURIComponent(name)); - xhr.setRequestHeader("Content-Type", "application/octet-stream"); - xhr.setRequestHeader("X-CSRF-Token", $("meta[name='csrf-token']").attr("content")); - xhr.setRequestHeader("Accept", "application/json"); - xhr.send(file); - }, - _onComplete: function(id, xhr){ - // the request was aborted/cancelled - if (!this._files[id]) return; - - var name = this.getName(id); - var size = this.getSize(id); - - this._options.onProgress(id, name, size, size); - - if (xhr.status == 200){ - this.log("xhr - server response received"); - this.log("responseText = " + xhr.responseText); - - var response; - - try { - response = eval("(" + xhr.responseText + ")"); - } catch(err){ - response = {}; - } - - this._completed_files.push({file: this._files[id], response: response}); - this._options.onComplete(id, name, response); - - } else { - this._completed_files.push({file: this._files[id], response: {}}); - this._options.onComplete(id, name, {}); - } - - this._files[id] = null; - this._xhrs[id] = null; - this._dequeue(id); - }, - _cancel: function(id){ - this._options.onCancel(id, this.getName(id)); - - this._files[id] = null; - - if (this._xhrs[id]){ - this._xhrs[id].abort(); - this._xhrs[id] = null; - } - } -}); -// @license-end diff --git a/spec/javascripts/app/views/publisher_view_spec.js b/spec/javascripts/app/views/publisher_view_spec.js index fb527b1869..c672b33fae 100644 --- a/spec/javascripts/app/views/publisher_view_spec.js +++ b/spec/javascripts/app/views/publisher_view_spec.js @@ -512,11 +512,11 @@ describe("app.views.Publisher", function() { ); }); - it('initializes the file uploader plugin', function() { - spyOn(qq, 'FileUploaderBasic'); + it("initializes the FineUploader plugin", function() { + spyOn(qq, "FineUploaderBasic"); new app.views.Publisher(); - expect(qq.FileUploaderBasic).toHaveBeenCalled(); + expect(qq.FineUploaderBasic).toHaveBeenCalled(); }); context('event handlers', function() { -- GitLab