From 7fb751b00cc5486a4bfa4c62d589387a21639ec7 Mon Sep 17 00:00:00 2001 From: maxwell Date: Fri, 19 Nov 2010 01:06:38 -0800 Subject: [PATCH] making photo uploader work better --- app/controllers/aspects_controller.rb | 2 +- app/controllers/photos_controller.rb | 5 +- app/controllers/status_messages_controller.rb | 1 + app/models/photo.rb | 11 + app/models/post.rb | 3 + app/views/photos/_new_photo.haml | 49 +- app/views/shared/_publisher.haml | 19 +- public/javascripts/vendor/fileuploader.js | 1326 ++++++++++------- public/stylesheets/sass/application.sass | 1 - 9 files changed, 800 insertions(+), 617 deletions(-) diff --git a/app/controllers/aspects_controller.rb b/app/controllers/aspects_controller.rb index 95ae47f8d..101901daa 100644 --- a/app/controllers/aspects_controller.rb +++ b/app/controllers/aspects_controller.rb @@ -9,7 +9,7 @@ class AspectsController < ApplicationController respond_to :json, :only => :show def index - @posts = current_user.visible_posts(:by_members_of => :all).paginate :page => params[:page], :per_page => 15, :order => 'created_at DESC' + @posts = current_user.visible_posts(:by_members_of => :all, :pending => false).paginate :page => params[:page], :per_page => 15, :order => 'created_at DESC' @aspect = :all if current_user.getting_started == true diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 063e98b4c..492a673c6 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -34,7 +34,6 @@ class PhotosController < ApplicationController end def create - begin params[:photo][:user_file] = file_handler(params) @@ -43,9 +42,9 @@ class PhotosController < ApplicationController if @photo.save raise 'MongoMapper failed to catch a failed save' unless @photo.id - current_user.dispatch_post(@photo, :to => params[:photo][:to]) + current_user.dispatch_post(@photo, :to => params[:photo][:to]) unless @photo.pending respond_to do |format| - format.json{render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )} + format.json{ render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )} end else respond_with :location => photos_path, :error => message diff --git a/app/controllers/status_messages_controller.rb b/app/controllers/status_messages_controller.rb index d41d4da02..cf38e3087 100644 --- a/app/controllers/status_messages_controller.rb +++ b/app/controllers/status_messages_controller.rb @@ -9,6 +9,7 @@ class StatusMessagesController < ApplicationController respond_to :json, :only => :show def create + puts params.inspect public_flag = params[:status_message][:public] public_flag.to_s.match(/(true)/) ? public_flag = true : public_flag = false params[:status_message][:public] = public_flag diff --git a/app/models/photo.rb b/app/models/photo.rb index 28cedf8ea..1e6ad021e 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -14,6 +14,7 @@ class Photo < Post key :remote_photo_path key :remote_photo_name key :random_string + timestamps! @@ -76,5 +77,15 @@ class Photo < Post 1.upto(len) { |i| string << chars[rand(chars.size-1)] } return string end + + def as_json(opts={}) + { + :photo => { + :id => self.id, + :url => self.url(:thumb_small) + } + } + end + end diff --git a/app/models/post.rb b/app/models/post.rb index 842af2b2e..cd4a666c1 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -20,6 +20,7 @@ class Post key :diaspora_handle, String key :user_refs, Integer, :default => 0 + key :pending, Boolean, :default => false many :comments, :class_name => 'Comment', :foreign_key => :post_id, :order => 'created_at ASC' belongs_to :person, :class_name => 'Person' @@ -33,10 +34,12 @@ class Post after_destroy :destroy_comments attr_accessible :user_refs + def self.instantiate params new_post = self.new params.to_hash new_post.person = params[:person] new_post.public = params[:public] + new_post.pending = params[:pending] new_post.diaspora_handle = new_post.person.diaspora_handle new_post end diff --git a/app/views/photos/_new_photo.haml b/app/views/photos/_new_photo.haml index 71636bd1e..d19f119c3 100644 --- a/app/views/photos/_new_photo.haml +++ b/app/views/photos/_new_photo.haml @@ -3,31 +3,24 @@ -# the COPYRIGHT file. :javascript - function createUploader(){ - var uploader = new qq.FileUploader({ - element: document.getElementById('file-upload'), - params: {'photo' : { 'to' : "#{aspect_id}"}, 'set_profile_image' : "#{set_profile_image if defined?(set_profile_image)}"}, - allowedExtensions: ['jpg', 'jpeg', 'png', 'gif'], - action: "#{photos_path}", - template: '
' + - '
#{t('.drop')}
' + - '
#{t('.upload')}
' + - '' + - '
', - fileTemplate: '
  • ' + - '' + - '' + - '' + - '#{t('cancel')}' + - '#{t('.failed')}' + - '
  • ', - messages: { - typeError: "#{t('.invalid_ext')}", - sizeError: "#{t('.size_error')}", - emptyError: "#{t('.empty')}" - } - }); - } - window.onload = createUploader; - -#file-upload + function createUploader(){ + var uploader = new qq.FileUploaderBasic({ + element: document.getElementById('file-upload'), + params: {'photo' : {'pending' : 'true', 'to' : "#{aspect_id}"}, 'set_profile_image' : "#{set_profile_image if defined?(set_profile_image)}"}, + allowedExtensions: ['jpg', 'jpeg', 'png', 'gif'], + action: "#{photos_path}", + debug: true, + button: document.getElementById('file-upload'), + sizeLimit: 5000048, + onComplete: function(id, fileName, responseJSON){ + //var obj = jQuery.parseJSON(responseJSON.data); + alert(responseJSON.data.photo.url); + var id = responseJSON.data.photo.id; + alert($('#new_status_message').length); + $('#new_status_message').append(""); + + } + + }); + } + window.onload = createUploader; diff --git a/app/views/shared/_publisher.haml b/app/views/shared/_publisher.haml index bdccd7ead..361010478 100644 --- a/app/views/shared/_publisher.haml +++ b/app/views/shared/_publisher.haml @@ -11,9 +11,9 @@ }; }); - $("#publisher textarea, #publisher input").live("focus", function(evt){ - $("#publisher .options_and_submit").fadeIn(50); - }); + //$("#publisher textarea, #publisher input").live("focus", function(evt){ + // $("#publisher .options_and_submit").fadeIn(50); + //}); $("#publisher form").live("submit", function(evt){ $("#publisher .options_and_submit").hide(); @@ -22,15 +22,18 @@ #publisher = owner_image_link - = form_for StatusMessage.new, :remote => true do |status| + = form_for StatusMessage.new, :html => {:multipart => true,}, :remote => true do |status| + = status.error_messages - %p + %params = status.label :message, t('.post_a_message_to', :aspect => (aspect == :all ? "everyone" : aspect)) = status.text_area :message, :rows => 2, :value => params[:prefill] = status.hidden_field :to, :value => (aspect == :all ? aspect : aspect.id) .options_and_submit + #file-upload.button + drag a photo to upload - if aspect == :all = status.submit t('.share'), :title => t('.share_with_all'), :disable_with => t('.posting') - else @@ -51,9 +54,3 @@ .fancybox_content #question_mark_pane = render 'shared/public_explain' - - - /#publisher_photo_upload - /= t('or') - /= render 'photos/new_photo', :aspect_id => (aspect == :all ? aspect : aspect.id) - diff --git a/public/javascripts/vendor/fileuploader.js b/public/javascripts/vendor/fileuploader.js index 19b1c104c..89c09ebf5 100644 --- a/public/javascripts/vendor/fileuploader.js +++ b/public/javascripts/vendor/fileuploader.js @@ -2,41 +2,491 @@ * http://github.com/valums/file-uploader * * Multiple file upload component with progress-bar, drag-and-drop. - * © 2010 Andrew Valums andrew(at)valums.com + * © 2010 Andrew Valums ( andrew(at)valums.com ) * * Licensed under GNU GPL 2 or later, see license.txt. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . */ - + +// +// Helper functions +// + var qq = qq || {}; /** - * Class that creates our multiple file upload widget + * 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){}, + 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); + }, + 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 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 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){ - this._options = { - // container element DOM node (ex. $(selector)[0] for jQuery users) + // call parent constructor + qq.FileUploaderBasic.apply(this, arguments); + + // additional options + qq.extend(this._options, { element: null, - // url of the server-side upload script, should be on the same domain - action: '/server/upload', - // additional data to send, name-value pairs - params: {}, - // ex. ['jpg', 'jpeg', 'png', 'gif'] or [] - allowedExtensions: [], - // size limit in bytes, 0 - no limit - // this option isn't supported in all browsers - sizeLimit: 0, - onSubmit: function(id, fileName){}, - onComplete: function(id, fileName, responseJSON){}, - - // - // UI customizations - + // if set, will be used instead of qq-upload-list in template + listElement: null, + template: '
    ' + - '
    Drop photos here to upload
    ' + - '
    Upload a photo
    ' + + '
    Drop files here to upload
    ' + + '
    Upload a file
    ' + '
      ' + '
      ', @@ -47,8 +497,8 @@ qq.FileUploader = function(o){ '' + 'Cancel' + 'Failed' + - '', - + '', + classes: { // used to get elements from templates button: 'qq-upload-button', @@ -65,115 +515,41 @@ qq.FileUploader = function(o){ // used in css to hide progress spinner success: 'qq-upload-success', fail: 'qq-upload-fail' - }, - messages: { - //serverError: "Some files were not uploaded, please contact support and/or try again.", - typeError: "{file} has invalid extension. Only {extensions} are allowed.", - sizeError: "{file} is too large, maximum file size is {sizeLimit}.", - emptyError: "{file} is empty, please select files again without it." - }, - showMessage: function(message){ - alert(message); } - }; - + }); + // overwrite options with user supplied qq.extend(this._options, o); - - this._element = this._options.element; - if (this._element.nodeType != 1){ - throw new Error('element param of FileUploader should be dom node'); - } + this._element = this._options.element; + this._element.innerHTML = this._options.template; + this._listElement = this._options.listElement || this._find(this._element, 'list'); - this._element.innerHTML = this._options.template; - - // number of files being uploaded - this._filesInProgress = 0; - - // easier access this._classes = this._options.classes; - - this._handler = this._createUploadHandler(); + + this._button = this._createUploadButton(this._find(this._element, 'button')); this._bindCancelEvent(); - - var self = this; - this._button = new qq.UploadButton({ - element: this._getElement('button'), - multiple: qq.UploadHandlerXhr.isSupported(), - onChange: function(input){ - self._onInputChange(input); - } - }); - this._setupDragDrop(); }; -qq.FileUploader.prototype = { - setParams: function(params){ - this._options.params = params; - }, - /** - * Returns true if some files are being uploaded, false otherwise - */ - isUploading: function(){ - return !!this._filesInProgress; - }, +// 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 - * - * First optional element is root for search, - * this._element is default value. - * - * Usage - * 1. this._getElement('button'); - * 2. this._getElement(item, 'file'); **/ - _getElement: function(parent, type){ - if (typeof parent == 'string'){ - // parent was not passed - type = parent; - parent = this._element; - } - - var element = qq.getByClass(parent, this._options.classes[type])[0]; - + _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; }, - _error: function(code, fileName){ - var message = this._options.messages[code]; - message = message.replace('{file}', this._formatFileName(fileName)); - message = message.replace('{extensions}', this._options.allowedExtensions.join(', ')); - message = message.replace('{sizeLimit}', this._formatSize(this._options.sizeLimit)); - 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 this._options.sizeLimit){ - this._error('sizeError',name); - return false; - } - - return true; - }, - _addToList: function(id, fileName){ - var item = qq.toElement(this._options.fileTemplate); - item.qqFileId = id; - - var fileElement = this._getElement(item, 'file'); - qq.setText(fileElement, this._formatFileName(fileName)); - this._getElement(item, 'size').style.display = 'none'; - - this._getElement('list').appendChild(item); - - this._filesInProgress++; - }, - _updateProgress: function(id, loaded, total){ var item = this._getItemByFileId(id); - var size = this._getElement(item, 'size'); + var size = this._find(item, 'size'); size.style.display = 'inline'; var text; @@ -344,27 +605,39 @@ qq.FileUploader.prototype = { text = this._formatSize(total); } - qq.setText(size, text); + qq.setText(size, text); }, - _formatSize: function(bytes){ - var i = -1; - do { - bytes = bytes / 1024; - i++; - } while (bytes > 99); + _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')); - return Math.max(bytes, 0.1).toFixed(1) + ['kB', 'MB', 'GB', 'TB', 'PB', 'EB'][i]; + 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._getElement('list').firstChild; + var item = this._listElement.firstChild; - // there can't be text nodes in our dynamically created list - // because of that we can safely use nextSibling + // there can't be txt nodes in dynamically created list + // and we can use nextSibling while (item){ - if (item.qqFileId == id){ - return item; - } - + if (item.qqFileId == id) return item; item = item.nextSibling; } }, @@ -373,24 +646,23 @@ qq.FileUploader.prototype = { **/ _bindCancelEvent: function(){ var self = this, - list = this._getElement('list'); + list = this._listElement; - qq.attach(list, 'click', function(e){ + qq.attach(list, 'click', function(e){ e = e || window.event; var target = e.target || e.srcElement; - - if (qq.hasClass(target, self._classes.cancel)){ + + 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, @@ -414,8 +686,10 @@ qq.UploadDropZone.prototype = { if (!qq.UploadDropZone.dropOutsideDisabled ){ qq.attach(document, 'dragover', function(e){ - e.dataTransfer.dropEffect = 'none'; - e.preventDefault(); + if (e.dataTransfer){ + e.dataTransfer.dropEffect = 'none'; + e.preventDefault(); + } }); qq.UploadDropZone.dropOutsideDisabled = true; @@ -536,8 +810,6 @@ qq.UploadButton.prototype = { right: 0, top: 0, fontFamily: 'Arial', - // when button is big it becomes visible in IE8 on SOME PCs - // probably related to http://social.msdn.microsoft.com/forums/en-US/iewebdevelopment/thread/29d0b0e7-4326-4b3e-823c-51420d4cf253 // 4 persons reported this, the max values that worked for them were 243, 236, 236, 118 fontSize: '118px', margin: 0, @@ -577,26 +849,117 @@ qq.UploadButton.prototype = { } }; +/** + * 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){}, + onCancel: function(id, fileName){} + }; + qq.extend(this._options, o); + + this._queue = []; + // params for files in queue + this._params = []; +}; +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= max){ + var nextId = this._queue[max-1]; + this._upload(nextId, this._params[nextId]); + } + } +}; + /** * Class for uploading files using form and iframe + * @inherits qq.UploadHandlerAbstract */ qq.UploadHandlerForm = function(o){ - this._options = { - // URL of the server-side upload script, - // should be on the same domain to get response - action: '/upload', - // fires for each file, when iframe finishes loading - onComplete: function(id, fileName, response){} - }; - qq.extend(this._options, o); + qq.UploadHandlerAbstract.apply(this, arguments); this._inputs = {}; }; -qq.UploadHandlerForm.prototype = { - /** - * Adds file input to the queue - * Returns id to use with upload, cancel - **/ +// @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(); @@ -610,17 +973,32 @@ qq.UploadHandlerForm.prototype = { return id; }, - /** - * Sends the file identified by id and additional query params to the server - * @param {Object} params name-value string pairs - */ - upload: function(id, params){ + 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); @@ -628,8 +1006,13 @@ qq.UploadHandlerForm.prototype = { form.appendChild(input); var self = this; - this._attachLoadEvent(iframe, function(){ - self._options.onComplete(id, fileName, self._getIframeContentJSON(iframe)); + 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 @@ -642,26 +1025,7 @@ qq.UploadHandlerForm.prototype = { qq.remove(form); return id; - }, - cancel: function(id){ - if (id in this._inputs){ - 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); - } - }, - getName: function(id){ - // get input value and remove path to normalize - return this._inputs[id].value.replace(/.*(\/|\\)/, ""); - }, + }, _attachLoadEvent: function(iframe, callback){ qq.attach(iframe, 'load', function(){ // when we remove iframe from dom @@ -692,12 +1056,15 @@ qq.UploadHandlerForm.prototype = { // iframe.contentWindow.document - for IE<7 var doc = iframe.contentDocument ? iframe.contentDocument: iframe.contentWindow.document, response; - - try{ + + 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; }, @@ -732,32 +1099,29 @@ qq.UploadHandlerForm.prototype = { // Because in this case file won't be attached to request var form = qq.toElement('
      '); - var queryString = '?' + qq.obj2url(params); + var queryString = qq.obj2url(params, this._options.action); - form.setAttribute('action', this._options.action + queryString); + 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){ - this._options = { - // url of the server-side upload script, - // should be on the same domain - action: '/upload', - onProgress: function(id, fileName, loaded, total){}, - onComplete: function(id, fileName, response){} - }; - qq.extend(this._options, o); + qq.UploadHandlerAbstract.apply(this, arguments); this._files = []; this._xhrs = []; + + // current loaded size in bytes for each file + this._loaded = []; }; // static method @@ -771,297 +1135,113 @@ qq.UploadHandlerXhr.isSupported = function(){ typeof (new XMLHttpRequest()).upload != "undefined" ); }; -qq.UploadHandlerXhr.prototype = { +// @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){ + _upload: function(id, params){ var file = this._files[id], name = this.getName(id), size = this.getSize(id); - - if (!file){ - throw new Error('file with passed id was not added, or already uploaded or cancelled'); - } - + + 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(){ - // the request was aborted/cancelled - if (!self._files[id]){ - return; - } - + xhr.onreadystatechange = function(){ if (xhr.readyState == 4){ - - self._options.onProgress(id, name, size, size); - - if (xhr.status == 200){ - var response; - - try { - response = eval("(" + xhr.responseText + ")"); - } catch(err){ - response = {}; - } - - self._options.onComplete(id, name, response); - - } else { - self._options.onComplete(id, name, {}); - } - - self._files[id] = null; - self._xhrs[id] = null; + self._onComplete(id, xhr); } }; // build query string - var queryString = '?qqfile=' + encodeURIComponent(name) + '&' + qq.obj2url(params); + params = params || {}; + params['qqfile'] = name; + var queryString = qq.obj2url(params, this._options.action); - xhr.open("POST", this._options.action + queryString, true); + 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.send(file); - }, - cancel: function(id){ + _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._options.onComplete(id, name, response); + + } else { + 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; } - }, - getName: function(id){ - // fix missing name in Safari 4 - var file = this._files[id]; - return file.fileName != null ? file.fileName : file.name; - }, - getSize: function(id){ - // fix missing size in Safari 4 - var file = this._files[id]; - return file.fileSize != null ? file.fileSize : file.size; } -}; - -// -// Helper functions -// - -var qq = qq || {}; - -// -// Useful generic functions - -/** - * Adds all missing properties from obj2 to obj1 - */ -qq.extend = function(obj1, obj2){ - for (var prop in obj2){ - obj1[prop] = obj2[prop]; - } -}; - -/** - * @return {Number} unique id - */ -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() - * - * @param Object JSON-Object - * @param String current querystring-part - * @return String encoded querystring - */ -qq.obj2url = function(obj, temp){ - var uristrings = [], - add = function(nextObj, i){ - - var nextTemp = temp - ? (/\[\]$/.test(temp)) // prevent double-encoding - ? temp - : temp+'['+i+']' - : i; - - uristrings.push(typeof nextObj === 'object' - ? qq.obj2url(nextObj, nextTemp) - : (Object.prototype.toString.call(nextObj) === '[object Function]') - ? encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj()) - : encodeURIComponent(nextTemp) + '=' + encodeURIComponent(nextObj)); - }; - - if (Object.prototype.toString.call(obj) === '[object Array]'){ - // 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 ((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('&').replace(/%20/g, '+'); -}; - +}); \ No newline at end of file diff --git a/public/stylesheets/sass/application.sass b/public/stylesheets/sass/application.sass index e444e9b45..bbdec31e7 100644 --- a/public/stylesheets/sass/application.sass +++ b/public/stylesheets/sass/application.sass @@ -762,7 +762,6 @@ label .options_and_submit :min-height 21px :position relative - :display none :padding :top 8px :margin