upjs-rails 0.17.0 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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