Home | History | Annotate | Download | only in iron-dropdown
      1 <!--
      2 @license
      3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
      4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
      5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
      6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
      7 Code distributed by Google as part of the polymer project is also
      8 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
      9 -->
     10 
     11 <link rel="import" href="../polymer/polymer.html">
     12 <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
     13 
     14 <script>
     15   (function() {
     16     'use strict';
     17 
     18     /**
     19      * The IronDropdownScrollManager is intended to provide a central source
     20      * of authority and control over which elements in a document are currently
     21      * allowed to scroll.
     22      */
     23 
     24     Polymer.IronDropdownScrollManager = {
     25 
     26       /**
     27        * The current element that defines the DOM boundaries of the
     28        * scroll lock. This is always the most recently locking element.
     29        */
     30       get currentLockingElement() {
     31         return this._lockingElements[this._lockingElements.length - 1];
     32       },
     33 
     34 
     35       /**
     36        * Returns true if the provided element is "scroll locked," which is to
     37        * say that it cannot be scrolled via pointer or keyboard interactions.
     38        *
     39        * @param {HTMLElement} element An HTML element instance which may or may
     40        * not be scroll locked.
     41        */
     42       elementIsScrollLocked: function(element) {
     43         var currentLockingElement = this.currentLockingElement;
     44 
     45         if (currentLockingElement === undefined)
     46           return false;
     47 
     48         var scrollLocked;
     49 
     50         if (this._hasCachedLockedElement(element)) {
     51           return true;
     52         }
     53 
     54         if (this._hasCachedUnlockedElement(element)) {
     55           return false;
     56         }
     57 
     58         scrollLocked = !!currentLockingElement &&
     59           currentLockingElement !== element &&
     60           !this._composedTreeContains(currentLockingElement, element);
     61 
     62         if (scrollLocked) {
     63           this._lockedElementCache.push(element);
     64         } else {
     65           this._unlockedElementCache.push(element);
     66         }
     67 
     68         return scrollLocked;
     69       },
     70 
     71       /**
     72        * Push an element onto the current scroll lock stack. The most recently
     73        * pushed element and its children will be considered scrollable. All
     74        * other elements will not be scrollable.
     75        *
     76        * Scroll locking is implemented as a stack so that cases such as
     77        * dropdowns within dropdowns are handled well.
     78        *
     79        * @param {HTMLElement} element The element that should lock scroll.
     80        */
     81       pushScrollLock: function(element) {
     82         // Prevent pushing the same element twice
     83         if (this._lockingElements.indexOf(element) >= 0) {
     84           return;
     85         }
     86 
     87         if (this._lockingElements.length === 0) {
     88           this._lockScrollInteractions();
     89         }
     90 
     91         this._lockingElements.push(element);
     92 
     93         this._lockedElementCache = [];
     94         this._unlockedElementCache = [];
     95       },
     96 
     97       /**
     98        * Remove an element from the scroll lock stack. The element being
     99        * removed does not need to be the most recently pushed element. However,
    100        * the scroll lock constraints only change when the most recently pushed
    101        * element is removed.
    102        *
    103        * @param {HTMLElement} element The element to remove from the scroll
    104        * lock stack.
    105        */
    106       removeScrollLock: function(element) {
    107         var index = this._lockingElements.indexOf(element);
    108 
    109         if (index === -1) {
    110           return;
    111         }
    112 
    113         this._lockingElements.splice(index, 1);
    114 
    115         this._lockedElementCache = [];
    116         this._unlockedElementCache = [];
    117 
    118         if (this._lockingElements.length === 0) {
    119           this._unlockScrollInteractions();
    120         }
    121       },
    122 
    123       _lockingElements: [],
    124 
    125       _lockedElementCache: null,
    126 
    127       _unlockedElementCache: null,
    128 
    129       _originalBodyStyles: {},
    130 
    131       _isScrollingKeypress: function(event) {
    132         return Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(
    133           event, 'pageup pagedown home end up left down right');
    134       },
    135 
    136       _hasCachedLockedElement: function(element) {
    137         return this._lockedElementCache.indexOf(element) > -1;
    138       },
    139 
    140       _hasCachedUnlockedElement: function(element) {
    141         return this._unlockedElementCache.indexOf(element) > -1;
    142       },
    143 
    144       _composedTreeContains: function(element, child) {
    145         // NOTE(cdata): This method iterates over content elements and their
    146         // corresponding distributed nodes to implement a contains-like method
    147         // that pierces through the composed tree of the ShadowDOM. Results of
    148         // this operation are cached (elsewhere) on a per-scroll-lock basis, to
    149         // guard against potentially expensive lookups happening repeatedly as
    150         // a user scrolls / touchmoves.
    151         var contentElements;
    152         var distributedNodes;
    153         var contentIndex;
    154         var nodeIndex;
    155 
    156         if (element.contains(child)) {
    157           return true;
    158         }
    159 
    160         contentElements = Polymer.dom(element).querySelectorAll('content');
    161 
    162         for (contentIndex = 0; contentIndex < contentElements.length; ++contentIndex) {
    163 
    164           distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistributedNodes();
    165 
    166           for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
    167 
    168             if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
    169               return true;
    170             }
    171           }
    172         }
    173 
    174         return false;
    175       },
    176 
    177       _scrollInteractionHandler: function(event) {
    178         var scrolledElement =
    179             /** @type {HTMLElement} */(Polymer.dom(event).rootTarget);
    180         if (Polymer
    181               .IronDropdownScrollManager
    182               .elementIsScrollLocked(scrolledElement)) {
    183           if (event.type === 'keydown' &&
    184               !Polymer.IronDropdownScrollManager._isScrollingKeypress(event)) {
    185             return;
    186           }
    187 
    188           event.preventDefault();
    189         }
    190       },
    191 
    192       _lockScrollInteractions: function() {
    193         // Memoize body inline styles:
    194         this._originalBodyStyles.overflow = document.body.style.overflow;
    195         this._originalBodyStyles.overflowX = document.body.style.overflowX;
    196         this._originalBodyStyles.overflowY = document.body.style.overflowY;
    197 
    198         // Disable overflow scrolling on body:
    199         // TODO(cdata): It is technically not sufficient to hide overflow on
    200         // body alone. A better solution might be to traverse all ancestors of
    201         // the current scroll locking element and hide overflow on them. This
    202         // becomes expensive, though, as it would have to be redone every time
    203         // a new scroll locking element is added.
    204         document.body.style.overflow = 'hidden';
    205         document.body.style.overflowX = 'hidden';
    206         document.body.style.overflowY = 'hidden';
    207 
    208         // Modern `wheel` event for mouse wheel scrolling:
    209         document.addEventListener('wheel', this._scrollInteractionHandler, true);
    210         // Older, non-standard `mousewheel` event for some FF:
    211         document.addEventListener('mousewheel', this._scrollInteractionHandler, true);
    212         // IE:
    213         document.addEventListener('DOMMouseScroll', this._scrollInteractionHandler, true);
    214         // Mobile devices can scroll on touch move:
    215         document.addEventListener('touchmove', this._scrollInteractionHandler, true);
    216         // Capture keydown to prevent scrolling keys (pageup, pagedown etc.)
    217         document.addEventListener('keydown', this._scrollInteractionHandler, true);
    218       },
    219 
    220       _unlockScrollInteractions: function() {
    221         document.body.style.overflow = this._originalBodyStyles.overflow;
    222         document.body.style.overflowX = this._originalBodyStyles.overflowX;
    223         document.body.style.overflowY = this._originalBodyStyles.overflowY;
    224 
    225         document.removeEventListener('wheel', this._scrollInteractionHandler, true);
    226         document.removeEventListener('mousewheel', this._scrollInteractionHandler, true);
    227         document.removeEventListener('DOMMouseScroll', this._scrollInteractionHandler, true);
    228         document.removeEventListener('touchmove', this._scrollInteractionHandler, true);
    229         document.removeEventListener('keydown', this._scrollInteractionHandler, true);
    230       }
    231     };
    232   })();
    233 </script>
    234