unpoly-rails 0.26.2 → 0.27.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 (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +33 -1
  3. data/dist/unpoly.js +704 -446
  4. data/dist/unpoly.min.js +3 -3
  5. data/lib/assets/javascripts/unpoly/browser.js.coffee +18 -9
  6. data/lib/assets/javascripts/unpoly/bus.js.coffee +28 -1
  7. data/lib/assets/javascripts/unpoly/flow.js.coffee +1 -1
  8. data/lib/assets/javascripts/unpoly/history.js.coffee +54 -22
  9. data/lib/assets/javascripts/unpoly/link.js.coffee +1 -1
  10. data/lib/assets/javascripts/unpoly/log.js.coffee +19 -12
  11. data/lib/assets/javascripts/unpoly/modal.js.coffee +119 -124
  12. data/lib/assets/javascripts/unpoly/motion.js.coffee +1 -0
  13. data/lib/assets/javascripts/unpoly/navigation.js.coffee +2 -6
  14. data/lib/assets/javascripts/unpoly/popup.js.coffee +136 -126
  15. data/lib/assets/javascripts/unpoly/proxy.js.coffee +0 -2
  16. data/lib/assets/javascripts/unpoly/syntax.js.coffee +1 -1
  17. data/lib/assets/javascripts/unpoly/tooltip.js.coffee +101 -46
  18. data/lib/assets/javascripts/unpoly/util.js.coffee +76 -7
  19. data/lib/unpoly/rails/version.rb +1 -1
  20. data/spec_app/Gemfile.lock +1 -1
  21. data/spec_app/app/assets/stylesheets/integration_test.sass +4 -0
  22. data/spec_app/app/assets/stylesheets/jasmine_specs.sass +5 -0
  23. data/spec_app/app/views/css_test/modal.erb +3 -0
  24. data/spec_app/app/views/css_test/modal_contents.erb +5 -0
  25. data/spec_app/app/views/css_test/popup.erb +11 -11
  26. data/spec_app/app/views/css_test/tooltip.erb +12 -5
  27. data/spec_app/app/views/pages/start.erb +4 -0
  28. data/spec_app/spec/javascripts/helpers/reset_up.js.coffee +2 -3
  29. data/spec_app/spec/javascripts/up/flow_spec.js.coffee +97 -88
  30. data/spec_app/spec/javascripts/up/history_spec.js.coffee +100 -1
  31. data/spec_app/spec/javascripts/up/link_spec.js.coffee +18 -16
  32. data/spec_app/spec/javascripts/up/modal_spec.js.coffee +102 -97
  33. data/spec_app/spec/javascripts/up/motion_spec.js.coffee +89 -75
  34. data/spec_app/spec/javascripts/up/navigation_spec.js.coffee +17 -5
  35. data/spec_app/spec/javascripts/up/popup_spec.js.coffee +89 -70
  36. data/spec_app/spec/javascripts/up/util_spec.js.coffee +23 -0
  37. metadata +4 -2
@@ -73,6 +73,7 @@ up.motion = (($) ->
73
73
  enabled: true
74
74
 
75
75
  reset = ->
76
+ finish()
76
77
  animations = u.copy(defaultAnimations)
77
78
  transitions = u.copy(defaultTransitions)
78
79
  config.reset()
@@ -39,12 +39,8 @@ up.navigation = (($) ->
39
39
  SELECTOR_SECTION = 'a, [up-href]'
40
40
 
41
41
  normalizeUrl = (url) ->
42
- if u.isPresent(url)
43
- u.normalizeUrl(url,
44
- search: false
45
- stripTrailingSlash: true
46
- )
47
-
42
+ u.normalizeUrl(url) if u.isPresent(url)
43
+
48
44
  sectionUrls = ($section) ->
49
45
  urls = []
50
46
  for attr in ['href', 'up-href', 'up-alias']
@@ -46,27 +46,6 @@ up.popup = (($) ->
46
46
 
47
47
  u = up.util
48
48
 
49
- ###*
50
- Returns the source URL for the fragment displayed
51
- in the current popup, or `undefined` if no popup is open.
52
-
53
- @function up.popup.url
54
- @return {String}
55
- the source URL
56
- @stable
57
- ###
58
- currentUrl = undefined
59
-
60
- ###*
61
- Returns the URL of the page or modal behind the popup.
62
-
63
- @function up.popup.coveredUrl
64
- @return {String}
65
- @experimental
66
- ###
67
- coveredUrl = ->
68
- $('.up-popup').attr('up-covered-url')
69
-
70
49
  ###*
71
50
  Sets default options for future popups.
72
51
 
@@ -97,68 +76,86 @@ up.popup = (($) ->
97
76
  config = u.config
98
77
  openAnimation: 'fade-in'
99
78
  closeAnimation: 'fade-out'
100
- openDuration: null
101
- closeDuration: null
79
+ openDuration: 150
80
+ closeDuration: 100
102
81
  openEasing: null
103
82
  closeEasing: null
104
83
  position: 'bottom-right'
105
84
  history: false
106
85
 
86
+ ###*
87
+ Returns the source URL for the fragment displayed
88
+ in the current popup, or `undefined` if no popup is open.
89
+
90
+ @function up.popup.url
91
+ @return {String}
92
+ the source URL
93
+ @stable
94
+ ###
95
+
96
+ ###*
97
+ Returns the URL of the page or modal behind the popup.
98
+
99
+ @function up.popup.coveredUrl
100
+ @return {String}
101
+ @experimental
102
+ ###
103
+
104
+ state = u.config
105
+ phase: 'closed' # can be 'opening', 'opened', 'closing' and 'closed'
106
+ $anchor: null # the element to which the tooltip is anchored
107
+ $popup: null # the popup container
108
+ position: null # the position of the popup container element relative to its anchor
109
+ sticky: null
110
+ url: null
111
+ coveredUrl: null
112
+ coveredTitle: null
113
+
114
+ chain = new u.DivertibleChain()
115
+
107
116
  reset = ->
108
- close(animation: false)
117
+ state.$popup?.remove()
118
+ state.reset()
119
+ chain.reset()
109
120
  config.reset()
110
121
 
111
- setPosition = ($link, position) ->
122
+ align = ->
112
123
  css = {}
113
124
 
114
- $popup = $('.up-popup')
115
- popupBox = u.measure($popup)
125
+ popupBox = u.measure(state.$popup)
116
126
 
117
- if u.isFixed($link)
118
- linkBox = $link.get(0).getBoundingClientRect()
127
+ if u.isFixed(state.$anchor)
128
+ linkBox = state.$anchor.get(0).getBoundingClientRect()
119
129
  css['position'] = 'fixed'
120
130
  else
121
- linkBox = u.measure($link)
131
+ linkBox = u.measure(state.$anchor)
122
132
 
123
- switch position
124
- when "bottom-right" # anchored to bottom-right of link, opens towards bottom-left
133
+ switch state.position
134
+ when 'bottom-right' # anchored to bottom-right of link, opens towards bottom-left
125
135
  css['top'] = linkBox.top + linkBox.height
126
136
  css['left'] = linkBox.left + linkBox.width - popupBox.width
127
- when "bottom-left" # anchored to bottom-left of link, opens towards bottom-right
137
+ when 'bottom-left' # anchored to bottom-left of link, opens towards bottom-right
128
138
  css['top'] = linkBox.top + linkBox.height
129
139
  css['left'] = linkBox.left
130
- when "top-right" # anchored to top-right of link, opens to top-left
140
+ when 'top-right' # anchored to top-right of link, opens to top-left
131
141
  css['top'] = linkBox.top - popupBox.height
132
142
  css['left'] = linkBox.left + linkBox.width - popupBox.width
133
- when "top-left" # anchored to top-left of link, opens to top-right
143
+ when 'top-left' # anchored to top-left of link, opens to top-right
134
144
  css['top'] = linkBox.top - popupBox.height
135
145
  css['left'] = linkBox.left
136
146
  else
137
- u.error("Unknown position option '%s'", position)
147
+ u.error("Unknown position option '%s'", state.position)
138
148
 
139
- $popup.attr('up-position', position)
140
- $popup.css(css)
149
+ state.$popup.attr('up-position', state.position)
150
+ state.$popup.css(css)
141
151
 
142
- discardHistory = ->
143
- $popup = $('.up-popup')
144
- $popup.removeAttr('up-covered-url')
145
- $popup.removeAttr('up-covered-title')
146
-
147
- createFrame = (target, options) ->
148
- promise = u.resolvedPromise()
149
- if isOpen()
150
- promise = promise.then -> close()
151
- promise = promise.then ->
152
- $popup = u.$createElementFromSelector('.up-popup')
153
- $popup.attr('up-sticky', '') if options.sticky
154
- $popup.attr('up-covered-url', up.browser.url())
155
- $popup.attr('up-covered-title', document.title)
156
- # Create an empty element that will match the
157
- # selector that is being replaced.
158
- u.$createPlaceholder(target, $popup)
159
- $popup.appendTo(document.body)
160
- $popup
161
- return promise
152
+ createFrame = (target) ->
153
+ $popup = u.$createElementFromSelector('.up-popup')
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
+ state.$popup = $popup
162
159
 
163
160
  ###*
164
161
  Returns whether popup modal is currently open.
@@ -167,13 +164,13 @@ up.popup = (($) ->
167
164
  @stable
168
165
  ###
169
166
  isOpen = ->
170
- $('.up-popup').length > 0
167
+ state.phase == 'opened' || state.phase == 'opening'
171
168
 
172
169
  ###*
173
170
  Attaches a popup overlay to the given element or selector.
174
171
 
175
172
  Emits events [`up:popup:open`](/up:popup:open) and [`up:popup:opened`](/up:popup:opened).
176
-
173
+
177
174
  @function up.popup.attach
178
175
  @param {Element|jQuery|String} elementOrSelector
179
176
  @param {String} [options.url]
@@ -205,41 +202,51 @@ up.popup = (($) ->
205
202
  the opening animation has completed.
206
203
  @stable
207
204
  ###
208
- attach = (linkOrSelector, options) ->
209
- $link = $(linkOrSelector)
210
- $link.length or u.error('Cannot attach popup to non-existing element %o', linkOrSelector)
211
-
205
+ attachAsap = (elementOrSelector, options) ->
206
+ curriedAttachNow = -> attachNow(elementOrSelector, options)
207
+ if isOpen()
208
+ chain.asap(closeNow, curriedAttachNow)
209
+ else
210
+ chain.asap(curriedAttachNow)
211
+ chain.promise()
212
+
213
+ attachNow = (elementOrSelector, options) ->
214
+ $anchor = $(elementOrSelector)
215
+ $anchor.length or u.error('Cannot attach popup to non-existing element %o', elementOrSelector)
216
+
212
217
  options = u.options(options)
213
- url = u.option(u.pluckKey(options, 'url'), $link.attr('up-href'), $link.attr('href'))
218
+ url = u.option(u.pluckKey(options, 'url'), $anchor.attr('up-href'), $anchor.attr('href'))
214
219
  html = u.option(u.pluckKey(options, 'html'))
215
- target = u.option(u.pluckKey(options, 'target'), $link.attr('up-popup'), 'body')
216
- options.position = u.option(options.position, $link.attr('up-position'), config.position)
217
- options.animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation)
218
- options.sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'), config.sticky)
219
- options.history = if up.browser.canPushState() then u.option(options.history, u.castedAttr($link, 'up-history'), config.history) else false
220
- options.confirm = u.option(options.confirm, $link.attr('up-confirm'))
221
- options.method = up.link.followMethod($link, options)
222
- animateOptions = up.motion.animateOptions(options, $link, duration: config.openDuration, easing: config.openEasing)
223
-
224
- up.browser.confirm(options).then ->
225
- if up.bus.nobodyPrevents('up:popup:open', url: url, message: 'Opening popup')
226
- options.beforeSwap = -> createFrame(target, options)
220
+ target = u.option(u.pluckKey(options, 'target'), $anchor.attr('up-popup'), 'body')
221
+ position = u.option(options.position, $anchor.attr('up-position'), config.position)
222
+ options.animation = u.option(options.animation, $anchor.attr('up-animation'), config.openAnimation)
223
+ options.sticky = u.option(options.sticky, u.castedAttr($anchor, 'up-sticky'), config.sticky)
224
+ options.history = if up.browser.canPushState() then u.option(options.history, u.castedAttr($anchor, 'up-history'), config.history) else false
225
+ options.confirm = u.option(options.confirm, $anchor.attr('up-confirm'))
226
+ options.method = up.link.followMethod($anchor, options)
227
+ animateOptions = up.motion.animateOptions(options, $anchor, duration: config.openDuration, easing: config.openEasing)
228
+
229
+ up.browser.whenConfirmed(options).then ->
230
+ up.bus.whenEmitted('up:popup:open', url: url, message: 'Opening popup').then ->
231
+ state.phase = 'opening'
232
+ state.$anchor = $anchor
233
+ state.position = position
234
+ state.coveredUrl = up.browser.url()
235
+ state.coveredTitle = document.title
236
+ state.sticky = options.sticky
237
+ options.beforeSwap = -> createFrame(target)
227
238
  extractOptions = u.merge(options, animation: false)
228
239
  if html
229
240
  promise = up.extract(target, html, extractOptions)
230
241
  else
231
242
  promise = up.replace(target, url, extractOptions)
232
243
  promise = promise.then ->
233
- setPosition($link, options.position)
244
+ align()
245
+ up.animate(state.$popup, options.animation, animateOptions)
234
246
  promise = promise.then ->
235
- up.animate($('.up-popup'), options.animation, animateOptions)
236
- promise = promise.then ->
237
- up.emit('up:popup:opened', message: 'Popup opened')
247
+ state.phase = 'opened'
248
+ up.emit('up:popup:opened', message: 'Popup opened')#
238
249
  promise
239
- else
240
- # Although someone prevented the destruction, keep a uniform API for
241
- # callers by returning a promise that will never be resolved.
242
- u.unresolvablePromise()
243
250
 
244
251
  ###*
245
252
  This event is [emitted](/up.emit) when a popup is starting to open.
@@ -256,7 +263,7 @@ up.popup = (($) ->
256
263
  @event up:popup:opened
257
264
  @stable
258
265
  ###
259
-
266
+
260
267
  ###*
261
268
  Closes a currently opened popup overlay.
262
269
 
@@ -272,27 +279,35 @@ up.popup = (($) ->
272
279
  animation has finished.
273
280
  @stable
274
281
  ###
275
- close = (options) ->
276
- $popup = $('.up-popup')
277
- if $popup.length
278
- if up.bus.nobodyPrevents('up:popup:close', $element: $popup)
279
- options = u.options(options,
280
- animation: config.closeAnimation,
281
- url: $popup.attr('up-covered-url'),
282
- title: $popup.attr('up-covered-title')
283
- )
284
- animateOptions = up.motion.animateOptions(options, duration: config.closeDuration, easing: config.closeEasing)
285
- u.extend(options, animateOptions)
286
- currentUrl = undefined
287
- promise = up.destroy($popup, options)
288
- promise = promise.then -> up.emit('up:popup:closed', message: 'Popup closed')
289
- promise
290
- else
291
- # Although someone prevented the destruction, keep a uniform API
292
- # for callers by returning a promise that will never be resolved.
293
- u.unresolvablePromise()
294
- else
295
- u.resolvedPromise()
282
+ closeAsap = (options) ->
283
+ if isOpen()
284
+ chain.asap -> closeNow(options)
285
+ chain.promise()
286
+
287
+ closeNow = (options) ->
288
+ unless isOpen() # this can happen when a request fails and the chain proceeds to the next task
289
+ return u.resolvedPromise()
290
+
291
+ options = u.options(options,
292
+ animation: config.closeAnimation
293
+ url: state.coveredUrl,
294
+ title: state.coveredTitle
295
+ )
296
+ animateOptions = up.motion.animateOptions(options, duration: config.closeDuration, easing: config.closeEasing)
297
+ u.extend(options, animateOptions)
298
+
299
+ up.bus.whenEmitted('up:popup:close', message: 'Closing popup', $element: state.$popup).then ->
300
+ state.phase = 'closing'
301
+ state.url = null
302
+ state.coveredUrl = null
303
+ state.coveredTitle = null
304
+
305
+ up.destroy(state.$popup, options).then ->
306
+ state.phase = 'closed'
307
+ state.$popup = null
308
+ state.$anchor = null
309
+ state.sticky = null
310
+ up.emit('up:popup:closed', message: 'Popup closed')
296
311
 
297
312
  ###*
298
313
  This event is [emitted](/up.emit) when a popup dialog
@@ -313,9 +328,7 @@ up.popup = (($) ->
313
328
  ###
314
329
 
315
330
  autoclose = ->
316
- unless $('.up-popup').is('[up-sticky]')
317
- discardHistory()
318
- close()
331
+ closeAsap() unless state.sticky
319
332
 
320
333
  ###*
321
334
  Returns whether the given element or selector is contained
@@ -363,29 +376,29 @@ up.popup = (($) ->
363
376
  ###
364
377
  up.link.onAction('[up-popup]', ($link) ->
365
378
  if $link.is('.up-current')
366
- close()
379
+ closeAsap()
367
380
  else
368
- attach($link)
381
+ attachAsap($link)
369
382
  )
370
383
 
371
384
  # Close the popup when someone clicks outside the popup
372
385
  # (but not on a popup opener).
373
- up.on('click', 'body', (event, $body) ->
386
+ up.on('mousedown', 'body', (event, $body) ->
374
387
  $target = $(event.target)
375
- unless $target.closest('.up-popup').length || $target.closest('[up-popup]').length
376
- close()
388
+ unless $target.closest('.up-popup, [up-popup]').length
389
+ closeAsap()
377
390
  )
378
391
 
379
392
  up.on('up:fragment:inserted', (event, $fragment) ->
380
393
  if contains($fragment)
381
394
  if newSource = $fragment.attr('up-source')
382
- currentUrl = newSource
395
+ state.url = newSource
383
396
  else if contains(event.origin)
384
397
  autoclose()
385
398
  )
386
399
 
387
400
  # Close the pop-up overlay when the user presses ESC.
388
- up.bus.onEscape(-> close())
401
+ up.bus.onEscape(closeAsap)
389
402
 
390
403
  ###*
391
404
  When an element with this attribute is clicked,
@@ -402,8 +415,8 @@ up.popup = (($) ->
402
415
  @stable
403
416
  ###
404
417
  up.on('click', '[up-close]', (event, $element) ->
405
- if $element.closest('.up-popup').length
406
- close()
418
+ if contains($element)
419
+ closeAsap()
407
420
  # Only prevent the default when we actually closed a popup.
408
421
  # This way we can have buttons that close a popup when within a popup,
409
422
  # but link to a destination if not.
@@ -414,15 +427,12 @@ up.popup = (($) ->
414
427
  up.on 'up:framework:reset', reset
415
428
 
416
429
  knife: eval(Knife?.point)
417
- attach: attach
418
- close: close
419
- url: -> currentUrl
420
- coveredUrl: coveredUrl
430
+ attach: attachAsap
431
+ close: closeAsap
432
+ url: -> state.url
433
+ coveredUrl: -> state.coveredUrl
421
434
  config: config
422
- defaults: -> u.error('up.popup.defaults(...) no longer exists. Set values on he up.popup.config property instead.')
423
435
  contains: contains
424
- open: -> up.error('up.popup.open no longer exists. Please use up.popup.attach instead.')
425
- source: -> up.error('up.popup.source no longer exists. Please use up.popup.url instead.')
426
436
  isOpen: isOpen
427
437
 
428
438
  )(jQuery)
@@ -231,8 +231,6 @@ up.proxy = (($) ->
231
231
  loadStarted()
232
232
  promise.always(loadEnded)
233
233
 
234
- console.groupEnd()
235
-
236
234
  promise
237
235
 
238
236
  ###*
@@ -50,7 +50,7 @@ up.syntax = (($) ->
50
50
  // your code here
51
51
  });
52
52
 
53
- The functions will be called on elements maching `.action` when
53
+ The functions will be called on elements matching `.action` when
54
54
  the page loads, or whenever a matching fragment is [updated through Unpoly](/up.replace)
55
55
  later.
56
56
 
@@ -44,46 +44,68 @@ up.tooltip = (($) ->
44
44
  The animation used to open a tooltip.
45
45
  @param {String} [config.closeAnimation='fade-out']
46
46
  The animation used to close a tooltip.
47
+ @param {Number} [config.openDuration]
48
+ The duration of the open animation (in milliseconds).
49
+ @param {Number} [config.closeDuration]
50
+ The duration of the close animation (in milliseconds).
51
+ @param {String} [config.openEasing]
52
+ The timing function controlling the acceleration of the opening animation.
53
+ @param {String} [config.closeEasing]
54
+ The timing function controlling the acceleration of the closing animation.
47
55
  @stable
48
56
  ###
49
57
  config = u.config
50
58
  position: 'top'
51
59
  openAnimation: 'fade-in'
52
60
  closeAnimation: 'fade-out'
61
+ openDuration: 100
62
+ closeDuration: 50
63
+ openEasing: null
64
+ closeEasing: null
65
+
66
+ state = u.config
67
+ phase: 'closed' # can be 'opening', 'opened', 'closing' and 'closed'
68
+ $anchor: null # the element to which the tooltip is anchored
69
+ $tooltip: null # the tooltiop element
70
+ position: null # the position of the tooltip element relative to its anchor
71
+
72
+ chain = new u.DivertibleChain()
53
73
 
54
74
  reset = ->
55
75
  # Destroy the tooltip container regardless whether it's currently in a closing animation
56
- close(animation: false)
76
+ state.$tooltip?.remove()
77
+ state.reset()
78
+ chain.reset()
57
79
  config.reset()
58
80
 
59
- setPosition = ($link, $tooltip, position) ->
81
+ align = ->
60
82
  css = {}
61
- tooltipBox = u.measure($tooltip)
83
+ tooltipBox = u.measure(state.$tooltip)
62
84
 
63
- if u.isFixed($link)
64
- linkBox = $link.get(0).getBoundingClientRect()
85
+ if u.isFixed(state.$anchor)
86
+ linkBox = state.$anchor.get(0).getBoundingClientRect()
65
87
  css['position'] = 'fixed'
66
88
  else
67
- linkBox = u.measure($link)
89
+ linkBox = u.measure(state.$anchor)
68
90
 
69
- switch position
70
- when "top"
91
+ switch state.position
92
+ when 'top'
71
93
  css['top'] = linkBox.top - tooltipBox.height
72
94
  css['left'] = linkBox.left + 0.5 * (linkBox.width - tooltipBox.width)
73
- when "left"
95
+ when 'left'
74
96
  css['top'] = linkBox.top + 0.5 * (linkBox.height - tooltipBox.height)
75
97
  css['left'] = linkBox.left - tooltipBox.width
76
- when "right"
98
+ when 'right'
77
99
  css['top'] = linkBox.top + 0.5 * (linkBox.height - tooltipBox.height)
78
100
  css['left'] = linkBox.left + linkBox.width
79
- when "bottom"
101
+ when 'bottom'
80
102
  css['top'] = linkBox.top + linkBox.height
81
103
  css['left'] = linkBox.left + 0.5 * (linkBox.width - tooltipBox.width)
82
104
  else
83
- u.error("Unknown position option '%s'", position)
105
+ u.error("Unknown position option '%s'", state.position)
84
106
 
85
- $tooltip.attr('up-position', position)
86
- $tooltip.css(css)
107
+ state.$tooltip.attr('up-position', state.position)
108
+ state.$tooltip.css(css)
87
109
 
88
110
  createElement = (options) ->
89
111
  $element = u.$createElementFromSelector('.up-tooltip')
@@ -92,7 +114,7 @@ up.tooltip = (($) ->
92
114
  else
93
115
  $element.html(options.html)
94
116
  $element.appendTo(document.body)
95
- $element
117
+ state.$tooltip = $element
96
118
 
97
119
  ###*
98
120
  Opens a tooltip over the given element.
@@ -100,7 +122,7 @@ up.tooltip = (($) ->
100
122
  up.tooltip.attach('.help', {
101
123
  html: 'Enter multiple words or phrases'
102
124
  });
103
-
125
+
104
126
  @function up.tooltip.attach
105
127
  @param {Element|jQuery|String} elementOrSelector
106
128
  @param {String} [options.html]
@@ -114,33 +136,68 @@ up.tooltip = (($) ->
114
136
  A promise that will be resolved when the tooltip's opening animation has finished.
115
137
  @stable
116
138
  ###
117
- attach = (linkOrSelector, options = {}) ->
118
- $link = $(linkOrSelector)
119
- html = u.option(options.html, $link.attr('up-tooltip-html'))
120
- text = u.option(options.text, $link.attr('up-tooltip'))
121
- position = u.option(options.position, $link.attr('up-position'), config.position)
122
- animation = u.option(options.animation, u.castedAttr($link, 'up-animation'), config.openAnimation)
123
- animateOptions = up.motion.animateOptions(options, $link)
124
- close()
125
- $tooltip = createElement(text: text, html: html)
126
- setPosition($link, $tooltip, position)
127
- up.animate($tooltip, animation, animateOptions)
139
+ attachAsap = (elementOrSelector, options = {}) ->
140
+ curriedAttachNow = -> attachNow(elementOrSelector, options)
141
+ if isOpen()
142
+ chain.asap(closeNow, curriedAttachNow)
143
+ else
144
+ chain.asap(curriedAttachNow)
145
+ chain.promise()
146
+
147
+ attachNow = (elementOrSelector, options) ->
148
+ $anchor = $(elementOrSelector)
149
+ options = u.options(options)
150
+ html = u.option(options.html, $anchor.attr('up-tooltip-html'))
151
+ text = u.option(options.text, $anchor.attr('up-tooltip'))
152
+ position = u.option(options.position, $anchor.attr('up-position'), config.position)
153
+ animation = u.option(options.animation, u.castedAttr($anchor, 'up-animation'), config.openAnimation)
154
+ animateOptions = up.motion.animateOptions(options, $anchor, duration: config.openDuration, easing: config.openEasing)
155
+
156
+ state.phase = 'opening'
157
+ state.$anchor = $anchor
158
+ createElement(text: text, html: html)
159
+ state.position = position
160
+ align()
161
+ up.animate(state.$tooltip, animation, animateOptions).then ->
162
+ state.phase = 'opened'
128
163
 
129
164
  ###*
130
165
  Closes a currently shown tooltip.
131
166
  Does nothing if no tooltip is currently shown.
132
-
167
+
133
168
  @function up.tooltip.close
134
169
  @param {Object} options
135
170
  See options for [`up.animate`](/up.animate).
171
+ @return {Promise}
172
+ A promise for the end of the closing animation.
136
173
  @stable
137
174
  ###
138
- close = (options) ->
139
- $tooltip = $('.up-tooltip')
140
- if $tooltip.length
141
- options = u.options(options, animation: config.closeAnimation)
142
- options = u.merge(options, up.motion.animateOptions(options))
143
- up.destroy($tooltip, options)
175
+ closeAsap = (options) ->
176
+ if isOpen()
177
+ chain.asap -> closeNow(options)
178
+ chain.promise()
179
+
180
+ closeNow = (options) ->
181
+ unless isOpen() # this can happen when a request fails and the chain proceeds to the next task
182
+ return u.resolvedPromise()
183
+
184
+ options = u.options(options, animation: config.closeAnimation)
185
+ animateOptions = up.motion.animateOptions(options, duration: config.closeDuration, easing: config.closeEasing)
186
+ u.extend(options, animateOptions)
187
+ state.phase = 'closing'
188
+ up.destroy(state.$tooltip, options).then ->
189
+ state.phase = 'closed'
190
+ state.$tooltip = null
191
+ state.$anchor = null
192
+
193
+ ###*
194
+ Returns whether a tooltip is currently showing.
195
+
196
+ @function up.tooltip.isOpen
197
+ @stable
198
+ ###
199
+ isOpen = ->
200
+ state.phase == 'opening' || state.phase == 'opened'
144
201
 
145
202
  ###*
146
203
  Displays a tooltip with text content when hovering the mouse over this element:
@@ -171,30 +228,28 @@ up.tooltip = (($) ->
171
228
  @selector [up-tooltip-html]
172
229
  @stable
173
230
  ###
174
- up.compiler('[up-tooltip], [up-tooltip-html]', ($link) ->
231
+ up.compiler('[up-tooltip], [up-tooltip-html]', ($opener) ->
175
232
  # Don't register these events on document since *every*
176
233
  # mouse move interaction bubbles up to the document.
177
- $link.on('mouseenter', -> attach($link))
178
- $link.on('mouseleave', -> close())
234
+ $opener.on('mouseenter', -> attachAsap($opener))
235
+ $opener.on('mouseleave', -> closeAsap())
179
236
  )
180
237
 
181
238
  # Close the tooltip when someone clicks anywhere.
182
239
  up.on('click', 'body', (event, $body) ->
183
- close()
240
+ closeAsap()
184
241
  )
185
242
 
186
243
  # The framework is reset between tests, so also close
187
244
  # a currently open tooltip.
188
- up.on 'up:framework:reset', close
245
+ up.on 'up:framework:reset', reset
189
246
 
190
247
  # Close the tooltip when the user presses ESC.
191
- up.bus.onEscape(-> close())
192
-
193
- # The framework is reset between tests
194
- up.on 'up:framework:reset', reset
248
+ up.bus.onEscape(-> closeAsap())
195
249
 
196
- attach: attach
197
- close: close
198
- open: -> u.error('up.tooltip.open no longer exists. Use up.tooltip.attach instead.')
250
+ config: config
251
+ attach: attachAsap
252
+ isOpen: isOpen
253
+ close: closeAsap
199
254
 
200
255
  )(jQuery)