UNPKG

spincycle

Version:

A reactive message router and object manager that lets clients subscribe to object property changes on the server

360 lines (315 loc) 12.9 kB
<!-- @license Copyright (c) 2015 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 --> <link rel="import" href="../polymer/polymer.html"> <script> (function() { 'use strict'; /** * Used to calculate the scroll direction during touch events. * @type {!Object} */ var lastTouchPosition = { pageX: 0, pageY: 0 }; /** * Used to avoid computing event.path and filter scrollable nodes (better perf). * @type {?EventTarget} */ var lastRootTarget = null; /** * @type {!Array<Node>} */ var lastScrollableNodes = []; /** * The IronDropdownScrollManager is intended to provide a central source * of authority and control over which elements in a document are currently * allowed to scroll. */ Polymer.IronDropdownScrollManager = { /** * The current element that defines the DOM boundaries of the * scroll lock. This is always the most recently locking element. */ get currentLockingElement() { return this._lockingElements[this._lockingElements.length - 1]; }, /** * 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. */ elementIsScrollLocked: function(element) { var currentLockingElement = this.currentLockingElement; if (currentLockingElement === undefined) return false; var scrollLocked; if (this._hasCachedLockedElement(element)) { return true; } if (this._hasCachedUnlockedElement(element)) { return false; } scrollLocked = !!currentLockingElement && currentLockingElement !== element && !this._composedTreeContains(currentLockingElement, element); if (scrollLocked) { this._lockedElementCache.push(element); } else { this._unlockedElementCache.push(element); } return scrollLocked; }, /** * 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. */ pushScrollLock: function(element) { // Prevent pushing the same element twice if (this._lockingElements.indexOf(element) >= 0) { return; } if (this._lockingElements.length === 0) { this._lockScrollInteractions(); } this._lockingElements.push(element); this._lockedElementCache = []; this._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. */ removeScrollLock: function(element) { var index = this._lockingElements.indexOf(element); if (index === -1) { return; } this._lockingElements.splice(index, 1); this._lockedElementCache = []; this._unlockedElementCache = []; if (this._lockingElements.length === 0) { this._unlockScrollInteractions(); } }, _lockingElements: [], _lockedElementCache: null, _unlockedElementCache: null, _hasCachedLockedElement: function(element) { return this._lockedElementCache.indexOf(element) > -1; }, _hasCachedUnlockedElement: function(element) { return this._unlockedElementCache.indexOf(element) > -1; }, _composedTreeContains: function(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. var contentElements; var distributedNodes; var contentIndex; var nodeIndex; if (element.contains(child)) { return true; } contentElements = Polymer.dom(element).querySelectorAll('content'); for (contentIndex = 0; contentIndex < contentElements.length; ++contentIndex) { distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistributedNodes(); for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) { if (this._composedTreeContains(distributedNodes[nodeIndex], child)) { return true; } } } return false; }, _scrollInteractionHandler: function(event) { // Avoid canceling an event with cancelable=false, e.g. scrolling is in // progress and cannot be interrupted. if (event.cancelable && this._shouldPreventScrolling(event)) { event.preventDefault(); } // If event has targetTouches (touch event), update last touch position. if (event.targetTouches) { var touch = event.targetTouches[0]; lastTouchPosition.pageX = touch.pageX; lastTouchPosition.pageY = touch.pageY; } }, _lockScrollInteractions: function() { this._boundScrollHandler = this._boundScrollHandler || this._scrollInteractionHandler.bind(this); // Modern `wheel` event for mouse wheel scrolling: document.addEventListener('wheel', this._boundScrollHandler, true); // Older, non-standard `mousewheel` event for some FF: document.addEventListener('mousewheel', this._boundScrollHandler, true); // IE: document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true); // Save the lastScrollableNodes on touchstart, to be used on touchmove. document.addEventListener('touchstart', this._boundScrollHandler, true); // Mobile devices can scroll on touch move: document.addEventListener('touchmove', this._boundScrollHandler, true); }, _unlockScrollInteractions: function() { document.removeEventListener('wheel', this._boundScrollHandler, true); document.removeEventListener('mousewheel', this._boundScrollHandler, true); document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, true); document.removeEventListener('touchstart', this._boundScrollHandler, true); document.removeEventListener('touchmove', this._boundScrollHandler, true); }, /** * 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} * @private */ _shouldPreventScrolling: function(event) { // Update if root target changed. For touch events, ensure we don't // update during touchmove. var target = Polymer.dom(event).rootTarget; if (event.type !== 'touchmove' && lastRootTarget !== target) { lastRootTarget = target; lastScrollableNodes = this._getScrollableNodes(Polymer.dom(event).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. var info = this._getScrollInfo(event); // Prevent if there is no child that can scroll. return !this._getScrollingNode(lastScrollableNodes, info.deltaX, info.deltaY); }, /** * 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 * @private */ _getScrollableNodes: function(nodes) { var scrollables = []; var lockingIndex = nodes.indexOf(this.currentLockingElement); // Loop from root target to locking element (included). for (var i = 0; i <= lockingIndex; i++) { // Skip non-Element nodes. if (nodes[i].nodeType !== Node.ELEMENT_NODE) { continue; } var node = /** @type {!Element} */ (nodes[i]); // Check inline style before checking computed style. var style = node.style; if (style.overflow !== 'scroll' && style.overflow !== 'auto') { style = window.getComputedStyle(node); } if (style.overflow === 'scroll' || style.overflow === 'auto') { scrollables.push(node); } } return scrollables; }, /** * 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} * @private */ _getScrollingNode: function(nodes, deltaX, deltaY) { // No scroll. if (!deltaX && !deltaY) { return; } // Check only one axis according to where there is more scroll. // Prefer vertical to horizontal. var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); for (var i = 0; i < nodes.length; i++) { var node = nodes[i]; var 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; } } }, /** * 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). * @private */ _getScrollInfo: function(event) { var info = { deltaX: event.deltaX, deltaY: event.deltaY }; // Already available. if ('deltaX' in event) { // do nothing, values are already good. } // Safari has scroll info in `wheelDeltaX/Y`. else if ('wheelDeltaX' in event) { info.deltaX = -event.wheelDeltaX; info.deltaY = -event.wheelDeltaY; } // Firefox has scroll info in `detail` and `axis`. else if ('axis' in event) { info.deltaX = event.axis === 1 ? event.detail : 0; info.deltaY = event.axis === 2 ? event.detail : 0; } // On mobile devices, calculate scroll direction. else if (event.targetTouches) { var 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; } }; })(); </script>