UNPKG

@advanced-rest-client/arc-overlay-mixin

Version:

arc-overlay-mixin is a port of iron-overlay-behavior that works with web components

378 lines (340 loc) 11.9 kB
/* eslint-disable import/no-mutable-exports */ /* istanbul ignore file */ /** @license Copyright (c) 2017 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt */ /* eslint-disable no-plusplus */ /* eslint-disable no-continue */ /** * Used to calculate the scroll direction during touch events. * @type {!Object} */ const lastTouchPosition = { pageX: 0, pageY: 0 }; /** * Used to avoid computing event.path and filter scrollable nodes (better perf). * @type {?EventTarget} */ let lastRootTarget = null; /** * @type {!Array<!Node>} */ let lastScrollableNodes = []; /** * @type {!Array<string>} */ const scrollEvents = [ // Modern `wheel` event for mouse wheel scrolling: 'wheel', // Older, non-standard `mousewheel` event for some FF: 'mousewheel', // IE: 'DOMMouseScroll', // Touch enabled devices 'touchstart', 'touchmove' ]; // must be defined for modulizer let _boundScrollHandler; let currentLockingElement; export const _lockingElements = []; export let _lockedElementCache = null; export let _unlockedElementCache = null; /** * The IronScrollManager is intended to provide a central source * of authority and control over which elements in a document are currently * allowed to scroll. * */ /** * The current element that defines the DOM boundaries of the * scroll lock. This is always the most recently locking element. * * @return {!Node|undefined} */ export { currentLockingElement }; /** * Returns scroll `deltaX` and `deltaY`. * @param {!Event} event The scroll event * @return {{deltaX: number, deltaY: number}} Object containing the * x-axis scroll delta (positive: scroll right, negative: scroll left, * 0: no scroll), and the y-axis scroll delta (positive: scroll down, * negative: scroll up, 0: no scroll). * @package */ export function _getScrollInfo(event) { // @ts-ignore const info = { deltaX: event.deltaX, deltaY: event.deltaY }; // Already available. if ('deltaX' in event) { // do nothing, values are already good. } else if ('wheelDeltaX' in event && 'wheelDeltaY' in event) { // Safari has scroll info in `wheelDeltaX/Y`. // @ts-ignore info.deltaX = -event.wheelDeltaX; // @ts-ignore info.deltaY = -event.wheelDeltaY; } else if ('wheelDelta' in event) { // IE10 has only vertical scroll info in `wheelDelta`. info.deltaX = 0; // @ts-ignore info.deltaY = -event.wheelDelta; } else if ('axis' in event) { // Firefox has scroll info in `detail` and `axis`. // @ts-ignore info.deltaX = event.axis === 1 ? event.detail : 0; // @ts-ignore info.deltaY = event.axis === 2 ? event.detail : 0; // @ts-ignore } else if (event.targetTouches) { // On mobile devices, calculate scroll direction. // @ts-ignore const touch = event.targetTouches[0]; // Touch moves from right to left => scrolling goes right. info.deltaX = lastTouchPosition.pageX - touch.pageX; // Touch moves from down to up => scrolling goes down. info.deltaY = lastTouchPosition.pageY - touch.pageY; } return info; } /** * Returns the node that is scrolling. If there is no scrolling, * returns undefined. * @param {!Array<!Node>} nodes * @param {number} deltaX Scroll delta on the x-axis * @param {number} deltaY Scroll delta on the y-axis * @return {!Node|undefined} * @package */ export function _getScrollingNode(nodes, deltaX, deltaY) { // No scroll. if (!deltaX && !deltaY) { return undefined; } // Check only one axis according to where there is more scroll. // Prefer vertical to horizontal. const verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); for (let i = 0; i < nodes.length; i++) { const node = /** @type HTMLElement */ (nodes[i]); let canScroll = false; if (verticalScroll) { // delta < 0 is scroll up, delta > 0 is scroll down. canScroll = deltaY < 0 ? node.scrollTop > 0 : node.scrollTop < node.scrollHeight - node.clientHeight; } else { // delta < 0 is scroll left, delta > 0 is scroll right. canScroll = deltaX < 0 ? node.scrollLeft > 0 : node.scrollLeft < node.scrollWidth - node.clientWidth; } if (canScroll) { return node; } } return undefined; } /** * Returns an array of scrollable nodes up to the current locking element, * which is included too if scrollable. * @param {!Array<!Node>} nodes * @return {!Array<!Node>} scrollables * @package */ export function _getScrollableNodes(nodes) { const scrollables = []; const lockingIndex = nodes.indexOf(currentLockingElement); // Loop from root target to locking element (included). for (let i = 0; i <= lockingIndex; i++) { // Skip non-Element nodes. if (nodes[i].nodeType !== Node.ELEMENT_NODE) { continue; } const node = /** @type {!HTMLElement} */ (nodes[i]); // Check inline style before checking computed style. let { style } = node; if (!style.overflow.includes('scroll') && !style.overflow.includes('auto')) { style = window.getComputedStyle(node); } if (style.overflow.includes('scroll') || style.overflow.includes('auto')) { scrollables.push(node); } } return scrollables; } export function _hasCachedLockedElement(element) { return _lockedElementCache.indexOf(element) > -1; } export function _hasCachedUnlockedElement(element) { return _unlockedElementCache.indexOf(element) > -1; } export function _composedTreeContains(element, child) { // NOTE(cdata): This method iterates over content elements and their // corresponding distributed nodes to implement a contains-like method // that pierces through the composed tree of the ShadowDOM. Results of // this operation are cached (elsewhere) on a per-scroll-lock basis, to // guard against potentially expensive lookups happening repeatedly as // a user scrolls / touchmoves. let distributedNodes; let contentIndex; let nodeIndex; if (element.contains(child)) { return true; } const contentElements = element.querySelectorAll('slot'); for (contentIndex = 0; contentIndex < contentElements.length; ++contentIndex) { const slot = contentElements[contentIndex]; distributedNodes = slot.assignedNodes(); for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) { // Polymer 2.x returns slot.assignedNodes which can contain text nodes. if (distributedNodes[nodeIndex].nodeType !== Node.ELEMENT_NODE) { continue; } if (_composedTreeContains(distributedNodes[nodeIndex], child)) { return true; } } } return false; } /** * Returns true if the event causes scroll outside the current locking * element, e.g. pointer/keyboard interactions, or scroll "leaking" * outside the locking element when it is already at its scroll boundaries. * @param {!Event} event * @return {boolean} * @package */ export function _shouldPreventScrolling(event) { // Update if root target changed. For touch events, ensure we don't // update during touchmove. const cp = event.composedPath && event.composedPath(); // @ts-ignore const path = cp || event.path; const target = path[0]; if (event.type !== 'touchmove' && lastRootTarget !== target) { lastRootTarget = target; lastScrollableNodes = _getScrollableNodes(path); } // Prevent event if no scrollable nodes. if (!lastScrollableNodes.length) { return true; } // Don't prevent touchstart event inside the locking element when it has // scrollable nodes. if (event.type === 'touchstart') { return false; } // Get deltaX/Y. const info = _getScrollInfo(event); // Prevent if there is no child that can scroll. return !_getScrollingNode(lastScrollableNodes, info.deltaX, info.deltaY); } export function _scrollInteractionHandler(event) { // Avoid canceling an event with cancelable=false, e.g. scrolling is in // progress and cannot be interrupted. if (event.cancelable && _shouldPreventScrolling(event)) { event.preventDefault(); } // If event has targetTouches (touch event), update last touch position. if (event.targetTouches) { const touch = event.targetTouches[0]; lastTouchPosition.pageX = touch.pageX; lastTouchPosition.pageY = touch.pageY; } } /** * Returns true if the provided element is "scroll locked", which is to * say that it cannot be scrolled via pointer or keyboard interactions. * * @param {!HTMLElement} element An HTML element instance which may or may * not be scroll locked. * @return {Boolean} */ export function elementIsScrollLocked(element) { const lockingElement = currentLockingElement; if (lockingElement === undefined) { return false; } if (_hasCachedLockedElement(element)) { return true; } if (_hasCachedUnlockedElement(element)) { return false; } const scrollLocked = !!lockingElement && lockingElement !== element && !_composedTreeContains(lockingElement, element); if (scrollLocked) { _lockedElementCache.push(element); } else { _unlockedElementCache.push(element); } return scrollLocked; } export function _lockScrollInteractions() { _boundScrollHandler = _boundScrollHandler || _scrollInteractionHandler.bind(undefined); for (let i = 0, l = scrollEvents.length; i < l; i++) { // NOTE: browsers that don't support objects as third arg will // interpret it as boolean, hence useCapture = true in this case. document.addEventListener(scrollEvents[i], _boundScrollHandler, { capture: true, passive: false }); } } export function _unlockScrollInteractions() { for (let i = 0, l = scrollEvents.length; i < l; i++) { // NOTE: browsers that don't support objects as third arg will // interpret it as boolean, hence useCapture = true in this case. // @ts-ignore document.removeEventListener(scrollEvents[i], _boundScrollHandler, { capture: true, passive: false }); } } /** * Push an element onto the current scroll lock stack. The most recently * pushed element and its children will be considered scrollable. All * other elements will not be scrollable. * * Scroll locking is implemented as a stack so that cases such as * dropdowns within dropdowns are handled well. * * @param {!HTMLElement} element The element that should lock scroll. */ export function pushScrollLock(element) { // Prevent pushing the same element twice if (_lockingElements.indexOf(element) >= 0) { return; } if (_lockingElements.length === 0) { _lockScrollInteractions(); } _lockingElements.push(element); currentLockingElement = _lockingElements[_lockingElements.length - 1]; _lockedElementCache = []; _unlockedElementCache = []; } /** * Remove an element from the scroll lock stack. The element being * removed does not need to be the most recently pushed element. However, * the scroll lock constraints only change when the most recently pushed * element is removed. * * @param {!HTMLElement} element The element to remove from the scroll * lock stack. */ export function removeScrollLock(element) { const index = _lockingElements.indexOf(element); if (index === -1) { return; } _lockingElements.splice(index, 1); currentLockingElement = _lockingElements[_lockingElements.length - 1]; _lockedElementCache = []; _unlockedElementCache = []; if (_lockingElements.length === 0) { _unlockScrollInteractions(); } } /** * @package */ export { _boundScrollHandler };