unpoly-rails 0.26.2 → 0.27.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 (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)