###* Pop-up overlays =============== Instead of [linking to a page fragment](/up.link), you can choose to show a fragment in a popup overlay that rolls down from an anchoring element. To open a popup, add an [`up-popup` attribute](/up-popup) to a link, or call the Javascript function [`up.popup.attach`](/up.popup.attach). For modal dialogs see [up.modal](/up.modal) instead. \#\#\#\# Customizing the popup design Loading the Unpoly stylesheet will give you a minimal popup design: - Popup contents are displayed in a white box - There is a a subtle box shadow around the popup - The box will grow to fit the popup contents The easiest way to change how the popup looks is by overriding the [default CSS styles](https://github.com/unpoly/unpoly/blob/master/lib/assets/stylesheets/up/popup.css.sass). By default the popup uses the following DOM structure:
...
\#\#\#\# Closing behavior The popup closes when the user clicks anywhere outside the popup area. By default the popup also closes *when a link within the popup changes a fragment behind the popup*. This is useful to have the popup interact with the page that opened it, e.g. by updating parts of a larger form or by signing in a user and revealing additional information. To disable this behavior, give the opening link an `up-sticky` attribute: Settings @class up.popup ### 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. Valid values are `bottom-right`, `bottom-left`, `top-right` and `top-left`. @param {String} [config.history=false] Whether opening a popup will add a browser history entry. @param {String} [config.openAnimation='fade-in'] The animation used to open a popup. @param {String} [config.closeAnimation='fade-out'] The animation used to close a popup. @param {String} [config.openDuration] The duration of the open animation (in milliseconds). @param {String} [config.closeDuration] The duration of the close animation (in milliseconds). @param {String} [config.openEasing] The timing function controlling the acceleration of the opening animation. @param {String} [config.closeEasing] The timing function controlling the acceleration of the closing animation. @param {Boolean} [options.sticky=false] If set to `true`, the popup remains open even it changes the page in the background. @stable ### config = u.config openAnimation: 'fade-in' closeAnimation: 'fade-out' openDuration: null closeDuration: null openEasing: null closeEasing: null position: 'bottom-right' history: false reset = -> close(animation: false) config.reset() setPosition = ($link, position) -> linkBox = u.measure($link, full: true) css = switch position when "bottom-right" right: linkBox.right top: linkBox.top + linkBox.height when "bottom-left" left: linkBox.left top: linkBox.top + linkBox.height when "top-right" right: linkBox.right bottom: linkBox.top when "top-left" left: linkBox.left bottom: linkBox.top else u.error("Unknown position option '%s'", position) if u.isFixed($link) css['position'] = 'fixed' $popup = $('.up-popup') $popup.attr('up-position', position) $popup.css(css) ensureInViewport($popup) ensureInViewport = ($popup) -> box = u.measure($popup, full: true) errorX = null errorY = null if box.right < 0 errorX = -box.right # errorX is positive if box.bottom < 0 errorY = -box.bottom # errorY is positive if box.left < 0 errorX = box.left # errorX is negative if box.top < 0 errorY = box.top # errorY is negative if errorX # We use parseInt to: # 1) convert "50px" to 50 # 2) convert "auto" to NaN if left = parseInt($popup.css('left')) $popup.css('left', left - errorX) else if right = parseInt($popup.css('right')) $popup.css('right', right + errorX) if errorY if top = parseInt($popup.css('top')) $popup.css('top', top - errorY) else if bottom = parseInt($popup.css('bottom')) $popup.css('bottom', bottom + errorY) 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 ###* Returns whether popup modal is currently open. @function up.popup.isOpen @stable ### isOpen = -> $('.up-popup').length > 0 ###* 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. @param {String} [options.position='bottom-right'] Defines where the popup is attached to the opening element. Valid values are `bottom-right`, `bottom-left`, `top-right` and `top-left`. @param {String} [options.confirm] A message that will be displayed in a cancelable confirmation dialog before the modal is being opened. @param {String} [options.animation] The animation to use when opening the popup. @param {Number} [options.duration] The duration of the animation. See [`up.animate`](/up.animate). @param {Number} [options.delay] The delay before the animation starts. See [`up.animate`](/up.animate). @param {String} [options.easing] The timing function that controls the animation's acceleration. [`up.animate`](/up.animate). @param {Boolean} [options.sticky=false] If set to `true`, the popup remains open even if the page changes in the background. @param {Object} [options.history=false] @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) options = u.options(options) url = u.option(u.pluckKey(options, 'url'), $link.attr('up-href'), $link.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')) animateOptions = up.motion.animateOptions(options, $link, 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) 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) promise = promise.then -> up.animate($('.up-popup'), options.animation, animateOptions) promise = promise.then -> 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 @param event.preventDefault() Event listeners may call this method to prevent the popup from opening. @stable ### ###* 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. Emits events [`up:popup:close`](/up:popup:close) and [`up:popup:closed`](/up:popup:closed). @function up.popup.close @param {Object} options See options for [`up.animate`](/up.animate). @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() ###* This event is [emitted](/up.emit) when a popup dialog is starting to [close](/up.popup.close). @event up:popup:close @param event.preventDefault() Event listeners may call this method to prevent the popup from closing. @stable ### ###* This event is [emitted](/up.emit) when a popup dialog is done [closing](/up.popup.close). @event up:popup:closed @stable ### autoclose = -> unless $('.up-popup').is('[up-sticky]') discardHistory() close() ###* Returns whether the given element or selector is contained within the current popup. @methods up.popup.contains @param {String} elementOrSelector @stable ### contains = (elementOrSelector) -> $element = $(elementOrSelector) $element.closest('.up-popup').length > 0 ###* Opens this link's destination of in a popup overlay: Switch deck If the `up-sticky` attribute is set, the dialog does not auto-close if a page fragment behind the popup overlay updates: Switch deck Settings @selector [up-popup] @param [up-position] Defines where the popup is attached to the opening element. Valid values are `bottom-right`, `bottom-left`, `top-right` and `top-left`. @param {String} [up-confirm] A message that will be displayed in a cancelable confirmation dialog before the popup is opened. @param [up-sticky] If set to `true`, the popup remains open even if the page changes in the background. @param {String} [up-history='false'] Whether to push an entry to the browser history for the popup's source URL. Set this to `'false'` to prevent the URL bar from being updated. Set this to a URL string to update the history with the given URL. @stable ### up.link.onAction('[up-popup]', ($link) -> if $link.is('.up-current') close() else attach($link) ) # Close the popup when someone clicks outside the popup # (but not on a popup opener). up.on('click', 'body', (event, $body) -> $target = $(event.target) unless $target.closest('.up-popup').length || $target.closest('[up-popup]').length close() ) up.on('up:fragment:inserted', (event, $fragment) -> if contains($fragment) if newSource = $fragment.attr('up-source') currentUrl = newSource else if contains(event.origin) autoclose() ) # Close the pop-up overlay when the user presses ESC. up.bus.onEscape(-> close()) ###* When an element with this attribute is clicked, a currently open popup is closed. Does nothing if no popup is currently open. To make a link that closes the current popup, but follows to a fallback destination if no popup is open: Okay @selector [up-close] @stable ### up.on('click', '[up-close]', (event, $element) -> if $element.closest('.up-popup').length close() # 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 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)