unpoly-rails 0.24.1 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of unpoly-rails might be problematic. Click here for more details.

Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +31 -0
  3. data/README_RAILS.md +8 -1
  4. data/dist/unpoly.css +22 -10
  5. data/dist/unpoly.js +406 -196
  6. data/dist/unpoly.min.css +1 -1
  7. data/dist/unpoly.min.js +3 -3
  8. data/lib/assets/javascripts/unpoly/flow.js.coffee +36 -19
  9. data/lib/assets/javascripts/unpoly/form.js.coffee +1 -2
  10. data/lib/assets/javascripts/unpoly/link.js.coffee +2 -2
  11. data/lib/assets/javascripts/unpoly/modal.js.coffee +174 -81
  12. data/lib/assets/javascripts/unpoly/navigation.js.coffee +3 -1
  13. data/lib/assets/javascripts/unpoly/popup.js.coffee +62 -37
  14. data/lib/assets/javascripts/unpoly/proxy.js.coffee +1 -0
  15. data/lib/assets/javascripts/unpoly/syntax.js.coffee +12 -4
  16. data/lib/assets/javascripts/unpoly/util.js.coffee +55 -13
  17. data/lib/assets/stylesheets/unpoly/modal.css.sass +28 -12
  18. data/lib/unpoly/rails/inspector.rb +26 -0
  19. data/lib/unpoly/rails/version.rb +1 -1
  20. data/spec_app/Gemfile.lock +1 -1
  21. data/spec_app/app/controllers/binding_test_controller.rb +6 -0
  22. data/spec_app/spec/controllers/binding_test_controller_spec.rb +82 -11
  23. data/spec_app/spec/javascripts/up/flow_spec.js.coffee +21 -7
  24. data/spec_app/spec/javascripts/up/form_spec.js.coffee +15 -0
  25. data/spec_app/spec/javascripts/up/link_spec.js.coffee +11 -10
  26. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +232 -30
  27. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +33 -27
  28. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +72 -0
  29. data/spec_app/spec/javascripts/up/syntax_spec.js.coffee +51 -13
  30. metadata +2 -2
@@ -6,7 +6,7 @@ Instead of [linking to a page fragment](/up.link), you can choose
6
6
  to show a fragment in a modal dialog. The existing page will remain
7
7
  open in the background and reappear once the modal is closed.
8
8
 
9
- To open a modal, add an [`up-modal` attribute](/a-up-modal) to a link,
9
+ To open a modal, add an [`up-modal` attribute](/up-modal) to a link,
10
10
  or call the Javascript functions [`up.modal.follow`](/up.modal.follow)
11
11
  and [`up.modal.visit`](/up.modal.visit).
12
12
 
@@ -46,7 +46,7 @@ configure Unpoly to [use a different HTML structure](/up.modal.config).
46
46
  \#\#\#\# Closing behavior
47
47
 
48
48
  By default the dialog automatically closes
49
- *whenever a page fragment behind the dialog is updated*.
49
+ *when a link inside a modal changes a fragment behind the modal*.
50
50
  This is useful to have the dialog interact with the page that
51
51
  opened it, e.g. by updating parts of a larger form or by signing in a user
52
52
  and revealing additional information.
@@ -109,6 +109,9 @@ up.modal = (($) ->
109
109
  The timing function controlling the acceleration of the opening animation.
110
110
  @param {String} [config.closeEasing]
111
111
  The timing function controlling the acceleration of the closing animation.
112
+ @param {Boolean} [options.sticky=false]
113
+ If set to `true`, the modal remains
114
+ open even it changes the page in the background.
112
115
  @stable
113
116
  ###
114
117
  config = u.config
@@ -119,13 +122,14 @@ up.modal = (($) ->
119
122
  history: true
120
123
  openAnimation: 'fade-in'
121
124
  closeAnimation: 'fade-out'
122
- closeDuration: null
123
- closeEasing: null
124
125
  openDuration: null
126
+ closeDuration: null
125
127
  openEasing: null
128
+ closeEasing: null
126
129
  backdropOpenAnimation: 'fade-in'
127
130
  backdropCloseAnimation: 'fade-out'
128
131
  closeLabel: '×'
132
+ flavors: { default: {} }
129
133
 
130
134
  template: (config) ->
131
135
  """
@@ -133,8 +137,8 @@ up.modal = (($) ->
133
137
  <div class="up-modal-backdrop"></div>
134
138
  <div class="up-modal-viewport">
135
139
  <div class="up-modal-dialog">
136
- <div class="up-modal-close" up-close>#{config.closeLabel}</div>
137
140
  <div class="up-modal-content"></div>
141
+ <div class="up-modal-close" up-close>#{flavorDefault('closeLabel')}</div>
138
142
  </div>
139
143
  </div>
140
144
  </div>
@@ -151,6 +155,8 @@ up.modal = (($) ->
151
155
  ###
152
156
  currentUrl = undefined
153
157
 
158
+ currentFlavor = undefined
159
+
154
160
  ###*
155
161
  Returns the URL of the page behind the modal overlay.
156
162
 
@@ -165,10 +171,11 @@ up.modal = (($) ->
165
171
  # Destroy the modal container regardless whether it's currently in a closing animation
166
172
  close(animation: false)
167
173
  currentUrl = undefined
174
+ currentFlavor = undefined
168
175
  config.reset()
169
176
 
170
177
  templateHtml = ->
171
- template = config.template
178
+ template = flavorDefault('template')
172
179
  if u.isFunction(template)
173
180
  template(config)
174
181
  else
@@ -180,20 +187,27 @@ up.modal = (($) ->
180
187
  $modal.removeAttr('up-covered-title')
181
188
 
182
189
  createFrame = (target, options) ->
183
- $modal = $(templateHtml())
184
- $modal.attr('up-sticky', '') if options.sticky
185
- $modal.attr('up-covered-url', up.browser.url())
186
- $modal.attr('up-covered-title', document.title)
187
- $dialog = $modal.find('.up-modal-dialog')
188
- $dialog.css('width', options.width) if u.isPresent(options.width)
189
- $dialog.css('max-width', options.maxWidth) if u.isPresent(options.maxWidth)
190
- $dialog.css('height', options.height) if u.isPresent(options.height)
191
- $content = $modal.find('.up-modal-content')
192
- # Create an empty element that will match the
193
- # selector that is being replaced.
194
- u.$createPlaceholder(target, $content)
195
- $modal.appendTo(document.body)
196
- $modal
190
+ promise = u.resolvedPromise()
191
+ if isOpen()
192
+ promise = promise.then -> close()
193
+ promise = promise.then ->
194
+ currentFlavor = options.flavor
195
+ $modal = $(templateHtml())
196
+ $modal.attr('up-flavor', currentFlavor)
197
+ $modal.attr('up-sticky', '') if options.sticky
198
+ $modal.attr('up-covered-url', up.browser.url())
199
+ $modal.attr('up-covered-title', document.title)
200
+ $dialog = $modal.find('.up-modal-dialog')
201
+ $dialog.css('width', options.width) if u.isPresent(options.width)
202
+ $dialog.css('max-width', options.maxWidth) if u.isPresent(options.maxWidth)
203
+ $dialog.css('height', options.height) if u.isPresent(options.height)
204
+ $content = $modal.find('.up-modal-content')
205
+ # Create an empty element that will match the
206
+ # selector that is being replaced.
207
+ u.$createPlaceholder(target, $content)
208
+ $modal.appendTo(document.body)
209
+ return promise
210
+
197
211
  unshifters = []
198
212
 
199
213
  # Gives `<body>` a right padding in the width of a scrollbar.
@@ -204,32 +218,35 @@ up.modal = (($) ->
204
218
  # modal overlay, which has its own scroll bar.
205
219
  # This is screwed up, but Bootstrap does the same.
206
220
  shiftElements = ->
207
- if unshifters.length
208
- u.error('Tried to call shiftElements multiple times %o', unshifters.length)
209
- $('.up-modal').addClass('up-modal-ready')
210
- scrollbarWidth = u.scrollbarWidth()
211
- bodyRightPadding = parseInt($('body').css('padding-right'))
212
- bodyRightShift = scrollbarWidth + bodyRightPadding
213
- unshiftBody = u.temporaryCss($('body'),
214
- 'padding-right': "#{bodyRightShift}px",
215
- 'overflow-y': 'hidden'
216
- )
217
- unshifters.push(unshiftBody)
218
- up.layout.anchoredRight().each ->
219
- $element = $(this)
220
- elementRight = parseInt($element.css('right'))
221
- elementRightShift = scrollbarWidth + elementRight
222
- unshifter = u.temporaryCss($element, 'right': elementRightShift)
223
- unshifters.push(unshifter)
221
+ return if unshifters.length > 0
222
+
223
+ if u.documentHasVerticalScrollbar()
224
+ $body = $('body')
225
+ scrollbarWidth = u.scrollbarWidth()
226
+ bodyRightPadding = parseInt($body.css('padding-right'))
227
+ bodyRightShift = scrollbarWidth + bodyRightPadding
228
+ unshiftBody = u.temporaryCss($body,
229
+ 'padding-right': "#{bodyRightShift}px",
230
+ 'overflow-y': 'hidden'
231
+ )
232
+ unshifters.push(unshiftBody)
233
+ up.layout.anchoredRight().each ->
234
+ $element = $(this)
235
+ elementRight = parseInt($element.css('right'))
236
+ elementRightShift = scrollbarWidth + elementRight
237
+ unshifter = u.temporaryCss($element, 'right': elementRightShift)
238
+ unshifters.push(unshifter)
239
+
224
240
 
225
241
  # Reverts the effects of `shiftElements`.
226
242
  unshiftElements = ->
227
- $('.up-modal').removeClass('up-modal-ready')
228
243
  unshifter() while unshifter = unshifters.pop()
229
244
 
230
245
  ###*
231
246
  Returns whether a modal is currently open.
232
247
 
248
+ This also returns `true` if the modal is in an opening or closing animation.
249
+
233
250
  @function up.modal.isOpen
234
251
  @stable
235
252
  ###
@@ -259,7 +276,7 @@ up.modal = (($) ->
259
276
  By [default](/up.modal.config) the dialog will grow to fit its contents.
260
277
  @param {Boolean} [options.sticky=false]
261
278
  If set to `true`, the modal remains
262
- open even if the page changes in the background.
279
+ open even it changes the page in the background.
263
280
  @param {String} [options.confirm]
264
281
  A message that will be displayed in a cancelable confirmation dialog
265
282
  before the modal is being opened.
@@ -353,42 +370,38 @@ up.modal = (($) ->
353
370
  options = u.options(options)
354
371
  $link = u.option(u.pluckKey(options, '$link'), u.nullJQuery())
355
372
  url = u.option(u.pluckKey(options, 'url'), $link.attr('up-href'), $link.attr('href'))
356
- html = u.pluckKey(options, 'html')
373
+ html = u.option(u.pluckKey(options, 'html'))
357
374
  target = u.option(u.pluckKey(options, 'target'), $link.attr('up-modal'), 'body')
358
- options.width = u.option(options.width, $link.attr('up-width'), config.width)
359
- options.maxWidth = u.option(options.maxWidth, $link.attr('up-max-width'), config.maxWidth)
360
- options.height = u.option(options.height, $link.attr('up-height'), config.height)
361
- options.animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation)
362
- options.backdropAnimation = u.option(options.backdropAnimation, $link.attr('up-backdrop-animation'), config.backdropOpenAnimation)
363
- options.sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'))
375
+ options.flavor = u.option(options.flavor, $link.attr('up-flavor'))
376
+ options.width = u.option(options.width, $link.attr('up-width'), flavorDefault('width', options.flavor))
377
+ options.maxWidth = u.option(options.maxWidth, $link.attr('up-max-width'), flavorDefault('maxWidth', options.flavor))
378
+ options.height = u.option(options.height, $link.attr('up-height'), flavorDefault('height'))
379
+ options.animation = u.option(options.animation, $link.attr('up-animation'), flavorDefault('openAnimation', options.flavor))
380
+ options.backdropAnimation = u.option(options.backdropAnimation, $link.attr('up-backdrop-animation'), flavorDefault('backdropOpenAnimation', options.flavor))
381
+ options.sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'), flavorDefault('sticky', options.flavor))
364
382
  options.confirm = u.option(options.confirm, $link.attr('up-confirm'))
365
- animateOptions = up.motion.animateOptions(options, $link, { duration: config.openDuration, easing: config.openEasing })
383
+ animateOptions = up.motion.animateOptions(options, $link, duration: flavorDefault('openDuration', options.flavor), easing: flavorDefault('openEasing', options.flavor))
366
384
 
367
385
  # Although we usually fall back to full page loads if a browser doesn't support pushState,
368
386
  # in the case of modals we assume that the developer would rather see a dialog
369
387
  # without an URL update.
370
- options.history = u.option(options.history, u.castedAttr($link, 'up-history'), config.history)
388
+ options.history = u.option(options.history, u.castedAttr($link, 'up-history'), flavorDefault('history', options.flavor))
371
389
  options.history = false unless up.browser.canPushState()
372
390
 
373
391
  up.browser.confirm(options).then ->
374
392
  if up.bus.nobodyPrevents('up:modal:open', url: url, message: 'Opening modal')
375
- wasOpen = isOpen()
376
- close(animation: false) if wasOpen
377
393
  options.beforeSwap = -> createFrame(target, options)
378
394
  extractOptions = u.merge(options, animation: false)
379
- if url
380
- promise = up.replace(target, url, extractOptions)
381
- else
395
+ if html
382
396
  promise = up.extract(target, html, extractOptions)
383
- # If we're not animating the dialog, don't animate the backdrop either
384
- unless wasOpen || up.motion.isNone(options.animation)
385
- promise = promise.then ->
386
- $.when(
387
- up.animate($('.up-modal-backdrop'), options.backdropAnimation, animateOptions),
388
- up.animate($('.up-modal-viewport'), options.animation, animateOptions)
389
- )
397
+ else
398
+ promise = up.replace(target, url, extractOptions)
390
399
  promise = promise.then ->
391
400
  shiftElements()
401
+
402
+ promise = promise.then -> animate(options.animation, options.backdropAnimation, animateOptions)
403
+
404
+ promise = promise.then ->
392
405
  up.emit('up:modal:opened', message: 'Modal opened')
393
406
  promise
394
407
  else
@@ -432,18 +445,15 @@ up.modal = (($) ->
432
445
  $modal = $('.up-modal')
433
446
  if $modal.length
434
447
  if up.bus.nobodyPrevents('up:modal:close', $element: $modal, message: 'Closing modal')
435
- unshiftElements()
436
- viewportCloseAnimation = u.option(options.animation, config.closeAnimation)
437
- backdropCloseAnimation = u.option(options.backdropAnimation, config.backdropCloseAnimation)
438
- animateOptions = up.motion.animateOptions(options, { duration: config.closeDuration, easing: config.closeEasing })
439
- if up.motion.isNone(viewportCloseAnimation)
440
- # If we're not animating the dialog, don't animate the backdrop either
441
- promise = u.resolvedPromise()
442
- else
443
- promise = $.when(
444
- up.animate($('.up-modal-viewport'), viewportCloseAnimation, animateOptions),
445
- up.animate($('.up-modal-backdrop'), backdropCloseAnimation, animateOptions)
446
- )
448
+ viewportCloseAnimation = u.option(options.animation, flavorDefault('closeAnimation'))
449
+ backdropCloseAnimation = u.option(options.backdropAnimation, flavorDefault('backdropCloseAnimation'))
450
+ animateOptions = up.motion.animateOptions(options, duration: flavorDefault('closeDuration'), easing: flavorDefault('closeEasing'))
451
+
452
+ promise = u.resolvedPromise()
453
+
454
+ promise = promise.then ->
455
+ animate(viewportCloseAnimation, backdropCloseAnimation, animateOptions)
456
+
447
457
  promise = promise.then ->
448
458
  destroyOptions = u.options(
449
459
  u.except(options, 'animation', 'duration', 'easing', 'delay'),
@@ -454,16 +464,37 @@ up.modal = (($) ->
454
464
  # since up.navigation listens to up:fragment:destroyed and then
455
465
  # re-assigns .up-current classes.
456
466
  currentUrl = undefined
457
- up.destroy($modal, destroyOptions)
467
+
468
+ return up.destroy($modal, destroyOptions)
469
+
470
+ promise = promise.then ->
471
+ unshiftElements()
472
+ currentFlavor = undefined
458
473
  up.emit('up:modal:closed', message: 'Modal closed')
474
+
459
475
  promise
460
476
  else
461
- # Although someone prevented the destruction,
462
- # keep a uniform API for callers by returning
463
- # a Deferred that will never be resolved.
464
- u.unresolvableDeferred()
477
+ # Although someone prevented the destruction, keep a uniform API
478
+ # for callers by returning a promise that will never be resolved.
479
+ u.unresolvablePromise()
480
+ else
481
+ u.resolvedPromise()
482
+
483
+ markAsAnimating = (state = true) ->
484
+ $('.up-modal').toggleClass('up-modal-animating', state)
485
+
486
+ animate = (viewportAnimation, backdropAnimation, animateOptions) ->
487
+ # If we're not animating the dialog, don't animate the backdrop either
488
+ if up.motion.isNone(viewportAnimation)
489
+ u.resolvedPromise()
465
490
  else
466
- u.resolvedDeferred()
491
+ markAsAnimating()
492
+ promise = $.when(
493
+ up.animate($('.up-modal-viewport'), viewportAnimation, animateOptions),
494
+ up.animate($('.up-modal-backdrop'), backdropAnimation, animateOptions)
495
+ )
496
+ promise = promise.then -> markAsAnimating(false)
497
+ promise
467
498
 
468
499
  ###*
469
500
  This event is [emitted](/up.emit) when a modal dialog
@@ -500,6 +531,66 @@ up.modal = (($) ->
500
531
  $element = $(elementOrSelector)
501
532
  $element.closest('.up-modal').length > 0
502
533
 
534
+ ###*
535
+ Register a new modal variant with its own default configuration, CSS or HTML template.
536
+
537
+ \#\#\#\# Example
538
+
539
+ Let's implement a drawer that slides in from the right:
540
+
541
+ up.modal.flavor('drawer', {
542
+ openAnimation: 'move-from-right',
543
+ closeAnimation: 'move-to-right',
544
+ maxWidth: 400
545
+ }
546
+
547
+ Modals with that flavor will have a container `<div class='up-modal' up-flavor='drawer'>...</div>`.
548
+ We can target the `up-flavor` attribute override the default dialog styles:
549
+
550
+ .up-modal[up-flavor='drawer'] {
551
+
552
+ // Align drawer on the right
553
+ .up-modal-viewport { text-align: right; }
554
+
555
+ // Remove margin so the drawer starts at the screen edge
556
+ .up-modal-dialog { margin: 0; }
557
+
558
+ // Stretch drawer background to full window height
559
+ .up-modal-content { min-height: 100vh; }
560
+ }
561
+
562
+ @function up.modal.flavor
563
+ @param {String} name
564
+ The name of the new flavor.
565
+ @param {Object} [overrideConfig]
566
+ An object whose properties override the defaults in [`/up.modal.config`](/up.modal.config).
567
+ @experimental
568
+ ###
569
+ flavor = (name, overrideConfig = {}) ->
570
+ u.extend(flavorOverrides(name), overrideConfig)
571
+
572
+ ###*
573
+ Returns a config object for the given flavor.
574
+ Properties in that config should be preferred to the defaults in
575
+ [`/up.modal.config`](/up.modal.config).
576
+
577
+ @function flavorOverrides
578
+ @internal
579
+ ###
580
+ flavorOverrides = (flavor) ->
581
+ config.flavors[flavor] ||= {}
582
+
583
+ ###*
584
+ Returns the config option for the current flavor.
585
+
586
+ @function flavorDefault
587
+ @internal
588
+ ###
589
+ flavorDefault = (key, flavorName = currentFlavor) ->
590
+ value = flavorOverrides(flavorName)[key] if flavorName
591
+ value = config[key] if u.isMissing(value)
592
+ value
593
+
503
594
  ###*
504
595
  Clicking this link will load the destination via AJAX and open
505
596
  the given selector in a modal dialog.
@@ -513,7 +604,7 @@ up.modal = (($) ->
513
604
  and place the matching `.blog-list` tag will be placed in
514
605
  a modal dialog.
515
606
 
516
- @selector a[up-modal]
607
+ @selector [up-modal]
517
608
  @param {String} [up-confirm]
518
609
  A message that will be displayed in a cancelable confirmation dialog
519
610
  before the modal is opened.
@@ -527,11 +618,12 @@ up.modal = (($) ->
527
618
  @param {String} [up-height]
528
619
  The width of the dialog in pixels.
529
620
  By [default](/up.modal.config) the dialog will grow to fit its contents.
530
- @param [up-width]
621
+ @param {String} [up-width]
531
622
  The width of the dialog in pixels.
532
623
  By [default](/up.modal.config) the dialog will grow to fit its contents.
533
- @param [up-history="true"]
624
+ @param {String} [up-history="true"]
534
625
  Whether to add a browser history entry for the modal's source URL.
626
+
535
627
  @stable
536
628
  ###
537
629
  up.link.onAction '[up-modal]', ($link) ->
@@ -594,5 +686,6 @@ up.modal = (($) ->
594
686
  contains: contains
595
687
  source: -> up.error('up.modal.source no longer exists. Please use up.popup.url instead.')
596
688
  isOpen: isOpen
689
+ flavor: flavor
597
690
 
598
691
  )(jQuery)
@@ -150,7 +150,9 @@ up.navigation = (($) ->
150
150
  $element = findClickArea(elementOrSelector, options)
151
151
  $element.removeClass(CLASS_ACTIVE)
152
152
 
153
- withActiveMark = (elementOrSelector, options, block) ->
153
+ withActiveMark = (elementOrSelector, args...) ->
154
+ block = args.pop()
155
+ options = u.options(args.pop())
154
156
  $element = $(elementOrSelector)
155
157
  markActive($element, options)
156
158
  promise = block()
@@ -5,7 +5,7 @@ Pop-up overlays
5
5
  Instead of [linking to a page fragment](/up.link), you can choose
6
6
  to show a fragment in a popup overlay that rolls down from an anchoring element.
7
7
 
8
- To open a popup, add an [`up-popup` attribute](/a-up-popup) to a link,
8
+ To open a popup, add an [`up-popup` attribute](/up-popup) to a link,
9
9
  or call the Javascript function [`up.popup.attach`](/up.popup.attach).
10
10
 
11
11
  For modal dialogs see [up.modal](/up.modal) instead.
@@ -31,7 +31,7 @@ By default the popup uses the following DOM structure:
31
31
  The popup closes when the user clicks anywhere outside the popup area.
32
32
 
33
33
  By default the popup also closes
34
- *whenever a page fragment behind the popup is updated*.
34
+ *when a link within the popup changes a fragment behind the popup*.
35
35
  This is useful to have the popup interact with the page that
36
36
  opened it, e.g. by updating parts of a larger form or by signing in a user
37
37
  and revealing additional information.
@@ -71,21 +71,36 @@ up.popup = (($) ->
71
71
  Sets default options for future popups.
72
72
 
73
73
  @property up.popup.config
74
- @param {String} [config.openAnimation='fade-in']
75
- The animation used to open a popup.
76
- @param {String} [config.closeAnimation='fade-out']
77
- The animation used to close a popup.
78
74
  @param {String} [config.position='bottom-right']
79
75
  Defines where the popup is attached to the opening element.
80
76
 
81
77
  Valid values are `bottom-right`, `bottom-left`, `top-right` and `top-left`.
82
78
  @param {String} [config.history=false]
83
79
  Whether opening a popup will add a browser history entry.
80
+ @param {String} [config.openAnimation='fade-in']
81
+ The animation used to open a popup.
82
+ @param {String} [config.closeAnimation='fade-out']
83
+ The animation used to close a popup.
84
+ @param {String} [config.openDuration]
85
+ The duration of the open animation (in milliseconds).
86
+ @param {String} [config.closeDuration]
87
+ The duration of the close animation (in milliseconds).
88
+ @param {String} [config.openEasing]
89
+ The timing function controlling the acceleration of the opening animation.
90
+ @param {String} [config.closeEasing]
91
+ The timing function controlling the acceleration of the closing animation.
92
+ @param {Boolean} [options.sticky=false]
93
+ If set to `true`, the popup remains
94
+ open even it changes the page in the background.
84
95
  @stable
85
96
  ###
86
97
  config = u.config
87
98
  openAnimation: 'fade-in'
88
99
  closeAnimation: 'fade-out'
100
+ openDuration: null
101
+ closeDuration: null
102
+ openEasing: null
103
+ closeEasing: null
89
104
  position: 'bottom-right'
90
105
  history: false
91
106
 
@@ -147,15 +162,20 @@ up.popup = (($) ->
147
162
  $popup.removeAttr('up-covered-title')
148
163
 
149
164
  createFrame = (target, options) ->
150
- $popup = u.$createElementFromSelector('.up-popup')
151
- $popup.attr('up-sticky', '') if options.sticky
152
- $popup.attr('up-covered-url', up.browser.url())
153
- $popup.attr('up-covered-title', document.title)
154
- # Create an empty element that will match the
155
- # selector that is being replaced.
156
- u.$createPlaceholder(target, $popup)
157
- $popup.appendTo(document.body)
158
- $popup
165
+ promise = u.resolvedPromise()
166
+ if isOpen()
167
+ promise = promise.then -> close()
168
+ promise = promise.then ->
169
+ $popup = u.$createElementFromSelector('.up-popup')
170
+ $popup.attr('up-sticky', '') if options.sticky
171
+ $popup.attr('up-covered-url', up.browser.url())
172
+ $popup.attr('up-covered-title', document.title)
173
+ # Create an empty element that will match the
174
+ # selector that is being replaced.
175
+ u.$createPlaceholder(target, $popup)
176
+ $popup.appendTo(document.body)
177
+ $popup
178
+ return promise
159
179
 
160
180
  ###*
161
181
  Returns whether popup modal is currently open.
@@ -174,6 +194,8 @@ up.popup = (($) ->
174
194
  @function up.popup.attach
175
195
  @param {Element|jQuery|String} elementOrSelector
176
196
  @param {String} [options.url]
197
+ @param {String} [options.target]
198
+ A CSS selector that will be extracted from the response and placed into the popup.
177
199
  @param {String} [options.position='bottom-right']
178
200
  Defines where the popup is attached to the opening element.
179
201
 
@@ -203,33 +225,35 @@ up.popup = (($) ->
203
225
  $link.length or u.error('Cannot attach popup to non-existing element %o', linkOrSelector)
204
226
 
205
227
  options = u.options(options)
206
- url = u.option(options.url, $link.attr('href'))
207
- target = u.option(options.target, $link.attr('up-popup'), 'body')
228
+ url = u.option(u.pluckKey(options, 'url'), $link.attr('up-href'), $link.attr('href'))
229
+ html = u.option(u.pluckKey(options, 'html'))
230
+ target = u.option(u.pluckKey(options, 'target'), $link.attr('up-popup'), 'body')
208
231
  options.position = u.option(options.position, $link.attr('up-position'), config.position)
209
232
  options.animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation)
210
- options.sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'))
233
+ options.sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'), config.sticky)
211
234
  options.history = if up.browser.canPushState() then u.option(options.history, u.castedAttr($link, 'up-history'), config.history) else false
212
235
  options.confirm = u.option(options.confirm, $link.attr('up-confirm'))
213
- animateOptions = up.motion.animateOptions(options, $link)
236
+ animateOptions = up.motion.animateOptions(options, $link, duration: config.openDuration, easing: config.openEasing)
214
237
 
215
238
  up.browser.confirm(options).then ->
216
239
  if up.bus.nobodyPrevents('up:popup:open', url: url, message: 'Opening popup')
217
- wasOpen = isOpen()
218
- close(animation: false) if wasOpen
219
240
  options.beforeSwap = -> createFrame(target, options)
220
- promise = up.replace(target, url, u.merge(options, animation: false))
241
+ extractOptions = u.merge(options, animation: false)
242
+ if html
243
+ promise = up.extract(target, html, extractOptions)
244
+ else
245
+ promise = up.replace(target, url, extractOptions)
221
246
  promise = promise.then ->
222
247
  setPosition($link, options.position)
223
- unless wasOpen
224
- promise = promise.then ->
225
- up.animate($('.up-popup'), options.animation, animateOptions)
248
+ promise = promise.then ->
249
+ up.animate($('.up-popup'), options.animation, animateOptions)
226
250
  promise = promise.then ->
227
251
  up.emit('up:popup:opened', message: 'Popup opened')
228
252
  promise
229
253
  else
230
254
  # Although someone prevented the destruction, keep a uniform API for
231
- # callers by returning a Deferred that will never be resolved.
232
- u.unresolvableDeferred()
255
+ # callers by returning a promise that will never be resolved.
256
+ u.unresolvablePromise()
233
257
 
234
258
  ###*
235
259
  This event is [emitted](/up.emit) when a popup is starting to open.
@@ -257,7 +281,7 @@ up.popup = (($) ->
257
281
  @function up.popup.close
258
282
  @param {Object} options
259
283
  See options for [`up.animate`](/up.animate).
260
- @return {Deferred}
284
+ @return {Promise}
261
285
  A promise that will be resolved once the modal's close
262
286
  animation has finished.
263
287
  @stable
@@ -271,17 +295,18 @@ up.popup = (($) ->
271
295
  url: $popup.attr('up-covered-url'),
272
296
  title: $popup.attr('up-covered-title')
273
297
  )
298
+ animateOptions = up.motion.animateOptions(options, duration: config.closeDuration, easing: config.closeEasing)
299
+ u.extend(options, animateOptions)
274
300
  currentUrl = undefined
275
- deferred = up.destroy($popup, options)
276
- deferred.then -> up.emit('up:popup:closed', message: 'Popup closed')
277
- deferred
301
+ promise = up.destroy($popup, options)
302
+ promise = promise.then -> up.emit('up:popup:closed', message: 'Popup closed')
303
+ promise
278
304
  else
279
- # Although someone prevented the destruction,
280
- # keep a uniform API for callers by returning
281
- # a Deferred that will never be resolved.
282
- u.unresolvableDeferred()
305
+ # Although someone prevented the destruction, keep a uniform API
306
+ # for callers by returning a promise that will never be resolved.
307
+ u.unresolvablePromise()
283
308
  else
284
- u.resolvedDeferred()
309
+ u.resolvedPromise()
285
310
 
286
311
  ###*
287
312
  This event is [emitted](/up.emit) when a popup dialog
@@ -329,7 +354,7 @@ up.popup = (($) ->
329
354
  <a href="/decks" up-popup=".deck_list">Switch deck</a>
330
355
  <a href="/settings" up-popup=".options" up-sticky>Settings</a>
331
356
 
332
- @selector a[up-popup]
357
+ @selector [up-popup]
333
358
  @param [up-position]
334
359
  Defines where the popup is attached to the opening element.
335
360
 
@@ -394,6 +394,7 @@ up.proxy = (($) ->
394
394
  promise = load(entry.request)
395
395
  promise.done (args...) -> entry.deferred.resolve(args...)
396
396
  promise.fail (args...) -> entry.deferred.reject(args...)
397
+ return
397
398
 
398
399
  ###*
399
400
  Makes the proxy assume that `newRequest` has the same response as the
@@ -255,6 +255,10 @@ up.syntax = (($) ->
255
255
  buildCompiler = (selector, args...) ->
256
256
  callback = args.pop()
257
257
  options = u.options(args[0], priority: 0)
258
+ if options.priority == 'first'
259
+ options.priority = Number.POSITIVE_INFINITY
260
+ else if options.priority == 'last'
261
+ options.priority = Number.NEGATIVE_INFINITY
258
262
  selector: selector
259
263
  callback: callback
260
264
  priority: options.priority
@@ -266,7 +270,7 @@ up.syntax = (($) ->
266
270
  return unless up.browser.isSupported()
267
271
  newCompiler = buildCompiler(args...)
268
272
  index = 0
269
- while (oldCompiler = queue[index]) && (oldCompiler.priority <= newCompiler.priority)
273
+ while (oldCompiler = queue[index]) && (oldCompiler.priority >= newCompiler.priority)
270
274
  index += 1
271
275
  queue.splice(index, 0, newCompiler)
272
276
 
@@ -322,6 +326,7 @@ up.syntax = (($) ->
322
326
  u.findWithSelf($fragment, ".#{DESTROYABLE_CLASS}").each ->
323
327
  $element = $(this)
324
328
  destroyer = $element.data(DESTROYER_KEY)
329
+ $element.removeClass(DESTROYABLE_CLASS)
325
330
  destroyer()
326
331
 
327
332
  ###*
@@ -407,8 +412,9 @@ up.syntax = (($) ->
407
412
  @internal
408
413
  ###
409
414
  snapshot = ->
410
- for compiler in compilers
411
- compiler.isDefault = true
415
+ setDefault = (compiler) -> compiler.isDefault = true
416
+ u.each(compilers, setDefault)
417
+ u.each(macros, setDefault)
412
418
 
413
419
  ###*
414
420
  Resets the list of registered compiler directives to the
@@ -417,7 +423,9 @@ up.syntax = (($) ->
417
423
  @internal
418
424
  ###
419
425
  reset = ->
420
- compilers = u.select compilers, (compiler) -> compiler.isDefault
426
+ isDefault = (compiler) -> compiler.isDefault
427
+ compilers = u.select(compilers, isDefault)
428
+ macros = u.select(macros, isDefault)
421
429
 
422
430
  up.on 'up:framework:boot', snapshot
423
431
  up.on 'up:framework:reset', reset