lib/assets/javascripts/unpoly/popup.js.coffee in unpoly-rails-0.26.2 vs lib/assets/javascripts/unpoly/popup.js.coffee in unpoly-rails-0.27.0

- old
+ new

@@ -45,31 +45,10 @@ up.popup = (($) -> u = up.util ###* - Returns the source URL for the fragment displayed - in the current popup, or `undefined` if no popup is open. - - @function up.popup.url - @return {String} - the source URL - @stable - ### - currentUrl = undefined - - ###* - Returns the URL of the page or modal behind the popup. - - @function up.popup.coveredUrl - @return {String} - @experimental - ### - coveredUrl = -> - $('.up-popup').attr('up-covered-url') - - ###* Sets default options for future popups. @property up.popup.config @param {String} [config.position='bottom-right'] Defines where the popup is attached to the opening element. @@ -95,87 +74,105 @@ @stable ### config = u.config openAnimation: 'fade-in' closeAnimation: 'fade-out' - openDuration: null - closeDuration: null + openDuration: 150 + closeDuration: 100 openEasing: null closeEasing: null position: 'bottom-right' history: false + ###* + Returns the source URL for the fragment displayed + in the current popup, or `undefined` if no popup is open. + + @function up.popup.url + @return {String} + the source URL + @stable + ### + + ###* + Returns the URL of the page or modal behind the popup. + + @function up.popup.coveredUrl + @return {String} + @experimental + ### + + state = u.config + phase: 'closed' # can be 'opening', 'opened', 'closing' and 'closed' + $anchor: null # the element to which the tooltip is anchored + $popup: null # the popup container + position: null # the position of the popup container element relative to its anchor + sticky: null + url: null + coveredUrl: null + coveredTitle: null + + chain = new u.DivertibleChain() + reset = -> - close(animation: false) + state.$popup?.remove() + state.reset() + chain.reset() config.reset() - setPosition = ($link, position) -> + align = -> css = {} - $popup = $('.up-popup') - popupBox = u.measure($popup) + popupBox = u.measure(state.$popup) - if u.isFixed($link) - linkBox = $link.get(0).getBoundingClientRect() + if u.isFixed(state.$anchor) + linkBox = state.$anchor.get(0).getBoundingClientRect() css['position'] = 'fixed' else - linkBox = u.measure($link) + linkBox = u.measure(state.$anchor) - switch position - when "bottom-right" # anchored to bottom-right of link, opens towards bottom-left + switch state.position + when 'bottom-right' # anchored to bottom-right of link, opens towards bottom-left css['top'] = linkBox.top + linkBox.height css['left'] = linkBox.left + linkBox.width - popupBox.width - when "bottom-left" # anchored to bottom-left of link, opens towards bottom-right + when 'bottom-left' # anchored to bottom-left of link, opens towards bottom-right css['top'] = linkBox.top + linkBox.height css['left'] = linkBox.left - when "top-right" # anchored to top-right of link, opens to top-left + when 'top-right' # anchored to top-right of link, opens to top-left css['top'] = linkBox.top - popupBox.height css['left'] = linkBox.left + linkBox.width - popupBox.width - when "top-left" # anchored to top-left of link, opens to top-right + when 'top-left' # anchored to top-left of link, opens to top-right css['top'] = linkBox.top - popupBox.height css['left'] = linkBox.left else - u.error("Unknown position option '%s'", position) + u.error("Unknown position option '%s'", state.position) - $popup.attr('up-position', position) - $popup.css(css) + state.$popup.attr('up-position', state.position) + state.$popup.css(css) - discardHistory = -> - $popup = $('.up-popup') - $popup.removeAttr('up-covered-url') - $popup.removeAttr('up-covered-title') - - createFrame = (target, options) -> - promise = u.resolvedPromise() - if isOpen() - promise = promise.then -> close() - promise = promise.then -> - $popup = u.$createElementFromSelector('.up-popup') - $popup.attr('up-sticky', '') if options.sticky - $popup.attr('up-covered-url', up.browser.url()) - $popup.attr('up-covered-title', document.title) - # Create an empty element that will match the - # selector that is being replaced. - u.$createPlaceholder(target, $popup) - $popup.appendTo(document.body) - $popup - return promise + createFrame = (target) -> + $popup = u.$createElementFromSelector('.up-popup') + # Create an empty element that will match the + # selector that is being replaced. + u.$createPlaceholder(target, $popup) + $popup.appendTo(document.body) + state.$popup = $popup ###* Returns whether popup modal is currently open. @function up.popup.isOpen @stable ### isOpen = -> - $('.up-popup').length > 0 + state.phase == 'opened' || state.phase == 'opening' ###* Attaches a popup overlay to the given element or selector. Emits events [`up:popup:open`](/up:popup:open) and [`up:popup:opened`](/up:popup:opened). - + @function up.popup.attach @param {Element|jQuery|String} elementOrSelector @param {String} [options.url] @param {String} [options.target] A CSS selector that will be extracted from the response and placed into the popup. @@ -203,45 +200,55 @@ @return {Promise} A promise that will be resolved when the popup has been loaded and the opening animation has completed. @stable ### - attach = (linkOrSelector, options) -> - $link = $(linkOrSelector) - $link.length or u.error('Cannot attach popup to non-existing element %o', linkOrSelector) - + attachAsap = (elementOrSelector, options) -> + curriedAttachNow = -> attachNow(elementOrSelector, options) + if isOpen() + chain.asap(closeNow, curriedAttachNow) + else + chain.asap(curriedAttachNow) + chain.promise() + + attachNow = (elementOrSelector, options) -> + $anchor = $(elementOrSelector) + $anchor.length or u.error('Cannot attach popup to non-existing element %o', elementOrSelector) + options = u.options(options) - url = u.option(u.pluckKey(options, 'url'), $link.attr('up-href'), $link.attr('href')) + url = u.option(u.pluckKey(options, 'url'), $anchor.attr('up-href'), $anchor.attr('href')) html = u.option(u.pluckKey(options, 'html')) - target = u.option(u.pluckKey(options, 'target'), $link.attr('up-popup'), 'body') - options.position = u.option(options.position, $link.attr('up-position'), config.position) - options.animation = u.option(options.animation, $link.attr('up-animation'), config.openAnimation) - options.sticky = u.option(options.sticky, u.castedAttr($link, 'up-sticky'), config.sticky) - options.history = if up.browser.canPushState() then u.option(options.history, u.castedAttr($link, 'up-history'), config.history) else false - options.confirm = u.option(options.confirm, $link.attr('up-confirm')) - options.method = up.link.followMethod($link, options) - animateOptions = up.motion.animateOptions(options, $link, duration: config.openDuration, easing: config.openEasing) + target = u.option(u.pluckKey(options, 'target'), $anchor.attr('up-popup'), 'body') + position = u.option(options.position, $anchor.attr('up-position'), config.position) + options.animation = u.option(options.animation, $anchor.attr('up-animation'), config.openAnimation) + options.sticky = u.option(options.sticky, u.castedAttr($anchor, 'up-sticky'), config.sticky) + options.history = if up.browser.canPushState() then u.option(options.history, u.castedAttr($anchor, 'up-history'), config.history) else false + options.confirm = u.option(options.confirm, $anchor.attr('up-confirm')) + options.method = up.link.followMethod($anchor, options) + animateOptions = up.motion.animateOptions(options, $anchor, duration: config.openDuration, easing: config.openEasing) - up.browser.confirm(options).then -> - if up.bus.nobodyPrevents('up:popup:open', url: url, message: 'Opening popup') - options.beforeSwap = -> createFrame(target, options) + up.browser.whenConfirmed(options).then -> + up.bus.whenEmitted('up:popup:open', url: url, message: 'Opening popup').then -> + state.phase = 'opening' + state.$anchor = $anchor + state.position = position + state.coveredUrl = up.browser.url() + state.coveredTitle = document.title + state.sticky = options.sticky + options.beforeSwap = -> createFrame(target) extractOptions = u.merge(options, animation: false) if html promise = up.extract(target, html, extractOptions) else promise = up.replace(target, url, extractOptions) promise = promise.then -> - setPosition($link, options.position) + align() + up.animate(state.$popup, options.animation, animateOptions) promise = promise.then -> - up.animate($('.up-popup'), options.animation, animateOptions) - promise = promise.then -> - up.emit('up:popup:opened', message: 'Popup opened') + state.phase = 'opened' + up.emit('up:popup:opened', message: 'Popup opened')# promise - else - # Although someone prevented the destruction, keep a uniform API for - # callers by returning a promise that will never be resolved. - u.unresolvablePromise() ###* This event is [emitted](/up.emit) when a popup is starting to open. @event up:popup:open @@ -254,11 +261,11 @@ This event is [emitted](/up.emit) when a popup has finished opening. @event up:popup:opened @stable ### - + ###* Closes a currently opened popup overlay. Does nothing if no popup is currently open. @@ -270,32 +277,40 @@ @return {Promise} A promise that will be resolved once the modal's close animation has finished. @stable ### - close = (options) -> - $popup = $('.up-popup') - if $popup.length - if up.bus.nobodyPrevents('up:popup:close', $element: $popup) - options = u.options(options, - animation: config.closeAnimation, - url: $popup.attr('up-covered-url'), - title: $popup.attr('up-covered-title') - ) - animateOptions = up.motion.animateOptions(options, duration: config.closeDuration, easing: config.closeEasing) - u.extend(options, animateOptions) - currentUrl = undefined - promise = up.destroy($popup, options) - promise = promise.then -> up.emit('up:popup:closed', message: 'Popup closed') - promise - else - # Although someone prevented the destruction, keep a uniform API - # for callers by returning a promise that will never be resolved. - u.unresolvablePromise() - else - u.resolvedPromise() + closeAsap = (options) -> + if isOpen() + chain.asap -> closeNow(options) + chain.promise() + closeNow = (options) -> + unless isOpen() # this can happen when a request fails and the chain proceeds to the next task + return u.resolvedPromise() + + options = u.options(options, + animation: config.closeAnimation + url: state.coveredUrl, + title: state.coveredTitle + ) + animateOptions = up.motion.animateOptions(options, duration: config.closeDuration, easing: config.closeEasing) + u.extend(options, animateOptions) + + up.bus.whenEmitted('up:popup:close', message: 'Closing popup', $element: state.$popup).then -> + state.phase = 'closing' + state.url = null + state.coveredUrl = null + state.coveredTitle = null + + up.destroy(state.$popup, options).then -> + state.phase = 'closed' + state.$popup = null + state.$anchor = null + state.sticky = null + up.emit('up:popup:closed', message: 'Popup closed') + ###* This event is [emitted](/up.emit) when a popup dialog is starting to [close](/up.popup.close). @event up:popup:close @@ -311,13 +326,11 @@ @event up:popup:closed @stable ### autoclose = -> - unless $('.up-popup').is('[up-sticky]') - discardHistory() - close() + closeAsap() unless state.sticky ###* Returns whether the given element or selector is contained within the current popup. @@ -361,33 +374,33 @@ @stable ### up.link.onAction('[up-popup]', ($link) -> if $link.is('.up-current') - close() + closeAsap() else - attach($link) + attachAsap($link) ) # Close the popup when someone clicks outside the popup # (but not on a popup opener). - up.on('click', 'body', (event, $body) -> + up.on('mousedown', 'body', (event, $body) -> $target = $(event.target) - unless $target.closest('.up-popup').length || $target.closest('[up-popup]').length - close() + unless $target.closest('.up-popup, [up-popup]').length + closeAsap() ) up.on('up:fragment:inserted', (event, $fragment) -> if contains($fragment) if newSource = $fragment.attr('up-source') - currentUrl = newSource + state.url = newSource else if contains(event.origin) autoclose() ) # Close the pop-up overlay when the user presses ESC. - up.bus.onEscape(-> close()) + up.bus.onEscape(closeAsap) ###* When an element with this attribute is clicked, a currently open popup is closed. @@ -400,29 +413,26 @@ @selector [up-close] @stable ### up.on('click', '[up-close]', (event, $element) -> - if $element.closest('.up-popup').length - close() + if contains($element) + closeAsap() # Only prevent the default when we actually closed a popup. # This way we can have buttons that close a popup when within a popup, # but link to a destination if not. event.preventDefault() ) # The framework is reset between tests up.on 'up:framework:reset', reset knife: eval(Knife?.point) - attach: attach - close: close - url: -> currentUrl - coveredUrl: coveredUrl + attach: attachAsap + close: closeAsap + url: -> state.url + coveredUrl: -> state.coveredUrl config: config - defaults: -> u.error('up.popup.defaults(...) no longer exists. Set values on he up.popup.config property instead.') contains: contains - open: -> up.error('up.popup.open no longer exists. Please use up.popup.attach instead.') - source: -> up.error('up.popup.source no longer exists. Please use up.popup.url instead.') isOpen: isOpen )(jQuery)