diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 111ede90a..3c0fe4b4e 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -9,8 +9,30 @@ class PhotosController < ApplicationController album = Album.find_by_id params[:album_id] begin + + ######################## dealing with local files ############# + # get file name + file_name = params[:qqfile] + # get file content type + att_content_type = (request.content_type.to_s == "") ? "application/octet-stream" : request.content_type.to_s + # create temporal file + file = Tempfile.new(file_name) + # put data into this file from raw post request + file.print request.raw_post + + # create several required methods for this temporal file + Tempfile.send(:define_method, "content_type") {return att_content_type} + Tempfile.send(:define_method, "original_filename") {return file_name} + + ############## + + + params[:user_file] = file @photo = current_user.post(:photo, params) - respond_with @photo + + respond_to do |format| + format.json{render(:layout => false , :json => {"success" => true, "data" => @photo}.to_json )} + end rescue TypeError message = "Photo upload failed. Are you sure an image was added?" diff --git a/app/models/photo.rb b/app/models/photo.rb index 412817585..976d354a9 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -22,7 +22,7 @@ class Photo < Post before_destroy :ensure_user_picture def self.instantiate(params = {}) - image_file = params[:user_file].first + image_file = params[:user_file] params.delete :user_file photo = Photo.new(params) diff --git a/app/views/albums/show.html.haml b/app/views/albums/show.html.haml index 18e042ad9..80abbacd6 100644 --- a/app/views/albums/show.html.haml +++ b/app/views/albums/show.html.haml @@ -1,5 +1,10 @@ -- content_for :head do - = javascript_include_tag 'photos', 'jquery.html5_upload' +:javascript + $(document).ready(function(){ + $(".image_thumb img").load( function() { + $(this).fadeIn("slow"); + }); + }); + .album_id{:id => @album.id, :style => "display:hidden;"} .back= link_to '⇧ albums', albums_path @@ -13,6 +18,8 @@ = image_tag 'ajax-loader.gif' = link_to 'Add Photos', '#new_photo_pane', :class => 'button', :id => "add_photo_button" + =render 'photos/new_photo' + .yo{:style => "display:none;"} #new_photo_pane diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7baae3bb0..7bdd0f5e1 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -10,12 +10,14 @@ = stylesheet_link_tag "application", "ui", 'bubble' = stylesheet_link_tag "/../javascripts/fancybox/jquery.fancybox-1.3.1" + = stylesheet_link_tag "fileuploader" /= javascript_include_tag "http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js" = javascript_include_tag 'jquery-1.4.2.min', 'rails', 'google' = javascript_include_tag 'jquery.infieldlabel', 'jquery.cycle/jquery.cycle.min.js' = javascript_include_tag 'fancybox/jquery.fancybox-1.3.1.pack' + = javascript_include_tag 'fileuploader' = javascript_include_tag 'view', 'image_picker', 'group_nav', 'stream' = render 'js/websocket_js' diff --git a/app/views/photos/_new_photo.haml b/app/views/photos/_new_photo.haml index b33f03c83..cfc67bdbf 100644 --- a/app/views/photos/_new_photo.haml +++ b/app/views/photos/_new_photo.haml @@ -1,35 +1,12 @@ :javascript - $(function() { - $("#photo_image").html5_upload({ - // WE INSERT ALBUM_ID PARAM HERE - url: "/photos?album_id=#{@album.id}", - sendBoundary: window.FormData || $.browser.mozilla, - onStart: function(event, total) { - return confirm("You are about to upload " + total + " photos. Are you sure?"); - }, - onFinish: function(event, total){ - $("#add_photo_button .button").html( "Add Photos" ); - $("#add_photo_loader").fadeOut(400); - }, - onStart: function(event, total){ - $("#add_photo_pane").fadeOut(400); - $("#add_photo_button .button").html( "Uploading Photos" ); - $("#add_photo_loader").fadeIn(400); - return true; - } - }); - }); + function createUploader(){ + var uploader = new qq.FileUploader({ + element: document.getElementById('file-upload'), + params: {'album_id' : "#{@album.id}"}, + allowedExtensions: ['jpg', 'jpeg', 'png'], + action: "/photos" + }); + } + window.onload = createUploader; -%h1 - %span{:id=>"photo_title_status"} - Add photos to - %i= @album.name -= form_for @photo, :html => {:multipart => true} do |f| - = f.error_messages - = f.hidden_field :album_id, :value => @album.id - = f.file_field :image, :multiple => 'multiple' - -#progress_report{ :style => "display:none;text-align:center;" } - = image_tag "ajax-loader.gif" - #progress_report_name - #progress_report_status +#file-upload diff --git a/public/javascripts/fileuploader.js b/public/javascripts/fileuploader.js new file mode 100755 index 000000000..9f5477b34 --- /dev/null +++ b/public/javascripts/fileuploader.js @@ -0,0 +1,1067 @@ +/** + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +var qq = qq || {}; + +/** + * Class that creates our multiple file upload widget + */ +qq.FileUploader = function(o){ + this._options = { + // container element DOM node (ex. $(selector)[0] for jQuery users) + 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 + + template: '
' + + '
Drop files here to upload
' + + '
Upload a file
' + + '' + + '
', + + // template for one item in file list + fileTemplate: '
  • ' + + '' + + '' + + '' + + 'Cancel' + + 'Failed' + + '
  • ', + + 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' + }, + 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); + } + }; + + 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.innerHTML = this._options.template; + + // number of files being uploaded + this._filesInProgress = 0; + + // easier access + this._classes = this._options.classes; + + this._handler = this._createUploadHandler(); + + 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; + }, + /** + * 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]; + + 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'); + 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); + }, + _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]; + }, + _getItemByFileId: function(id){ + var item = this._getElement('list').firstChild; + + // there can't be text nodes in our dynamically created list + // because of that we can safely 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._getElement('list'); + + 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){ + 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', + // 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, + padding: 0, + cursor: 'pointer', + 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 using form and iframe + */ +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); + + this._inputs = {}; +}; +qq.UploadHandlerForm.prototype = { + /** + * Adds file input to the queue + * Returns id to use with upload, cancel + **/ + 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; + }, + /** + * 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 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); + + var self = this; + this._attachLoadEvent(iframe, function(){ + self._options.onComplete(id, fileName, self._getIframeContentJSON(iframe)); + + 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; + }, + 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 + // 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; + + 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('