upjs-rails 0.18.1 → 0.19.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.
@@ -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
+