Commit 892502d227b31f259bae9bb2c19a3d824afcaa2e

Authored by Rodrigo Souto
2 parents a441e326 10ecc338

Merge branch 'media-panel-improvements' into stoa

Conflicts:
	app/views/layouts/_javascript.html.erb
	public/javascripts/jquery.typewatch.js
app/controllers/my_profile/cms_controller.rb
... ... @@ -17,6 +17,9 @@ class CmsController < MyProfileController
17 17 end
18 18  
19 19 before_filter :login_required, :except => [:suggest_an_article]
  20 + before_filter :load_recent_files, :only => [:new, :edit]
  21 +
  22 + helper_method :file_types
20 23  
21 24 protect_if :only => :upload_files do |c, user, profile|
22 25 article_id = c.params[:parent_id]
... ... @@ -318,15 +321,26 @@ class CmsController < MyProfileController
318 321 end
319 322  
320 323 def media_upload
321   - files_uploaded = []
322 324 parent = check_parent(params[:parent_id])
323   - files = [:file1,:file2, :file3].map { |f| params[f] }.compact
324 325 if request.post?
325   - files.each do |file|
326   - files_uploaded << UploadedFile.create(:uploaded_data => file, :profile => profile, :parent => parent) unless file == ''
  326 + begin
  327 + @file = UploadedFile.create!(:uploaded_data => params[:file], :profile => profile, :parent => parent) unless params[:file] == ''
  328 + @file = FilePresenter.for(@file)
  329 + rescue Exception => exception
  330 + render :text => exception.to_s, :status => :bad_request
327 331 end
328 332 end
329   - render :text => article_list_to_json(files_uploaded), :content_type => 'text/plain'
  333 + end
  334 +
  335 + def published_media_items
  336 + load_recent_files(params[:parent_id], params[:q])
  337 + render :partial => 'published_media_items'
  338 + end
  339 +
  340 + def view_all_media
  341 + paginate_options = {:page => 1}
  342 + @key = params[:key].to_sym
  343 + load_recent_files(params[:parent_id], params[:q], paginate_options)
330 344 end
331 345  
332 346 protected
... ... @@ -421,4 +435,36 @@ class CmsController &lt; MyProfileController
421 435 end
422 436 end
423 437  
  438 + def file_types
  439 + {:images => _('Images'), :generics => _('Files')}
  440 + end
  441 +
  442 + def load_recent_files(parent_id = nil, q = nil, paginate_options = {:page => 1, :per_page => 6})
  443 + #TODO Since we only have special support for images, I'm limiting myself to
  444 + # consider generic files as non-images. In the future, with more supported
  445 + # file types we'll need to have a smart way to fetch from the database
  446 + # scopes of each supported type as well as the non-supported types as a
  447 + # whole.
  448 + @recent_files = {}
  449 +
  450 + parent = parent_id.present? ? profile.articles.find(parent_id) : nil
  451 + if parent.present?
  452 + files = parent.children.files
  453 + else
  454 + files = profile.files
  455 + end
  456 +
  457 + files = files.more_recent
  458 + images = files.images
  459 + generics = files.no_images
  460 +
  461 + if q.present?
  462 + @recent_files[:images] = find_by_contents(:images, images, q, paginate_options)[:results]
  463 + @recent_files[:generics] = find_by_contents(:generics, generics, q, paginate_options)[:results]
  464 + else
  465 + @recent_files[:images] = images.paginate(paginate_options)
  466 + @recent_files[:generics] = generics.paginate(paginate_options)
  467 + end
  468 + end
  469 +
424 470 end
... ...
app/helpers/forms_helper.rb
... ... @@ -265,7 +265,7 @@ module FormsHelper
265 265 )
266 266 end
267 267  
268   - def select_profile_folder(label_text, field_id, profile, default_value='', html_options = {}, js_options = {}, find_options = {})
  268 + def select_profile_folder(label_text, field_id, profile, default_value='', html_options = {}, js_options = {}, find_options = {}, extra_options = {})
269 269 if find_options.empty?
270 270 folders = profile.folders
271 271 else
... ... @@ -276,7 +276,7 @@ module FormsHelper
276 276 select_tag(
277 277 field_id,
278 278 options_for_select(
279   - [[profile.identifier, '']] +
  279 + [[(extra_options[:root_label] || profile.identifier), '']] +
280 280 folders.collect {|f| [ profile.identifier + '/' + f.full_name, f.id.to_s ] },
281 281 default_value.to_s
282 282 ),
... ...
app/models/article.rb
... ... @@ -458,7 +458,9 @@ class Article &lt; ActiveRecord::Base
458 458 scope :no_folders, lambda {|profile|{:conditions => ['type NOT IN (?)', profile.folder_types]}}
459 459 scope :galleries, :conditions => { :type => 'Gallery' }
460 460 scope :images, :conditions => { :is_image => true }
  461 + scope :no_images, :conditions => { :is_image => false }
461 462 scope :text_articles, :conditions => [ 'articles.type IN (?)', text_article_types ]
  463 + scope :files, :conditions => { :type => 'UploadedFile' }
462 464 scope :with_types, lambda { |types| { :conditions => [ 'articles.type IN (?)', types ] } }
463 465  
464 466 scope :more_popular, :order => 'hits DESC'
... ...
app/views/cms/_drag_and_drop_note.html.erb
1 1 <p>
2 2 <em>
3 3 <%= _('Drag images to add them to the text.') %>
4   - <%= _('Drag file names to the text to add links.') %>
  4 + <%= _('Click on file names to add links to the text.') %>
5 5 </em>
6 6 </p>
... ...
app/views/cms/_published_media_items.html.erb 0 → 100644
... ... @@ -0,0 +1,15 @@
  1 +<% file_types.each do |key, header| %>
  2 + <% display = @recent_files[key].present? ? '' : 'none' %>
  3 + <div class='<%= key.to_s %>' style='display: <%= display%>;'>
  4 + <div class='section-title'>
  5 + <h3><%= header %></h3>
  6 + <% if @recent_files[key].total_pages > 1 %>
  7 + <%= link_to(_('View all'), {:controller => 'cms', :action => 'view_all_media', :profile => profile.identifier, :key => key}, :class => 'view-all colorbox', 'data-key' => key) %>
  8 + <% end %>
  9 + </div>
  10 + <% @recent_files[key].each do |file| %>
  11 + <% @file = file %>
  12 + <%= render :partial => "cms/media_panel/#{key.to_s.singularize}" %>
  13 + <% end %>
  14 + </div>
  15 +<% end %>
... ...
app/views/cms/_text_editor_sidebar.html.erb
... ... @@ -16,37 +16,33 @@
16 16 "type='Folder' or type='Gallery'"
17 17 ) %>
18 18 </div>
19   - <p><%= file_field_tag('file1') %></p>
20   - <p><%= file_field_tag('file2') %></p>
21   - <p><%= file_field_tag('file3') %></p>
22   - <% button_bar do %>
23   - <%= submit_button(:save, _('Upload')) %>
24   - <% end %>
  19 + <p><%= file_field_tag('file', :multiple => true) %></p>
25 20 <% end %>
26 21 </div>
27   - <div id='media-upload-results' style='display: none'>
28   - <%= render :partial => 'drag_and_drop_note' %>
29   - <div class='items'>
30   - </div>
31   - <p><%= link_to(_('Upload more files ...'), '#', :id => 'media-upload-more-files')%></p>
  22 + <div class='hide-and-show-uploads'>
  23 + <%= link_to(_('Hide all uploads'), nil, :id => 'hide-uploads', :style => 'display: none;', 'data-bootstraped' => false) %>
  24 + <%= link_to(_('Show all uploads'), nil, :id => 'show-uploads', :style => 'display: none;') %>
32 25 </div>
33 26 </div>
34   - <div id='media-search-box' class='text-editor-sidebar-box'>
35   - <div class='header'><strong><%= _('Media search') %></strong></div>
36   - <p>
37   - <%= form_tag({ :action => 'search' }) do %>
38   - <span class='formfield'>
39   - <input name='q' type='text' id='media-search-query' style='width: 250px;'/>
40   - </span>
41   - <%= submit_button :search, _('Search'), :id => 'media-search-button' %>
42   - <% end %>
43   - </p>
44   - <div id='media-search-results' style='display: none'>
45   - <%= render :partial => 'drag_and_drop_note' %>
46   - <div class='items'>
47   - </div>
  27 +
  28 + <div id='published-media' class='text-editor-sidebar-box' data-url='<%= url_for({:controller => 'cms', :action => 'published_media_items', :profile => profile.identifier}) %>'>
  29 + <div class='header'><strong><%= _('Published media') %></strong></div>
  30 + <%= select_profile_folder(nil, :parent_id, profile, 'recent-media', {}, {},
  31 + "type='Folder' or type='Gallery'", {:root_label => _('Recent media')}) %>
  32 + <%= labelled_form_field _('Search'), text_field_tag('q') %>
  33 + <%= render :partial => 'drag_and_drop_note' %>
  34 + <div class='items'>
  35 + <%= render :partial => 'published_media_items' %>
48 36 </div>
49 37 </div>
50 38 </div>
51 39  
  40 +<script id="template-upload" type="text/x-tmpl">
  41 + <div id="file-{%= o.id %}" class="upload" title="{%= o.name %}">
  42 + <div class="file-name">{%=o.name%}</div>
  43 + <div class="percentage"></div>
  44 + <div class="progress"><div class="bar" style="width: 0%;"></div></div>
  45 + </div>
  46 +</script>
52 47  
  48 +<%= javascript_include_tag 'jquery.fileupload.js', 'tmpl.js', 'media-panel.js' %>
... ...
app/views/cms/media_panel/_generic.html.erb 0 → 100644
... ... @@ -0,0 +1,5 @@
  1 +<div class="item file <%= icon_for_article(@file) %>" data-item="div">
  2 + <div>
  3 + <a class="add-to-text" href="<%= url_for(@file.url) %>" title="<%= @file.title %>"><%= @file.title %></a>
  4 + </div>
  5 +</div>
... ...
app/views/cms/media_panel/_image.html.erb 0 → 100644
... ... @@ -0,0 +1,9 @@
  1 +<div class="item image" data-item="span" title="<%= @file.name %>">
  2 + <span>
  3 + <img src="<%= @file.public_filename(:uploaded) %>"/>
  4 + </span>
  5 + <div class="controls image-controls">
  6 + <a class="button icon-add add-to-text" href="#"><span><%= _('Add to the text') %></span></a>
  7 + <a class="button icon-zoom zoom" href="#" title="<%= _('Zoom in') %>"><span><%= _('Zoom in') %></span></a>
  8 + </div>
  9 +</div>
... ...
app/views/cms/media_upload.js.erb 0 → 100644
... ... @@ -0,0 +1,3 @@
  1 +<% klass = @file.class.name.split('::').last.to_css_class %>
  2 +jQuery("#published-media .items .<%= klass.pluralize %> .section-title").after("<%= j render :partial => "cms/media_panel/#{klass}" %>");
  3 +jQuery("#published-media .items .<%= klass.pluralize %>").show();
... ...
app/views/cms/view_all_media.html.erb 0 → 100644
... ... @@ -0,0 +1,8 @@
  1 +<h1><%= file_types[@key] %></h1>
  2 +
  3 +<div class='view-all-media view-all-<%= @key %>'>
  4 + <% @recent_files[@key].each do |file| %>
  5 + <% @file = file %>
  6 + <%= render :partial => "cms/media_panel/#{@key.to_s.singularize}" %>
  7 + <% end %>
  8 +</div>
... ...
app/views/layouts/_javascript.html.erb
... ... @@ -2,7 +2,7 @@
2 2 'jquery-2.1.1.min', 'jquery-migrate-1.2.1',
3 3 'jquery.noconflict.js', 'jquery.cycle.all.min.js', 'thickbox.js', 'lightbox', 'colorbox',
4 4 'jquery-ui-1.10.4/js/jquery-ui-1.10.4.min', 'jquery.scrollTo', 'jquery.form.js', 'jquery-validation/jquery.validate',
5   -'jquery.cookie', 'jquery.ba-bbq.min.js', 'reflection', 'jquery.tokeninput', 'jquery.typewatch','jquery.textchange',
  5 +'jquery.cookie', 'jquery.ba-bbq.min.js', 'reflection', 'jquery.tokeninput', 'jquery.typewatch', 'jquery.textchange',
6 6 'add-and-join', 'report-abuse', 'catalog', 'manage-products', 'autogrow', 'select-or-die/_src/selectordie',
7 7 'jquery-timepicker-addon/dist/jquery-ui-timepicker-addon', 'application.js', 'rails.js', :cache => 'cache/application' %>
8 8  
... ...
public/javascripts/article.js
... ... @@ -79,6 +79,11 @@ jQuery(function($) {
79 79 return '<div class="item" data-item="div"><div><img src="' + img + '" style="max-width: 640px; max-height: 480px"/></div>' + '<div class="button-bar">' + add_to_text_button('with-text') + '&nbsp;&nbsp;&nbsp;' + close_button('with-text') + '</div></div>'
80 80 }
81 81  
  82 + $('.view-all-images .item').live('click', function(){
  83 + insert_item_in_text(jQuery(this).find('span'));
  84 + $.colorbox.close();
  85 + });
  86 +
82 87 $('a.add-to-text').live('click', function() {
83 88 var $item = $(this).closest('.item');
84 89 var html_selector = $item.attr('data-item');
... ...
public/javascripts/jquery.fileupload.js 0 → 100644
... ... @@ -0,0 +1,1438 @@
  1 +/*
  2 + * jQuery File Upload Plugin 5.40.3
  3 + * https://github.com/blueimp/jQuery-File-Upload
  4 + *
  5 + * Copyright 2010, Sebastian Tschan
  6 + * https://blueimp.net
  7 + *
  8 + * Licensed under the MIT license:
  9 + * http://www.opensource.org/licenses/MIT
  10 + */
  11 +
  12 +/* jshint nomen:false */
  13 +/* global define, window, document, location, Blob, FormData */
  14 +
  15 +(function (factory) {
  16 + 'use strict';
  17 + if (typeof define === 'function' && define.amd) {
  18 + // Register as an anonymous AMD module:
  19 + define([
  20 + 'jquery',
  21 + 'jquery.ui.widget'
  22 + ], factory);
  23 + } else {
  24 + // Browser globals:
  25 + factory(window.jQuery);
  26 + }
  27 +}(function ($) {
  28 + 'use strict';
  29 +
  30 + // Detect file input support, based on
  31 + // http://viljamis.com/blog/2012/file-upload-support-on-mobile/
  32 + $.support.fileInput = !(new RegExp(
  33 + // Handle devices which give false positives for the feature detection:
  34 + '(Android (1\\.[0156]|2\\.[01]))' +
  35 + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
  36 + '|(w(eb)?OSBrowser)|(webOS)' +
  37 + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
  38 + ).test(window.navigator.userAgent) ||
  39 + // Feature detection for all other devices:
  40 + $('<input type="file">').prop('disabled'));
  41 +
  42 + // The FileReader API is not actually used, but works as feature detection,
  43 + // as some Safari versions (5?) support XHR file uploads via the FormData API,
  44 + // but not non-multipart XHR file uploads.
  45 + // window.XMLHttpRequestUpload is not available on IE10, so we check for
  46 + // window.ProgressEvent instead to detect XHR2 file upload capability:
  47 + $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
  48 + $.support.xhrFormDataFileUpload = !!window.FormData;
  49 +
  50 + // Detect support for Blob slicing (required for chunked uploads):
  51 + $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
  52 + Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
  53 +
  54 + // The fileupload widget listens for change events on file input fields defined
  55 + // via fileInput setting and paste or drop events of the given dropZone.
  56 + // In addition to the default jQuery Widget methods, the fileupload widget
  57 + // exposes the "add" and "send" methods, to add or directly send files using
  58 + // the fileupload API.
  59 + // By default, files added via file input selection, paste, drag & drop or
  60 + // "add" method are uploaded immediately, but it is possible to override
  61 + // the "add" callback option to queue file uploads.
  62 + $.widget('blueimp.fileupload', {
  63 +
  64 + options: {
  65 + // The drop target element(s), by the default the complete document.
  66 + // Set to null to disable drag & drop support:
  67 + dropZone: $(document),
  68 + // The paste target element(s), by the default the complete document.
  69 + // Set to null to disable paste support:
  70 + pasteZone: $(document),
  71 + // The file input field(s), that are listened to for change events.
  72 + // If undefined, it is set to the file input fields inside
  73 + // of the widget element on plugin initialization.
  74 + // Set to null to disable the change listener.
  75 + fileInput: undefined,
  76 + // By default, the file input field is replaced with a clone after
  77 + // each input field change event. This is required for iframe transport
  78 + // queues and allows change events to be fired for the same file
  79 + // selection, but can be disabled by setting the following option to false:
  80 + replaceFileInput: true,
  81 + // The parameter name for the file form data (the request argument name).
  82 + // If undefined or empty, the name property of the file input field is
  83 + // used, or "files[]" if the file input name property is also empty,
  84 + // can be a string or an array of strings:
  85 + paramName: undefined,
  86 + // By default, each file of a selection is uploaded using an individual
  87 + // request for XHR type uploads. Set to false to upload file
  88 + // selections in one request each:
  89 + singleFileUploads: true,
  90 + // To limit the number of files uploaded with one XHR request,
  91 + // set the following option to an integer greater than 0:
  92 + limitMultiFileUploads: undefined,
  93 + // The following option limits the number of files uploaded with one
  94 + // XHR request to keep the request size under or equal to the defined
  95 + // limit in bytes:
  96 + limitMultiFileUploadSize: undefined,
  97 + // Multipart file uploads add a number of bytes to each uploaded file,
  98 + // therefore the following option adds an overhead for each file used
  99 + // in the limitMultiFileUploadSize configuration:
  100 + limitMultiFileUploadSizeOverhead: 512,
  101 + // Set the following option to true to issue all file upload requests
  102 + // in a sequential order:
  103 + sequentialUploads: false,
  104 + // To limit the number of concurrent uploads,
  105 + // set the following option to an integer greater than 0:
  106 + limitConcurrentUploads: undefined,
  107 + // Set the following option to true to force iframe transport uploads:
  108 + forceIframeTransport: false,
  109 + // Set the following option to the location of a redirect url on the
  110 + // origin server, for cross-domain iframe transport uploads:
  111 + redirect: undefined,
  112 + // The parameter name for the redirect url, sent as part of the form
  113 + // data and set to 'redirect' if this option is empty:
  114 + redirectParamName: undefined,
  115 + // Set the following option to the location of a postMessage window,
  116 + // to enable postMessage transport uploads:
  117 + postMessage: undefined,
  118 + // By default, XHR file uploads are sent as multipart/form-data.
  119 + // The iframe transport is always using multipart/form-data.
  120 + // Set to false to enable non-multipart XHR uploads:
  121 + multipart: true,
  122 + // To upload large files in smaller chunks, set the following option
  123 + // to a preferred maximum chunk size. If set to 0, null or undefined,
  124 + // or the browser does not support the required Blob API, files will
  125 + // be uploaded as a whole.
  126 + maxChunkSize: undefined,
  127 + // When a non-multipart upload or a chunked multipart upload has been
  128 + // aborted, this option can be used to resume the upload by setting
  129 + // it to the size of the already uploaded bytes. This option is most
  130 + // useful when modifying the options object inside of the "add" or
  131 + // "send" callbacks, as the options are cloned for each file upload.
  132 + uploadedBytes: undefined,
  133 + // By default, failed (abort or error) file uploads are removed from the
  134 + // global progress calculation. Set the following option to false to
  135 + // prevent recalculating the global progress data:
  136 + recalculateProgress: true,
  137 + // Interval in milliseconds to calculate and trigger progress events:
  138 + progressInterval: 100,
  139 + // Interval in milliseconds to calculate progress bitrate:
  140 + bitrateInterval: 500,
  141 + // By default, uploads are started automatically when adding files:
  142 + autoUpload: true,
  143 +
  144 + // Error and info messages:
  145 + messages: {
  146 + uploadedBytes: 'Uploaded bytes exceed file size'
  147 + },
  148 +
  149 + // Translation function, gets the message key to be translated
  150 + // and an object with context specific data as arguments:
  151 + i18n: function (message, context) {
  152 + message = this.messages[message] || message.toString();
  153 + if (context) {
  154 + $.each(context, function (key, value) {
  155 + message = message.replace('{' + key + '}', value);
  156 + });
  157 + }
  158 + return message;
  159 + },
  160 +
  161 + // Additional form data to be sent along with the file uploads can be set
  162 + // using this option, which accepts an array of objects with name and
  163 + // value properties, a function returning such an array, a FormData
  164 + // object (for XHR file uploads), or a simple object.
  165 + // The form of the first fileInput is given as parameter to the function:
  166 + formData: function (form) {
  167 + return form.serializeArray();
  168 + },
  169 +
  170 + // The add callback is invoked as soon as files are added to the fileupload
  171 + // widget (via file input selection, drag & drop, paste or add API call).
  172 + // If the singleFileUploads option is enabled, this callback will be
  173 + // called once for each file in the selection for XHR file uploads, else
  174 + // once for each file selection.
  175 + //
  176 + // The upload starts when the submit method is invoked on the data parameter.
  177 + // The data object contains a files property holding the added files
  178 + // and allows you to override plugin options as well as define ajax settings.
  179 + //
  180 + // Listeners for this callback can also be bound the following way:
  181 + // .bind('fileuploadadd', func);
  182 + //
  183 + // data.submit() returns a Promise object and allows to attach additional
  184 + // handlers using jQuery's Deferred callbacks:
  185 + // data.submit().done(func).fail(func).always(func);
  186 + add: function (e, data) {
  187 + if (e.isDefaultPrevented()) {
  188 + return false;
  189 + }
  190 + if (data.autoUpload || (data.autoUpload !== false &&
  191 + $(this).fileupload('option', 'autoUpload'))) {
  192 + data.process().done(function () {
  193 + data.submit();
  194 + });
  195 + }
  196 + },
  197 +
  198 + // Other callbacks:
  199 +
  200 + // Callback for the submit event of each file upload:
  201 + // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
  202 +
  203 + // Callback for the start of each file upload request:
  204 + // send: function (e, data) {}, // .bind('fileuploadsend', func);
  205 +
  206 + // Callback for successful uploads:
  207 + // done: function (e, data) {}, // .bind('fileuploaddone', func);
  208 +
  209 + // Callback for failed (abort or error) uploads:
  210 + // fail: function (e, data) {}, // .bind('fileuploadfail', func);
  211 +
  212 + // Callback for completed (success, abort or error) requests:
  213 + // always: function (e, data) {}, // .bind('fileuploadalways', func);
  214 +
  215 + // Callback for upload progress events:
  216 + // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
  217 +
  218 + // Callback for global upload progress events:
  219 + // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
  220 +
  221 + // Callback for uploads start, equivalent to the global ajaxStart event:
  222 + // start: function (e) {}, // .bind('fileuploadstart', func);
  223 +
  224 + // Callback for uploads stop, equivalent to the global ajaxStop event:
  225 + // stop: function (e) {}, // .bind('fileuploadstop', func);
  226 +
  227 + // Callback for change events of the fileInput(s):
  228 + // change: function (e, data) {}, // .bind('fileuploadchange', func);
  229 +
  230 + // Callback for paste events to the pasteZone(s):
  231 + // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
  232 +
  233 + // Callback for drop events of the dropZone(s):
  234 + // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
  235 +
  236 + // Callback for dragover events of the dropZone(s):
  237 + // dragover: function (e) {}, // .bind('fileuploaddragover', func);
  238 +
  239 + // Callback for the start of each chunk upload request:
  240 + // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
  241 +
  242 + // Callback for successful chunk uploads:
  243 + // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
  244 +
  245 + // Callback for failed (abort or error) chunk uploads:
  246 + // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
  247 +
  248 + // Callback for completed (success, abort or error) chunk upload requests:
  249 + // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
  250 +
  251 + // The plugin options are used as settings object for the ajax calls.
  252 + // The following are jQuery ajax settings required for the file uploads:
  253 + processData: false,
  254 + contentType: false,
  255 + cache: false
  256 + },
  257 +
  258 + // A list of options that require reinitializing event listeners and/or
  259 + // special initialization code:
  260 + _specialOptions: [
  261 + 'fileInput',
  262 + 'dropZone',
  263 + 'pasteZone',
  264 + 'multipart',
  265 + 'forceIframeTransport'
  266 + ],
  267 +
  268 + _blobSlice: $.support.blobSlice && function () {
  269 + var slice = this.slice || this.webkitSlice || this.mozSlice;
  270 + return slice.apply(this, arguments);
  271 + },
  272 +
  273 + _BitrateTimer: function () {
  274 + this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
  275 + this.loaded = 0;
  276 + this.bitrate = 0;
  277 + this.getBitrate = function (now, loaded, interval) {
  278 + var timeDiff = now - this.timestamp;
  279 + if (!this.bitrate || !interval || timeDiff > interval) {
  280 + this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
  281 + this.loaded = loaded;
  282 + this.timestamp = now;
  283 + }
  284 + return this.bitrate;
  285 + };
  286 + },
  287 +
  288 + _isXHRUpload: function (options) {
  289 + return !options.forceIframeTransport &&
  290 + ((!options.multipart && $.support.xhrFileUpload) ||
  291 + $.support.xhrFormDataFileUpload);
  292 + },
  293 +
  294 + _getFormData: function (options) {
  295 + var formData;
  296 + if ($.type(options.formData) === 'function') {
  297 + return options.formData(options.form);
  298 + }
  299 + if ($.isArray(options.formData)) {
  300 + return options.formData;
  301 + }
  302 + if ($.type(options.formData) === 'object') {
  303 + formData = [];
  304 + $.each(options.formData, function (name, value) {
  305 + formData.push({name: name, value: value});
  306 + });
  307 + return formData;
  308 + }
  309 + return [];
  310 + },
  311 +
  312 + _getTotal: function (files) {
  313 + var total = 0;
  314 + $.each(files, function (index, file) {
  315 + total += file.size || 1;
  316 + });
  317 + return total;
  318 + },
  319 +
  320 + _initProgressObject: function (obj) {
  321 + var progress = {
  322 + loaded: 0,
  323 + total: 0,
  324 + bitrate: 0
  325 + };
  326 + if (obj._progress) {
  327 + $.extend(obj._progress, progress);
  328 + } else {
  329 + obj._progress = progress;
  330 + }
  331 + },
  332 +
  333 + _initResponseObject: function (obj) {
  334 + var prop;
  335 + if (obj._response) {
  336 + for (prop in obj._response) {
  337 + if (obj._response.hasOwnProperty(prop)) {
  338 + delete obj._response[prop];
  339 + }
  340 + }
  341 + } else {
  342 + obj._response = {};
  343 + }
  344 + },
  345 +
  346 + _onProgress: function (e, data) {
  347 + if (e.lengthComputable) {
  348 + var now = ((Date.now) ? Date.now() : (new Date()).getTime()),
  349 + loaded;
  350 + if (data._time && data.progressInterval &&
  351 + (now - data._time < data.progressInterval) &&
  352 + e.loaded !== e.total) {
  353 + return;
  354 + }
  355 + data._time = now;
  356 + loaded = Math.floor(
  357 + e.loaded / e.total * (data.chunkSize || data._progress.total)
  358 + ) + (data.uploadedBytes || 0);
  359 + // Add the difference from the previously loaded state
  360 + // to the global loaded counter:
  361 + this._progress.loaded += (loaded - data._progress.loaded);
  362 + this._progress.bitrate = this._bitrateTimer.getBitrate(
  363 + now,
  364 + this._progress.loaded,
  365 + data.bitrateInterval
  366 + );
  367 + data._progress.loaded = data.loaded = loaded;
  368 + data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
  369 + now,
  370 + loaded,
  371 + data.bitrateInterval
  372 + );
  373 + // Trigger a custom progress event with a total data property set
  374 + // to the file size(s) of the current upload and a loaded data
  375 + // property calculated accordingly:
  376 + this._trigger(
  377 + 'progress',
  378 + $.Event('progress', {delegatedEvent: e}),
  379 + data
  380 + );
  381 + // Trigger a global progress event for all current file uploads,
  382 + // including ajax calls queued for sequential file uploads:
  383 + this._trigger(
  384 + 'progressall',
  385 + $.Event('progressall', {delegatedEvent: e}),
  386 + this._progress
  387 + );
  388 + }
  389 + },
  390 +
  391 + _initProgressListener: function (options) {
  392 + var that = this,
  393 + xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();
  394 + // Accesss to the native XHR object is required to add event listeners
  395 + // for the upload progress event:
  396 + if (xhr.upload) {
  397 + $(xhr.upload).bind('progress', function (e) {
  398 + var oe = e.originalEvent;
  399 + // Make sure the progress event properties get copied over:
  400 + e.lengthComputable = oe.lengthComputable;
  401 + e.loaded = oe.loaded;
  402 + e.total = oe.total;
  403 + that._onProgress(e, options);
  404 + });
  405 + options.xhr = function () {
  406 + return xhr;
  407 + };
  408 + }
  409 + },
  410 +
  411 + _isInstanceOf: function (type, obj) {
  412 + // Cross-frame instanceof check
  413 + return Object.prototype.toString.call(obj) === '[object ' + type + ']';
  414 + },
  415 +
  416 + _initXHRData: function (options) {
  417 + var that = this,
  418 + formData,
  419 + file = options.files[0],
  420 + // Ignore non-multipart setting if not supported:
  421 + multipart = options.multipart || !$.support.xhrFileUpload,
  422 + paramName = $.type(options.paramName) === 'array' ?
  423 + options.paramName[0] : options.paramName;
  424 + options.headers = $.extend({}, options.headers);
  425 + if (options.contentRange) {
  426 + options.headers['Content-Range'] = options.contentRange;
  427 + }
  428 + if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
  429 + options.headers['Content-Disposition'] = 'attachment; filename="' +
  430 + encodeURI(file.name) + '"';
  431 + }
  432 + if (!multipart) {
  433 + options.contentType = file.type || 'application/octet-stream';
  434 + options.data = options.blob || file;
  435 + } else if ($.support.xhrFormDataFileUpload) {
  436 + if (options.postMessage) {
  437 + // window.postMessage does not allow sending FormData
  438 + // objects, so we just add the File/Blob objects to
  439 + // the formData array and let the postMessage window
  440 + // create the FormData object out of this array:
  441 + formData = this._getFormData(options);
  442 + if (options.blob) {
  443 + formData.push({
  444 + name: paramName,
  445 + value: options.blob
  446 + });
  447 + } else {
  448 + $.each(options.files, function (index, file) {
  449 + formData.push({
  450 + name: ($.type(options.paramName) === 'array' &&
  451 + options.paramName[index]) || paramName,
  452 + value: file
  453 + });
  454 + });
  455 + }
  456 + } else {
  457 + if (that._isInstanceOf('FormData', options.formData)) {
  458 + formData = options.formData;
  459 + } else {
  460 + formData = new FormData();
  461 + $.each(this._getFormData(options), function (index, field) {
  462 + formData.append(field.name, field.value);
  463 + });
  464 + }
  465 + if (options.blob) {
  466 + formData.append(paramName, options.blob, file.name);
  467 + } else {
  468 + $.each(options.files, function (index, file) {
  469 + // This check allows the tests to run with
  470 + // dummy objects:
  471 + if (that._isInstanceOf('File', file) ||
  472 + that._isInstanceOf('Blob', file)) {
  473 + formData.append(
  474 + ($.type(options.paramName) === 'array' &&
  475 + options.paramName[index]) || paramName,
  476 + file,
  477 + file.uploadName || file.name
  478 + );
  479 + }
  480 + });
  481 + }
  482 + }
  483 + options.data = formData;
  484 + }
  485 + // Blob reference is not needed anymore, free memory:
  486 + options.blob = null;
  487 + },
  488 +
  489 + _initIframeSettings: function (options) {
  490 + var targetHost = $('<a></a>').prop('href', options.url).prop('host');
  491 + // Setting the dataType to iframe enables the iframe transport:
  492 + options.dataType = 'iframe ' + (options.dataType || '');
  493 + // The iframe transport accepts a serialized array as form data:
  494 + options.formData = this._getFormData(options);
  495 + // Add redirect url to form data on cross-domain uploads:
  496 + if (options.redirect && targetHost && targetHost !== location.host) {
  497 + options.formData.push({
  498 + name: options.redirectParamName || 'redirect',
  499 + value: options.redirect
  500 + });
  501 + }
  502 + },
  503 +
  504 + _initDataSettings: function (options) {
  505 + if (this._isXHRUpload(options)) {
  506 + if (!this._chunkedUpload(options, true)) {
  507 + if (!options.data) {
  508 + this._initXHRData(options);
  509 + }
  510 + this._initProgressListener(options);
  511 + }
  512 + if (options.postMessage) {
  513 + // Setting the dataType to postmessage enables the
  514 + // postMessage transport:
  515 + options.dataType = 'postmessage ' + (options.dataType || '');
  516 + }
  517 + } else {
  518 + this._initIframeSettings(options);
  519 + }
  520 + },
  521 +
  522 + _getParamName: function (options) {
  523 + var fileInput = $(options.fileInput),
  524 + paramName = options.paramName;
  525 + if (!paramName) {
  526 + paramName = [];
  527 + fileInput.each(function () {
  528 + var input = $(this),
  529 + name = input.prop('name') || 'files[]',
  530 + i = (input.prop('files') || [1]).length;
  531 + while (i) {
  532 + paramName.push(name);
  533 + i -= 1;
  534 + }
  535 + });
  536 + if (!paramName.length) {
  537 + paramName = [fileInput.prop('name') || 'files[]'];
  538 + }
  539 + } else if (!$.isArray(paramName)) {
  540 + paramName = [paramName];
  541 + }
  542 + return paramName;
  543 + },
  544 +
  545 + _initFormSettings: function (options) {
  546 + // Retrieve missing options from the input field and the
  547 + // associated form, if available:
  548 + if (!options.form || !options.form.length) {
  549 + options.form = $(options.fileInput.prop('form'));
  550 + // If the given file input doesn't have an associated form,
  551 + // use the default widget file input's form:
  552 + if (!options.form.length) {
  553 + options.form = $(this.options.fileInput.prop('form'));
  554 + }
  555 + }
  556 + options.paramName = this._getParamName(options);
  557 + if (!options.url) {
  558 + options.url = options.form.prop('action') || location.href;
  559 + }
  560 + // The HTTP request method must be "POST" or "PUT":
  561 + options.type = (options.type ||
  562 + ($.type(options.form.prop('method')) === 'string' &&
  563 + options.form.prop('method')) || ''
  564 + ).toUpperCase();
  565 + if (options.type !== 'POST' && options.type !== 'PUT' &&
  566 + options.type !== 'PATCH') {
  567 + options.type = 'POST';
  568 + }
  569 + if (!options.formAcceptCharset) {
  570 + options.formAcceptCharset = options.form.attr('accept-charset');
  571 + }
  572 + },
  573 +
  574 + _getAJAXSettings: function (data) {
  575 + var options = $.extend({}, this.options, data);
  576 + this._initFormSettings(options);
  577 + this._initDataSettings(options);
  578 + return options;
  579 + },
  580 +
  581 + // jQuery 1.6 doesn't provide .state(),
  582 + // while jQuery 1.8+ removed .isRejected() and .isResolved():
  583 + _getDeferredState: function (deferred) {
  584 + if (deferred.state) {
  585 + return deferred.state();
  586 + }
  587 + if (deferred.isResolved()) {
  588 + return 'resolved';
  589 + }
  590 + if (deferred.isRejected()) {
  591 + return 'rejected';
  592 + }
  593 + return 'pending';
  594 + },
  595 +
  596 + // Maps jqXHR callbacks to the equivalent
  597 + // methods of the given Promise object:
  598 + _enhancePromise: function (promise) {
  599 + promise.success = promise.done;
  600 + promise.error = promise.fail;
  601 + promise.complete = promise.always;
  602 + return promise;
  603 + },
  604 +
  605 + // Creates and returns a Promise object enhanced with
  606 + // the jqXHR methods abort, success, error and complete:
  607 + _getXHRPromise: function (resolveOrReject, context, args) {
  608 + var dfd = $.Deferred(),
  609 + promise = dfd.promise();
  610 + context = context || this.options.context || promise;
  611 + if (resolveOrReject === true) {
  612 + dfd.resolveWith(context, args);
  613 + } else if (resolveOrReject === false) {
  614 + dfd.rejectWith(context, args);
  615 + }
  616 + promise.abort = dfd.promise;
  617 + return this._enhancePromise(promise);
  618 + },
  619 +
  620 + // Adds convenience methods to the data callback argument:
  621 + _addConvenienceMethods: function (e, data) {
  622 + var that = this,
  623 + getPromise = function (args) {
  624 + return $.Deferred().resolveWith(that, args).promise();
  625 + };
  626 + data.process = function (resolveFunc, rejectFunc) {
  627 + if (resolveFunc || rejectFunc) {
  628 + data._processQueue = this._processQueue =
  629 + (this._processQueue || getPromise([this])).pipe(
  630 + function () {
  631 + if (data.errorThrown) {
  632 + return $.Deferred()
  633 + .rejectWith(that, [data]).promise();
  634 + }
  635 + return getPromise(arguments);
  636 + }
  637 + ).pipe(resolveFunc, rejectFunc);
  638 + }
  639 + return this._processQueue || getPromise([this]);
  640 + };
  641 + data.submit = function () {
  642 + if (this.state() !== 'pending') {
  643 + data.jqXHR = this.jqXHR =
  644 + (that._trigger(
  645 + 'submit',
  646 + $.Event('submit', {delegatedEvent: e}),
  647 + this
  648 + ) !== false) && that._onSend(e, this);
  649 + }
  650 + return this.jqXHR || that._getXHRPromise();
  651 + };
  652 + data.abort = function () {
  653 + if (this.jqXHR) {
  654 + return this.jqXHR.abort();
  655 + }
  656 + this.errorThrown = 'abort';
  657 + that._trigger('fail', null, this);
  658 + return that._getXHRPromise(false);
  659 + };
  660 + data.state = function () {
  661 + if (this.jqXHR) {
  662 + return that._getDeferredState(this.jqXHR);
  663 + }
  664 + if (this._processQueue) {
  665 + return that._getDeferredState(this._processQueue);
  666 + }
  667 + };
  668 + data.processing = function () {
  669 + return !this.jqXHR && this._processQueue && that
  670 + ._getDeferredState(this._processQueue) === 'pending';
  671 + };
  672 + data.progress = function () {
  673 + return this._progress;
  674 + };
  675 + data.response = function () {
  676 + return this._response;
  677 + };
  678 + },
  679 +
  680 + // Parses the Range header from the server response
  681 + // and returns the uploaded bytes:
  682 + _getUploadedBytes: function (jqXHR) {
  683 + var range = jqXHR.getResponseHeader('Range'),
  684 + parts = range && range.split('-'),
  685 + upperBytesPos = parts && parts.length > 1 &&
  686 + parseInt(parts[1], 10);
  687 + return upperBytesPos && upperBytesPos + 1;
  688 + },
  689 +
  690 + // Uploads a file in multiple, sequential requests
  691 + // by splitting the file up in multiple blob chunks.
  692 + // If the second parameter is true, only tests if the file
  693 + // should be uploaded in chunks, but does not invoke any
  694 + // upload requests:
  695 + _chunkedUpload: function (options, testOnly) {
  696 + options.uploadedBytes = options.uploadedBytes || 0;
  697 + var that = this,
  698 + file = options.files[0],
  699 + fs = file.size,
  700 + ub = options.uploadedBytes,
  701 + mcs = options.maxChunkSize || fs,
  702 + slice = this._blobSlice,
  703 + dfd = $.Deferred(),
  704 + promise = dfd.promise(),
  705 + jqXHR,
  706 + upload;
  707 + if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs)) ||
  708 + options.data) {
  709 + return false;
  710 + }
  711 + if (testOnly) {
  712 + return true;
  713 + }
  714 + if (ub >= fs) {
  715 + file.error = options.i18n('uploadedBytes');
  716 + return this._getXHRPromise(
  717 + false,
  718 + options.context,
  719 + [null, 'error', file.error]
  720 + );
  721 + }
  722 + // The chunk upload method:
  723 + upload = function () {
  724 + // Clone the options object for each chunk upload:
  725 + var o = $.extend({}, options),
  726 + currentLoaded = o._progress.loaded;
  727 + o.blob = slice.call(
  728 + file,
  729 + ub,
  730 + ub + mcs,
  731 + file.type
  732 + );
  733 + // Store the current chunk size, as the blob itself
  734 + // will be dereferenced after data processing:
  735 + o.chunkSize = o.blob.size;
  736 + // Expose the chunk bytes position range:
  737 + o.contentRange = 'bytes ' + ub + '-' +
  738 + (ub + o.chunkSize - 1) + '/' + fs;
  739 + // Process the upload data (the blob and potential form data):
  740 + that._initXHRData(o);
  741 + // Add progress listeners for this chunk upload:
  742 + that._initProgressListener(o);
  743 + jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
  744 + that._getXHRPromise(false, o.context))
  745 + .done(function (result, textStatus, jqXHR) {
  746 + ub = that._getUploadedBytes(jqXHR) ||
  747 + (ub + o.chunkSize);
  748 + // Create a progress event if no final progress event
  749 + // with loaded equaling total has been triggered
  750 + // for this chunk:
  751 + if (currentLoaded + o.chunkSize - o._progress.loaded) {
  752 + that._onProgress($.Event('progress', {
  753 + lengthComputable: true,
  754 + loaded: ub - o.uploadedBytes,
  755 + total: ub - o.uploadedBytes
  756 + }), o);
  757 + }
  758 + options.uploadedBytes = o.uploadedBytes = ub;
  759 + o.result = result;
  760 + o.textStatus = textStatus;
  761 + o.jqXHR = jqXHR;
  762 + that._trigger('chunkdone', null, o);
  763 + that._trigger('chunkalways', null, o);
  764 + if (ub < fs) {
  765 + // File upload not yet complete,
  766 + // continue with the next chunk:
  767 + upload();
  768 + } else {
  769 + dfd.resolveWith(
  770 + o.context,
  771 + [result, textStatus, jqXHR]
  772 + );
  773 + }
  774 + })
  775 + .fail(function (jqXHR, textStatus, errorThrown) {
  776 + o.jqXHR = jqXHR;
  777 + o.textStatus = textStatus;
  778 + o.errorThrown = errorThrown;
  779 + that._trigger('chunkfail', null, o);
  780 + that._trigger('chunkalways', null, o);
  781 + dfd.rejectWith(
  782 + o.context,
  783 + [jqXHR, textStatus, errorThrown]
  784 + );
  785 + });
  786 + };
  787 + this._enhancePromise(promise);
  788 + promise.abort = function () {
  789 + return jqXHR.abort();
  790 + };
  791 + upload();
  792 + return promise;
  793 + },
  794 +
  795 + _beforeSend: function (e, data) {
  796 + if (this._active === 0) {
  797 + // the start callback is triggered when an upload starts
  798 + // and no other uploads are currently running,
  799 + // equivalent to the global ajaxStart event:
  800 + this._trigger('start');
  801 + // Set timer for global bitrate progress calculation:
  802 + this._bitrateTimer = new this._BitrateTimer();
  803 + // Reset the global progress values:
  804 + this._progress.loaded = this._progress.total = 0;
  805 + this._progress.bitrate = 0;
  806 + }
  807 + // Make sure the container objects for the .response() and
  808 + // .progress() methods on the data object are available
  809 + // and reset to their initial state:
  810 + this._initResponseObject(data);
  811 + this._initProgressObject(data);
  812 + data._progress.loaded = data.loaded = data.uploadedBytes || 0;
  813 + data._progress.total = data.total = this._getTotal(data.files) || 1;
  814 + data._progress.bitrate = data.bitrate = 0;
  815 + this._active += 1;
  816 + // Initialize the global progress values:
  817 + this._progress.loaded += data.loaded;
  818 + this._progress.total += data.total;
  819 + },
  820 +
  821 + _onDone: function (result, textStatus, jqXHR, options) {
  822 + var total = options._progress.total,
  823 + response = options._response;
  824 + if (options._progress.loaded < total) {
  825 + // Create a progress event if no final progress event
  826 + // with loaded equaling total has been triggered:
  827 + this._onProgress($.Event('progress', {
  828 + lengthComputable: true,
  829 + loaded: total,
  830 + total: total
  831 + }), options);
  832 + }
  833 + response.result = options.result = result;
  834 + response.textStatus = options.textStatus = textStatus;
  835 + response.jqXHR = options.jqXHR = jqXHR;
  836 + this._trigger('done', null, options);
  837 + },
  838 +
  839 + _onFail: function (jqXHR, textStatus, errorThrown, options) {
  840 + var response = options._response;
  841 + if (options.recalculateProgress) {
  842 + // Remove the failed (error or abort) file upload from
  843 + // the global progress calculation:
  844 + this._progress.loaded -= options._progress.loaded;
  845 + this._progress.total -= options._progress.total;
  846 + }
  847 + response.jqXHR = options.jqXHR = jqXHR;
  848 + response.textStatus = options.textStatus = textStatus;
  849 + response.errorThrown = options.errorThrown = errorThrown;
  850 + this._trigger('fail', null, options);
  851 + },
  852 +
  853 + _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
  854 + // jqXHRorResult, textStatus and jqXHRorError are added to the
  855 + // options object via done and fail callbacks
  856 + this._trigger('always', null, options);
  857 + },
  858 +
  859 + _onSend: function (e, data) {
  860 + if (!data.submit) {
  861 + this._addConvenienceMethods(e, data);
  862 + }
  863 + var that = this,
  864 + jqXHR,
  865 + aborted,
  866 + slot,
  867 + pipe,
  868 + options = that._getAJAXSettings(data),
  869 + send = function () {
  870 + that._sending += 1;
  871 + // Set timer for bitrate progress calculation:
  872 + options._bitrateTimer = new that._BitrateTimer();
  873 + jqXHR = jqXHR || (
  874 + ((aborted || that._trigger(
  875 + 'send',
  876 + $.Event('send', {delegatedEvent: e}),
  877 + options
  878 + ) === false) &&
  879 + that._getXHRPromise(false, options.context, aborted)) ||
  880 + that._chunkedUpload(options) || $.ajax(options)
  881 + ).done(function (result, textStatus, jqXHR) {
  882 + that._onDone(result, textStatus, jqXHR, options);
  883 + }).fail(function (jqXHR, textStatus, errorThrown) {
  884 + that._onFail(jqXHR, textStatus, errorThrown, options);
  885 + }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
  886 + that._onAlways(
  887 + jqXHRorResult,
  888 + textStatus,
  889 + jqXHRorError,
  890 + options
  891 + );
  892 + that._sending -= 1;
  893 + that._active -= 1;
  894 + if (options.limitConcurrentUploads &&
  895 + options.limitConcurrentUploads > that._sending) {
  896 + // Start the next queued upload,
  897 + // that has not been aborted:
  898 + var nextSlot = that._slots.shift();
  899 + while (nextSlot) {
  900 + if (that._getDeferredState(nextSlot) === 'pending') {
  901 + nextSlot.resolve();
  902 + break;
  903 + }
  904 + nextSlot = that._slots.shift();
  905 + }
  906 + }
  907 + if (that._active === 0) {
  908 + // The stop callback is triggered when all uploads have
  909 + // been completed, equivalent to the global ajaxStop event:
  910 + that._trigger('stop');
  911 + }
  912 + });
  913 + return jqXHR;
  914 + };
  915 + this._beforeSend(e, options);
  916 + if (this.options.sequentialUploads ||
  917 + (this.options.limitConcurrentUploads &&
  918 + this.options.limitConcurrentUploads <= this._sending)) {
  919 + if (this.options.limitConcurrentUploads > 1) {
  920 + slot = $.Deferred();
  921 + this._slots.push(slot);
  922 + pipe = slot.pipe(send);
  923 + } else {
  924 + this._sequence = this._sequence.pipe(send, send);
  925 + pipe = this._sequence;
  926 + }
  927 + // Return the piped Promise object, enhanced with an abort method,
  928 + // which is delegated to the jqXHR object of the current upload,
  929 + // and jqXHR callbacks mapped to the equivalent Promise methods:
  930 + pipe.abort = function () {
  931 + aborted = [undefined, 'abort', 'abort'];
  932 + if (!jqXHR) {
  933 + if (slot) {
  934 + slot.rejectWith(options.context, aborted);
  935 + }
  936 + return send();
  937 + }
  938 + return jqXHR.abort();
  939 + };
  940 + return this._enhancePromise(pipe);
  941 + }
  942 + return send();
  943 + },
  944 +
  945 + _onAdd: function (e, data) {
  946 + var that = this,
  947 + result = true,
  948 + options = $.extend({}, this.options, data),
  949 + files = data.files,
  950 + filesLength = files.length,
  951 + limit = options.limitMultiFileUploads,
  952 + limitSize = options.limitMultiFileUploadSize,
  953 + overhead = options.limitMultiFileUploadSizeOverhead,
  954 + batchSize = 0,
  955 + paramName = this._getParamName(options),
  956 + paramNameSet,
  957 + paramNameSlice,
  958 + fileSet,
  959 + i,
  960 + j = 0;
  961 + if (limitSize && (!filesLength || files[0].size === undefined)) {
  962 + limitSize = undefined;
  963 + }
  964 + if (!(options.singleFileUploads || limit || limitSize) ||
  965 + !this._isXHRUpload(options)) {
  966 + fileSet = [files];
  967 + paramNameSet = [paramName];
  968 + } else if (!(options.singleFileUploads || limitSize) && limit) {
  969 + fileSet = [];
  970 + paramNameSet = [];
  971 + for (i = 0; i < filesLength; i += limit) {
  972 + fileSet.push(files.slice(i, i + limit));
  973 + paramNameSlice = paramName.slice(i, i + limit);
  974 + if (!paramNameSlice.length) {
  975 + paramNameSlice = paramName;
  976 + }
  977 + paramNameSet.push(paramNameSlice);
  978 + }
  979 + } else if (!options.singleFileUploads && limitSize) {
  980 + fileSet = [];
  981 + paramNameSet = [];
  982 + for (i = 0; i < filesLength; i = i + 1) {
  983 + batchSize += files[i].size + overhead;
  984 + if (i + 1 === filesLength ||
  985 + ((batchSize + files[i + 1].size + overhead) > limitSize) ||
  986 + (limit && i + 1 - j >= limit)) {
  987 + fileSet.push(files.slice(j, i + 1));
  988 + paramNameSlice = paramName.slice(j, i + 1);
  989 + if (!paramNameSlice.length) {
  990 + paramNameSlice = paramName;
  991 + }
  992 + paramNameSet.push(paramNameSlice);
  993 + j = i + 1;
  994 + batchSize = 0;
  995 + }
  996 + }
  997 + } else {
  998 + paramNameSet = paramName;
  999 + }
  1000 + data.originalFiles = files;
  1001 + $.each(fileSet || files, function (index, element) {
  1002 + var newData = $.extend({}, data);
  1003 + newData.files = fileSet ? element : [element];
  1004 + newData.paramName = paramNameSet[index];
  1005 + that._initResponseObject(newData);
  1006 + that._initProgressObject(newData);
  1007 + that._addConvenienceMethods(e, newData);
  1008 + result = that._trigger(
  1009 + 'add',
  1010 + $.Event('add', {delegatedEvent: e}),
  1011 + newData
  1012 + );
  1013 + return result;
  1014 + });
  1015 + return result;
  1016 + },
  1017 +
  1018 + _replaceFileInput: function (input) {
  1019 + var inputClone = input.clone(true);
  1020 + $('<form></form>').append(inputClone)[0].reset();
  1021 + // Detaching allows to insert the fileInput on another form
  1022 + // without loosing the file input value:
  1023 + input.after(inputClone).detach();
  1024 + // Avoid memory leaks with the detached file input:
  1025 + $.cleanData(input.unbind('remove'));
  1026 + // Replace the original file input element in the fileInput
  1027 + // elements set with the clone, which has been copied including
  1028 + // event handlers:
  1029 + this.options.fileInput = this.options.fileInput.map(function (i, el) {
  1030 + if (el === input[0]) {
  1031 + return inputClone[0];
  1032 + }
  1033 + return el;
  1034 + });
  1035 + // If the widget has been initialized on the file input itself,
  1036 + // override this.element with the file input clone:
  1037 + if (input[0] === this.element[0]) {
  1038 + this.element = inputClone;
  1039 + }
  1040 + },
  1041 +
  1042 + _handleFileTreeEntry: function (entry, path) {
  1043 + var that = this,
  1044 + dfd = $.Deferred(),
  1045 + errorHandler = function (e) {
  1046 + if (e && !e.entry) {
  1047 + e.entry = entry;
  1048 + }
  1049 + // Since $.when returns immediately if one
  1050 + // Deferred is rejected, we use resolve instead.
  1051 + // This allows valid files and invalid items
  1052 + // to be returned together in one set:
  1053 + dfd.resolve([e]);
  1054 + },
  1055 + successHandler = function (entries) {
  1056 + that._handleFileTreeEntries(
  1057 + entries,
  1058 + path + entry.name + '/'
  1059 + ).done(function (files) {
  1060 + dfd.resolve(files);
  1061 + }).fail(errorHandler);
  1062 + },
  1063 + readEntries = function () {
  1064 + dirReader.readEntries(function (results) {
  1065 + if (!results.length) {
  1066 + successHandler(entries);
  1067 + } else {
  1068 + entries = entries.concat(results);
  1069 + readEntries();
  1070 + }
  1071 + }, errorHandler);
  1072 + },
  1073 + dirReader, entries = [];
  1074 + path = path || '';
  1075 + if (entry.isFile) {
  1076 + if (entry._file) {
  1077 + // Workaround for Chrome bug #149735
  1078 + entry._file.relativePath = path;
  1079 + dfd.resolve(entry._file);
  1080 + } else {
  1081 + entry.file(function (file) {
  1082 + file.relativePath = path;
  1083 + dfd.resolve(file);
  1084 + }, errorHandler);
  1085 + }
  1086 + } else if (entry.isDirectory) {
  1087 + dirReader = entry.createReader();
  1088 + readEntries();
  1089 + } else {
  1090 + // Return an empy list for file system items
  1091 + // other than files or directories:
  1092 + dfd.resolve([]);
  1093 + }
  1094 + return dfd.promise();
  1095 + },
  1096 +
  1097 + _handleFileTreeEntries: function (entries, path) {
  1098 + var that = this;
  1099 + return $.when.apply(
  1100 + $,
  1101 + $.map(entries, function (entry) {
  1102 + return that._handleFileTreeEntry(entry, path);
  1103 + })
  1104 + ).pipe(function () {
  1105 + return Array.prototype.concat.apply(
  1106 + [],
  1107 + arguments
  1108 + );
  1109 + });
  1110 + },
  1111 +
  1112 + _getDroppedFiles: function (dataTransfer) {
  1113 + dataTransfer = dataTransfer || {};
  1114 + var items = dataTransfer.items;
  1115 + if (items && items.length && (items[0].webkitGetAsEntry ||
  1116 + items[0].getAsEntry)) {
  1117 + return this._handleFileTreeEntries(
  1118 + $.map(items, function (item) {
  1119 + var entry;
  1120 + if (item.webkitGetAsEntry) {
  1121 + entry = item.webkitGetAsEntry();
  1122 + if (entry) {
  1123 + // Workaround for Chrome bug #149735:
  1124 + entry._file = item.getAsFile();
  1125 + }
  1126 + return entry;
  1127 + }
  1128 + return item.getAsEntry();
  1129 + })
  1130 + );
  1131 + }
  1132 + return $.Deferred().resolve(
  1133 + $.makeArray(dataTransfer.files)
  1134 + ).promise();
  1135 + },
  1136 +
  1137 + _getSingleFileInputFiles: function (fileInput) {
  1138 + fileInput = $(fileInput);
  1139 + var entries = fileInput.prop('webkitEntries') ||
  1140 + fileInput.prop('entries'),
  1141 + files,
  1142 + value;
  1143 + if (entries && entries.length) {
  1144 + return this._handleFileTreeEntries(entries);
  1145 + }
  1146 + files = $.makeArray(fileInput.prop('files'));
  1147 + if (!files.length) {
  1148 + value = fileInput.prop('value');
  1149 + if (!value) {
  1150 + return $.Deferred().resolve([]).promise();
  1151 + }
  1152 + // If the files property is not available, the browser does not
  1153 + // support the File API and we add a pseudo File object with
  1154 + // the input value as name with path information removed:
  1155 + files = [{name: value.replace(/^.*\\/, '')}];
  1156 + } else if (files[0].name === undefined && files[0].fileName) {
  1157 + // File normalization for Safari 4 and Firefox 3:
  1158 + $.each(files, function (index, file) {
  1159 + file.name = file.fileName;
  1160 + file.size = file.fileSize;
  1161 + });
  1162 + }
  1163 + return $.Deferred().resolve(files).promise();
  1164 + },
  1165 +
  1166 + _getFileInputFiles: function (fileInput) {
  1167 + if (!(fileInput instanceof $) || fileInput.length === 1) {
  1168 + return this._getSingleFileInputFiles(fileInput);
  1169 + }
  1170 + return $.when.apply(
  1171 + $,
  1172 + $.map(fileInput, this._getSingleFileInputFiles)
  1173 + ).pipe(function () {
  1174 + return Array.prototype.concat.apply(
  1175 + [],
  1176 + arguments
  1177 + );
  1178 + });
  1179 + },
  1180 +
  1181 + _onChange: function (e) {
  1182 + var that = this,
  1183 + data = {
  1184 + fileInput: $(e.target),
  1185 + form: $(e.target.form)
  1186 + };
  1187 + this._getFileInputFiles(data.fileInput).always(function (files) {
  1188 + data.files = files;
  1189 + if (that.options.replaceFileInput) {
  1190 + that._replaceFileInput(data.fileInput);
  1191 + }
  1192 + if (that._trigger(
  1193 + 'change',
  1194 + $.Event('change', {delegatedEvent: e}),
  1195 + data
  1196 + ) !== false) {
  1197 + that._onAdd(e, data);
  1198 + }
  1199 + });
  1200 + },
  1201 +
  1202 + _onPaste: function (e) {
  1203 + var items = e.originalEvent && e.originalEvent.clipboardData &&
  1204 + e.originalEvent.clipboardData.items,
  1205 + data = {files: []};
  1206 + if (items && items.length) {
  1207 + $.each(items, function (index, item) {
  1208 + var file = item.getAsFile && item.getAsFile();
  1209 + if (file) {
  1210 + data.files.push(file);
  1211 + }
  1212 + });
  1213 + if (this._trigger(
  1214 + 'paste',
  1215 + $.Event('paste', {delegatedEvent: e}),
  1216 + data
  1217 + ) !== false) {
  1218 + this._onAdd(e, data);
  1219 + }
  1220 + }
  1221 + },
  1222 +
  1223 + _onDrop: function (e) {
  1224 + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
  1225 + var that = this,
  1226 + dataTransfer = e.dataTransfer,
  1227 + data = {};
  1228 + if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
  1229 + e.preventDefault();
  1230 + this._getDroppedFiles(dataTransfer).always(function (files) {
  1231 + data.files = files;
  1232 + if (that._trigger(
  1233 + 'drop',
  1234 + $.Event('drop', {delegatedEvent: e}),
  1235 + data
  1236 + ) !== false) {
  1237 + that._onAdd(e, data);
  1238 + }
  1239 + });
  1240 + }
  1241 + },
  1242 +
  1243 + _onDragOver: function (e) {
  1244 + e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
  1245 + var dataTransfer = e.dataTransfer;
  1246 + if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
  1247 + this._trigger(
  1248 + 'dragover',
  1249 + $.Event('dragover', {delegatedEvent: e})
  1250 + ) !== false) {
  1251 + e.preventDefault();
  1252 + dataTransfer.dropEffect = 'copy';
  1253 + }
  1254 + },
  1255 +
  1256 + _initEventHandlers: function () {
  1257 + if (this._isXHRUpload(this.options)) {
  1258 + this._on(this.options.dropZone, {
  1259 + dragover: this._onDragOver,
  1260 + drop: this._onDrop
  1261 + });
  1262 + this._on(this.options.pasteZone, {
  1263 + paste: this._onPaste
  1264 + });
  1265 + }
  1266 + if ($.support.fileInput) {
  1267 + this._on(this.options.fileInput, {
  1268 + change: this._onChange
  1269 + });
  1270 + }
  1271 + },
  1272 +
  1273 + _destroyEventHandlers: function () {
  1274 + this._off(this.options.dropZone, 'dragover drop');
  1275 + this._off(this.options.pasteZone, 'paste');
  1276 + this._off(this.options.fileInput, 'change');
  1277 + },
  1278 +
  1279 + _setOption: function (key, value) {
  1280 + var reinit = $.inArray(key, this._specialOptions) !== -1;
  1281 + if (reinit) {
  1282 + this._destroyEventHandlers();
  1283 + }
  1284 + this._super(key, value);
  1285 + if (reinit) {
  1286 + this._initSpecialOptions();
  1287 + this._initEventHandlers();
  1288 + }
  1289 + },
  1290 +
  1291 + _initSpecialOptions: function () {
  1292 + var options = this.options;
  1293 + if (options.fileInput === undefined) {
  1294 + options.fileInput = this.element.is('input[type="file"]') ?
  1295 + this.element : this.element.find('input[type="file"]');
  1296 + } else if (!(options.fileInput instanceof $)) {
  1297 + options.fileInput = $(options.fileInput);
  1298 + }
  1299 + if (!(options.dropZone instanceof $)) {
  1300 + options.dropZone = $(options.dropZone);
  1301 + }
  1302 + if (!(options.pasteZone instanceof $)) {
  1303 + options.pasteZone = $(options.pasteZone);
  1304 + }
  1305 + },
  1306 +
  1307 + _getRegExp: function (str) {
  1308 + var parts = str.split('/'),
  1309 + modifiers = parts.pop();
  1310 + parts.shift();
  1311 + return new RegExp(parts.join('/'), modifiers);
  1312 + },
  1313 +
  1314 + _isRegExpOption: function (key, value) {
  1315 + return key !== 'url' && $.type(value) === 'string' &&
  1316 + /^\/.*\/[igm]{0,3}$/.test(value);
  1317 + },
  1318 +
  1319 + _initDataAttributes: function () {
  1320 + var that = this,
  1321 + options = this.options,
  1322 + clone = $(this.element[0].cloneNode(false));
  1323 + // Initialize options set via HTML5 data-attributes:
  1324 + $.each(
  1325 + clone.data(),
  1326 + function (key, value) {
  1327 + var dataAttributeName = 'data-' +
  1328 + // Convert camelCase to hyphen-ated key:
  1329 + key.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
  1330 + if (clone.attr(dataAttributeName)) {
  1331 + if (that._isRegExpOption(key, value)) {
  1332 + value = that._getRegExp(value);
  1333 + }
  1334 + options[key] = value;
  1335 + }
  1336 + }
  1337 + );
  1338 + },
  1339 +
  1340 + _create: function () {
  1341 + this._initDataAttributes();
  1342 + this._initSpecialOptions();
  1343 + this._slots = [];
  1344 + this._sequence = this._getXHRPromise(true);
  1345 + this._sending = this._active = 0;
  1346 + this._initProgressObject(this);
  1347 + this._initEventHandlers();
  1348 + },
  1349 +
  1350 + // This method is exposed to the widget API and allows to query
  1351 + // the number of active uploads:
  1352 + active: function () {
  1353 + return this._active;
  1354 + },
  1355 +
  1356 + // This method is exposed to the widget API and allows to query
  1357 + // the widget upload progress.
  1358 + // It returns an object with loaded, total and bitrate properties
  1359 + // for the running uploads:
  1360 + progress: function () {
  1361 + return this._progress;
  1362 + },
  1363 +
  1364 + // This method is exposed to the widget API and allows adding files
  1365 + // using the fileupload API. The data parameter accepts an object which
  1366 + // must have a files property and can contain additional options:
  1367 + // .fileupload('add', {files: filesList});
  1368 + add: function (data) {
  1369 + var that = this;
  1370 + if (!data || this.options.disabled) {
  1371 + return;
  1372 + }
  1373 + if (data.fileInput && !data.files) {
  1374 + this._getFileInputFiles(data.fileInput).always(function (files) {
  1375 + data.files = files;
  1376 + that._onAdd(null, data);
  1377 + });
  1378 + } else {
  1379 + data.files = $.makeArray(data.files);
  1380 + this._onAdd(null, data);
  1381 + }
  1382 + },
  1383 +
  1384 + // This method is exposed to the widget API and allows sending files
  1385 + // using the fileupload API. The data parameter accepts an object which
  1386 + // must have a files or fileInput property and can contain additional options:
  1387 + // .fileupload('send', {files: filesList});
  1388 + // The method returns a Promise object for the file upload call.
  1389 + send: function (data) {
  1390 + if (data && !this.options.disabled) {
  1391 + if (data.fileInput && !data.files) {
  1392 + var that = this,
  1393 + dfd = $.Deferred(),
  1394 + promise = dfd.promise(),
  1395 + jqXHR,
  1396 + aborted;
  1397 + promise.abort = function () {
  1398 + aborted = true;
  1399 + if (jqXHR) {
  1400 + return jqXHR.abort();
  1401 + }
  1402 + dfd.reject(null, 'abort', 'abort');
  1403 + return promise;
  1404 + };
  1405 + this._getFileInputFiles(data.fileInput).always(
  1406 + function (files) {
  1407 + if (aborted) {
  1408 + return;
  1409 + }
  1410 + if (!files.length) {
  1411 + dfd.reject();
  1412 + return;
  1413 + }
  1414 + data.files = files;
  1415 + jqXHR = that._onSend(null, data);
  1416 + jqXHR.then(
  1417 + function (result, textStatus, jqXHR) {
  1418 + dfd.resolve(result, textStatus, jqXHR);
  1419 + },
  1420 + function (jqXHR, textStatus, errorThrown) {
  1421 + dfd.reject(jqXHR, textStatus, errorThrown);
  1422 + }
  1423 + );
  1424 + }
  1425 + );
  1426 + return this._enhancePromise(promise);
  1427 + }
  1428 + data.files = $.makeArray(data.files);
  1429 + if (data.files.length) {
  1430 + return this._onSend(null, data);
  1431 + }
  1432 + }
  1433 + return this._getXHRPromise(false, data && data.context);
  1434 + }
  1435 +
  1436 + });
  1437 +
  1438 +}));
... ...
public/javascripts/jquery.typewatch.js
1 1 /*
2   -* TypeWatch 2.2
  2 +* TypeWatch 2.2.1
3 3 *
4 4 * Examples/Docs: github.com/dennyferra/TypeWatch
5   -*
6   -* Copyright(c) 2013
  5 +*
  6 +* Copyright(c) 2014
7 7 * Denny Ferrassoli - dennyferra.com
8 8 * Charles Christolini
9   -*
  9 +*
10 10 * Dual licensed under the MIT and GPL licenses:
11 11 * http://www.opensource.org/licenses/mit-license.php
12 12 * http://www.gnu.org/licenses/gpl.html
13 13 */
14 14  
15   -(function(jQuery) {
16   - jQuery.fn.typeWatch = function(o) {
  15 +!function(root, factory) {
  16 + if (typeof define === 'function' && define.amd) {
  17 + define(['jquery'], factory);
  18 + } else if (typeof exports === 'object') {
  19 + factory(require('jquery'));
  20 + } else {
  21 + factory(root.jQuery);
  22 + }
  23 +}(this, function($) {
  24 + 'use strict';
  25 + $.fn.typeWatch = function(o) {
17 26 // The default input types that are supported
18 27 var _supportedInputTypes =
19 28 ['TEXT', 'TEXTAREA', 'PASSWORD', 'TEL', 'SEARCH', 'URL', 'EMAIL', 'DATETIME', 'DATE', 'MONTH', 'WEEK', 'TIME', 'DATETIME-LOCAL', 'NUMBER', 'RANGE'];
20 29  
21 30 // Options
22   - var options = jQuery.extend({
  31 + var options = $.extend({
23 32 wait: 750,
24 33 callback: function() { },
25 34 highlight: true,
... ... @@ -28,7 +37,7 @@
28 37 }, o);
29 38  
30 39 function checkElement(timer, override) {
31   - var value = jQuery(timer.el).val();
  40 + var value = $(timer.el).val();
32 41  
33 42 // Fire if text >= options.captureLength AND text != saved text OR if override AND text >= options.captureLength
34 43 if ((value.length >= options.captureLength && value.toUpperCase() != timer.text)
... ... @@ -40,14 +49,13 @@
40 49 };
41 50  
42 51 function watchElement(elem) {
43   - console.log('Watch element: '+ elem)
44 52 var elementType = elem.type.toUpperCase();
45   - if (jQuery.inArray(elementType, options.inputTypes) >= 0) {
  53 + if ($.inArray(elementType, options.inputTypes) >= 0) {
46 54  
47 55 // Allocate timer element
48 56 var timer = {
49 57 timer: null,
50   - text: jQuery(elem).val().toUpperCase(),
  58 + text: $(elem).val().toUpperCase(),
51 59 cb: options.callback,
52 60 el: elem,
53 61 wait: options.wait
... ... @@ -55,7 +63,7 @@
55 63  
56 64 // Set focus action (highlight)
57 65 if (options.highlight) {
58   - jQuery(elem).focus(
  66 + $(elem).focus(
59 67 function() {
60 68 this.select();
61 69 });
... ... @@ -63,13 +71,12 @@
63 71  
64 72 // Key watcher / clear and reset the timer
65 73 var startWatch = function(evt) {
66   - console.log('Starting watch on: ' + evt)
67 74 var timerWait = timer.wait;
68 75 var overrideBool = false;
69 76 var evtElementType = this.type.toUpperCase();
70 77  
71 78 // If enter key is pressed and not a TEXTAREA and matched inputTypes
72   - if (typeof evt.keyCode != 'undefined' && evt.keyCode == 13 && evtElementType != 'TEXTAREA' && jQuery.inArray(evtElementType, options.inputTypes) >= 0) {
  79 + if (typeof evt.keyCode != 'undefined' && evt.keyCode == 13 && evtElementType != 'TEXTAREA' && $.inArray(evtElementType, options.inputTypes) >= 0) {
73 80 timerWait = 1;
74 81 overrideBool = true;
75 82 }
... ... @@ -78,25 +85,19 @@
78 85 checkElement(timer, overrideBool)
79 86 }
80 87  
81   - // Clear timer
  88 + // Clear timer
82 89 clearTimeout(timer.timer);
83 90 timer.timer = setTimeout(timerCallbackFx, timerWait);
84 91 };
85 92  
86   - //FIXME Monkey patch to fix the on with multiple actions
87   - //jQuery(elem).on('keydown paste cut input', startWatch);
88   - jQuery(elem).keydown(startWatch);
89   - jQuery(elem).paste(startWatch);
90   - jQuery(elem).cut(startWatch);
91   - jQuery(elem).input(startWatch);
  93 + $(elem).on('keydown paste cut input', startWatch);
92 94 }
93 95 };
94 96  
95 97 // Watch Each Element
96 98 return this.each(function() {
97   - console.log('Watching: '+ this);
98 99 watchElement(this);
99 100 });
100 101  
101 102 };
102   -})(jQuery);
  103 +});
... ...
public/javascripts/media-panel.js 0 → 100644
... ... @@ -0,0 +1,86 @@
  1 +var file_id = 1;
  2 +
  3 +jQuery('#file').fileupload({
  4 + add: function(e, data){
  5 + data.files[0].id = file_id;
  6 + file_id++;
  7 + data.context = jQuery(tmpl("template-upload", data.files[0]));
  8 + jQuery('#media-upload-form').append(data.context);
  9 + data.submit();
  10 + },
  11 + progress: function (e, data) {
  12 + if (jQuery('#hide-uploads').data('bootstraped') == false) {
  13 + jQuery('#hide-uploads').show();
  14 + jQuery('#hide-uploads').data('bootstraped', true);
  15 + }
  16 + if (data.context) {
  17 + progress = parseInt(data.loaded / data.total * 100, 10);
  18 + data.context.find('.bar').css('width', progress + '%');
  19 + data.context.find('.percentage').text(progress + '%');
  20 + }
  21 + },
  22 + fail: function(e, data){
  23 + var file_id = '#file-'+data.files[0].id;
  24 + jQuery(file_id).find('.progress .bar').addClass('error');
  25 + jQuery(file_id).append("<div class='error-message'>" + data.jqXHR.responseText + "</div>")
  26 + }
  27 +});
  28 +
  29 +jQuery('#hide-uploads').click(function(){
  30 + jQuery('#hide-uploads').hide();
  31 + jQuery('#show-uploads').show();
  32 + jQuery('.upload').slideUp();
  33 + return false;
  34 +});
  35 +
  36 +jQuery('#show-uploads').click(function(){
  37 + jQuery('#hide-uploads').show();
  38 + jQuery('#show-uploads').hide();
  39 + jQuery('.upload').slideDown();
  40 + return false;
  41 +});
  42 +
  43 +function loadPublishedMedia() {
  44 + var parent_id = jQuery('#published-media #parent_id').val();
  45 + var q = jQuery('#published-media #q').val();
  46 + var url = jQuery('#published-media').data('url');
  47 +
  48 + jQuery('#published-media').addClass('fetching');
  49 + jQuery.ajax({
  50 + url: url,
  51 + data: {'parent_id': parent_id, 'q': q},
  52 + dataType: 'html',
  53 + success: function(response) {
  54 + jQuery("#published-media .items").html(response);
  55 + jQuery('#published-media').removeClass('fetching');
  56 + updateViewAllLinks();
  57 + },
  58 + error: function(response, textStatus, xhr) {
  59 + console.log(response);
  60 + console.log(textStatus);
  61 + }
  62 + });
  63 +}
  64 +
  65 +function updateViewAllLinks() {
  66 + var parent_id = jQuery('#published-media #parent_id').val();
  67 + var q = jQuery('#published-media #q').val();
  68 + jQuery('#published-media .view-all').each(function(){
  69 + var key = jQuery(this).data('key');
  70 + var params = {parent_id: parent_id, q: q, key: key}
  71 + var href = jQuery(this).attr('href');
  72 + href = href.replace(/\?.*/, '?'+jQuery.param(params));
  73 + jQuery(this).attr('href', href);
  74 + });
  75 +}
  76 +
  77 +jQuery('#published-media #parent_id').change(function(){ loadPublishedMedia() });
  78 +
  79 +jQuery("#published-media #q").typeWatch({
  80 + callback: function (value) { loadPublishedMedia() },
  81 + wait: 750,
  82 + highlight: true,
  83 + captureLength: 2
  84 +});
  85 +
  86 +jQuery("#published-media #q").bind('notext', function(){ loadPublishedMedia() });
... ...
public/javascripts/tmpl.js 0 → 100644
... ... @@ -0,0 +1,87 @@
  1 +/*
  2 + * JavaScript Templates 2.4.1
  3 + * https://github.com/blueimp/JavaScript-Templates
  4 + *
  5 + * Copyright 2011, Sebastian Tschan
  6 + * https://blueimp.net
  7 + *
  8 + * Licensed under the MIT license:
  9 + * http://www.opensource.org/licenses/MIT
  10 + *
  11 + * Inspired by John Resig's JavaScript Micro-Templating:
  12 + * http://ejohn.org/blog/javascript-micro-templating/
  13 + */
  14 +
  15 +/*jslint evil: true, regexp: true, unparam: true */
  16 +/*global document, define */
  17 +
  18 +(function ($) {
  19 + "use strict";
  20 + var tmpl = function (str, data) {
  21 + var f = !/[^\w\-\.:]/.test(str) ? tmpl.cache[str] = tmpl.cache[str] ||
  22 + tmpl(tmpl.load(str)) :
  23 + new Function(
  24 + tmpl.arg + ',tmpl',
  25 + "var _e=tmpl.encode" + tmpl.helper + ",_s='" +
  26 + str.replace(tmpl.regexp, tmpl.func) +
  27 + "';return _s;"
  28 + );
  29 + return data ? f(data, tmpl) : function (data) {
  30 + return f(data, tmpl);
  31 + };
  32 + };
  33 + tmpl.cache = {};
  34 + tmpl.load = function (id) {
  35 + return document.getElementById(id).innerHTML;
  36 + };
  37 + tmpl.regexp = /([\s'\\])(?!(?:[^{]|\{(?!%))*%\})|(?:\{%(=|#)([\s\S]+?)%\})|(\{%)|(%\})/g;
  38 + tmpl.func = function (s, p1, p2, p3, p4, p5) {
  39 + if (p1) { // whitespace, quote and backspace in HTML context
  40 + return {
  41 + "\n": "\\n",
  42 + "\r": "\\r",
  43 + "\t": "\\t",
  44 + " " : " "
  45 + }[p1] || "\\" + p1;
  46 + }
  47 + if (p2) { // interpolation: {%=prop%}, or unescaped: {%#prop%}
  48 + if (p2 === "=") {
  49 + return "'+_e(" + p3 + ")+'";
  50 + }
  51 + return "'+(" + p3 + "==null?'':" + p3 + ")+'";
  52 + }
  53 + if (p4) { // evaluation start tag: {%
  54 + return "';";
  55 + }
  56 + if (p5) { // evaluation end tag: %}
  57 + return "_s+='";
  58 + }
  59 + };
  60 + tmpl.encReg = /[<>&"'\x00]/g;
  61 + tmpl.encMap = {
  62 + "<" : "&lt;",
  63 + ">" : "&gt;",
  64 + "&" : "&amp;",
  65 + "\"" : "&quot;",
  66 + "'" : "&#39;"
  67 + };
  68 + tmpl.encode = function (s) {
  69 + /*jshint eqnull:true */
  70 + return (s == null ? "" : "" + s).replace(
  71 + tmpl.encReg,
  72 + function (c) {
  73 + return tmpl.encMap[c] || "";
  74 + }
  75 + );
  76 + };
  77 + tmpl.arg = "o";
  78 + tmpl.helper = ",print=function(s,e){_s+=e?(s==null?'':s):_e(s);}" +
  79 + ",include=function(s,d){_s+=tmpl(s,d);}";
  80 + if (typeof define === "function" && define.amd) {
  81 + define(function () {
  82 + return tmpl;
  83 + });
  84 + } else {
  85 + $.tmpl = tmpl;
  86 + }
  87 +}(this));
... ...
public/stylesheets/application.css
... ... @@ -3399,18 +3399,71 @@ table.cms-articles .icon:hover {
3399 3399  
3400 3400 .controller-cms div.with_media_panel {
3401 3401 float: left;
3402   - width: 500px;
3403   -}
3404   -div.with_media_panel .formfield input {
3405   - width: 100%;
  3402 + width: 600px;
3406 3403 }
3407 3404 div.with_media_panel .formfield input[type="checkbox"] {
3408 3405 width: auto;
3409 3406 }
3410 3407  
  3408 +#media-upload-form {
  3409 + padding-bottom: 5px;
  3410 +}
  3411 +
  3412 +#media-upload-form .upload {
  3413 + border-top: solid 1px #CCC;
  3414 + width: 100%;
  3415 + padding-top: 5px;
  3416 + margin-top: 5px;
  3417 +}
  3418 +
  3419 +#media-upload-form .percentage {
  3420 + float: right;
  3421 +}
  3422 +
  3423 +#media-upload-form .bar {
  3424 + height: 10px;
  3425 + background: #729fcf;
  3426 + border-radius: 3px;
  3427 +}
  3428 +
  3429 +#media-upload-form .bar.error {
  3430 + background: #ef2929;
  3431 +}
  3432 +
  3433 +#media-upload-form input#file {
  3434 + width: 100%;
  3435 +}
  3436 +
  3437 +#media-upload-form .error-message {
  3438 + color: #a40000;
  3439 + font-size: 10px;
  3440 +
  3441 +}
  3442 +
  3443 +#media-upload-box .hide-and-show-uploads {
  3444 + text-align: center;
  3445 +}
  3446 +
  3447 +#media-upload-box .upload .file-name,
  3448 +#media-upload-box .upload .percentage {
  3449 + display: inline-block;
  3450 +}
  3451 +
  3452 +#media-upload-box .upload .file-name{
  3453 + width: 87%;
  3454 + white-space: nowrap;
  3455 + overflow: hidden;
  3456 + text-overflow: ellipsis;
  3457 +}
  3458 +
  3459 +#media-upload-box .upload .percentage {
  3460 + width: 12%;
  3461 +}
  3462 +
  3463 +
3411 3464 .text-editor-sidebar {
3412 3465 position: absolute;
3413   - width: 380px;
  3466 + width: 280px;
3414 3467 right: 20px;
3415 3468 top: 70px;
3416 3469 }
... ... @@ -3418,7 +3471,7 @@ div.with_media_panel .formfield input[type=&quot;checkbox&quot;] {
3418 3471 .text-editor-sidebar-box {
3419 3472 background: #fcfcf9;
3420 3473 border: 1px solid #d3d7cf;
3421   - padding: 10px 10px 0px 10px;
  3474 + padding: 10px;
3422 3475 margin-bottom: 10px;
3423 3476 }
3424 3477  
... ... @@ -3444,44 +3497,96 @@ div.with_media_panel .formfield input[type=&quot;checkbox&quot;] {
3444 3497 margin-bottom: 10px;
3445 3498 }
3446 3499  
3447   -.text-editor-sidebar .items .file {
  3500 +.text-editor-sidebar .image,
  3501 +.view-all-media .image {
  3502 + display: inline-block;
  3503 + vertical-align: top;
  3504 + text-align: center;
  3505 + position: relative;
  3506 +}
  3507 +
  3508 +.text-editor-sidebar .image {
  3509 + width: 80px;
  3510 + margin: 2px;
  3511 + height: 80px;
  3512 + line-height: 80px;
  3513 +}
  3514 +
  3515 +.view-all-media .image {
  3516 + width: 150px;
  3517 + margin: 3px;
  3518 + height: 125px;
  3519 + line-height: 125px;
  3520 +}
  3521 +
  3522 +.view-all-images .image:hover {
  3523 + cursor: pointer;
  3524 + background-color: #EEE;
  3525 +}
  3526 +
  3527 +.text-editor-sidebar img,
  3528 +.view-all-media img {
  3529 + vertical-align: middle;
  3530 + max-height: 100%;
  3531 + max-width: 100%;
  3532 +}
  3533 +
  3534 +.view-all-images {
  3535 + text-align: center;
  3536 +}
  3537 +
  3538 +.text-editor-sidebar .file,
  3539 +.view-all-media .file {
3448 3540 background-repeat: no-repeat;
3449 3541 background-position: 0px 0px;
3450 3542 border: none;
3451 3543 margin-bottom: 12px;
  3544 + padding-left: 20px;
  3545 + overflow: hidden;
  3546 + text-align: left;
3452 3547 }
3453 3548  
3454   -.text-editor-sidebar .items .image {
3455   - display: inline-block;
3456   - vertical-align: middle;
3457   - margin: 5px;
3458   - position: relative;
  3549 +.text-editor-sidebar .file,
  3550 +.view-all-media .file {
  3551 + white-space: nowrap;
  3552 + overflow: hidden;
3459 3553 }
3460 3554  
3461   -.text-editor-sidebar .items .file {
3462   - padding-left: 20px;
  3555 +.text-editor-sidebar select,
  3556 +.text-editor-sidebar input {
  3557 + width: 100%;
3463 3558 }
3464 3559  
3465   -.text-editor-sidebar .items :hover {
  3560 +.text-editor-sidebar .items :hover,
  3561 +.view-all-media :hover {
3466 3562 background-color: transparent;
3467 3563 }
3468 3564  
3469   -.text-editor-sidebar .items .image-controls {
  3565 +.text-editor-sidebar .items .image-controls,
  3566 +.view-all-media .image-controls {
3470 3567 display: none;
3471 3568 position: absolute;
3472 3569 top: 5px;
3473 3570 left: 5px;
  3571 + line-height: 16px;
3474 3572 }
3475 3573  
3476   -.text-editor-sidebar .items .item:hover .image-controls {
  3574 +.text-editor-sidebar .items .item:hover .image-controls,
  3575 +.view-all-media .item:hover .image-controls {
3477 3576 display: block;
3478 3577 }
3479 3578  
3480   -.text-editor-sidebar .items .file-controls {
  3579 +.view-all-images .item:hover .image-controls {
  3580 + display: none;
  3581 +}
  3582 +
  3583 +.text-editor-sidebar .items .file-controls,
  3584 +.view-all-media .file-controls {
3481 3585 margin-top: 5px;
3482 3586 }
3483 3587  
3484   -.text-editor-sidebar .items .image-controls .icon-zoom {
  3588 +.text-editor-sidebar .items .image-controls .icon-zoom,
  3589 +.view-all-media .image-controls .icon-zoom {
3485 3590 background-image: url(../images/zoom-dark.png);
3486 3591 display: block;
3487 3592 width: 0px;
... ... @@ -3492,10 +3597,6 @@ div.with_media_panel .formfield input[type=&quot;checkbox&quot;] {
3492 3597 background-repeat: no-repeat;
3493 3598 background-position: 98% 10px;
3494 3599 }
3495   -.text-editor-sidebar img {
3496   - max-height: 70px;
3497   - max-width: 110px;
3498   -}
3499 3600 .text-editor-sidebar .media-upload-error {
3500 3601 color: red;
3501 3602 }
... ... @@ -3503,6 +3604,29 @@ div.with_media_panel .formfield input[type=&quot;checkbox&quot;] {
3503 3604 max-width: 355px;
3504 3605 }
3505 3606  
  3607 +.view-all-media {
  3608 + max-width: 830px;
  3609 + max-height: 600px;
  3610 +}
  3611 +
  3612 +.view-all-media a.button {
  3613 + background-color: #EEE;
  3614 +}
  3615 +
  3616 +#published-media .section-title {
  3617 + width: 100%;
  3618 + height: 40px;
  3619 +}
  3620 +
  3621 +#published-media .section-title h3 {
  3622 + float: left;
  3623 +}
  3624 +
  3625 +#published-media .section-title a.view-all {
  3626 + float: right;
  3627 + line-height: 40px
  3628 +}
  3629 +
3506 3630 /* ==> public/stylesheets/controller_contact.css <== */
3507 3631 /*** SELECT CITY ***/
3508 3632  
... ...