###** Scrolling viewports =================== The `up.viewport` module controls the scroll position of scrollable containers ("viewports"). The default viewport for any web application is the main document. An application may define additional viewports by giving the CSS property `{ overflow-y: scroll }` to any `
`. \#\#\# Revealing new content When following a [link to a fragment](/a-up-target) Unpoly will automatically scroll the document's viewport to [reveal](/up.viewport) the updated content. You should [make Unpoly aware](/up.viewport.config#config.fixedTop) of fixed elements in your layout, such as navigation bars or headers. Unpoly will respect these sticky elements when [revealing updated fragments](/up.reveal). You should also [tell Unpoly](/up.viewport.config#config.viewports) when your application has more than one viewport, so Unpoly can pick the right viewport to scroll for each fragment update. \#\#\# Bootstrap integration When using Bootstrap integration (`unpoly-bootstrap3.js` and `unpoly-bootstrap3.css`) Unpoly will automatically be aware of sticky Bootstrap components such as [fixed navbar](https://getbootstrap.com/examples/navbar-fixed-top/). @module up.viewport ### up.viewport = do -> u = up.util e = up.element ###** Configures the application layout. @property up.viewport.config @param {Array} [config.viewports] An array of CSS selectors that find viewports (containers that scroll their contents). @param {Array} [config.fixedTop] An array of CSS selectors that find elements fixed to the top edge of the screen (using `position: fixed`). See [`[up-fixed="top"]`](/up-fixed-top) for details. @param {Array} [config.fixedBottom] An array of CSS selectors that find elements fixed to the bottom edge of the screen (using `position: fixed`). See [`[up-fixed="bottom"]`](/up-fixed-bottom) for details. @param {Array} [config.anchoredRight] An array of CSS selectors that find elements anchored to the right edge of the screen (using `right:0` with `position: fixed` or `position: absolute`). See [`[up-anchored="right"]`](/up-anchored-right) for details. @param {number} [config.revealSnap=50] When [revealing](/up.reveal) elements, Unpoly will scroll an viewport to the top when the revealed element is closer to the top than `config.revealSnap`. @param {number} [config.revealPadding=0] The desired padding between a [revealed](/up.reveal) element and the closest [viewport](/up.viewport) edge (in pixels). @param {number} [config.scrollSpeed=1] The speed of the scrolling motion when [scrolling](/up.scroll) with `{ behavior: 'smooth' }`. The default value (`1`) roughly corresponds to the speed of Chrome's [native smooth scrolling](https://developer.mozilla.org/en-US/docs/Web/API/ScrollToOptions/behavior). @stable ### config = new up.Config duration: 0 viewports: ['.up-modal-viewport', '[up-viewport]', '[up-fixed]'] fixedTop: ['[up-fixed~=top]'] fixedBottom: ['[up-fixed~=bottom]'] anchoredRight: ['[up-anchored~=right]', '[up-fixed~=top]', '[up-fixed~=bottom]', '[up-fixed~=right]'] revealSnap: 50 revealPadding: 0, scrollSpeed: 1 # up.legacy.renamedProperty(config, 'snap', 'revealSnap') # up.legacy.removedProperty(config, 'easing') # up.legacy.removedProperty(config, 'duration') lastScrollTops = new up.Cache size: 30, key: up.history.normalizeUrl scrollingController = new up.MotionController('scrolling') reset = -> config.reset() lastScrollTops.clear() scrollingController.reset() ###** Scrolls the given viewport to the given Y-position. A "viewport" is an element that has scrollbars, e.g. `` or a container with `overflow-x: scroll`. \#\#\# Example This will scroll a `
...
` to a Y-position of 100 pixels: up.scroll('.main', 100) \#\#\# Animating the scrolling motion The scrolling can (optionally) be animated. up.scroll('.main', 100, { behavior: 'smooth' }) If the given viewport is already in a scroll animation when `up.scroll()` is called a second time, the previous animation will instantly jump to the last frame before the next animation is started. @function up.scroll @param {string|Element|jQuery} viewport The container element to scroll. @param {number} scrollPos The absolute number of pixels to set the scroll position to. @param {string}[options.behavior='auto'] When set to `'auto'`, this will immediately scroll to the new position. When set to `'smooth'`, this will scroll smoothly to the new position. @param {number}[options.speed] The speed of the scrolling motion when scrolling with `{ behavior: 'smooth' }`. Defaults to `up.viewport.config.scrollSpeed`. @return {Promise} A promise that will be fulfilled when the scrolling ends. @experimental ### scroll = (viewport, scrollTop, options) -> motion = new up.ScrollMotion(viewport, scrollTop, options) scrollingController.startMotion(viewport, motion, options) ###** Finishes scrolling animations in the given element, its ancestors or its descendants. @function up.viewport.finishScrolling @param {string|Element|jQuery} @return {Promise} @internal ### finishScrolling = (element) -> # Don't emit expensive events if no animation can be running anyway return Promise.resolve() unless up.motion.isEnabled() scrollable = closest(element) scrollingController.finish(scrollable) ###** @function up.viewport.anchoredRight @internal ### anchoredRight = -> selector = config.anchoredRight.join(',') e.all(selector) ###** @function measureObstruction @return {Object} @internal ### measureObstruction = (viewportHeight) -> composeHeight = (obstructor, distanceFromEdgeProps) -> distanceFromEdge = u.sum(distanceFromEdgeProps, (prop) -> e.styleNumber(obstructor, prop)) || 0 distanceFromEdge + obstructor.offsetHeight measureTopObstructor = (obstructor) -> composeHeight(obstructor, ['top', 'margin-top']) measureBottomObstructor = (obstructor) -> composeHeight(obstructor, ['bottom', 'margin-bottom']) topObstructors = e.all(config.fixedTop.join(', ')) bottomObstructors = e.all(config.fixedBottom.join(', ')) topObstructions = u.map(topObstructors, measureTopObstructor) bottomObstructions = u.map(bottomObstructors, measureBottomObstructor) top: Math.max(0, topObstructions...) bottom: Math.max(0, bottomObstructions...) ###** Scroll's the given element's viewport so the first rows of the element are visible for the user. By default Unpoly will always reveal an element before updating it with JavaScript functions like [`up.replace()`](/up.replace) or UJS behavior like [`[up-target]`](/a-up-target). \#\#\# How Unpoly finds the viewport The viewport (the container that is going to be scrolled) is the closest parent of the element that is either: - the currently open [modal](/up.modal) - an element with the attribute `[up-viewport]` - the `` element - an element matching the selector you have configured using `up.viewport.config.viewports.push('my-custom-selector')` \#\#\# Fixed elements obstruction the viewport Many applications have a navigation bar fixed to the top or bottom, obstructing the view on an element. You can make `up.reveal()` aware of these fixed elements so it can scroll the viewport far enough so the revealed element is fully visible. To make `up.reveal()` aware fixed elements you can either: - give the element an attribute [`up-fixed="top"`](/up-fixed-top) or [`up-fixed="bottom"`](up-fixed-bottom) - [configure default options](/up.viewport.config) for `fixedTop` or `fixedBottom` @function up.reveal @param {string|Element|jQuery} element @param {number} [options.speed] @param {string} [options.snap] @param {string|Element|jQuery} [options.viewport] @param {boolean} [options.top=false] Whether to scroll the viewport so that the first element row aligns with the top edge of the viewport. @param {string}[options.behavior='auto'] When set to `'auto'`, this will immediately scroll to the new position. When set to `'smooth'`, this will scroll smoothly to the new position. @param {number}[options.speed] The speed of the scrolling motion when scrolling with `{ behavior: 'smooth' }`. Defaults to `up.viewport.config.scrollSpeed`. @param {number} [config.padding=0] The desired padding between the revealed element and the closest [viewport](/up.viewport) edge (in pixels). @param {number|boolean} [config.snap] Whether to snap to the top of the viewport if the new scroll position after revealing the element is close to the top edge. You may pass a maximum number of pixels under which to snap to the top. Passing `false` will disable snapping. Passing `true` will use the snap pixel value from `up.viewport.config.revealSnap`. @return {Promise} A promise that fulfills when the element is revealed. @stable ### reveal = (elementOrSelector, options) -> element = e.get(elementOrSelector) motion = new up.RevealMotion(element, options) scrollingController.startMotion(element, motion, options) ###** @function up.viewport.scrollAfterInsertFragment @param {boolean|object} [options.restoreScroll] @param {boolean|string|jQuery|Element} [options.reveal] @param {boolean|string} [options.reveal] @return {Promise} A promise that is fulfilled when the scrolling has finished. @internal ### scrollAfterInsertFragment = (element, options = {}) -> hashOpt = options.hash revealOpt = options.reveal restoreScrollOpt = options.restoreScroll scrollOptions = u.only(options, 'scrollBehavior', 'scrollSpeed') if restoreScrollOpt # If options.restoreScroll is an object, its keys map viewport selectors # to scroll positions. If it is just true, we leave the scrollTops option # undefined and let restoreScroll() retrieve previous scrollTops from cache. givenTops = u.presence(restoreScrollOpt, u.isObject) return restoreScroll(around: element, scrollTops: givenTops) else if hashOpt && revealOpt == true # hash revealing can be disabled with { reveal: false } return revealHash(hashOpt, scrollOptions) else if revealOpt # We allow to pass another element as { reveal } option if u.isElement(revealOpt) || u.isJQuery(revealOpt) element = e.get(revealOpt) # We allow to pass a selector as { reveal } option else if u.isString(revealOpt) selector = e.resolveSelector(revealOpt, options.origin) element = up.fragment.first(selector) else # We reveal the given element # If selectorOrElement was a CSS selector, don't blow up by calling reveal() # with an empty jQuery collection. This might happen if a failed form submission # reveals the first validation error message, but the error is shown in an # unexpected element. if element return reveal(element, scrollOptions) else # If we didn't need to scroll above, just return a resolved promise # to fulfill this function's signature. return Promise.resolve() ###** [Reveals](/up.reveal) an element matching the given `#hash` anchor. Other than the default behavior found in browsers, `up.revealHash` works with [multiple viewports](/up-viewport) and honors [fixed elements](/up-fixed-top) obstructing the user's view of the viewport. When the page loads initially, this function is automatically called with the hash from the current URL. If no element matches the given `#hash` anchor, a resolved promise is returned. \#\#\# Example up.revealHash('#chapter2') @function up.viewport.revealHash @param {string} hash @return {Promise} A promise that is fulfilled when scroll position has changed to match the location hash. @experimental ### revealHash = (hash) -> if (hash) && (match = firstHashTarget(hash)) reveal(match, top: true) else Promise.resolve() allSelector = -> # On Edge the document viewport can be changed from CSS [rootSelector(), config.viewports...].join(',') ###** Returns the scrolling container for the given element. Returns the [document's scrolling element](/up.viewport.root) if no closer viewport exists. @function up.viewport.closest @param {string|Element|jQuery} selectorOrElement @return {Element} @experimental ### closest = (selectorOrElement) -> element = e.get(selectorOrElement) e.closest(element, allSelector()) ###** Returns a jQuery collection of all the viewports contained within the given selector or element. @function up.viewport.subtree @param {string|Element|jQuery} selectorOrElement @return List @internal ### getSubtree = (selectorOrElement) -> element = e.get(selectorOrElement) e.subtree(element, allSelector()) getAround = (selectorOrElement) -> element = e.get(selectorOrElement) e.list(closest(element), getSubtree(element)) ###** Returns a list of all the viewports on the screen. @function up.viewport.all @internal ### getAll = -> e.all(allSelector()) rootSelector = -> # The spec says this should be in standards mode # and in quirks mode. However, it is currently (2018-07) # always in Webkit browsers (not Blink). Luckily Webkit # also supports document.scrollingElement. if element = document.scrollingElement element.tagName else # IE11 'html' ###** Return the [scrolling element](https://developer.mozilla.org/en-US/docs/Web/API/document/scrollingElement) for the browser's main content area. @function up.viewport.root @return {Element} @experimental ### getRoot = -> document.querySelector(rootSelector()) rootWidth = -> # This should happen on the element, regardless of document.scrollingElement e.root().clientWidth rootHeight = -> # This should happen on the element, regardless of document.scrollingElement e.root().clientHeight isRoot = (element) -> e.matches(element, rootSelector()) ###** Returns whether the given element is currently showing a vertical scrollbar. @function up.viewport.rootHasVerticalScrollbar @internal ### rootHasVerticalScrollbar = -> # We could also check if scrollHeight > offsetHeight for the document viewport. # However, we would also need to check overflow-y for that element. # Also we have no control whether developers set the property on or . # https://tylercipriani.com/blog/2014/07/12/crossbrowser-javascript-scrollbar-detection/ window.innerWidth > document.documentElement.offsetWidth ###** Returns the element that controls the `overflow-y` behavior for the [document viewport](/up.viewport.root()). @function up.viewport.rootOverflowElement @internal ### rootOverflowElement = -> body = document.body html = document.documentElement element = u.find([html, body], wasChosenAsOverflowingElement) element || getRoot() ###** Returns whether the given element was chosen as the overflowing element by the developer. We have no control whether developers set the property on or . The developer also won't know what is going to be the [scrolling element](/up.viewport.root()) on the user's brower. @function wasChosenAsOverflowingElement @internal ### wasChosenAsOverflowingElement = (element) -> overflowY = e.style(element, 'overflow-y') overflowY == 'auto' || overflowY == 'scroll' ###** Returns the width of a scrollbar. This only runs once per page load. @function up.viewport.scrollbarWidth @internal ### scrollbarWidth = u.memoize -> # This is how Bootstrap does it also: # https://github.com/twbs/bootstrap/blob/c591227602996c542b9fd0cb65cff3cc9519bdd5/dist/js/bootstrap.js#L1187 outerStyle = position: 'absolute' top: '0' left: '0' width: '100px' height: '100px' # Firefox needs at least 100px to show a scrollbar overflowY: 'scroll' outer = up.element.affix(document.body, '[up-viewport]', { style: outerStyle }) width = outer.offsetWidth - outer.clientWidth up.element.remove(outer) width scrollTopKey = (viewport) -> e.toSelector(viewport) ###** Returns a hash with scroll positions. Each key in the hash is a viewport selector. The corresponding value is the viewport's top scroll position: up.viewport.scrollTops() => { '.main': 0, '.sidebar': 73 } @function up.viewport.scrollTops @return Object @internal ### scrollTops = -> u.mapObject getAll(), (viewport) -> [scrollTopKey(viewport), viewport.scrollTop] ###** @function up.viewport.fixedElements @internal ### fixedElements = (root = document) -> queryParts = ['[up-fixed]'].concat(config.fixedTop).concat(config.fixedBottom) root.querySelectorAll(queryParts.join(',')) ###** Saves the top scroll positions of all the viewports configured in [`up.viewport.config.viewports`](/up.viewport.config). The scroll positions will be associated with the current URL. They can later be restored by calling [`up.viewport.restoreScroll()`](/up.viewport.restoreScroll) at the same URL. Unpoly automatically saves scroll positions whenever a fragment was updated on the page. @function up.viewport.saveScroll @param {string} [options.url] @param {Object} [options.tops] @experimental ### saveScroll = (options = {}) -> url = options.url ? up.history.url() tops = options.tops ? scrollTops() lastScrollTops.set(url, tops) ###** Restores [previously saved](/up.viewport.saveScroll) scroll positions of viewports viewports configured in [`up.viewport.config.viewports`](/up.viewport.config). Unpoly automatically restores scroll positions when the user presses the back button. You can disable this behavior by setting [`up.history.config.restoreScroll = false`](/up.history.config). @function up.viewport.restoreScroll @param {Element} [options.around] If set, only restores viewports that are either an ancestor or descendant of the given element. @return {Promise} A promise that will be fulfilled once scroll positions have been restored. @experimental ### restoreScroll = (options = {}) -> url = up.history.url() viewports = if options.around then getAround(options.around) else getAll() scrollTopsForUrl = options.scrollTops || lastScrollTops.get(url) || {} up.log.group 'Restoring scroll positions for URL %s to %o', url, scrollTopsForUrl, -> allScrollPromises = u.map viewports, (viewport) -> key = scrollTopKey(viewport) scrollTop = scrollTopsForUrl[key] || 0 scroll(viewport, scrollTop, duration: 0) Promise.all(allScrollPromises) ###** @internal ### absolutize = (elementOrSelector, options = {}) -> element = e.get(elementOrSelector) viewport = up.viewport.closest(element) viewportRect = viewport.getBoundingClientRect() originalRect = element.getBoundingClientRect() boundsRect = new up.Rect left: originalRect.left - viewportRect.left top: originalRect.top - viewportRect.top width: originalRect.width height: originalRect.height # Allow the caller to run code before we start shifting elements around. options.afterMeasure?() e.setStyle element, # If the element had a layout context before, make sure the # ghost will have layout context as well (and vice versa). position: if element.style.position == 'static' then 'static' else 'relative' top: 'auto' # CSS default right: 'auto' # CSS default bottom: 'auto' # CSS default left: 'auto' # CSS default width: '100%' # stretch to the .up-bounds width we set below height: '100%' # stretch to the .up-bounds height we set below # Wrap the ghost in another container so its margin can expand # freely. If we would position the element directly (old implementation), # it would gain a layout context which cannot be crossed by margins. bounds = e.createFromSelector('.up-bounds') # Insert the bounds object before our element, then move element into it. e.insertBefore(element, bounds) bounds.appendChild(element) moveBounds = (diffX, diffY) -> boundsRect.left += diffX boundsRect.top += diffY e.setStyle(bounds, boundsRect) # Position the bounds initially moveBounds(0, 0) # In theory, element should not have moved visually. However, element # (or a child of element) might collapse its margin against a previous # sibling element, and now that it is absolute it does not have the # same sibling. So we manually correct element's top position so it aligns # with the previous top position. newElementRect = element.getBoundingClientRect() moveBounds(originalRect.left - newElementRect.left, originalRect.top - newElementRect.top) u.each(fixedElements(element), e.fixedToAbsolute) bounds: bounds moveBounds: moveBounds ###** Marks this element as a scrolling container ("viewport"). Apply this attribute if your app uses a custom panel layout with fixed positioning instead of scrolling ``. As an alternative you can also push a selector matching your custom viewport to the [`up.viewport.config.viewports`](/up.viewport.config) array. [`up.reveal()`](/up.reveal) will always try to scroll the viewport closest to the element that is being revealed. By default this is the `` element. \#\#\# Example Here is an example for a layout for an e-mail client, showing a list of e-mails on the left side and the e-mail text on the right side: .side { position: fixed; top: 0; bottom: 0; left: 0; width: 100px; overflow-y: scroll; } .main { position: fixed; top: 0; bottom: 0; left: 100px; right: 0; overflow-y: scroll; } This would be the HTML (notice the `up-viewport` attribute):

Re: Your Invoice

Lorem ipsum dolor sit amet, consetetur sadipscing elitr. Stet clita kasd gubergren, no sea takimata sanctus est.

@selector [up-viewport] @stable ### ###** Marks this element as being fixed to the top edge of the screen using `position: fixed`. When [following a fragment link](/a-up-target), the viewport is scrolled so the targeted element becomes visible. By using this attribute you can make Unpoly aware of fixed elements that are obstructing the viewport contents. Unpoly will then scroll the viewport far enough that the revealed element is fully visible. Instead of using this attribute, you can also configure a selector in [`up.viewport.config.fixedTop`](/up.viewport.config#config.fixedTop). \#\#\# Example
...
@selector [up-fixed=top] @stable ### ###** Marks this element as being fixed to the bottom edge of the screen using `position: fixed`. When [following a fragment link](/a-up-target), the viewport is scrolled so the targeted element becomes visible. By using this attribute you can make Unpoly aware of fixed elements that are obstructing the viewport contents. Unpoly will then scroll the viewport far enough that the revealed element is fully visible. Instead of using this attribute, you can also configure a selector in [`up.viewport.config.fixedBottom`](/up.viewport.config#config.fixedBottom). \#\#\# Example
...
@selector [up-fixed=bottom] @stable ### ###** Marks this element as being anchored to the right edge of the screen, typically fixed navigation bars. Since [modal dialogs](/up.modal) hide the document scroll bar, elements anchored to the right appear to jump when the dialog opens or closes. Applying this attribute to anchored elements will make Unpoly aware of the issue and adjust the `right` property accordingly. You should give this attribute to layout elements with a CSS of `right: 0` with `position: fixed` or `position:absolute`. Instead of giving this attribute to any affected element, you can also configure a selector in [`up.viewport.config.anchoredRight`](/up.viewport.config#config.anchoredRight). \#\#\# Example Here is the CSS for a navigation bar that is anchored to the top edge of the screen: .top-nav { position: fixed; top: 0; left: 0; right: 0; } By adding an `up-anchored="right"` attribute to the element, we can prevent the `right` edge from jumping when a [modal dialog](/up.modal) opens or closes:
...
@selector [up-anchored=right] @stable ### ###** @function up.viewport.firstHashTarget @internal ### firstHashTarget = (hash) -> if hash = pureHash(hash) selector = [ # First match an <* up-id="hash">. This won't be picked up without JS, # preventing the scroll position from jump if up.viewport.revealPadding # is set. e.attributeSelector('up-id', hash), # Match an <* id="hash"> e.attributeSelector('id', hash), # Match an 'a' + e.attributeSelector('name', hash) ].join(',') up.fragment.first(selector) ###** Returns `'foo'` if the hash is `'#foo'`. Returns undefined if the hash is `'#'`, `''` or `undefined`. @function pureHash @internal ### pureHash = (value) -> if value && value[0] == '#' value = value.substr(1) u.presence(value) up.on 'up:app:booted', -> revealHash(location.hash) up.on 'up:framework:reset', reset <% if ENV['JS_KNIFE'] %>knife: eval(Knife.point)<% end %> reveal: reveal revealHash: revealHash firstHashTarget: firstHashTarget scroll: scroll config: config closest: closest subtree: getSubtree around: getAround all: getAll rootSelector: rootSelector root: getRoot rootWidth: rootWidth rootHeight: rootHeight rootHasVerticalScrollbar: rootHasVerticalScrollbar rootOverflowElement: rootOverflowElement isRoot: isRoot scrollbarWidth: scrollbarWidth scrollTops: scrollTops saveScroll: saveScroll restoreScroll: restoreScroll scrollAfterInsertFragment: scrollAfterInsertFragment anchoredRight: anchoredRight fixedElements: fixedElements absolutize: absolutize up.scroll = up.viewport.scroll up.reveal = up.viewport.reveal up.revealHash = up.viewport.revealHash up.legacy.renamedModule 'layout', 'viewport'