card 1.16.3 → 1.16.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/VERSION +1 -1
  3. data/card.gemspec +1 -1
  4. data/db/migrate_core_cards/20150724123438_update_file_and_image_cards.rb +1 -1
  5. data/db/migrate_core_cards/20150824135418_update_file_history.rb +20 -0
  6. data/db/migrate_core_cards/20150903130006_attachment_upload_cards.rb +13 -0
  7. data/db/seed/new/card_actions.yml +16 -0
  8. data/db/seed/new/card_acts.yml +1 -1
  9. data/db/seed/new/card_changes.yml +56 -80
  10. data/db/seed/new/card_references.yml +282 -58
  11. data/db/seed/new/cards.yml +1348 -1312
  12. data/db/seed/test/fixtures/card_actions.yml +884 -868
  13. data/db/seed/test/fixtures/card_acts.yml +250 -250
  14. data/db/seed/test/fixtures/card_changes.yml +1935 -1959
  15. data/db/seed/test/fixtures/card_references.yml +1024 -800
  16. data/db/seed/test/fixtures/cards.yml +2402 -2366
  17. data/db/version_core_cards.txt +1 -1
  18. data/lib/card.rb +3 -1
  19. data/lib/card/cache.rb +5 -5
  20. data/lib/card/chunk.rb +2 -0
  21. data/lib/card/env.rb +1 -1
  22. data/lib/card/query/card_clause.rb +59 -58
  23. data/lib/card/set.rb +7 -0
  24. data/lib/card/success.rb +143 -0
  25. data/mod/01_core/chunk/query_reference.rb +16 -7
  26. data/mod/01_core/chunk/reference.rb +3 -3
  27. data/mod/01_core/set/all/collection.rb +1 -1
  28. data/mod/01_core/set/all/name.rb +2 -1
  29. data/mod/01_core/set/all/phases.rb +12 -2
  30. data/mod/01_core/set/all/type.rb +5 -5
  31. data/mod/01_history/set/all/actions.rb +6 -2
  32. data/mod/01_history/set/all/content_history.rb +17 -2
  33. data/mod/01_history/set/all/history.rb +1 -1
  34. data/mod/02_basic_types/set/all/file.rb +0 -31
  35. data/mod/03_machines/lib/javascript/jquery.fileupload.js +539 -182
  36. data/mod/03_machines/lib/javascript/wagn.js.coffee +3 -0
  37. data/mod/03_machines/lib/javascript/wagn_mod.js.coffee +20 -18
  38. data/mod/03_machines/lib/stylesheets/style_cards.scss +28 -1
  39. data/mod/05_email/set/all/notify.rb +1 -1
  40. data/mod/05_standard/file/favicon/image-icon.png +0 -0
  41. data/mod/05_standard/file/favicon/image-large.png +0 -0
  42. data/mod/05_standard/file/favicon/image-medium.png +0 -0
  43. data/mod/05_standard/file/favicon/image-original.png +0 -0
  44. data/mod/05_standard/file/favicon/image-small.png +0 -0
  45. data/mod/05_standard/lib/carrier_wave/cardmount.rb +25 -6
  46. data/mod/05_standard/lib/file_uploader.rb +27 -11
  47. data/mod/05_standard/lib/image_uploader.rb +7 -4
  48. data/mod/05_standard/set/abstract/attachment.rb +132 -14
  49. data/mod/05_standard/set/right/account.rb +2 -2
  50. data/mod/05_standard/set/self/signin.rb +0 -1
  51. data/mod/05_standard/set/type/file.rb +48 -19
  52. data/mod/05_standard/set/type/image.rb +9 -12
  53. data/mod/05_standard/spec/chunk/include_spec.rb +13 -12
  54. data/mod/05_standard/spec/chunk/query_reference_spec.rb +50 -0
  55. data/mod/05_standard/spec/set/right/account_spec.rb +24 -25
  56. data/mod/05_standard/spec/set/type/file_spec.rb +1 -1
  57. data/spec/lib/card/reference_spec.rb +14 -0
  58. data/spec/lib/card/success_spec.rb +142 -0
  59. data/tmpsets/set/mod001-01_core/all/collection.rb +1 -1
  60. data/tmpsets/set/mod001-01_core/all/name.rb +2 -1
  61. data/tmpsets/set/mod001-01_core/all/phases.rb +12 -2
  62. data/tmpsets/set/mod001-01_core/all/type.rb +5 -5
  63. data/tmpsets/set/mod002-01_history/all/actions.rb +6 -2
  64. data/tmpsets/set/mod002-01_history/all/content_history.rb +17 -2
  65. data/tmpsets/set/mod002-01_history/all/history.rb +1 -1
  66. data/tmpsets/set/mod003-02_basic_types/all/file.rb +0 -24
  67. data/tmpsets/set/mod003-02_basic_types/all/rss.rb +8 -5
  68. data/tmpsets/set/mod003-02_basic_types/type/pointer.rb +2 -2
  69. data/tmpsets/set/mod005-04_settings/right/structure.rb +7 -2
  70. data/tmpsets/set/mod006-05_email/all/notify.rb +1 -1
  71. data/tmpsets/set/mod007-05_standard/abstract/attachment.rb +132 -14
  72. data/tmpsets/set/mod007-05_standard/all/links.rb +8 -0
  73. data/tmpsets/set/mod007-05_standard/all/rich_html/header.rb +5 -7
  74. data/tmpsets/set/mod007-05_standard/right/account.rb +2 -2
  75. data/tmpsets/set/mod007-05_standard/self/signin.rb +0 -1
  76. data/tmpsets/set/mod007-05_standard/type/file.rb +49 -20
  77. data/tmpsets/set/mod007-05_standard/type/image.rb +9 -12
  78. data/tmpsets/set/mod007-05_standard/type/search_type.rb +40 -22
  79. data/tmpsets/set/mod008-06_bootstrap/self/bootswatch_shared.rb +1 -1
  80. metadata +10 -4
data/lib/card/set.rb CHANGED
@@ -444,6 +444,13 @@ EOF
444
444
  Card.set_specific_attributes ||= []
445
445
  Card.set_specific_attributes += args.map(&:to_s)
446
446
  end
447
+
448
+ def attachment name, args
449
+ include Abstract::Attachment
450
+ set_specific_attributes name, :load_from_mod, "remote_#{name}_url".to_sym,
451
+ uploader_class = args[:uploader] || FileUploader
452
+ mount_uploader name, uploader_class
453
+ end
447
454
  end
448
455
  end
449
456
 
@@ -0,0 +1,143 @@
1
+ class Card
2
+ class Success
3
+ include Card::Format::Location
4
+ include Card::HtmlFormat::Location
5
+
6
+ attr_accessor :params, :redirect, :id, :name, :card, :name_context
7
+
8
+ def initialize name_context=nil, success_params=nil
9
+ @name_context = name_context
10
+ @new_args = {}
11
+ @params = OpenStruct.new
12
+ case success_params
13
+ when Hash
14
+ apply(success_params)
15
+ when /^REDIRECT:\s*(.+)/
16
+ @redirect=true
17
+ self.target = $1
18
+ when nil ; self.name = '_self'
19
+ else ; self.target = success_params
20
+ end
21
+ end
22
+
23
+
24
+ def << value
25
+ case value
26
+ when Hash ; apply value
27
+ else ; self.target = value
28
+ end
29
+ end
30
+
31
+ def hard_redirect?
32
+ @redirect == true
33
+ end
34
+
35
+ # reset card object and override params with success params
36
+ def soft_redirect?
37
+ @redirect == :soft
38
+ end
39
+
40
+ def mark= value
41
+ case value
42
+ when Integer ; @id = value
43
+ when String ; @name = value
44
+ when Card ; @card = value
45
+ else ; self.target = value
46
+ end
47
+ end
48
+
49
+ def id= id
50
+ self.mark = id # for backwards compatibility use mark here: id was often used for the card name
51
+ end
52
+
53
+ def type= type
54
+ @new_args[:type] = type
55
+ end
56
+
57
+ def content= content
58
+ @new_args[:content] = content
59
+ end
60
+
61
+ def target= value
62
+ @id = @name = @card = nil
63
+ @target =
64
+ case value
65
+ when '*previous', :previous ; :previous
66
+ when /^(http|\/)/ ; value
67
+ when /^TEXT:\s*(.+)/ ; $1
68
+ when '' ; ''
69
+ else ; self.mark = value
70
+ end
71
+ end
72
+
73
+ def apply args
74
+ args.each_pair do |key, value|
75
+ self[key] = value
76
+ end
77
+ end
78
+
79
+ def card name_context=@name_context
80
+ if @card
81
+ @card
82
+ elsif @id
83
+ Card.find @id
84
+ elsif @name
85
+ Card.fetch @name.to_name.to_absolute(name_context), :new=>@new_args
86
+ end
87
+ end
88
+
89
+ def target name_context=@name_context
90
+ card(name_context) || ( @target == :previous ? previous_location : @target ) || Card.fetch(name_context)
91
+ end
92
+
93
+ def []= key, value
94
+ if respond_to? "#{key}="
95
+ send "#{key}=", value
96
+ elsif key.to_sym == :soft_redirect
97
+ @redirect = :soft
98
+ else
99
+ @params.send "#{key}=", value
100
+ end
101
+ end
102
+
103
+ def [] key
104
+ if respond_to? key.to_sym
105
+ send key.to_sym
106
+ elsif key.to_sym == :soft_redirect
107
+ @redirect == :soft
108
+ else
109
+ @params.send key.to_sym
110
+ end
111
+ end
112
+
113
+ def params
114
+ @params.marshal_dump
115
+ end
116
+
117
+ def to_url name_context=@name_context
118
+ case (target = target(name_context))
119
+ when Card
120
+ page_path target.cardname, params
121
+ else
122
+ target
123
+ end
124
+ end
125
+
126
+ def method_missing method, *args
127
+ case method
128
+ when /^(\w+)=$/
129
+ self[$1.to_sym] = args[0]
130
+ when /^(\w+)$/
131
+ self[$1.to_sym]
132
+ else
133
+ super
134
+ end
135
+ end
136
+
137
+ def session
138
+ Card::Env.session
139
+ end
140
+ end
141
+
142
+
143
+ end
@@ -10,11 +10,12 @@ module Card::Chunk
10
10
  # 3a) {"plus_right":["Alfred"]}
11
11
  # but not in
12
12
  # 2b) "content":"foo", "Alfred":"bar"
13
- # 3b) {"name":["Alfred", "Toni"]}
13
+ # 3b) {"name":["Alfred", "Toni"]} ("Alfred" is an operator here)
14
14
  # It's not possible to distinguish between 2a) and 2b) or 3a) and 3b) with a simple regex
15
15
  # hence we use a too general regex and check for query keywords after the match
16
16
  # which of course means that we don't find references with query keywords as name
17
17
  class QueryReference < Reference
18
+
18
19
  QUERY_KEYWORDS = ::Set.new(
19
20
  (
20
21
  Card::Query::MODIFIERS.keys +
@@ -26,15 +27,23 @@ module Card::Chunk
26
27
  )
27
28
  word = /\s*([^"]+)\s*/
28
29
 
29
- # we check for colon, comma or square bracket before a quote
30
- # OPTIMIZE: instead of comma or square bracket check for operator followed by comma or "plus_right"|"plus_left"|"plus" followed by square bracket
31
30
  Card::Chunk.register_class self, {
32
- :prefix_re => '(?<=[:,\\[])\\s*"', # we have to use a lookbehind, otherwise
33
- # if the colon matches it would be
34
- # identified mistakenly as an URI chunk
31
+ :prefix_re => '(?<=[:,\\[])\\s*"', # we check for colon, comma or square bracket before a quote
32
+ # we have to use a lookbehind, otherwise
33
+ # if the colon matches it would be
34
+ # identified mistakenly as an URI chunk
35
35
  :full_re => /"([^"]+)"/,
36
36
  :idx_char => '"'
37
37
  }
38
+ # OPTIMIZE: instead of comma or square bracket check for operator followed by comma or "plus_right"|"plus_left"|"plus" followed by square bracket
39
+ # something like
40
+ # prefix_patterns = [
41
+ # "\"\\s*(?:#{Card::Query::OPERATORS.keys.join('|')})\"\\s*,",
42
+ # "\"\\s*(?:#{Card::Query::CardClause::PLUS_ATTRIBUTES}.keys.join('|')})\\s*:\\s*\\[\\s*",
43
+ # "\"\\s*(?:#{(QUERY_KEYWORDS - Card::Query::CardClause::PLUS_ATTRIBUTES).join('|')})\"\\s*:",
44
+ # ]
45
+ # :prefix_re => '(?<=#{prefix_patterns.join('|')})\\s*"'
46
+ # But: What do we do with the "in" operator? After the first value there is no prefix which we can use to detect the following values as QueryReference chunks
38
47
 
39
48
  class << self
40
49
  def full_match content, prefix
@@ -59,7 +68,7 @@ module Card::Chunk
59
68
 
60
69
  def replace_reference old_name, new_name
61
70
  replace_name_reference old_name, new_name
62
- @text = "\"#{referee_name.to_s}\""
71
+ @text = "\"#{@name.to_s}\""
63
72
  end
64
73
  end
65
74
  end
@@ -5,12 +5,12 @@ module Card::Chunk
5
5
 
6
6
  def referee_name
7
7
  return if name.nil?
8
-
8
+
9
9
  @referee_name ||= begin
10
10
  rendered_name = render_obj( name )
11
11
  ref_card = case rendered_name
12
12
  when /^\~(\d+)$/ # get by id
13
- Card.fetch $1.to_i
13
+ Card.fetch $1.to_i
14
14
  when /^\:(\w+)$/ # get by codename
15
15
  Card.fetch $1.to_sym
16
16
  end
@@ -39,7 +39,7 @@ module Card::Chunk
39
39
  @name = name.to_name.replace_part( old_name, new_name )
40
40
  end
41
41
  end
42
-
42
+
43
43
  def render_obj raw
44
44
  if format && Card::Content===raw
45
45
  format.card.references_expired = nil # don't love this; this is to keep from running update_references again
@@ -313,7 +313,7 @@ format :html do
313
313
  args[:tab_type] ||= 'tabs'
314
314
  end
315
315
 
316
- view :pills_static, :view=>:tabs
316
+ view :pills_static, :view=>:tabs_static
317
317
  def default_tabs_static_args args
318
318
  args[:tab_type] ||= 'pills'
319
319
  end
@@ -233,7 +233,7 @@ end
233
233
 
234
234
 
235
235
  event :cascade_name_changes, :after=>:store, :on=>:update, :changed=>:name do
236
- Rails.logger.debug "-------------------#{name_was}- CASCADE #{self.name} -------------------------------------"
236
+ #Rails.logger.info "------------------- #{name_was} CASCADE #{self.name} -------------------------------------"
237
237
 
238
238
  self.update_referencers = false if self.update_referencers == 'false' #handle strings from cgi
239
239
  Card::Reference.update_on_rename self, name, self.update_referencers
@@ -245,6 +245,7 @@ event :cascade_name_changes, :after=>:store, :on=>:update, :changed=>:name do
245
245
 
246
246
  deps.each do |dep|
247
247
  # here we specifically want NOT to invoke recursive cascades on these cards, have to go this low level to avoid callbacks.
248
+ Rails.logger.info "cascading name: #{dep.name}"
248
249
  Card.expire dep.name #old name
249
250
  newname = dep.cardname.replace_part name_was, name
250
251
  Card.where( :id=> dep.id ).update_all :name => newname.to_s, :key => newname.key
@@ -11,7 +11,7 @@ def abort status, msg='action canceled'
11
11
  if status == :failure && errors.empty?
12
12
  errors.add :abort, msg
13
13
  elsif Hash === status and status[:success]
14
- Env.params[:success] = status[:success]
14
+ success << status[:success]
15
15
  status = :success
16
16
  end
17
17
  raise Card::Abort.new( status, msg)
@@ -49,11 +49,18 @@ end
49
49
  # perhaps above should be in separate module?
50
50
  #~~~~~~
51
51
 
52
- def approve
52
+ def prepare
53
53
  @action = identify_action
54
54
  # the following should really happen when type, name etc are changed
55
55
  reset_patterns
56
56
  include_set_modules
57
+ run_callbacks :prepare
58
+ rescue =>e
59
+ rescue_event e
60
+ end
61
+
62
+ def approve
63
+ @action ||= identify_action
57
64
  run_callbacks :approve
58
65
  expire_pieces if errors.any?
59
66
  errors.empty?
@@ -178,4 +185,7 @@ event :store_subcards, :after=>:store do
178
185
  end
179
186
  end
180
187
 
188
+ def success
189
+ Env[:success] ||= Card::Success.new(cardname)
190
+ end
181
191
 
@@ -28,7 +28,7 @@ end
28
28
 
29
29
  def get_type_id args={}
30
30
  return if args[:type_id] # type_id was set explicitly. no need to set again.
31
-
31
+
32
32
  type_id = case
33
33
  when args[:type_code]
34
34
  if code=args[:type_code]
@@ -60,11 +60,11 @@ event :validate_type_change, :before=>:approve, :on=>:update, :changed=>:type_id
60
60
  end
61
61
  end
62
62
 
63
- event :validate_type, :before=>:approve, :changed=>:type_id do
63
+ event :validate_type, :before=>:approve, :changed=>:type_id do
64
64
  if !type_name
65
65
  errors.add :type, "No such type"
66
66
  end
67
-
67
+
68
68
  if rt = structure and rt.assigns_type? and type_id!=rt.type_id
69
69
  errors.add :type, "can't be changed because #{name} is hard templated to #{rt.type_name}"
70
70
  end
@@ -72,7 +72,7 @@ end
72
72
 
73
73
  event :reset_type_specific_fields, :after=>:store do
74
74
  Auth.as_bot do
75
- Card.search :left=>{ :left=>type_name }, :right=>{:codename=>'type_plus_right'} do |set_card|
75
+ Card.search :left=>{ :left_id=>type_id }, :right=>{:codename=>'type_plus_right'} do |set_card|
76
76
  set_card.reset_set_patterns
77
77
  end
78
78
  end
@@ -82,4 +82,4 @@ end
82
82
  # Card["#{lef}"]
83
83
  # set_card.reset_set_patterns
84
84
  # end
85
-
85
+
@@ -1,13 +1,17 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  def select_action_by_params params
4
- action = find_action_by_params(params) and self.selected_action_id = action.id
4
+ if (action = find_action_by_params(params))
5
+ run_callbacks :select_action do
6
+ self.selected_action_id = action.id
7
+ end
8
+ end
5
9
  end
6
10
 
7
11
  def find_action_by_params args
8
12
  if args[:rev]
9
13
  nth_action args[:rev]
10
- elsif args[:rev_id] =~ /^\d+$/
14
+ elsif Integer === args[:rev_id] || args[:rev_id] =~ /^\d+$/
11
15
  if action = Action.fetch(args[:rev_id]) and action.card_id == id
12
16
  action
13
17
  end
@@ -4,7 +4,7 @@
4
4
  def content
5
5
  if @selected_action_id
6
6
  @selected_content ||= begin
7
- (change = last_change_on( :db_content, :not_after=> @selected_action_id ) and change.value) || db_content
7
+ (change = last_change_on( :db_content, :not_after=> @selected_action_id, :including_drafts=>true ) and change.value) || db_content
8
8
  end
9
9
  else
10
10
  super
@@ -24,7 +24,10 @@ def save_content_draft content
24
24
  end
25
25
 
26
26
  def last_change_on(field, opts={})
27
- where_sql = 'card_actions.card_id = :card_id AND field = :field AND (draft is not true) '
27
+ where_sql = 'card_actions.card_id = :card_id AND field = :field '
28
+ if !opts[:including_drafts]
29
+ where_sql += 'AND (draft is not true) '
30
+ end
28
31
  where_sql += if opts[:before]
29
32
  'AND card_action_id < :action_id'
30
33
  elsif opts[:not_after]
@@ -54,6 +57,18 @@ def selected_action
54
57
  selected_action_id and Action.fetch(selected_action_id)
55
58
  end
56
59
 
60
+ def with_selected_action_id action_id
61
+ current_action_id = @selected_action_id
62
+ run_callbacks :select_action do
63
+ self.selected_action_id = action_id
64
+ end
65
+ result = yield
66
+ run_callbacks :select_action do
67
+ self.selected_action_id = current_action_id
68
+ end
69
+ result
70
+ end
71
+
57
72
  def selected_content_action_id
58
73
  @selected_action_id ||
59
74
  (@current_action && (new_card? || @current_action.new_content? || db_content_changed?) && @current_action.id) ||
@@ -5,7 +5,7 @@ def history?
5
5
  end
6
6
 
7
7
  # must be called on all actions and before :set_name, :process_subcards and :validate_delete_children
8
- event :assign_act, :before=>:approve, :when=>proc {|c| c.history?} do
8
+ event :assign_act, :before=>:prepare, :when=>proc {|c| c.history?} do
9
9
  @current_act = (@supercard && @supercard.current_act) || Card::Act.create(:ip_address=>Env.ip)
10
10
  end
11
11
 
@@ -1,34 +1,3 @@
1
- # FIXME: these methods should move to type/file.rb but some machine stuff is failing if it's not in a "all" set
2
- def store_dir
3
- if (mod = mod_file?)
4
- "#{ Cardio.gem_root}/mod/#{mod}/file/#{codename}"
5
- elsif id
6
- "#{ Card.paths['files'].existent.first }/#{id}"
7
- else
8
- tmp_store_dir
9
- end
10
- end
11
-
12
- def tmp_store_dir
13
- "#{ Card.paths['files'].existent.first }/#{key}"
14
- end
15
-
16
- def mod_file?
17
- # when db_content was changed assume that it's no longer a mod file
18
- if !db_content_changed? && content.present?
19
- case content
20
- when /^:[^\/]+\/([^.]+)/ ; $1 # current mod_file format
21
- when /^\~/ ; false # current id file format
22
- else
23
- if lines = content.split("\n") and lines.size == 4 # old format, still used in card_changes.
24
- lines.last
25
- end
26
- end
27
- end
28
- end
29
-
30
-
31
-
32
1
  format :file do
33
2
 
34
3
  view :core do |args|
@@ -1,5 +1,5 @@
1
1
  /*
2
- * jQuery File Upload Plugin 5.19.8
2
+ * jQuery File Upload Plugin 5.42.3
3
3
  * https://github.com/blueimp/jQuery-File-Upload
4
4
  *
5
5
  * Copyright 2010, Sebastian Tschan
@@ -9,8 +9,8 @@
9
9
  * http://www.opensource.org/licenses/MIT
10
10
  */
11
11
 
12
- /*jslint nomen: true, unparam: true, regexp: true */
13
- /*global define, window, document, File, Blob, FormData, location */
12
+ /* jshint nomen:false */
13
+ /* global define, require, window, document, location, Blob, FormData */
14
14
 
15
15
  (function (factory) {
16
16
  'use strict';
@@ -20,6 +20,12 @@
20
20
  'jquery',
21
21
  'jquery.ui.widget'
22
22
  ], factory);
23
+ } else if (typeof exports === 'object') {
24
+ // Node/CommonJS:
25
+ factory(
26
+ require('jquery'),
27
+ require('./vendor/jquery.ui.widget')
28
+ );
23
29
  } else {
24
30
  // Browser globals:
25
31
  factory(window.jQuery);
@@ -27,12 +33,49 @@
27
33
  }(function ($) {
28
34
  'use strict';
29
35
 
36
+ // Detect file input support, based on
37
+ // http://viljamis.com/blog/2012/file-upload-support-on-mobile/
38
+ $.support.fileInput = !(new RegExp(
39
+ // Handle devices which give false positives for the feature detection:
40
+ '(Android (1\\.[0156]|2\\.[01]))' +
41
+ '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)' +
42
+ '|(w(eb)?OSBrowser)|(webOS)' +
43
+ '|(Kindle/(1\\.0|2\\.[05]|3\\.0))'
44
+ ).test(window.navigator.userAgent) ||
45
+ // Feature detection for all other devices:
46
+ $('<input type="file">').prop('disabled'));
47
+
30
48
  // The FileReader API is not actually used, but works as feature detection,
31
- // as e.g. Safari supports XHR file uploads via the FormData API,
32
- // but not non-multipart XHR file uploads:
33
- $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
49
+ // as some Safari versions (5?) support XHR file uploads via the FormData API,
50
+ // but not non-multipart XHR file uploads.
51
+ // window.XMLHttpRequestUpload is not available on IE10, so we check for
52
+ // window.ProgressEvent instead to detect XHR2 file upload capability:
53
+ $.support.xhrFileUpload = !!(window.ProgressEvent && window.FileReader);
34
54
  $.support.xhrFormDataFileUpload = !!window.FormData;
35
55
 
56
+ // Detect support for Blob slicing (required for chunked uploads):
57
+ $.support.blobSlice = window.Blob && (Blob.prototype.slice ||
58
+ Blob.prototype.webkitSlice || Blob.prototype.mozSlice);
59
+
60
+ // Helper function to create drag handlers for dragover/dragenter/dragleave:
61
+ function getDragHandler(type) {
62
+ var isDragOver = type === 'dragover';
63
+ return function (e) {
64
+ e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
65
+ var dataTransfer = e.dataTransfer;
66
+ if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1 &&
67
+ this._trigger(
68
+ type,
69
+ $.Event(type, {delegatedEvent: e})
70
+ ) !== false) {
71
+ e.preventDefault();
72
+ if (isDragOver) {
73
+ dataTransfer.dropEffect = 'copy';
74
+ }
75
+ }
76
+ };
77
+ }
78
+
36
79
  // The fileupload widget listens for change events on file input fields defined
37
80
  // via fileInput setting and paste or drop events of the given dropZone.
38
81
  // In addition to the default jQuery Widget methods, the fileupload widget
@@ -47,9 +90,9 @@
47
90
  // The drop target element(s), by the default the complete document.
48
91
  // Set to null to disable drag & drop support:
49
92
  dropZone: $(document),
50
- // The paste target element(s), by the default the complete document.
51
- // Set to null to disable paste support:
52
- pasteZone: $(document),
93
+ // The paste target element(s), by the default undefined.
94
+ // Set to a DOM node or jQuery object to enable file pasting:
95
+ pasteZone: undefined,
53
96
  // The file input field(s), that are listened to for change events.
54
97
  // If undefined, it is set to the file input fields inside
55
98
  // of the widget element on plugin initialization.
@@ -72,6 +115,14 @@
72
115
  // To limit the number of files uploaded with one XHR request,
73
116
  // set the following option to an integer greater than 0:
74
117
  limitMultiFileUploads: undefined,
118
+ // The following option limits the number of files uploaded with one
119
+ // XHR request to keep the request size under or equal to the defined
120
+ // limit in bytes:
121
+ limitMultiFileUploadSize: undefined,
122
+ // Multipart file uploads add a number of bytes to each uploaded file,
123
+ // therefore the following option adds an overhead for each file used
124
+ // in the limitMultiFileUploadSize configuration:
125
+ limitMultiFileUploadSizeOverhead: 512,
75
126
  // Set the following option to true to issue all file upload requests
76
127
  // in a sequential order:
77
128
  sequentialUploads: false,
@@ -112,6 +163,25 @@
112
163
  progressInterval: 100,
113
164
  // Interval in milliseconds to calculate progress bitrate:
114
165
  bitrateInterval: 500,
166
+ // By default, uploads are started automatically when adding files:
167
+ autoUpload: true,
168
+
169
+ // Error and info messages:
170
+ messages: {
171
+ uploadedBytes: 'Uploaded bytes exceed file size'
172
+ },
173
+
174
+ // Translation function, gets the message key to be translated
175
+ // and an object with context specific data as arguments:
176
+ i18n: function (message, context) {
177
+ message = this.messages[message] || message.toString();
178
+ if (context) {
179
+ $.each(context, function (key, value) {
180
+ message = message.replace('{' + key + '}', value);
181
+ });
182
+ }
183
+ return message;
184
+ },
115
185
 
116
186
  // Additional form data to be sent along with the file uploads can be set
117
187
  // using this option, which accepts an array of objects with name and
@@ -125,57 +195,95 @@
125
195
  // The add callback is invoked as soon as files are added to the fileupload
126
196
  // widget (via file input selection, drag & drop, paste or add API call).
127
197
  // If the singleFileUploads option is enabled, this callback will be
128
- // called once for each file in the selection for XHR file uplaods, else
198
+ // called once for each file in the selection for XHR file uploads, else
129
199
  // once for each file selection.
200
+ //
130
201
  // The upload starts when the submit method is invoked on the data parameter.
131
202
  // The data object contains a files property holding the added files
132
- // and allows to override plugin options as well as define ajax settings.
203
+ // and allows you to override plugin options as well as define ajax settings.
204
+ //
133
205
  // Listeners for this callback can also be bound the following way:
134
206
  // .bind('fileuploadadd', func);
207
+ //
135
208
  // data.submit() returns a Promise object and allows to attach additional
136
209
  // handlers using jQuery's Deferred callbacks:
137
210
  // data.submit().done(func).fail(func).always(func);
138
211
  add: function (e, data) {
139
- data.submit();
212
+ if (e.isDefaultPrevented()) {
213
+ return false;
214
+ }
215
+ if (data.autoUpload || (data.autoUpload !== false &&
216
+ $(this).fileupload('option', 'autoUpload'))) {
217
+ data.process().done(function () {
218
+ data.submit();
219
+ });
220
+ }
140
221
  },
141
222
 
142
223
  // Other callbacks:
224
+
143
225
  // Callback for the submit event of each file upload:
144
226
  // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);
227
+
145
228
  // Callback for the start of each file upload request:
146
229
  // send: function (e, data) {}, // .bind('fileuploadsend', func);
230
+
147
231
  // Callback for successful uploads:
148
232
  // done: function (e, data) {}, // .bind('fileuploaddone', func);
233
+
149
234
  // Callback for failed (abort or error) uploads:
150
235
  // fail: function (e, data) {}, // .bind('fileuploadfail', func);
236
+
151
237
  // Callback for completed (success, abort or error) requests:
152
238
  // always: function (e, data) {}, // .bind('fileuploadalways', func);
239
+
153
240
  // Callback for upload progress events:
154
241
  // progress: function (e, data) {}, // .bind('fileuploadprogress', func);
242
+
155
243
  // Callback for global upload progress events:
156
244
  // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);
245
+
157
246
  // Callback for uploads start, equivalent to the global ajaxStart event:
158
247
  // start: function (e) {}, // .bind('fileuploadstart', func);
248
+
159
249
  // Callback for uploads stop, equivalent to the global ajaxStop event:
160
250
  // stop: function (e) {}, // .bind('fileuploadstop', func);
251
+
161
252
  // Callback for change events of the fileInput(s):
162
253
  // change: function (e, data) {}, // .bind('fileuploadchange', func);
254
+
163
255
  // Callback for paste events to the pasteZone(s):
164
256
  // paste: function (e, data) {}, // .bind('fileuploadpaste', func);
257
+
165
258
  // Callback for drop events of the dropZone(s):
166
259
  // drop: function (e, data) {}, // .bind('fileuploaddrop', func);
260
+
167
261
  // Callback for dragover events of the dropZone(s):
168
262
  // dragover: function (e) {}, // .bind('fileuploaddragover', func);
169
263
 
264
+ // Callback for the start of each chunk upload request:
265
+ // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);
266
+
267
+ // Callback for successful chunk uploads:
268
+ // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);
269
+
270
+ // Callback for failed (abort or error) chunk uploads:
271
+ // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);
272
+
273
+ // Callback for completed (success, abort or error) chunk upload requests:
274
+ // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);
275
+
170
276
  // The plugin options are used as settings object for the ajax calls.
171
277
  // The following are jQuery ajax settings required for the file uploads:
172
278
  processData: false,
173
279
  contentType: false,
174
- cache: false
280
+ cache: false,
281
+ timeout: 0
175
282
  },
176
283
 
177
- // A list of options that require a refresh after assigning a new value:
178
- _refreshOptionsList: [
284
+ // A list of options that require reinitializing event listeners and/or
285
+ // special initialization code:
286
+ _specialOptions: [
179
287
  'fileInput',
180
288
  'dropZone',
181
289
  'pasteZone',
@@ -183,8 +291,13 @@
183
291
  'forceIframeTransport'
184
292
  ],
185
293
 
294
+ _blobSlice: $.support.blobSlice && function () {
295
+ var slice = this.slice || this.webkitSlice || this.mozSlice;
296
+ return slice.apply(this, arguments);
297
+ },
298
+
186
299
  _BitrateTimer: function () {
187
- this.timestamp = +(new Date());
300
+ this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
188
301
  this.loaded = 0;
189
302
  this.bitrate = 0;
190
303
  this.getBitrate = function (now, loaded, interval) {
@@ -206,13 +319,13 @@
206
319
 
207
320
  _getFormData: function (options) {
208
321
  var formData;
209
- if (typeof options.formData === 'function') {
322
+ if ($.type(options.formData) === 'function') {
210
323
  return options.formData(options.form);
211
324
  }
212
325
  if ($.isArray(options.formData)) {
213
326
  return options.formData;
214
327
  }
215
- if (options.formData) {
328
+ if ($.type(options.formData) === 'object') {
216
329
  formData = [];
217
330
  $.each(options.formData, function (name, value) {
218
331
  formData.push({name: name, value: value});
@@ -230,10 +343,35 @@
230
343
  return total;
231
344
  },
232
345
 
346
+ _initProgressObject: function (obj) {
347
+ var progress = {
348
+ loaded: 0,
349
+ total: 0,
350
+ bitrate: 0
351
+ };
352
+ if (obj._progress) {
353
+ $.extend(obj._progress, progress);
354
+ } else {
355
+ obj._progress = progress;
356
+ }
357
+ },
358
+
359
+ _initResponseObject: function (obj) {
360
+ var prop;
361
+ if (obj._response) {
362
+ for (prop in obj._response) {
363
+ if (obj._response.hasOwnProperty(prop)) {
364
+ delete obj._response[prop];
365
+ }
366
+ }
367
+ } else {
368
+ obj._response = {};
369
+ }
370
+ },
371
+
233
372
  _onProgress: function (e, data) {
234
373
  if (e.lengthComputable) {
235
- var now = +(new Date()),
236
- total,
374
+ var now = ((Date.now) ? Date.now() : (new Date()).getTime()),
237
375
  loaded;
238
376
  if (data._time && data.progressInterval &&
239
377
  (now - data._time < data.progressInterval) &&
@@ -241,16 +379,19 @@
241
379
  return;
242
380
  }
243
381
  data._time = now;
244
- total = data.total || this._getTotal(data.files);
245
- loaded = parseInt(
246
- e.loaded / e.total * (data.chunkSize || total),
247
- 10
382
+ loaded = Math.floor(
383
+ e.loaded / e.total * (data.chunkSize || data._progress.total)
248
384
  ) + (data.uploadedBytes || 0);
249
- this._loaded += loaded - (data.loaded || data.uploadedBytes || 0);
250
- data.lengthComputable = true;
251
- data.loaded = loaded;
252
- data.total = total;
253
- data.bitrate = data._bitrateTimer.getBitrate(
385
+ // Add the difference from the previously loaded state
386
+ // to the global loaded counter:
387
+ this._progress.loaded += (loaded - data._progress.loaded);
388
+ this._progress.bitrate = this._bitrateTimer.getBitrate(
389
+ now,
390
+ this._progress.loaded,
391
+ data.bitrateInterval
392
+ );
393
+ data._progress.loaded = data.loaded = loaded;
394
+ data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
254
395
  now,
255
396
  loaded,
256
397
  data.bitrateInterval
@@ -258,19 +399,18 @@
258
399
  // Trigger a custom progress event with a total data property set
259
400
  // to the file size(s) of the current upload and a loaded data
260
401
  // property calculated accordingly:
261
- this._trigger('progress', e, data);
402
+ this._trigger(
403
+ 'progress',
404
+ $.Event('progress', {delegatedEvent: e}),
405
+ data
406
+ );
262
407
  // Trigger a global progress event for all current file uploads,
263
408
  // including ajax calls queued for sequential file uploads:
264
- this._trigger('progressall', e, {
265
- lengthComputable: true,
266
- loaded: this._loaded,
267
- total: this._total,
268
- bitrate: this._bitrateTimer.getBitrate(
269
- now,
270
- this._loaded,
271
- data.bitrateInterval
272
- )
273
- });
409
+ this._trigger(
410
+ 'progressall',
411
+ $.Event('progressall', {delegatedEvent: e}),
412
+ this._progress
413
+ );
274
414
  }
275
415
  },
276
416
 
@@ -294,20 +434,29 @@
294
434
  }
295
435
  },
296
436
 
437
+ _isInstanceOf: function (type, obj) {
438
+ // Cross-frame instanceof check
439
+ return Object.prototype.toString.call(obj) === '[object ' + type + ']';
440
+ },
441
+
297
442
  _initXHRData: function (options) {
298
- var formData,
443
+ var that = this,
444
+ formData,
299
445
  file = options.files[0],
300
446
  // Ignore non-multipart setting if not supported:
301
447
  multipart = options.multipart || !$.support.xhrFileUpload,
302
- paramName = options.paramName[0];
303
- options.headers = options.headers || {};
448
+ paramName = $.type(options.paramName) === 'array' ?
449
+ options.paramName[0] : options.paramName;
450
+ options.headers = $.extend({}, options.headers);
304
451
  if (options.contentRange) {
305
452
  options.headers['Content-Range'] = options.contentRange;
306
453
  }
307
- if (!multipart) {
454
+ if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
308
455
  options.headers['Content-Disposition'] = 'attachment; filename="' +
309
456
  encodeURI(file.name) + '"';
310
- options.contentType = file.type;
457
+ }
458
+ if (!multipart) {
459
+ options.contentType = file.type || 'application/octet-stream';
311
460
  options.data = options.blob || file;
312
461
  } else if ($.support.xhrFormDataFileUpload) {
313
462
  if (options.postMessage) {
@@ -324,13 +473,14 @@
324
473
  } else {
325
474
  $.each(options.files, function (index, file) {
326
475
  formData.push({
327
- name: options.paramName[index] || paramName,
476
+ name: ($.type(options.paramName) === 'array' &&
477
+ options.paramName[index]) || paramName,
328
478
  value: file
329
479
  });
330
480
  });
331
481
  }
332
482
  } else {
333
- if (options.formData instanceof FormData) {
483
+ if (that._isInstanceOf('FormData', options.formData)) {
334
484
  formData = options.formData;
335
485
  } else {
336
486
  formData = new FormData();
@@ -339,21 +489,18 @@
339
489
  });
340
490
  }
341
491
  if (options.blob) {
342
- options.headers['Content-Disposition'] = 'attachment; filename="' +
343
- encodeURI(file.name) + '"';
344
492
  formData.append(paramName, options.blob, file.name);
345
493
  } else {
346
494
  $.each(options.files, function (index, file) {
347
- // Files are also Blob instances, but some browsers
348
- // (Firefox 3.6) support the File API but not Blobs.
349
495
  // This check allows the tests to run with
350
496
  // dummy objects:
351
- if ((window.Blob && file instanceof Blob) ||
352
- (window.File && file instanceof File)) {
497
+ if (that._isInstanceOf('File', file) ||
498
+ that._isInstanceOf('Blob', file)) {
353
499
  formData.append(
354
- options.paramName[index] || paramName,
500
+ ($.type(options.paramName) === 'array' &&
501
+ options.paramName[index]) || paramName,
355
502
  file,
356
- file.name
503
+ file.uploadName || file.name
357
504
  );
358
505
  }
359
506
  });
@@ -366,13 +513,13 @@
366
513
  },
367
514
 
368
515
  _initIframeSettings: function (options) {
516
+ var targetHost = $('<a></a>').prop('href', options.url).prop('host');
369
517
  // Setting the dataType to iframe enables the iframe transport:
370
518
  options.dataType = 'iframe ' + (options.dataType || '');
371
519
  // The iframe transport accepts a serialized array as form data:
372
520
  options.formData = this._getFormData(options);
373
521
  // Add redirect url to form data on cross-domain uploads:
374
- if (options.redirect && $('<a></a>').prop('href', options.url)
375
- .prop('host') !== location.host) {
522
+ if (options.redirect && targetHost && targetHost !== location.host) {
376
523
  options.formData.push({
377
524
  name: options.redirectParamName || 'redirect',
378
525
  value: options.redirect
@@ -394,7 +541,7 @@
394
541
  options.dataType = 'postmessage ' + (options.dataType || '');
395
542
  }
396
543
  } else {
397
- this._initIframeSettings(options, 'iframe');
544
+ this._initIframeSettings(options);
398
545
  }
399
546
  },
400
547
 
@@ -437,8 +584,10 @@
437
584
  options.url = options.form.prop('action') || location.href;
438
585
  }
439
586
  // The HTTP request method must be "POST" or "PUT":
440
- options.type = (options.type || options.form.prop('method') || '')
441
- .toUpperCase();
587
+ options.type = (options.type ||
588
+ ($.type(options.form.prop('method')) === 'string' &&
589
+ options.form.prop('method')) || ''
590
+ ).toUpperCase();
442
591
  if (options.type !== 'POST' && options.type !== 'PUT' &&
443
592
  options.type !== 'PATCH') {
444
593
  options.type = 'POST';
@@ -455,6 +604,21 @@
455
604
  return options;
456
605
  },
457
606
 
607
+ // jQuery 1.6 doesn't provide .state(),
608
+ // while jQuery 1.8+ removed .isRejected() and .isResolved():
609
+ _getDeferredState: function (deferred) {
610
+ if (deferred.state) {
611
+ return deferred.state();
612
+ }
613
+ if (deferred.isResolved()) {
614
+ return 'resolved';
615
+ }
616
+ if (deferred.isRejected()) {
617
+ return 'rejected';
618
+ }
619
+ return 'pending';
620
+ },
621
+
458
622
  // Maps jqXHR callbacks to the equivalent
459
623
  // methods of the given Promise object:
460
624
  _enhancePromise: function (promise) {
@@ -479,6 +643,66 @@
479
643
  return this._enhancePromise(promise);
480
644
  },
481
645
 
646
+ // Adds convenience methods to the data callback argument:
647
+ _addConvenienceMethods: function (e, data) {
648
+ var that = this,
649
+ getPromise = function (args) {
650
+ return $.Deferred().resolveWith(that, args).promise();
651
+ };
652
+ data.process = function (resolveFunc, rejectFunc) {
653
+ if (resolveFunc || rejectFunc) {
654
+ data._processQueue = this._processQueue =
655
+ (this._processQueue || getPromise([this])).pipe(
656
+ function () {
657
+ if (data.errorThrown) {
658
+ return $.Deferred()
659
+ .rejectWith(that, [data]).promise();
660
+ }
661
+ return getPromise(arguments);
662
+ }
663
+ ).pipe(resolveFunc, rejectFunc);
664
+ }
665
+ return this._processQueue || getPromise([this]);
666
+ };
667
+ data.submit = function () {
668
+ if (this.state() !== 'pending') {
669
+ data.jqXHR = this.jqXHR =
670
+ (that._trigger(
671
+ 'submit',
672
+ $.Event('submit', {delegatedEvent: e}),
673
+ this
674
+ ) !== false) && that._onSend(e, this);
675
+ }
676
+ return this.jqXHR || that._getXHRPromise();
677
+ };
678
+ data.abort = function () {
679
+ if (this.jqXHR) {
680
+ return this.jqXHR.abort();
681
+ }
682
+ this.errorThrown = 'abort';
683
+ that._trigger('fail', null, this);
684
+ return that._getXHRPromise(false);
685
+ };
686
+ data.state = function () {
687
+ if (this.jqXHR) {
688
+ return that._getDeferredState(this.jqXHR);
689
+ }
690
+ if (this._processQueue) {
691
+ return that._getDeferredState(this._processQueue);
692
+ }
693
+ };
694
+ data.processing = function () {
695
+ return !this.jqXHR && this._processQueue && that
696
+ ._getDeferredState(this._processQueue) === 'pending';
697
+ };
698
+ data.progress = function () {
699
+ return this._progress;
700
+ };
701
+ data.response = function () {
702
+ return this._response;
703
+ };
704
+ },
705
+
482
706
  // Parses the Range header from the server response
483
707
  // and returns the uploaded bytes:
484
708
  _getUploadedBytes: function (jqXHR) {
@@ -495,12 +719,13 @@
495
719
  // should be uploaded in chunks, but does not invoke any
496
720
  // upload requests:
497
721
  _chunkedUpload: function (options, testOnly) {
722
+ options.uploadedBytes = options.uploadedBytes || 0;
498
723
  var that = this,
499
724
  file = options.files[0],
500
725
  fs = file.size,
501
- ub = options.uploadedBytes = options.uploadedBytes || 0,
726
+ ub = options.uploadedBytes,
502
727
  mcs = options.maxChunkSize || fs,
503
- slice = file.slice || file.webkitSlice || file.mozSlice,
728
+ slice = this._blobSlice,
504
729
  dfd = $.Deferred(),
505
730
  promise = dfd.promise(),
506
731
  jqXHR,
@@ -513,7 +738,7 @@
513
738
  return true;
514
739
  }
515
740
  if (ub >= fs) {
516
- file.error = 'Uploaded bytes exceed file size';
741
+ file.error = options.i18n('uploadedBytes');
517
742
  return this._getXHRPromise(
518
743
  false,
519
744
  options.context,
@@ -521,9 +746,10 @@
521
746
  );
522
747
  }
523
748
  // The chunk upload method:
524
- upload = function (i) {
749
+ upload = function () {
525
750
  // Clone the options object for each chunk upload:
526
- var o = $.extend({}, options);
751
+ var o = $.extend({}, options),
752
+ currentLoaded = o._progress.loaded;
527
753
  o.blob = slice.call(
528
754
  file,
529
755
  ub,
@@ -540,13 +766,15 @@
540
766
  that._initXHRData(o);
541
767
  // Add progress listeners for this chunk upload:
542
768
  that._initProgressListener(o);
543
- jqXHR = ($.ajax(o) || that._getXHRPromise(false, o.context))
769
+ jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o)) ||
770
+ that._getXHRPromise(false, o.context))
544
771
  .done(function (result, textStatus, jqXHR) {
545
772
  ub = that._getUploadedBytes(jqXHR) ||
546
773
  (ub + o.chunkSize);
547
- // Create a progress event if upload is done and
548
- // no progress event has been invoked for this chunk:
549
- if (!o.loaded) {
774
+ // Create a progress event if no final progress event
775
+ // with loaded equaling total has been triggered
776
+ // for this chunk:
777
+ if (currentLoaded + o.chunkSize - o._progress.loaded) {
550
778
  that._onProgress($.Event('progress', {
551
779
  lengthComputable: true,
552
780
  loaded: ub - o.uploadedBytes,
@@ -554,6 +782,11 @@
554
782
  }), o);
555
783
  }
556
784
  options.uploadedBytes = o.uploadedBytes = ub;
785
+ o.result = result;
786
+ o.textStatus = textStatus;
787
+ o.jqXHR = jqXHR;
788
+ that._trigger('chunkdone', null, o);
789
+ that._trigger('chunkalways', null, o);
557
790
  if (ub < fs) {
558
791
  // File upload not yet complete,
559
792
  // continue with the next chunk:
@@ -566,6 +799,11 @@
566
799
  }
567
800
  })
568
801
  .fail(function (jqXHR, textStatus, errorThrown) {
802
+ o.jqXHR = jqXHR;
803
+ o.textStatus = textStatus;
804
+ o.errorThrown = errorThrown;
805
+ that._trigger('chunkfail', null, o);
806
+ that._trigger('chunkalways', null, o);
569
807
  dfd.rejectWith(
570
808
  o.context,
571
809
  [jqXHR, textStatus, errorThrown]
@@ -588,63 +826,66 @@
588
826
  this._trigger('start');
589
827
  // Set timer for global bitrate progress calculation:
590
828
  this._bitrateTimer = new this._BitrateTimer();
829
+ // Reset the global progress values:
830
+ this._progress.loaded = this._progress.total = 0;
831
+ this._progress.bitrate = 0;
591
832
  }
833
+ // Make sure the container objects for the .response() and
834
+ // .progress() methods on the data object are available
835
+ // and reset to their initial state:
836
+ this._initResponseObject(data);
837
+ this._initProgressObject(data);
838
+ data._progress.loaded = data.loaded = data.uploadedBytes || 0;
839
+ data._progress.total = data.total = this._getTotal(data.files) || 1;
840
+ data._progress.bitrate = data.bitrate = 0;
592
841
  this._active += 1;
593
842
  // Initialize the global progress values:
594
- this._loaded += data.uploadedBytes || 0;
595
- this._total += this._getTotal(data.files);
843
+ this._progress.loaded += data.loaded;
844
+ this._progress.total += data.total;
596
845
  },
597
846
 
598
847
  _onDone: function (result, textStatus, jqXHR, options) {
599
- if (!this._isXHRUpload(options)) {
600
- // Create a progress event for each iframe load:
848
+ var total = options._progress.total,
849
+ response = options._response;
850
+ if (options._progress.loaded < total) {
851
+ // Create a progress event if no final progress event
852
+ // with loaded equaling total has been triggered:
601
853
  this._onProgress($.Event('progress', {
602
854
  lengthComputable: true,
603
- loaded: 1,
604
- total: 1
855
+ loaded: total,
856
+ total: total
605
857
  }), options);
606
858
  }
607
- options.result = result;
608
- options.textStatus = textStatus;
609
- options.jqXHR = jqXHR;
859
+ response.result = options.result = result;
860
+ response.textStatus = options.textStatus = textStatus;
861
+ response.jqXHR = options.jqXHR = jqXHR;
610
862
  this._trigger('done', null, options);
611
863
  },
612
864
 
613
865
  _onFail: function (jqXHR, textStatus, errorThrown, options) {
614
- options.jqXHR = jqXHR;
615
- options.textStatus = textStatus;
616
- options.errorThrown = errorThrown;
617
- this._trigger('fail', null, options);
866
+ var response = options._response;
618
867
  if (options.recalculateProgress) {
619
868
  // Remove the failed (error or abort) file upload from
620
869
  // the global progress calculation:
621
- this._loaded -= options.loaded || options.uploadedBytes || 0;
622
- this._total -= options.total || this._getTotal(options.files);
870
+ this._progress.loaded -= options._progress.loaded;
871
+ this._progress.total -= options._progress.total;
623
872
  }
873
+ response.jqXHR = options.jqXHR = jqXHR;
874
+ response.textStatus = options.textStatus = textStatus;
875
+ response.errorThrown = options.errorThrown = errorThrown;
876
+ this._trigger('fail', null, options);
624
877
  },
625
878
 
626
879
  _onAlways: function (jqXHRorResult, textStatus, jqXHRorError, options) {
627
- this._active -= 1;
628
- options.textStatus = textStatus;
629
- if (jqXHRorError && jqXHRorError.always) {
630
- options.jqXHR = jqXHRorError;
631
- options.result = jqXHRorResult;
632
- } else {
633
- options.jqXHR = jqXHRorResult;
634
- options.errorThrown = jqXHRorError;
635
- }
880
+ // jqXHRorResult, textStatus and jqXHRorError are added to the
881
+ // options object via done and fail callbacks
636
882
  this._trigger('always', null, options);
637
- if (this._active === 0) {
638
- // The stop callback is triggered when all uploads have
639
- // been completed, equivalent to the global ajaxStop event:
640
- this._trigger('stop');
641
- // Reset the global progress values:
642
- this._loaded = this._total = 0;
643
- this._bitrateTimer = null;
644
- }
645
883
  },
646
884
 
647
885
  _onSend: function (e, data) {
886
+ if (!data.submit) {
887
+ this._addConvenienceMethods(e, data);
888
+ }
648
889
  var that = this,
649
890
  jqXHR,
650
891
  aborted,
@@ -656,7 +897,11 @@
656
897
  // Set timer for bitrate progress calculation:
657
898
  options._bitrateTimer = new that._BitrateTimer();
658
899
  jqXHR = jqXHR || (
659
- ((aborted || that._trigger('send', e, options) === false) &&
900
+ ((aborted || that._trigger(
901
+ 'send',
902
+ $.Event('send', {delegatedEvent: e}),
903
+ options
904
+ ) === false) &&
660
905
  that._getXHRPromise(false, options.context, aborted)) ||
661
906
  that._chunkedUpload(options) || $.ajax(options)
662
907
  ).done(function (result, textStatus, jqXHR) {
@@ -664,32 +909,32 @@
664
909
  }).fail(function (jqXHR, textStatus, errorThrown) {
665
910
  that._onFail(jqXHR, textStatus, errorThrown, options);
666
911
  }).always(function (jqXHRorResult, textStatus, jqXHRorError) {
667
- that._sending -= 1;
668
912
  that._onAlways(
669
913
  jqXHRorResult,
670
914
  textStatus,
671
915
  jqXHRorError,
672
916
  options
673
917
  );
918
+ that._sending -= 1;
919
+ that._active -= 1;
674
920
  if (options.limitConcurrentUploads &&
675
921
  options.limitConcurrentUploads > that._sending) {
676
922
  // Start the next queued upload,
677
923
  // that has not been aborted:
678
- var nextSlot = that._slots.shift(),
679
- isPending;
924
+ var nextSlot = that._slots.shift();
680
925
  while (nextSlot) {
681
- // jQuery 1.6 doesn't provide .state(),
682
- // while jQuery 1.8+ removed .isRejected():
683
- isPending = nextSlot.state ?
684
- nextSlot.state() === 'pending' :
685
- !nextSlot.isRejected();
686
- if (isPending) {
926
+ if (that._getDeferredState(nextSlot) === 'pending') {
687
927
  nextSlot.resolve();
688
928
  break;
689
929
  }
690
930
  nextSlot = that._slots.shift();
691
931
  }
692
932
  }
933
+ if (that._active === 0) {
934
+ // The stop callback is triggered when all uploads have
935
+ // been completed, equivalent to the global ajaxStop event:
936
+ that._trigger('stop');
937
+ }
693
938
  });
694
939
  return jqXHR;
695
940
  };
@@ -702,7 +947,8 @@
702
947
  this._slots.push(slot);
703
948
  pipe = slot.pipe(send);
704
949
  } else {
705
- pipe = (this._sequence = this._sequence.pipe(send, send));
950
+ this._sequence = this._sequence.pipe(send, send);
951
+ pipe = this._sequence;
706
952
  }
707
953
  // Return the piped Promise object, enhanced with an abort method,
708
954
  // which is delegated to the jqXHR object of the current upload,
@@ -726,49 +972,83 @@
726
972
  var that = this,
727
973
  result = true,
728
974
  options = $.extend({}, this.options, data),
975
+ files = data.files,
976
+ filesLength = files.length,
729
977
  limit = options.limitMultiFileUploads,
978
+ limitSize = options.limitMultiFileUploadSize,
979
+ overhead = options.limitMultiFileUploadSizeOverhead,
980
+ batchSize = 0,
730
981
  paramName = this._getParamName(options),
731
982
  paramNameSet,
732
983
  paramNameSlice,
733
984
  fileSet,
734
- i;
735
- if (!(options.singleFileUploads || limit) ||
985
+ i,
986
+ j = 0;
987
+ if (!filesLength) {
988
+ return false;
989
+ }
990
+ if (limitSize && files[0].size === undefined) {
991
+ limitSize = undefined;
992
+ }
993
+ if (!(options.singleFileUploads || limit || limitSize) ||
736
994
  !this._isXHRUpload(options)) {
737
- fileSet = [data.files];
995
+ fileSet = [files];
738
996
  paramNameSet = [paramName];
739
- } else if (!options.singleFileUploads && limit) {
997
+ } else if (!(options.singleFileUploads || limitSize) && limit) {
740
998
  fileSet = [];
741
999
  paramNameSet = [];
742
- for (i = 0; i < data.files.length; i += limit) {
743
- fileSet.push(data.files.slice(i, i + limit));
1000
+ for (i = 0; i < filesLength; i += limit) {
1001
+ fileSet.push(files.slice(i, i + limit));
744
1002
  paramNameSlice = paramName.slice(i, i + limit);
745
1003
  if (!paramNameSlice.length) {
746
1004
  paramNameSlice = paramName;
747
1005
  }
748
1006
  paramNameSet.push(paramNameSlice);
749
1007
  }
1008
+ } else if (!options.singleFileUploads && limitSize) {
1009
+ fileSet = [];
1010
+ paramNameSet = [];
1011
+ for (i = 0; i < filesLength; i = i + 1) {
1012
+ batchSize += files[i].size + overhead;
1013
+ if (i + 1 === filesLength ||
1014
+ ((batchSize + files[i + 1].size + overhead) > limitSize) ||
1015
+ (limit && i + 1 - j >= limit)) {
1016
+ fileSet.push(files.slice(j, i + 1));
1017
+ paramNameSlice = paramName.slice(j, i + 1);
1018
+ if (!paramNameSlice.length) {
1019
+ paramNameSlice = paramName;
1020
+ }
1021
+ paramNameSet.push(paramNameSlice);
1022
+ j = i + 1;
1023
+ batchSize = 0;
1024
+ }
1025
+ }
750
1026
  } else {
751
1027
  paramNameSet = paramName;
752
1028
  }
753
- data.originalFiles = data.files;
754
- $.each(fileSet || data.files, function (index, element) {
1029
+ data.originalFiles = files;
1030
+ $.each(fileSet || files, function (index, element) {
755
1031
  var newData = $.extend({}, data);
756
1032
  newData.files = fileSet ? element : [element];
757
1033
  newData.paramName = paramNameSet[index];
758
- newData.submit = function () {
759
- newData.jqXHR = this.jqXHR =
760
- (that._trigger('submit', e, this) !== false) &&
761
- that._onSend(e, this);
762
- return this.jqXHR;
763
- };
764
- result = that._trigger('add', e, newData);
1034
+ that._initResponseObject(newData);
1035
+ that._initProgressObject(newData);
1036
+ that._addConvenienceMethods(e, newData);
1037
+ result = that._trigger(
1038
+ 'add',
1039
+ $.Event('add', {delegatedEvent: e}),
1040
+ newData
1041
+ );
765
1042
  return result;
766
1043
  });
767
1044
  return result;
768
1045
  },
769
1046
 
770
- _replaceFileInput: function (input) {
771
- var inputClone = input.clone(true);
1047
+ _replaceFileInput: function (data) {
1048
+ var input = data.fileInput,
1049
+ inputClone = input.clone(true);
1050
+ // Add a reference for the new cloned file input to the data argument:
1051
+ data.fileInputClone = inputClone;
772
1052
  $('<form></form>').append(inputClone)[0].reset();
773
1053
  // Detaching allows to insert the fileInput on another form
774
1054
  // without loosing the file input value:
@@ -804,7 +1084,25 @@
804
1084
  // to be returned together in one set:
805
1085
  dfd.resolve([e]);
806
1086
  },
807
- dirReader;
1087
+ successHandler = function (entries) {
1088
+ that._handleFileTreeEntries(
1089
+ entries,
1090
+ path + entry.name + '/'
1091
+ ).done(function (files) {
1092
+ dfd.resolve(files);
1093
+ }).fail(errorHandler);
1094
+ },
1095
+ readEntries = function () {
1096
+ dirReader.readEntries(function (results) {
1097
+ if (!results.length) {
1098
+ successHandler(entries);
1099
+ } else {
1100
+ entries = entries.concat(results);
1101
+ readEntries();
1102
+ }
1103
+ }, errorHandler);
1104
+ },
1105
+ dirReader, entries = [];
808
1106
  path = path || '';
809
1107
  if (entry.isFile) {
810
1108
  if (entry._file) {
@@ -819,14 +1117,7 @@
819
1117
  }
820
1118
  } else if (entry.isDirectory) {
821
1119
  dirReader = entry.createReader();
822
- dirReader.readEntries(function (entries) {
823
- that._handleFileTreeEntries(
824
- entries,
825
- path + entry.name + '/'
826
- ).done(function (files) {
827
- dfd.resolve(files);
828
- }).fail(errorHandler);
829
- }, errorHandler);
1120
+ readEntries();
830
1121
  } else {
831
1122
  // Return an empy list for file system items
832
1123
  // other than files or directories:
@@ -928,84 +1219,99 @@
928
1219
  this._getFileInputFiles(data.fileInput).always(function (files) {
929
1220
  data.files = files;
930
1221
  if (that.options.replaceFileInput) {
931
- that._replaceFileInput(data.fileInput);
1222
+ that._replaceFileInput(data);
932
1223
  }
933
- if (that._trigger('change', e, data) !== false) {
1224
+ if (that._trigger(
1225
+ 'change',
1226
+ $.Event('change', {delegatedEvent: e}),
1227
+ data
1228
+ ) !== false) {
934
1229
  that._onAdd(e, data);
935
1230
  }
936
1231
  });
937
1232
  },
938
1233
 
939
1234
  _onPaste: function (e) {
940
- var cbd = e.originalEvent.clipboardData,
941
- items = (cbd && cbd.items) || [],
1235
+ var items = e.originalEvent && e.originalEvent.clipboardData &&
1236
+ e.originalEvent.clipboardData.items,
942
1237
  data = {files: []};
943
- $.each(items, function (index, item) {
944
- var file = item.getAsFile && item.getAsFile();
945
- if (file) {
946
- data.files.push(file);
1238
+ if (items && items.length) {
1239
+ $.each(items, function (index, item) {
1240
+ var file = item.getAsFile && item.getAsFile();
1241
+ if (file) {
1242
+ data.files.push(file);
1243
+ }
1244
+ });
1245
+ if (this._trigger(
1246
+ 'paste',
1247
+ $.Event('paste', {delegatedEvent: e}),
1248
+ data
1249
+ ) !== false) {
1250
+ this._onAdd(e, data);
947
1251
  }
948
- });
949
- if (this._trigger('paste', e, data) === false ||
950
- this._onAdd(e, data) === false) {
951
- return false;
952
1252
  }
953
1253
  },
954
1254
 
955
1255
  _onDrop: function (e) {
1256
+ e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
956
1257
  var that = this,
957
- dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer,
1258
+ dataTransfer = e.dataTransfer,
958
1259
  data = {};
959
1260
  if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
960
1261
  e.preventDefault();
1262
+ this._getDroppedFiles(dataTransfer).always(function (files) {
1263
+ data.files = files;
1264
+ if (that._trigger(
1265
+ 'drop',
1266
+ $.Event('drop', {delegatedEvent: e}),
1267
+ data
1268
+ ) !== false) {
1269
+ that._onAdd(e, data);
1270
+ }
1271
+ });
961
1272
  }
962
- this._getDroppedFiles(dataTransfer).always(function (files) {
963
- data.files = files;
964
- if (that._trigger('drop', e, data) !== false) {
965
- that._onAdd(e, data);
966
- }
967
- });
968
1273
  },
969
1274
 
970
- _onDragOver: function (e) {
971
- var dataTransfer = e.dataTransfer = e.originalEvent.dataTransfer;
972
- if (this._trigger('dragover', e) === false) {
973
- return false;
974
- }
975
- if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1) {
976
- dataTransfer.dropEffect = 'copy';
977
- e.preventDefault();
978
- }
979
- },
1275
+ _onDragOver: getDragHandler('dragover'),
1276
+
1277
+ _onDragEnter: getDragHandler('dragenter'),
1278
+
1279
+ _onDragLeave: getDragHandler('dragleave'),
980
1280
 
981
1281
  _initEventHandlers: function () {
982
1282
  if (this._isXHRUpload(this.options)) {
983
1283
  this._on(this.options.dropZone, {
984
1284
  dragover: this._onDragOver,
985
- drop: this._onDrop
1285
+ drop: this._onDrop,
1286
+ // event.preventDefault() on dragenter is required for IE10+:
1287
+ dragenter: this._onDragEnter,
1288
+ // dragleave is not required, but added for completeness:
1289
+ dragleave: this._onDragLeave
986
1290
  });
987
1291
  this._on(this.options.pasteZone, {
988
1292
  paste: this._onPaste
989
1293
  });
990
1294
  }
991
- this._on(this.options.fileInput, {
992
- change: this._onChange
993
- });
1295
+ if ($.support.fileInput) {
1296
+ this._on(this.options.fileInput, {
1297
+ change: this._onChange
1298
+ });
1299
+ }
994
1300
  },
995
1301
 
996
1302
  _destroyEventHandlers: function () {
997
- this._off(this.options.dropZone, 'dragover drop');
1303
+ this._off(this.options.dropZone, 'dragenter dragleave dragover drop');
998
1304
  this._off(this.options.pasteZone, 'paste');
999
1305
  this._off(this.options.fileInput, 'change');
1000
1306
  },
1001
1307
 
1002
1308
  _setOption: function (key, value) {
1003
- var refresh = $.inArray(key, this._refreshOptionsList) !== -1;
1004
- if (refresh) {
1309
+ var reinit = $.inArray(key, this._specialOptions) !== -1;
1310
+ if (reinit) {
1005
1311
  this._destroyEventHandlers();
1006
1312
  }
1007
1313
  this._super(key, value);
1008
- if (refresh) {
1314
+ if (reinit) {
1009
1315
  this._initSpecialOptions();
1010
1316
  this._initEventHandlers();
1011
1317
  }
@@ -1027,19 +1333,65 @@
1027
1333
  }
1028
1334
  },
1029
1335
 
1030
- _create: function () {
1031
- var options = this.options;
1336
+ _getRegExp: function (str) {
1337
+ var parts = str.split('/'),
1338
+ modifiers = parts.pop();
1339
+ parts.shift();
1340
+ return new RegExp(parts.join('/'), modifiers);
1341
+ },
1342
+
1343
+ _isRegExpOption: function (key, value) {
1344
+ return key !== 'url' && $.type(value) === 'string' &&
1345
+ /^\/.*\/[igm]{0,3}$/.test(value);
1346
+ },
1347
+
1348
+ _initDataAttributes: function () {
1349
+ var that = this,
1350
+ options = this.options,
1351
+ data = this.element.data();
1032
1352
  // Initialize options set via HTML5 data-attributes:
1033
- $.extend(options, $(this.element[0].cloneNode(false)).data());
1353
+ $.each(
1354
+ this.element[0].attributes,
1355
+ function (index, attr) {
1356
+ var key = attr.name.toLowerCase(),
1357
+ value;
1358
+ if (/^data-/.test(key)) {
1359
+ // Convert hyphen-ated key to camelCase:
1360
+ key = key.slice(5).replace(/-[a-z]/g, function (str) {
1361
+ return str.charAt(1).toUpperCase();
1362
+ });
1363
+ value = data[key];
1364
+ if (that._isRegExpOption(key, value)) {
1365
+ value = that._getRegExp(value);
1366
+ }
1367
+ options[key] = value;
1368
+ }
1369
+ }
1370
+ );
1371
+ },
1372
+
1373
+ _create: function () {
1374
+ this._initDataAttributes();
1034
1375
  this._initSpecialOptions();
1035
1376
  this._slots = [];
1036
1377
  this._sequence = this._getXHRPromise(true);
1037
- this._sending = this._active = this._loaded = this._total = 0;
1378
+ this._sending = this._active = 0;
1379
+ this._initProgressObject(this);
1038
1380
  this._initEventHandlers();
1039
1381
  },
1040
1382
 
1041
- _destroy: function () {
1042
- this._destroyEventHandlers();
1383
+ // This method is exposed to the widget API and allows to query
1384
+ // the number of active uploads:
1385
+ active: function () {
1386
+ return this._active;
1387
+ },
1388
+
1389
+ // This method is exposed to the widget API and allows to query
1390
+ // the widget upload progress.
1391
+ // It returns an object with loaded, total and bitrate properties
1392
+ // for the running uploads:
1393
+ progress: function () {
1394
+ return this._progress;
1043
1395
  },
1044
1396
 
1045
1397
  // This method is exposed to the widget API and allows adding files
@@ -1088,8 +1440,13 @@
1088
1440
  if (aborted) {
1089
1441
  return;
1090
1442
  }
1443
+ if (!files.length) {
1444
+ dfd.reject();
1445
+ return;
1446
+ }
1091
1447
  data.files = files;
1092
- jqXHR = that._onSend(null, data).then(
1448
+ jqXHR = that._onSend(null, data);
1449
+ jqXHR.then(
1093
1450
  function (result, textStatus, jqXHR) {
1094
1451
  dfd.resolve(result, textStatus, jqXHR);
1095
1452
  },