upjs-rails 0.17.0 → 0.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +46 -1
  3. data/dist/up.js +929 -374
  4. data/dist/up.min.js +2 -2
  5. data/lib/assets/javascripts/up/browser.js.coffee +31 -14
  6. data/lib/assets/javascripts/up/bus.js.coffee +87 -22
  7. data/lib/assets/javascripts/up/flow.js.coffee +119 -43
  8. data/lib/assets/javascripts/up/form.js.coffee +188 -57
  9. data/lib/assets/javascripts/up/link.js.coffee +57 -21
  10. data/lib/assets/javascripts/up/modal.js.coffee +77 -63
  11. data/lib/assets/javascripts/up/motion.js.coffee +10 -9
  12. data/lib/assets/javascripts/up/popup.js.coffee +54 -40
  13. data/lib/assets/javascripts/up/proxy.js.coffee +46 -17
  14. data/lib/assets/javascripts/up/rails.js.coffee +22 -4
  15. data/lib/assets/javascripts/up/syntax.js.coffee +2 -2
  16. data/lib/assets/javascripts/up/util.js.coffee +100 -16
  17. data/lib/upjs/rails/inspector.rb +3 -3
  18. data/lib/upjs/rails/version.rb +1 -1
  19. data/spec_app/Gemfile.lock +1 -4
  20. data/spec_app/app/controllers/test_controller.rb +2 -2
  21. data/spec_app/spec/controllers/test_controller_spec.rb +5 -5
  22. data/spec_app/spec/javascripts/helpers/browser_switches.js.coffee +9 -0
  23. data/spec_app/spec/javascripts/helpers/knife.js.coffee +0 -1
  24. data/spec_app/spec/javascripts/helpers/reset_path.js.coffee +4 -5
  25. data/spec_app/spec/javascripts/helpers/to_be_present.js.coffee +5 -0
  26. data/spec_app/spec/javascripts/helpers/to_have_request_method.js.coffee +8 -0
  27. data/spec_app/spec/javascripts/up/bus_spec.js.coffee +26 -0
  28. data/spec_app/spec/javascripts/up/flow_spec.js.coffee +203 -91
  29. data/spec_app/spec/javascripts/up/form_spec.js.coffee +244 -49
  30. data/spec_app/spec/javascripts/up/history_spec.js.coffee +8 -2
  31. data/spec_app/spec/javascripts/up/link_spec.js.coffee +83 -30
  32. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +23 -17
  33. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +4 -4
  34. data/spec_app/spec/javascripts/up/navigation_spec.js.coffee +1 -1
  35. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +26 -16
  36. data/spec_app/spec/javascripts/up/proxy_spec.js.coffee +45 -13
  37. data/spec_app/spec/javascripts/up/rails_spec.js.coffee +48 -0
  38. data/spec_app/spec/javascripts/up/util_spec.js.coffee +48 -0
  39. metadata +5 -2
@@ -88,6 +88,16 @@ up.proxy = (($) ->
88
88
 
89
89
  Note that your browser might [impose its own request limit](http://www.browserscope.org/?category=network)
90
90
  regardless of what you configure here.
91
+ @param {Array<String>} [config.wrapMethods]
92
+ An array of uppercase HTTP method names. AJAX requests with one of these methods
93
+ will be converted into a `POST` request and carry their original method as a `_method`
94
+ parameter. This is to [prevent unexpected redirect behavior](https://makandracards.com/makandra/38347).
95
+ @param {String} [config.wrapMethodParam]
96
+ The name of the POST parameter when wrapping HTTP methods in a `POST` request.
97
+ @param {Array<String>} [config.safeMethods]
98
+ An array of uppercase HTTP method names that are considered idempotent.
99
+ The proxy cache will only cache idempotent requests and will clear the entire
100
+ cache after a non-idempotent request.
91
101
  @stable
92
102
  ###
93
103
  config = u.config
@@ -96,13 +106,16 @@ up.proxy = (($) ->
96
106
  cacheSize: 70
97
107
  cacheExpiry: 1000 * 60 * 5
98
108
  maxRequests: 4
109
+ wrapMethods: ['PATCH', 'PUT', 'DELETE']
110
+ wrapMethodParam: '_method'
111
+ safeMethods: ['GET', 'OPTIONS', 'HEAD']
99
112
 
100
113
  cacheKey = (request) ->
101
114
  normalizeRequest(request)
102
115
  [ request.url,
103
116
  request.method,
104
117
  request.data,
105
- request.selector
118
+ request.target
106
119
  ].join('|')
107
120
 
108
121
  cache = u.cache
@@ -125,11 +138,11 @@ up.proxy = (($) ->
125
138
  get = (request) ->
126
139
  request = normalizeRequest(request)
127
140
  candidates = [request]
128
- unless request.selector is 'html'
129
- requestForHtml = u.merge(request, selector: 'html')
141
+ unless request.target is 'html'
142
+ requestForHtml = u.merge(request, target: 'html')
130
143
  candidates.push(requestForHtml)
131
- unless request.selector is 'body'
132
- requestForBody = u.merge(request, selector: 'body')
144
+ unless request.target is 'body'
145
+ requestForBody = u.merge(request, target: 'body')
133
146
  candidates.push(requestForBody)
134
147
  for candidate in candidates
135
148
  if response = cache.get(candidate)
@@ -141,7 +154,7 @@ up.proxy = (($) ->
141
154
  @function up.proxy.set
142
155
  @param {String} request.url
143
156
  @param {String} [request.method='GET']
144
- @param {String} [request.selector='body']
157
+ @param {String} [request.target='body']
145
158
  @param {Promise} response
146
159
  A promise for the response that is API-compatible with the
147
160
  promise returned by [`jQuery.ajax`](http://api.jquery.com/jquery.ajax/).
@@ -158,7 +171,7 @@ up.proxy = (($) ->
158
171
  @function up.proxy.remove
159
172
  @param {String} request.url
160
173
  @param {String} [request.method='GET']
161
- @param {String} [request.selector='body']
174
+ @param {String} [request.target='body']
162
175
  @experimental
163
176
  ###
164
177
  remove = cache.remove
@@ -200,7 +213,7 @@ up.proxy = (($) ->
200
213
  unless request._normalized
201
214
  request.method = u.normalizeMethod(request.method)
202
215
  request.url = u.normalizeUrl(request.url) if request.url
203
- request.selector ||= 'body'
216
+ request.target ||= 'body'
204
217
  request._normalized = true
205
218
  request
206
219
 
@@ -221,13 +234,15 @@ up.proxy = (($) ->
221
234
  @function up.proxy.ajax
222
235
  @param {String} request.url
223
236
  @param {String} [request.method='GET']
224
- @param {String} [request.selector='body']
237
+ @param {String} [request.target='body']
225
238
  @param {Boolean} [request.cache]
226
239
  Whether to use a cached response, if available.
227
240
  If set to `false` a network connection will always be attempted.
228
241
  @param {Object} [request.headers={}]
229
242
  An object of additional header key/value pairs to send along
230
243
  with the request.
244
+ @param {Object} [request.data={}]
245
+ An object of request parameters.
231
246
  @return
232
247
  A promise for the response that is API-compatible with the
233
248
  promise returned by [`jQuery.ajax`](http://api.jquery.com/jquery.ajax/).
@@ -238,7 +253,7 @@ up.proxy = (($) ->
238
253
  forceCache = (options.cache == true)
239
254
  ignoreCache = (options.cache == false)
240
255
 
241
- request = u.only(options, 'url', 'method', 'data', 'selector', 'headers', '_normalized')
256
+ request = u.only(options, 'url', 'method', 'data', 'target', 'headers', '_normalized')
242
257
 
243
258
  pending = true
244
259
 
@@ -276,8 +291,6 @@ up.proxy = (($) ->
276
291
 
277
292
  promise
278
293
 
279
- SAFE_HTTP_METHODS = ['GET', 'OPTIONS', 'HEAD']
280
-
281
294
  ###*
282
295
  Returns `true` if the proxy is not currently waiting
283
296
  for a request to finish. Returns `false` otherwise.
@@ -372,9 +385,25 @@ up.proxy = (($) ->
372
385
  deferred.promise()
373
386
 
374
387
  load = (request) ->
375
- u.debug('Loading URL %o', request.url)
388
+ u.debug('Fetching %o via %o', request.url, request.method)
376
389
  up.emit('up:proxy:load', request)
377
- promise = u.ajax(request)
390
+
391
+ # We will modify the request below for features like method wrapping.
392
+ # Let's not change the original request which would confuse API clients
393
+ # and cache key logic.
394
+ request = u.copy(request)
395
+
396
+ request.headers ||= {}
397
+ request.headers['X-Up-Target'] = request.target
398
+ request.data = u.requestDataAsArray(request.data)
399
+
400
+ if u.contains(config.wrapMethods, request.method)
401
+ request.data.push
402
+ name: config.wrapMethodParam
403
+ value: request.method
404
+ request.method = 'POST'
405
+
406
+ promise = $.ajax(request)
378
407
  promise.always ->
379
408
  up.emit('up:proxy:received', request)
380
409
  pokeQueue()
@@ -393,7 +422,7 @@ up.proxy = (($) ->
393
422
  @event up:proxy:load
394
423
  @param event.url
395
424
  @param event.method
396
- @param event.selector
425
+ @param event.target
397
426
  @experimental
398
427
  ###
399
428
 
@@ -404,13 +433,13 @@ up.proxy = (($) ->
404
433
  @event up:proxy:received
405
434
  @param event.url
406
435
  @param event.method
407
- @param event.selector
436
+ @param event.target
408
437
  @experimental
409
438
  ###
410
439
 
411
440
  isIdempotent = (request) ->
412
441
  normalizeRequest(request)
413
- u.contains(SAFE_HTTP_METHODS, request.method)
442
+ u.contains(config.safeMethods, request.method)
414
443
 
415
444
  checkPreload = ($link) ->
416
445
  delay = parseInt(u.presentAttr($link, 'up-delay')) || config.preloadDelay
@@ -10,9 +10,27 @@ up.rails = (($) ->
10
10
  willHandle = ($element) ->
11
11
  $element.is('[up-follow], [up-target], [up-modal], [up-popup]')
12
12
 
13
- up.compiler '[data-method]', ($element) ->
14
- if $.rails && willHandle($element)
15
- u.setMissingAttrs($element, 'up-method': $element.attr('data-method'))
16
- $element.removeAttr('data-method')
13
+ isRails = ->
14
+ u.isGiven($.rails)
15
+
16
+ u.each ['method', 'confirm'], (feature) ->
17
+
18
+ dataAttribute = "data-#{feature}"
19
+ upAttribute = "up-#{feature}"
20
+
21
+ up.compiler "[#{dataAttribute}]", ($element) ->
22
+ if isRails() && willHandle($element)
23
+ replacement = {}
24
+ replacement[upAttribute] = $element.attr(dataAttribute)
25
+ u.setMissingAttrs($element, replacement)
26
+ $element.removeAttr(dataAttribute)
27
+
28
+ csrfField = ->
29
+ if isRails()
30
+ name: $.rails.csrfParam()
31
+ value: $.rails.csrfToken()
32
+
33
+ csrfField: csrfField
34
+ isRails: isRails
17
35
 
18
36
  )(jQuery)
@@ -332,8 +332,8 @@ up.syntax = (($) ->
332
332
  ###
333
333
 
334
334
  up.on 'ready', (-> hello(document.body))
335
- up.on 'up:fragment:inserted', (event) -> compile(event.$element)
336
- up.on 'up:fragment:destroy', (event) -> runDestroyers(event.$element)
335
+ up.on 'up:fragment:inserted', (event, $element) -> compile($element)
336
+ up.on 'up:fragment:destroy', (event, $element) -> runDestroyers($element)
337
337
  up.on 'up:framework:boot', snapshot
338
338
  up.on 'up:framework:reset', reset
339
339
 
@@ -23,18 +23,6 @@ up.util = (($) ->
23
23
  cached = true
24
24
  cache = func(args...)
25
25
 
26
- ###*
27
- @function up.util.ajax
28
- @internal
29
- ###
30
- ajax = (request) ->
31
- request = copy(request)
32
- if request.selector
33
- request.headers ||= {}
34
- request.headers['X-Up-Selector'] = request.selector
35
- # Delegate to jQuery
36
- $.ajax(request)
37
-
38
26
  ###*
39
27
  Returns if the given port is the default port for the given protocol.
40
28
 
@@ -247,8 +235,8 @@ up.util = (($) ->
247
235
  selector = "##{id}"
248
236
  else if name = presence($element.attr("name"))
249
237
  selector = "[name='#{name}']"
250
- else if classString = presence($element.attr("class"))
251
- classes = classString.split(' ')
238
+ else if classes = presence(nonUpClasses($element))
239
+ console.log("using klass!", classes)
252
240
  selector = ''
253
241
  for klass in classes
254
242
  selector += ".#{klass}"
@@ -256,6 +244,11 @@ up.util = (($) ->
256
244
  selector = $element.prop('tagName').toLowerCase()
257
245
  selector
258
246
 
247
+ nonUpClasses = ($element) ->
248
+ classString = $element.attr('class') || ''
249
+ classes = classString.split(' ')
250
+ select classes, (klass) -> isPresent(klass) && !klass.match(/^up-/)
251
+
259
252
  # jQuery's implementation of $(...) cannot create elements that have
260
253
  # an <html> or <body> tag. So we're using native elements.
261
254
  # Also IE9 cannot set innerHTML on a <html> or <head> element.
@@ -761,6 +754,30 @@ up.util = (($) ->
761
754
  matches.push(element)
762
755
  matches
763
756
 
757
+ ###*
758
+ Returns all elements from the given array that do not return
759
+ a truthy value when passed to the given function.
760
+
761
+ @function up.util.reject
762
+ @param {Array<T>} array
763
+ @return {Array<T>}
764
+ @stable
765
+ ###
766
+ reject = (array, tester) ->
767
+ select(array, (element) -> !tester(element))
768
+
769
+ ###*
770
+ Returns the intersection of the given two arrays.
771
+
772
+ Implementation is not optimized. Don't use it for large arrays.
773
+
774
+ @function up.util.intersect
775
+ @internal
776
+ ###
777
+ intersect = (array1, array2) ->
778
+ select array1, (element) ->
779
+ contains(array2, element)
780
+
764
781
  ###*
765
782
  Returns the first [present](/up.util.isPresent) element attribute
766
783
  among the given list of attribute names.
@@ -1098,6 +1115,21 @@ up.util = (($) ->
1098
1115
  filtered[property] = object[property]
1099
1116
  filtered
1100
1117
 
1118
+ ###*
1119
+ Returns a copy of the given object that contains all except
1120
+ the given properties.
1121
+
1122
+ @function up.util.except
1123
+ @param {Object} object
1124
+ @param {Array} keys...
1125
+ @stable
1126
+ ###
1127
+ except = (object, properties...) ->
1128
+ filtered = copy(object)
1129
+ for property in properties
1130
+ delete filtered[property]
1131
+ filtered
1132
+
1101
1133
  ###*
1102
1134
  @function up.util.isUnmodifiedKeyEvent
1103
1135
  @internal
@@ -1179,7 +1211,20 @@ up.util = (($) ->
1179
1211
  joined.resolve = ->
1180
1212
  each deferreds, (deferred) -> deferred.resolve?()
1181
1213
  joined
1182
-
1214
+
1215
+ # resolvableSequence = (first, callbacks...) ->
1216
+ # sequence = $.Deferred().promise()
1217
+ # values = [first]
1218
+ # current = first
1219
+ # for callback in callbacks
1220
+ # current = current.then ->
1221
+ # value = callback()
1222
+ # values.push(value) if u.isPromise(value)
1223
+ # value
1224
+ # sequence.resolve = ->
1225
+ # each values, (deferred) -> deferred.resolve?()
1226
+ # sequence
1227
+
1183
1228
  ###*
1184
1229
  On the given element, set attributes that are still missing.
1185
1230
 
@@ -1446,6 +1491,43 @@ up.util = (($) ->
1446
1491
  # else
1447
1492
  # error('Could not parse argument names of %o', fun)
1448
1493
 
1494
+ ###*
1495
+ Normalizes the given params object to the form returned by
1496
+ [`jQuery.serializeArray`](https://api.jquery.com/serializeArray/).
1497
+
1498
+ @function up.util.requestDataAsArray
1499
+ @param {Object|Array|Undefined|Null} data
1500
+ @internal
1501
+ ###
1502
+ requestDataAsArray = (data) ->
1503
+ if isMissing(data)
1504
+ []
1505
+ else if isArray(data)
1506
+ data
1507
+ else if isObject(data)
1508
+ { name: name, value: value } for name, value of data
1509
+ else
1510
+ error('Unknown options.data type for %o', data)
1511
+
1512
+ ###*
1513
+ Returns an URL-encoded query string for the given params object.
1514
+
1515
+ @function up.util.requestDataAsQueryString
1516
+ @param {Object|Array|Undefined|Null} data
1517
+ @internal
1518
+ ###
1519
+ requestDataAsQueryString = (data) ->
1520
+ array = requestDataAsArray(data)
1521
+ query = ''
1522
+ if isPresent(array)
1523
+ query += '?'
1524
+ each array, (field, index) ->
1525
+ query += '&' unless index == 0
1526
+ query += encodeURIComponent(field.name) + '=' + encodeURIComponent(field.value)
1527
+ query
1528
+
1529
+ requestDataAsArray: requestDataAsArray
1530
+ requestDataAsQueryString: requestDataAsQueryString
1449
1531
  offsetParent: offsetParent
1450
1532
  fixedToAbsolute: fixedToAbsolute
1451
1533
  presentAttr: presentAttr
@@ -1456,7 +1538,6 @@ up.util = (($) ->
1456
1538
  createElementFromHtml: createElementFromHtml
1457
1539
  $createElementFromSelector: $createElementFromSelector
1458
1540
  selectorForElement: selectorForElement
1459
- ajax: ajax
1460
1541
  extend: extend
1461
1542
  copy: copy
1462
1543
  merge: merge
@@ -1471,6 +1552,8 @@ up.util = (($) ->
1471
1552
  any: any
1472
1553
  detect: detect
1473
1554
  select: select
1555
+ reject: reject
1556
+ intersect: intersect
1474
1557
  compact: compact
1475
1558
  uniq: uniq
1476
1559
  last: last
@@ -1514,6 +1597,7 @@ up.util = (($) ->
1514
1597
  methodFromXhr: methodFromXhr
1515
1598
  clientSize: clientSize
1516
1599
  only: only
1600
+ except: except
1517
1601
  trim: trim
1518
1602
  unresolvableDeferred: unresolvableDeferred
1519
1603
  unresolvablePromise: unresolvablePromise
@@ -16,7 +16,7 @@ module Upjs
16
16
  # [page fragment update](http://upjs.io/up.replace) triggered by an
17
17
  # Up.js frontend.
18
18
  def up?
19
- selector.present?
19
+ target.present?
20
20
  end
21
21
 
22
22
  ##
@@ -29,8 +29,8 @@ module Upjs
29
29
  #
30
30
  # Server-side code is free to optimize its response by only returning HTML
31
31
  # that matches this selector.
32
- def selector
33
- request.headers['X-Up-Selector']
32
+ def target
33
+ request.headers['X-Up-Target']
34
34
  end
35
35
 
36
36
  ##
@@ -4,6 +4,6 @@ module Upjs
4
4
  # The current version of the upjs-rails gem.
5
5
  # This version number is also used for releases of the Up.js
6
6
  # frontend code.
7
- VERSION = '0.17.0'
7
+ VERSION = '0.18.0'
8
8
  end
9
9
  end
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- upjs-rails (0.16.0)
4
+ upjs-rails (0.17.0)
5
5
  rails (>= 3)
6
6
 
7
7
  GEM
@@ -210,6 +210,3 @@ DEPENDENCIES
210
210
  uglifier (>= 1.3.0)
211
211
  upjs-rails!
212
212
  web-console (~> 2.0)
213
-
214
- BUNDLED WITH
215
- 1.11.2
@@ -4,8 +4,8 @@ class TestController < ActionController::Base
4
4
  render :text => up?.to_s
5
5
  end
6
6
 
7
- def up_selector
8
- render :text => up.selector
7
+ def up_target
8
+ render :text => up.target
9
9
  end
10
10
 
11
11
  def is_up_validate
@@ -2,13 +2,13 @@ describe TestController do
2
2
 
3
3
  describe '#up?' do
4
4
 
5
- it 'returns true if the request has an X-Up-Selector header' do
6
- request.headers['X-Up-Selector'] = 'body'
5
+ it 'returns true if the request has an X-Up-Target header' do
6
+ request.headers['X-Up-Target'] = 'body'
7
7
  get :is_up
8
8
  expect(response.body).to eq('true')
9
9
  end
10
10
 
11
- it 'returns false if the request has no X-Up-Selector header' do
11
+ it 'returns false if the request has no X-Up-Target header' do
12
12
  get :is_up
13
13
  expect(response.body).to eq('false')
14
14
  end
@@ -20,8 +20,8 @@ describe TestController do
20
20
  describe '#selector' do
21
21
 
22
22
  it 'returns the CSS selector that is requested via Up.js' do
23
- request.headers['X-Up-Selector'] = '.foo'
24
- get :up_selector
23
+ request.headers['X-Up-Target'] = '.foo'
24
+ get :up_target
25
25
  expect(response.body).to eq('.foo')
26
26
  end
27
27