###** 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](/a-up-popup) to a link: Show options When this link is clicked, Unpoly will request the path `/options` and extract an element matching the selector `.menu` from the response. The matching element will then be placed in the popup overlay. \#\#\# Closing behavior The popup closes when the user clicks anywhere outside the popup area. 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. To disable this behavior, give the opening link an [`up-sticky`](/a-up-popup#up-sticky) attribute. \#\#\# Customizing the popup design Popups have a minimal default 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 to override the [default CSS styles](https://github.com/unpoly/unpoly/blob/master/lib/assets/stylesheets/unpoly/popup.sass). The HTML of a popup element looks like this:
Fragment content here
The popup element is appended to the [viewport](/up.viewport) of the anchor element. @module up.popup ### up.popup = do -> u = up.util e = up.element ###** Sets default options for future popups. @property up.popup.config @param {string} [config.position='bottom'] Defines on which side of the opening element the popup is attached. Valid values are `'top'`, `'right'`, `'bottom'` and `'left'`. @param {string} [config.align='left'] Defines the alignment of the popup along its side. When the popup's `{ position }` is `'top'` or `'bottom'`, valid `{ align }` values are `'left'`, `center'` and `'right'`. When the popup's `{ position }` is `'left'` or `'right'`, valid `{ align }` values are `top'`, `center'` and `bottom'`. @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 = new up.Config openAnimation: 'fade-in' closeAnimation: 'fade-out' openDuration: 150 closeDuration: 100 openEasing: null closeEasing: null position: 'bottom' align: 'left' history: false ###** Returns the URL from which the current popup's contents were loaded. Returns `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 = new up.Config phase: 'closed' # can be 'opening', 'opened', 'closing' and 'closed' anchor: null # the element to which the tooltip is anchored popup: null # the .up-popup element content: null # the .up-popup-content element tether: null # the up.Tether instance controlling the popup's position position: null # the position of the popup container element relative to its anchor align: null sticky: null url: null coveredUrl: null coveredTitle: null chain = new up.DivertibleChain() reset = -> state.tether?.destroy() state.reset() chain.reset() config.reset() discardHistory = -> state.coveredTitle = null state.coveredUrl = null createHiddenFrame = (targetSelector) -> state.tether = new up.Tether(u.only(state, 'anchor', 'position', 'align')) state.popup = e.affix(state.tether.root, '.up-popup', 'up-position': state.position, 'up-align': state.align) state.content = e.affix(state.popup, '.up-popup-content') # Create an empty element that will match the # selector that is being replaced. up.fragment.createPlaceholder(targetSelector, state.content) e.hide(state.popup) unveilFrame = -> e.show(state.popup) ###** Forces the popup to update its position relative to its anchor element. Unpoly automatically keep popups aligned when the document is resized or scrolled. Complex layout changes may make it necessary to call this function. @function up.popup.sync @experimental ### syncPosition = -> state.tether?.sync() ###** Returns whether popup modal is currently open. @function up.popup.isOpen @return {boolean} @stable ### isOpen = -> 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} anchor The element to which the popup will be attached. @param {string} [options.url] The URL from which to fetch the popup contents. If omitted, the `href` or `up-href` attribute of the anchor element will be used. Will be ignored if `options.html` is given. @param {string} [options.target] A CSS selector that will be extracted from the response and placed into the popup. @param {string} [options.position='bottom'] Defines on which side of the opening element the popup is attached. Valid values are `'top'`, `'right'`, `'bottom'` and `'left'`. @param {string} [options.align='left'] Defines the alignment of the popup along its side. When the popup's `{ position }` is `'top'` or `'bottom'`, valid `{ align }` values are `'left'`, `center'` and `'right'`. When the popup's `{ position }` is `'left'` or `'right'`, valid `{ align }` values are `top'`, `center'` and `bottom'`. @param {string} [options.html] A string of HTML from which to extract the popup contents. No network request will be made. @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 {string} [options.method="GET"] Override the request method. @param {boolean} [options.sticky=false] If set to `true`, the popup remains open even if the page changes in the background. @param {boolean} [options.history=false] @return {Promise} A promise that will be fulfilled when the popup has been loaded and the opening animation has completed. @stable ### attachAsap = (elementOrSelector, options) -> chain.asap closeNow, (-> attachNow(elementOrSelector, options)) attachNow = (elementOrSelector, options) -> anchor = e.get(elementOrSelector) options ?= {} url = u.pluckKey(options, 'url') ? anchor.getAttribute('up-href') ? anchor.getAttribute('href') html = u.pluckKey(options, 'html') url or html or up.fail('up.popup.attach() requires either an { url } or { html } option') target = u.pluckKey(options, 'target') ? anchor.getAttribute('up-popup') or up.fail('No target selector given for [up-popup]') position = options.position ? anchor.getAttribute('up-position') ? config.position align = options.align ? anchor.getAttribute('up-align') ? config.align options.animation ?= anchor.getAttribute('up-animation') ? config.openAnimation options.sticky ?= e.booleanAttr(anchor, 'up-sticky') ? config.sticky options.history = if up.browser.canPushState() then (options.history ? e.booleanOrStringAttr(anchor, 'up-history') ? config.history) else false options.confirm ?= anchor.getAttribute('up-confirm') options.method = up.link.followMethod(anchor, options) options.layer = 'popup' options.failTarget ?= anchor.getAttribute('up-fail-target') options.failLayer ?= anchor.getAttribute('up-fail-layer') # This will prevent up.replace() from looking for fallbacks, since # it knows the target will always exist. options.provideTarget = -> createHiddenFrame(target) animateOptions = up.motion.animateOptions(options, anchor, duration: config.openDuration, easing: config.openEasing) extractOptions = u.merge(options, animation: false) if options.preload && url return up.replace(target, url, options) up.browser.whenConfirmed(options).then -> up.event.whenEmitted('up:popup:open', url: url, anchor: anchor, log: 'Opening popup').then -> state.phase = 'opening' state.anchor = anchor state.position = position state.align = align if options.history state.coveredUrl = up.browser.url() state.coveredTitle = document.title state.sticky = options.sticky if html promise = up.extract(target, html, extractOptions) else promise = up.replace(target, url, extractOptions) promise = promise.then -> unveilFrame() syncPosition() up.animate(state.popup, options.animation, animateOptions) promise = promise.then -> state.phase = 'opened' up.emit(state.popup, 'up:popup:opened', anchor: state.anchor, log: 'Popup opened')# promise ###** This event is [emitted](/up.emit) when a popup is starting to open. @event up:popup:open @param {Element} event.anchor The element to which the popup will be attached. @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 @param {Element} event.anchor The element to which the popup was attached. @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 fulfilled once the modal's close animation has finished. @stable ### closeAsap = (options) -> chain.asap -> closeNow(options) closeNow = (options) -> unless isOpen() # this can happen when a request fails and the chain proceeds to the next task return Promise.resolve() options = u.options(options, animation: config.closeAnimation history: state.coveredUrl, title: state.coveredTitle ) animateOptions = up.motion.animateOptions(options, duration: config.closeDuration, easing: config.closeEasing) u.assign(options, animateOptions) up.event.whenEmitted('up:popup:close', anchor: state.anchor, log: 'Closing popup').then -> state.phase = 'closing' state.url = null state.coveredUrl = null state.coveredTitle = null up.destroy(state.popup, options).then -> state.phase = 'closed' state.tether.destroy() state.tether = null state.popup = null state.content = null state.anchor = null state.sticky = null up.emit('up:popup:closed', anchor: state.anchor, log: 'Popup closed') preloadNow = (link, options) -> options = u.options(options) options.preload = true # Use attachNow() and not attachAsap() so (1) we don't close a currently open popup # and (2) our pending AJAX request does not prevent other popups from opening attachNow(link, options) toggleAsap = (link, options) -> if link.classList.contains('up-current') closeAsap() else attachAsap(link, options) ###** This event is [emitted](/up.emit) when a popup dialog is starting to [close](/up.popup.close). @event up:popup:close @param {Element} event.anchor The element to which the popup is attached. @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 @param {Element} event.anchor The element to which the popup was attached. @stable ### autoclose = -> unless state.sticky discardHistory() closeAsap() ###** Returns whether the given element or selector is contained within the current popup. @methods up.popup.contains @param {string} elementOrSelector The element to test @return {boolean} @stable ### contains = (elementOrSelector) -> element = e.get(elementOrSelector) !!e.closest(element, '.up-popup') ###** 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 a[up-popup] @param {string} up-popup The CSS selector that will be extracted from the response and displayed in a popup overlay. @param {string} [up-position] Defines on which side of the opening element the popup is attached. Valid values are `'top'`, `'right'`, `'bottom'` and `'left'`. @param {string} [up-align] Defines the alignment of the popup along its side. When the popup's `{ position }` is `'top'` or `'bottom'`, valid `{ align }` values are `'left'`, `center'` and `'right'`. When the popup's `{ position }` is `'left'` or `'right'`, valid `{ align }` values are `top'`, `center'` and `bottom'`. @param {string} [up-confirm] A message that will be displayed in a cancelable confirmation dialog before the popup is opened. @param {string} [up-method='GET'] Override the request method. @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.addFollowVariant '[up-popup]', # Don't just pass the `toggleAsap` function reference so we can stub it in tests follow: (link, options) -> toggleAsap(link, options) preload: (link, options) -> preloadNow(link, options) # We close the popup when someone clicks on the document. # We also need to listen to up:action:consumed in case an [up-instant] link # was followed on mousedown. up.on 'click up:action:consumed', (event) -> target = event.target # Don't close when the user clicked on a popup opener. unless e.closest(target, '.up-popup, [up-popup]') u.muteRejection closeAsap() # Do not halt the event chain here. The user is allowed to directly activate # a link in the background, even with a (now closing) popup open. up.on 'up:fragment:inserted', (event, fragment) -> if contains(fragment) if newSource = fragment.getAttribute('up-source') state.url = newSource else if event.origin && contains(event.origin) u.muteRejection autoclose() # Close the pop-up overlay when the user presses ESC. up.event.onEscape -> u.muteRejection closeAsap() ###** When this element is clicked, a currently open [popup](/up.popup) is closed. Does nothing if no popup is currently open. \#\#\# Example Clickin on this `` will close a currently open popup: Close this popup When a popup changes the current URL, you might need to deal with content being displayed as either a popup or a full page. To make a link that closes the current popup, but follows to a fallback destination if no popup is open: Okay @selector .up-popup [up-close] @stable ### up.on 'click', '.up-popup [up-close]', (event) -> u.muteRejection 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. up.event.consumeAction(event) # When the user uses the back button we will usually restore or a base container. # We close any open modal because it probably won't match the restored state. up.on 'up:history:restore', -> u.muteRejection closeAsap() # The framework is reset between tests up.on 'up:framework:reset', reset <% if ENV['JS_KNIFE'] %>knife: eval(Knife.point)<% end %> attach: attachAsap close: closeAsap url: -> state.url coveredUrl: -> state.coveredUrl config: config contains: contains isOpen: isOpen sync: syncPosition