unpoly-rails 0.24.1 → 0.25.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.

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