upjs-rails 0.18.1 → 0.19.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -46,6 +46,7 @@ up.log = (($) ->
46
46
  block = args.pop() # Coffeescript copies the arguments array
47
47
  if message
48
48
  up.browser.puts('groupCollapsed', prefix(message), args...)
49
+ # up.browser.puts('log', prefix(message), args...)
49
50
  try
50
51
  block()
51
52
  finally
@@ -346,10 +346,13 @@ up.motion = (($) ->
346
346
  if transitionOrName == 'none'
347
347
  transitionOrName = false
348
348
 
349
- up.log.group ('Morphing %o to %o (using %o)' if transitionOrName), source, target, transitionOrName, ->
349
+ $old = $(source)
350
+ $new = $(target)
351
+
352
+ ensureMorphable($old, transitionOrName)
353
+ ensureMorphable($new, transitionOrName)
350
354
 
351
- $old = $(source)
352
- $new = $(target)
355
+ up.log.group ('Morphing %o to %o (using %o)' if transitionOrName), source, target, transitionOrName, ->
353
356
 
354
357
  parsedOptions = u.only(options, 'reveal', 'restoreScroll', 'source')
355
358
  parsedOptions = u.extend(parsedOptions, animateOptions(options))
@@ -380,6 +383,12 @@ up.motion = (($) ->
380
383
  else
381
384
  return skipMorph($old, $new, parsedOptions)
382
385
 
386
+ ensureMorphable = ($element, transition) ->
387
+ if transition && $element.parents('body').length == 0
388
+ element = $element.get(0)
389
+ u.error("Can't morph a <%s> element (%o)", element.tagName, element)
390
+
391
+
383
392
  ###*
384
393
  This causes the side effects of a successful transition, but instantly.
385
394
  We use this to skip morphing for old browsers, or when the developer
@@ -231,7 +231,7 @@ up.proxy = (($) ->
231
231
  Once the response is received, a `up:proxy:receive` event will
232
232
  be emitted.
233
233
 
234
- @function up.proxy.ajax
234
+ @function up.ajax
235
235
  @param {String} request.url
236
236
  @param {String} [request.method='GET']
237
237
  @param {String} [request.target='body']
@@ -341,7 +341,7 @@ up.proxy = (($) ->
341
341
  emission()
342
342
 
343
343
  ###*
344
- This event is [emitted]/(up.emit) when [AJAX requests](/up.proxy.ajax)
344
+ This event is [emitted]/(up.emit) when [AJAX requests](/up.ajax)
345
345
  are taking long to finish.
346
346
 
347
347
  By default Up.js will wait 300 ms for an AJAX request to finish
@@ -366,7 +366,7 @@ up.proxy = (($) ->
366
366
  busyEventEmitted = false
367
367
 
368
368
  ###*
369
- This event is [emitted]/(up.emit) when [AJAX requests](/up.proxy.ajax)
369
+ This event is [emitted]/(up.emit) when [AJAX requests](/up.ajax)
370
370
  have [taken long to finish](/up:proxy:busy), but have finished now.
371
371
 
372
372
  @event up:proxy:idle
@@ -398,6 +398,7 @@ up.proxy = (($) ->
398
398
 
399
399
  request.headers ||= {}
400
400
  request.headers['X-Up-Target'] = request.target
401
+
401
402
  request.data = u.requestDataAsArray(request.data)
402
403
 
403
404
  if u.contains(config.wrapMethods, request.method)
@@ -422,7 +423,7 @@ up.proxy = (($) ->
422
423
  promise.fail (args...) -> entry.deferred.reject(args...)
423
424
 
424
425
  ###*
425
- This event is [emitted]/(up.emit) before an [AJAX request](/up.proxy.ajax)
426
+ This event is [emitted]/(up.emit) before an [AJAX request](/up.ajax)
426
427
  is starting to load.
427
428
 
428
429
  @event up:proxy:load
@@ -433,7 +434,7 @@ up.proxy = (($) ->
433
434
  ###
434
435
 
435
436
  ###*
436
- This event is [emitted]/(up.emit) when the response to an [AJAX request](/up.proxy.ajax)
437
+ This event is [emitted]/(up.emit) when the response to an [AJAX request](/up.ajax)
437
438
  has been received.
438
439
 
439
440
  @event up:proxy:received
@@ -516,3 +517,5 @@ up.proxy = (($) ->
516
517
  defaults: -> u.error('up.proxy.defaults(...) no longer exists. Set values on he up.proxy.config property instead.')
517
518
 
518
519
  )(jQuery)
520
+
521
+ up.ajax = up.proxy.ajax
@@ -181,6 +181,11 @@ up.syntax = (($) ->
181
181
  If set to `true` and a fragment insertion contains multiple
182
182
  elements matching the selector, `compiler` is only called once
183
183
  with a jQuery collection containing all matching elements.
184
+ @param {Boolean} [options.keep=false]
185
+ If set to `true` compiled fragment will be [persisted](/up-keep) during
186
+ [page updates](/a-up-target).
187
+
188
+ This has the same effect as setting an `up-keep` attribute on the element.
184
189
  @param {Function($element, data)} compiler
185
190
  The function to call when a matching element is inserted.
186
191
  The function takes the new element as the first argument (as a jQuery object).
@@ -200,23 +205,45 @@ up.syntax = (($) ->
200
205
  # Silently discard any compilers that are registered on unsupported browsers
201
206
  return unless up.browser.isSupported()
202
207
  compiler = args.pop()
203
- options = u.options(args[0], batch: false)
208
+ options = u.options(args[0])
204
209
  compilers.push
205
210
  selector: selector
206
211
  callback: compiler
207
212
  batch: options.batch
208
-
213
+ keep: options.keep
214
+
209
215
  applyCompiler = (compiler, $jqueryElement, nativeElement) ->
210
216
  up.puts ("Compiling '%s' on %o" unless compiler.isDefault), compiler.selector, nativeElement
217
+ if compiler.keep
218
+ value = if u.isString(compiler.keep) then compiler.keep else ''
219
+ $jqueryElement.attr('up-keep', value)
211
220
  destroyer = compiler.callback.apply(nativeElement, [$jqueryElement, data($jqueryElement)])
212
221
  if u.isFunction(destroyer)
213
222
  $jqueryElement.addClass(DESTROYABLE_CLASS)
214
223
  $jqueryElement.data(DESTROYER_KEY, destroyer)
215
224
 
216
- compile = ($fragment) ->
225
+ ###*
226
+ Applies all compilers on the given element and its descendants.
227
+ Unlike [`up.hello`](/up.hello), this doesn't emit any events.
228
+
229
+ @function up.syntax.compile
230
+ @param {Array<Element>} [options.skip]
231
+ A list of elements whose subtrees should not be compiled.
232
+ @internal
233
+ ###
234
+ compile = ($fragment, options) ->
235
+ options = u.options(options)
236
+ $skipSubtrees = $(options.skip)
237
+
217
238
  up.log.group "Compiling fragment %o", $fragment.get(0), ->
218
239
  for compiler in compilers
219
240
  $matches = u.findWithSelf($fragment, compiler.selector)
241
+
242
+ $matches = $matches.filter ->
243
+ $match = $(this)
244
+ u.all $skipSubtrees, (element) ->
245
+ $match.closest(element).length == 0
246
+
220
247
  if $matches.length
221
248
  up.log.group ("Compiling '%s' on %d element(s)" unless compiler.isDefault), compiler.selector, $matches.length, ->
222
249
  if compiler.batch
@@ -224,7 +251,15 @@ up.syntax = (($) ->
224
251
  else
225
252
  $matches.each -> applyCompiler(compiler, $(this), this)
226
253
 
227
- runDestroyers = ($fragment) ->
254
+ ###*
255
+ Runs any destroyers on the given fragment and its descendants.
256
+ Unlike [`up.destroy`](/up.destroy), this doesn't emit any events
257
+ and does not remove the element from the DOM.
258
+
259
+ @function up.syntax.clean
260
+ @internal
261
+ ###
262
+ clean = ($fragment) ->
228
263
  u.findWithSelf($fragment, ".#{DESTROYABLE_CLASS}").each ->
229
264
  $element = $(this)
230
265
  destroyer = $element.data(DESTROYER_KEY)
@@ -286,68 +321,17 @@ up.syntax = (($) ->
286
321
  reset = ->
287
322
  compilers = u.select compilers, (compiler) -> compiler.isDefault
288
323
 
289
- ###*
290
- Compiles a page fragment that has been inserted into the DOM
291
- without Up.js.
292
-
293
- **As long as you manipulate the DOM using Up.js, you will never
294
- need to call this method.** You only need to use `up.hello` if the
295
- DOM is manipulated without Up.js' involvement, e.g. by setting
296
- the `innerHTML` property or calling jQuery methods like
297
- `html`, `insertAfter` or `appendTo`:
298
-
299
- $element = $('.element');
300
- $element.html('<div>...</div>');
301
- up.hello($element);
302
-
303
- This function emits the [`up:fragment:inserted`](/up:fragment:inserted)
304
- event.
305
-
306
- @function up.hello
307
- @param {String|Element|jQuery} selectorOrElement
308
- @param {String|Element|jQuery} [options.origin]
309
- @return {jQuery}
310
- The compiled element
311
- @stable
312
- ###
313
- hello = (selectorOrElement, options) ->
314
- $element = $(selectorOrElement)
315
- eventAttrs = u.options options,
316
- $element: $element
317
- message: ['Inserted fragment %o', $element.get(0)]
318
- up.emit('up:fragment:inserted', eventAttrs)
319
- $element
320
-
321
- ###*
322
- When a page fragment has been [inserted or updated](/up.replace),
323
- this event is [emitted](/up.emit) on the fragment.
324
-
325
- \#\#\#\# Example
326
-
327
- up.on('up:fragment:inserted', function(event, $fragment) {
328
- console.log("Looks like we have a new %o!", $fragment);
329
- });
330
-
331
- @event up:fragment:inserted
332
- @param {jQuery} event.$element
333
- The fragment that has been inserted or updated.
334
- @stable
335
- ###
336
-
337
- up.on 'ready', (-> hello(document.body))
338
- up.on 'up:fragment:inserted', (event, $element) -> compile($element)
339
- up.on 'up:fragment:destroy', (event, $element) -> runDestroyers($element)
340
324
  up.on 'up:framework:boot', snapshot
341
325
  up.on 'up:framework:reset', reset
342
326
 
343
327
  compiler: compiler
344
- hello: hello
328
+ compile: compile
329
+ clean: clean
345
330
  data: data
346
331
 
347
332
  )(jQuery)
348
333
 
349
334
  up.compiler = up.syntax.compiler
350
- up.hello = up.syntax.hello
351
335
 
352
336
  up.ready = -> up.util.error('up.ready no longer exists. Please use up.hello instead.')
353
337
  up.awaken = -> up.util.error('up.awaken no longer exists. Please use up.compiler instead.')
@@ -163,7 +163,7 @@ up.util = (($) ->
163
163
  $element = $(element)
164
164
  selector = undefined
165
165
 
166
- # up.puts("Creating selector from element %o", $element.get(0))
166
+ up.puts("Creating selector from element %o", $element.get(0))
167
167
 
168
168
  if upId = presence($element.attr("up-id"))
169
169
  selector = "[up-id='#{upId}']"
@@ -172,7 +172,6 @@ up.util = (($) ->
172
172
  else if name = presence($element.attr("name"))
173
173
  selector = "[name='#{name}']"
174
174
  else if classes = presence(nonUpClasses($element))
175
- console.log("using klass!", classes)
176
175
  selector = ''
177
176
  for klass in classes
178
177
  selector += ".#{klass}"
@@ -646,6 +645,24 @@ up.util = (($) ->
646
645
  break
647
646
  match
648
647
 
648
+ ###*
649
+ Returns whether the given function returns a truthy value
650
+ for all elements in the given array.
651
+
652
+ @function up.util.all
653
+ @param {Array<T>} array
654
+ @param {Function<T>} tester
655
+ @return {Boolean}
656
+ @experimental
657
+ ###
658
+ all = (array, tester) ->
659
+ match = true
660
+ for element in array
661
+ unless tester(element)
662
+ match = false
663
+ break
664
+ match
665
+
649
666
  ###*
650
667
  Returns all elements from the given array that are
651
668
  neither `null` or `undefined`.
@@ -1436,31 +1453,30 @@ up.util = (($) ->
1436
1453
  @internal
1437
1454
  ###
1438
1455
  requestDataAsArray = (data) ->
1439
- if isMissing(data)
1440
- []
1441
- else if isArray(data)
1442
- data
1443
- else if isObject(data)
1444
- { name: name, value: value } for name, value of data
1445
- else
1446
- error('Unknown options.data type for %o', data)
1456
+ query = requestDataAsQuery(data)
1457
+ array = []
1458
+ for part in query.split('&')
1459
+ if isPresent(part)
1460
+ pair = part.split('=')
1461
+ array.push
1462
+ name: decodeURIComponent(pair[0])
1463
+ value: decodeURIComponent(pair[1])
1464
+ array
1447
1465
 
1448
1466
  ###*
1449
1467
  Returns an URL-encoded query string for the given params object.
1450
1468
 
1451
- @function up.util.requestDataAsQueryString
1469
+ @function up.util.requestDataAsQuery
1452
1470
  @param {Object|Array|Undefined|Null} data
1453
1471
  @internal
1454
1472
  ###
1455
- requestDataAsQueryString = (data) ->
1456
- array = requestDataAsArray(data)
1457
- query = ''
1458
- if isPresent(array)
1459
- query += '?'
1460
- each array, (field, index) ->
1461
- query += '&' unless index == 0
1462
- query += encodeURIComponent(field.name) + '=' + encodeURIComponent(field.value)
1463
- query
1473
+ requestDataAsQuery = (data) ->
1474
+ if data
1475
+ query = $.param(data)
1476
+ query = query.replace(/\+/g, '%20')
1477
+ query
1478
+ else
1479
+ ""
1464
1480
 
1465
1481
  ###*
1466
1482
  Throws a fatal error with the given message.
@@ -1485,7 +1501,7 @@ up.util = (($) ->
1485
1501
  throw new Error(asString)
1486
1502
 
1487
1503
  requestDataAsArray: requestDataAsArray
1488
- requestDataAsQueryString: requestDataAsQueryString
1504
+ requestDataAsQuery: requestDataAsQuery
1489
1505
  offsetParent: offsetParent
1490
1506
  fixedToAbsolute: fixedToAbsolute
1491
1507
  presentAttr: presentAttr
@@ -1507,6 +1523,7 @@ up.util = (($) ->
1507
1523
  map: map
1508
1524
  times: times
1509
1525
  any: any
1526
+ all: all
1510
1527
  detect: detect
1511
1528
  select: select
1512
1529
  reject: reject
@@ -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.18.1'
7
+ VERSION = '0.19.0'
8
8
  end
9
9
  end
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ..
3
3
  specs:
4
- upjs-rails (0.18.0)
4
+ upjs-rails (0.18.1)
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
@@ -1,5 +1,4 @@
1
1
  afterEach ->
2
2
  up.reset()
3
3
  $('.up-error').remove()
4
-
5
-
4
+ console.debug('--- Up.js was reset after example ---')
@@ -0,0 +1,5 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toBeBlank: (util, customEqualityTesters) ->
4
+ compare: (actual) ->
5
+ pass: up.util.isBlank(actual)
@@ -0,0 +1,5 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toBeGiven: (util, customEqualityTesters) ->
4
+ compare: (actual) ->
5
+ pass: up.util.isGiven(actual)
@@ -0,0 +1,5 @@
1
+ beforeEach ->
2
+ jasmine.addMatchers
3
+ toBeMissing: (util, customEqualityTesters) ->
4
+ compare: (actual) ->
5
+ pass: up.util.isMissing(actual)
@@ -47,9 +47,9 @@ describe 'up.flow', ->
47
47
  expect(@lastRequest().data()['bar-key']).toEqual(['bar-value'])
48
48
 
49
49
  it "encodes the given params into the URL of a GET request", ->
50
- givenParams = { 'foo-key': 'foo value', 'bar-key': 'bar value' }
50
+ givenParams = { 'foo-key': 'foo-value', 'bar-key': 'bar-value' }
51
51
  up.replace('.middle', '/path', method: 'get', data: givenParams)
52
- expect(@lastRequest().url).toEndWith('/path?foo-key=foo+value&bar-key=bar+value')
52
+ expect(@lastRequest().url).toEndWith('/path?foo-key=foo-value&bar-key=bar-value')
53
53
 
54
54
  it 'uses a HTTP method given as { method } option', ->
55
55
  up.replace('.middle', '/path', method: 'put')
@@ -58,7 +58,7 @@ describe 'up.flow', ->
58
58
  describe 'if the server responds with a non-200 status code', ->
59
59
 
60
60
  it 'replaces the <body> instead of the given selector', ->
61
- implantSpy = up.flow.knife.mock('implant') # can't have the example replace the Jasmine test runner UI
61
+ implantSpy = up.flow.knife.mock('extract') # can't have the example replace the Jasmine test runner UI
62
62
  up.replace('.middle', '/path')
63
63
  @respond(status: 500)
64
64
  expect(implantSpy).toHaveBeenCalledWith('body', jasmine.any(String), jasmine.any(Object))
@@ -391,7 +391,7 @@ describe 'up.flow', ->
391
391
  up.replace('.selector', '/path')
392
392
  expect(up.browser.loadPage).toHaveBeenCalledWith('/path', jasmine.anything())
393
393
 
394
- describe 'up.flow.implant', ->
394
+ describe 'up.extract', ->
395
395
 
396
396
  it 'Updates a selector on the current page with the same selector from the given HTML string', ->
397
397
 
@@ -406,7 +406,7 @@ describe 'up.flow', ->
406
406
  <div class="after">new-after</div>
407
407
  """
408
408
 
409
- up.flow.implant('.middle', html)
409
+ up.extract('.middle', html)
410
410
 
411
411
  expect($('.before')).toHaveText('old-before')
412
412
  expect($('.middle')).toHaveText('new-middle')
@@ -414,49 +414,299 @@ describe 'up.flow', ->
414
414
 
415
415
  it "throws an error if the selector can't be found on the current page", ->
416
416
  html = '<div class="foo-bar">text</div>'
417
- implant = -> up.flow.implant('.foo-bar', html)
418
- expect(implant).toThrowError(/Could not find selector ".foo-bar" in current body/i)
417
+ extract = -> up.extract('.foo-bar', html)
418
+ expect(extract).toThrowError(/Could not find selector ".foo-bar" in current body/i)
419
419
 
420
420
  it "throws an error if the selector can't be found in the given HTML string", ->
421
421
  affix('.foo-bar')
422
- implant = -> up.flow.implant('.foo-bar', '')
423
- expect(implant).toThrowError(/Could not find selector ".foo-bar" in response/i)
422
+ extract = -> up.extract('.foo-bar', '')
423
+ expect(extract).toThrowError(/Could not find selector ".foo-bar" in response/i)
424
424
 
425
425
  it "ignores an element that matches the selector but also matches .up-destroying", ->
426
426
  html = '<div class="foo-bar">text</div>'
427
427
  affix('.foo-bar.up-destroying')
428
- implant = -> up.flow.implant('.foo-bar', html)
429
- expect(implant).toThrowError(/Could not find selector/i)
428
+ extract = -> up.extract('.foo-bar', html)
429
+ expect(extract).toThrowError(/Could not find selector/i)
430
430
 
431
431
  it "ignores an element that matches the selector but also matches .up-ghost", ->
432
432
  html = '<div class="foo-bar">text</div>'
433
433
  affix('.foo-bar.up-ghost')
434
- implant = -> up.flow.implant('.foo-bar', html)
435
- expect(implant).toThrowError(/Could not find selector/i)
434
+ extract = -> up.extract('.foo-bar', html)
435
+ expect(extract).toThrowError(/Could not find selector/i)
436
436
 
437
437
  it "ignores an element that matches the selector but also has a parent matching .up-destroying", ->
438
438
  html = '<div class="foo-bar">text</div>'
439
439
  $parent = affix('.up-destroying')
440
440
  $child = affix('.foo-bar').appendTo($parent)
441
- implant = -> up.flow.implant('.foo-bar', html)
442
- expect(implant).toThrowError(/Could not find selector/i)
441
+ extract = -> up.extract('.foo-bar', html)
442
+ expect(extract).toThrowError(/Could not find selector/i)
443
443
 
444
444
  it "ignores an element that matches the selector but also has a parent matching .up-ghost", ->
445
445
  html = '<div class="foo-bar">text</div>'
446
446
  $parent = affix('.up-ghost')
447
447
  $child = affix('.foo-bar').appendTo($parent)
448
- implant = -> up.flow.implant('.foo-bar', html)
449
- expect(implant).toThrowError(/Could not find selector/i)
448
+ extract = -> up.extract('.foo-bar', html)
449
+ expect(extract).toThrowError(/Could not find selector/i)
450
450
 
451
451
  it 'only replaces the first element matching the selector', ->
452
452
  html = '<div class="foo-bar">text</div>'
453
453
  affix('.foo-bar')
454
454
  affix('.foo-bar')
455
- up.flow.implant('.foo-bar', html)
455
+ up.extract('.foo-bar', html)
456
456
  elements = $('.foo-bar')
457
457
  expect($(elements.get(0)).text()).toEqual('text')
458
458
  expect($(elements.get(1)).text()).toEqual('')
459
459
 
460
+ describe 'handling of [up-keep] elements', ->
461
+
462
+ squish = (string) ->
463
+ if u.isString(string)
464
+ string = string.replace(/^\s+/g, '')
465
+ string = string.replace(/\s+$/g, '')
466
+ string = string.replace(/\s+/g, ' ')
467
+ string
468
+
469
+ beforeEach ->
470
+ # Need to refactor this spec file so examples don't all share one example
471
+ $('.before, .middle, .after').remove()
472
+
473
+ it 'keeps an [up-keep] element, but does replace other elements around it', ->
474
+ $container = affix('.container')
475
+ $container.affix('.before').text('old-before')
476
+ $container.affix('.middle[up-keep]').text('old-middle')
477
+ $container.affix('.after').text('old-after')
478
+ up.extract '.container', """
479
+ <div class='container'>
480
+ <div class='before'>new-before</div>
481
+ <div class='middle' up-keep>new-middle</div>
482
+ <div class='after'>new-after</div>
483
+ </div>
484
+ """
485
+ expect($('.before')).toHaveText('new-before')
486
+ expect($('.middle')).toHaveText('old-middle')
487
+ expect($('.after')).toHaveText('new-after')
488
+
489
+ it 'keeps an [up-keep] element, but does replace text nodes around it', ->
490
+ $container = affix('.container')
491
+ $container.html """
492
+ old-before
493
+ <div class='element' up-keep>old-inside</div>
494
+ old-after
495
+ """
496
+ up.extract '.container', """
497
+ <div class='container'>
498
+ new-before
499
+ <div class='element' up-keep>new-inside</div>
500
+ new-after
501
+ </div>
502
+ """
503
+ expect(squish($('.container').text())).toEqual('new-before old-inside new-after')
504
+
505
+ describe 'if an [up-keep] element is itself a direct replacement target', ->
506
+
507
+ it "keeps that element", ->
508
+ affix('.keeper[up-keep]').text('old-inside')
509
+ up.extract '.keeper', "<div class='keeper' up-keep>new-inside</div>"
510
+ expect($('.keeper')).toHaveText('old-inside')
511
+
512
+ it "does not compile the kept element a second time"
513
+
514
+ it "only emits an event up:fragment:kept, but not an event up:fragment:inserted", ->
515
+ insertedListener = jasmine.createSpy('subscriber to up:fragment:inserted')
516
+ up.on('up:fragment:inserted', insertedListener)
517
+ keptListener = jasmine.createSpy('subscriber to up:fragment:kept')
518
+ up.on('up:fragment:kept', keptListener)
519
+ up.on 'up:fragment:inserted', insertedListener
520
+ $keeper = affix('.keeper[up-keep]').text('old-inside')
521
+ up.extract '.keeper', "<div class='keeper' up-keep>new-inside</div>"
522
+ expect(insertedListener).not.toHaveBeenCalled()
523
+ expect(keptListener).toHaveBeenCalledWith(jasmine.anything(), $('.keeper'), jasmine.anything())
524
+
525
+ it "removes an [up-keep] element if no matching element is found in the response", ->
526
+ $container = affix('.container')
527
+ $container.html """
528
+ <div class='foo'>old-foo</div>
529
+ <div class='bar' up-keep>old-bar</div>
530
+ """
531
+ up.extract '.container', """
532
+ <div class='container'>
533
+ <div class='foo'>new-foo</div>
534
+ </div>
535
+ """
536
+ expect($('.container .foo')).toExist()
537
+ expect($('.container .bar')).not.toExist()
538
+
539
+ it "updates an element if a matching element is found in the response, but that other element is no longer [up-keep]", ->
540
+ $container = affix('.container')
541
+ $container.html """
542
+ <div class='foo'>old-foo</div>
543
+ <div class='bar' up-keep>old-bar</div>
544
+ """
545
+ up.extract '.container', """
546
+ <div class='container'>
547
+ <div class='foo'>new-foo</div>
548
+ <div class='bar'>new-bar</div>
549
+ </div>
550
+ """
551
+ expect($('.container .foo')).toHaveText('new-foo')
552
+ expect($('.container .bar')).toHaveText('new-bar')
553
+
554
+ it 'moves a kept element to the ancestry position of the matching element in the response', ->
555
+ $container = affix('.container')
556
+ $container.html """
557
+ <div class="parent1">
558
+ <div class="keeper" up-keep>old-inside</div>
559
+ </div>
560
+ <div class="parent2">
561
+ </div>
562
+ """
563
+ up.extract '.container', """
564
+ <div class='container'>
565
+ <div class="parent1">
566
+ </div>
567
+ <div class="parent2">
568
+ <div class="keeper" up-keep>old-inside</div>
569
+ </div>
570
+ </div>
571
+ """
572
+ expect($('.keeper')).toHaveText('old-inside')
573
+ expect($('.keeper').parent()).toEqual($('.parent2'))
574
+
575
+ it 'lets developers choose a selector to match against as the value of the up-keep attribute', ->
576
+ $container = affix('.container')
577
+ $container.html """
578
+ <div class="keeper" up-keep=".stayer"></div>
579
+ """
580
+ up.extract '.container', """
581
+ <div class='container'>
582
+ <div up-keep class="stayer"></div>
583
+ </div>
584
+ """
585
+ expect('.keeper').toExist()
586
+
587
+ it 'does not compile a kept element a second time', ->
588
+ compiler = jasmine.createSpy('compiler')
589
+ up.compiler('.keeper', compiler)
590
+ $container = affix('.container')
591
+ $container.html """
592
+ <div class="keeper" up-keep>old-text</div>
593
+ """
594
+
595
+ console.log '*** before hello ***'
596
+ up.hello($container)
597
+ console.log '*** after hello ***'
598
+ expect(compiler.calls.count()).toEqual(1)
599
+
600
+ up.extract '.container', """
601
+ <div class='container'>
602
+ <div class="keeper" up-keep>new-text</div>
603
+ </div>
604
+ """
605
+ expect(compiler.calls.count()).toEqual(1)
606
+ expect('.keeper').toExist()
607
+
608
+ it 'lets listeners cancel the keeping by preventing default on an up:fragment:keep event', ->
609
+ $keeper = affix('.keeper[up-keep]').text('old-inside')
610
+ $keeper.on 'up:fragment:keep', (event) -> event.preventDefault()
611
+ up.extract '.keeper', "<div class='keeper' up-keep>new-inside</div>"
612
+ expect($('.keeper')).toHaveText('new-inside')
613
+
614
+ it 'lets listeners prevent up:fragment:keep event if the element was kept before (bugfix)', ->
615
+ $keeper = affix('.keeper[up-keep]').text('version 1')
616
+ $keeper.on 'up:fragment:keep', (event) ->
617
+ event.preventDefault() if event.$newElement.text() == 'version 3'
618
+ up.extract '.keeper', "<div class='keeper' up-keep>version 2</div>"
619
+ expect($('.keeper')).toHaveText('version 1')
620
+ up.extract '.keeper', "<div class='keeper' up-keep>version 3</div>"
621
+ expect($('.keeper')).toHaveText('version 3')
622
+
623
+ it 'emits an up:fragment:kept event on a kept element and up:fragment:inserted on an updated parent', ->
624
+ insertedListener = jasmine.createSpy()
625
+ up.on('up:fragment:inserted', insertedListener)
626
+ keptListener = jasmine.createSpy()
627
+ up.on('up:fragment:kept', keptListener)
628
+
629
+ $container = affix('.container')
630
+ $container.html """
631
+ <div class="keeper" up-keep></div>
632
+ """
633
+ up.extract '.container', """
634
+ <div class='container'>
635
+ <div class="keeper" up-keep></div>
636
+ </div>
637
+ """
638
+ expect(insertedListener).toHaveBeenCalledWith(jasmine.anything(), $('.container'), jasmine.anything())
639
+ expect(keptListener).toHaveBeenCalledWith(jasmine.anything(), $('.container .keeper'), jasmine.anything())
640
+
641
+ it 'emits an up:fragment:kept event on a kept element with a newData property corresponding to the up-data attribute value of the discarded element', ->
642
+ keptListener = jasmine.createSpy()
643
+ up.on 'up:fragment:kept', (event) -> keptListener(event.$element, event.newData)
644
+ $container = affix('.container')
645
+ $keeper = $container.affix('.keeper[up-keep]').text('old-inside')
646
+ up.extract '.container', """
647
+ <div class='container'>
648
+ <div class='keeper' up-keep up-data='{ "foo": "bar" }'>new-inside</div>
649
+ </div>
650
+ """
651
+ expect($('.keeper')).toHaveText('old-inside')
652
+ expect(keptListener).toHaveBeenCalledWith($keeper, { 'foo': 'bar' })
653
+
654
+ it 'emits an up:fragment:kept with { newData: {} } if the discarded element had no up-data value', ->
655
+ keptListener = jasmine.createSpy()
656
+ up.on('up:fragment:kept', keptListener)
657
+ $container = affix('.container')
658
+ $keeper = $container.affix('.keeper[up-keep]').text('old-inside')
659
+ up.extract '.keeper', """
660
+ <div class='container'>
661
+ <div class='keeper' up-keep>new-inside</div>
662
+ </div>
663
+ """
664
+ expect($('.keeper')).toHaveText('old-inside')
665
+ expect(keptListener).toEqual(jasmine.anything(), $('.keeper'), {})
666
+
667
+ it 'reuses the same element and emits up:fragment:kept during multiple extractions', ->
668
+ keptListener = jasmine.createSpy()
669
+ up.on 'up:fragment:kept', (event) -> keptListener(event.$element, event.newData)
670
+ $container = affix('.container')
671
+ $keeper = $container.affix('.keeper[up-keep]').text('old-inside')
672
+ up.extract '.keeper', """
673
+ <div class='container'>
674
+ <div class='keeper' up-keep up-data='{ \"key\": \"value1\" }'>new-inside</div>
675
+ </div>
676
+ """
677
+ up.extract '.keeper', """
678
+ <div class='container'>
679
+ <div class='keeper' up-keep up-data='{ \"key\": \"value2\" }'>new-inside</div>
680
+ """
681
+ $keeper = $('.keeper')
682
+ expect($keeper).toHaveText('old-inside')
683
+ expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value1' })
684
+ expect(keptListener).toHaveBeenCalledWith($keeper, { key: 'value2' })
685
+
686
+ it "doesn't let the discarded element appear in a transition", (done) ->
687
+ oldTextDuringTransition = undefined
688
+ newTextDuringTransition = undefined
689
+ transition = ($old, $new) ->
690
+ oldTextDuringTransition = squish($old.text())
691
+ newTextDuringTransition = squish($new.text())
692
+ u.resolvedDeferred()
693
+ $container = affix('.container')
694
+ $container.html """
695
+ <div class='foo'>old-foo</div>
696
+ <div class='bar' up-keep>old-bar</div>
697
+ """
698
+ newHtml = """
699
+ <div class='container'>
700
+ <div class='foo'>new-foo</div>
701
+ <div class='bar' up-keep>new-bar</div>
702
+ </div>
703
+ """
704
+ promise = up.extract('.container', newHtml, transition: transition)
705
+ promise.then ->
706
+ expect(oldTextDuringTransition).toEqual('old-foo old-bar')
707
+ expect(newTextDuringTransition).toEqual('new-foo old-bar')
708
+ done()
709
+
460
710
  describe 'up.destroy', ->
461
711
 
462
712
  it 'removes the element with the given selector', ->
@@ -502,3 +752,4 @@ describe 'up.flow', ->
502
752
  describe 'up.reset', ->
503
753
 
504
754
  it 'should have tests'
755
+