UNPKG

spincycle

Version:

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

354 lines (312 loc) 12.1 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"> <link rel="import" href="../iron-resizable-behavior/iron-resizable-behavior.html"> <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> <link rel="import" href="../iron-behaviors/iron-control-state.html"> <link rel="import" href="../iron-overlay-behavior/iron-overlay-behavior.html"> <link rel="import" href="../neon-animation/neon-animation-runner-behavior.html"> <link rel="import" href="../neon-animation/animations/opaque-animation.html"> <link rel="import" href="iron-dropdown-scroll-manager.html"> <!-- `<iron-dropdown>` is a generalized element that is useful when you have hidden content (`.dropdown-content`) that is revealed due to some change in state that should cause it to do so. Note that this is a low-level element intended to be used as part of other composite elements that cause dropdowns to be revealed. Examples of elements that might be implemented using an `iron-dropdown` include comboboxes, menubuttons, selects. The list goes on. The `<iron-dropdown>` element exposes attributes that allow the position of the `.dropdown-content` relative to the `.dropdown-trigger` to be configured. <iron-dropdown horizontal-align="right" vertical-align="top"> <div class="dropdown-content">Hello!</div> </iron-dropdown> In the above example, the `<div>` with class `.dropdown-content` will be hidden until the dropdown element has `opened` set to true, or when the `open` method is called on the element. @demo demo/index.html --> <dom-module id="iron-dropdown"> <template> <style> :host { position: fixed; } #contentWrapper ::content > * { overflow: auto; } #contentWrapper.animating ::content > * { overflow: hidden; } </style> <div id="contentWrapper"> <content id="content" select=".dropdown-content"></content> </div> </template> <script> (function() { 'use strict'; Polymer({ is: 'iron-dropdown', behaviors: [ Polymer.IronControlState, Polymer.IronA11yKeysBehavior, Polymer.IronOverlayBehavior, Polymer.NeonAnimationRunnerBehavior ], properties: { /** * The orientation against which to align the dropdown content * horizontally relative to the dropdown trigger. * Overridden from `Polymer.IronFitBehavior`. */ horizontalAlign: { type: String, value: 'left', reflectToAttribute: true }, /** * The orientation against which to align the dropdown content * vertically relative to the dropdown trigger. * Overridden from `Polymer.IronFitBehavior`. */ verticalAlign: { type: String, value: 'top', reflectToAttribute: true }, /** * An animation config. If provided, this will be used to animate the * opening of the dropdown. Pass an Array for multiple animations. * See `neon-animation` documentation for more animation configuration * details. */ openAnimationConfig: { type: Object }, /** * An animation config. If provided, this will be used to animate the * closing of the dropdown. Pass an Array for multiple animations. * See `neon-animation` documentation for more animation configuration * details. */ closeAnimationConfig: { type: Object }, /** * If provided, this will be the element that will be focused when * the dropdown opens. */ focusTarget: { type: Object }, /** * Set to true to disable animations when opening and closing the * dropdown. */ noAnimations: { type: Boolean, value: false }, /** * By default, the dropdown will constrain scrolling on the page * to itself when opened. * Set to true in order to prevent scroll from being constrained * to the dropdown when it opens. */ allowOutsideScroll: { type: Boolean, value: false }, /** * Callback for scroll events. * @type {Function} * @private */ _boundOnCaptureScroll: { type: Function, value: function() { return this._onCaptureScroll.bind(this); } } }, listeners: { 'neon-animation-finish': '_onNeonAnimationFinish' }, observers: [ '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)' ], /** * The element that is contained by the dropdown, if any. */ get containedElement() { return Polymer.dom(this.$.content).getDistributedNodes()[0]; }, /** * The element that should be focused when the dropdown opens. * @deprecated */ get _focusTarget() { return this.focusTarget || this.containedElement; }, ready: function() { // Memoized scrolling position, used to block scrolling outside. this._scrollTop = 0; this._scrollLeft = 0; // Used to perform a non-blocking refit on scroll. this._refitOnScrollRAF = null; }, attached: function () { if (!this.sizingTarget || this.sizingTarget === this) { this.sizingTarget = this.containedElement || this; } }, detached: function() { this.cancelAnimation(); document.removeEventListener('scroll', this._boundOnCaptureScroll); Polymer.IronDropdownScrollManager.removeScrollLock(this); }, /** * Called when the value of `opened` changes. * Overridden from `IronOverlayBehavior` */ _openedChanged: function() { if (this.opened && this.disabled) { this.cancel(); } else { this.cancelAnimation(); this._updateAnimationConfig(); this._saveScrollPosition(); if (this.opened) { document.addEventListener('scroll', this._boundOnCaptureScroll); !this.allowOutsideScroll && Polymer.IronDropdownScrollManager.pushScrollLock(this); } else { document.removeEventListener('scroll', this._boundOnCaptureScroll); Polymer.IronDropdownScrollManager.removeScrollLock(this); } Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments); } }, /** * Overridden from `IronOverlayBehavior`. */ _renderOpened: function() { if (!this.noAnimations && this.animationConfig.open) { this.$.contentWrapper.classList.add('animating'); this.playAnimation('open'); } else { Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this, arguments); } }, /** * Overridden from `IronOverlayBehavior`. */ _renderClosed: function() { if (!this.noAnimations && this.animationConfig.close) { this.$.contentWrapper.classList.add('animating'); this.playAnimation('close'); } else { Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this, arguments); } }, /** * Called when animation finishes on the dropdown (when opening or * closing). Responsible for "completing" the process of opening or * closing the dropdown by positioning it or setting its display to * none. */ _onNeonAnimationFinish: function() { this.$.contentWrapper.classList.remove('animating'); if (this.opened) { this._finishRenderOpened(); } else { this._finishRenderClosed(); } }, _onCaptureScroll: function() { if (!this.allowOutsideScroll) { this._restoreScrollPosition(); } else { this._refitOnScrollRAF && window.cancelAnimationFrame(this._refitOnScrollRAF); this._refitOnScrollRAF = window.requestAnimationFrame(this.refit.bind(this)); } }, /** * Memoizes the scroll position of the outside scrolling element. * @private */ _saveScrollPosition: function() { if (document.scrollingElement) { this._scrollTop = document.scrollingElement.scrollTop; this._scrollLeft = document.scrollingElement.scrollLeft; } else { // Since we don't know if is the body or html, get max. this._scrollTop = Math.max(document.documentElement.scrollTop, document.body.scrollTop); this._scrollLeft = Math.max(document.documentElement.scrollLeft, document.body.scrollLeft); } }, /** * Resets the scroll position of the outside scrolling element. * @private */ _restoreScrollPosition: function() { if (document.scrollingElement) { document.scrollingElement.scrollTop = this._scrollTop; document.scrollingElement.scrollLeft = this._scrollLeft; } else { // Since we don't know if is the body or html, set both. document.documentElement.scrollTop = this._scrollTop; document.documentElement.scrollLeft = this._scrollLeft; document.body.scrollTop = this._scrollTop; document.body.scrollLeft = this._scrollLeft; } }, /** * Constructs the final animation config from different properties used * to configure specific parts of the opening and closing animations. */ _updateAnimationConfig: function() { // Update the animation node to be the containedElement. var animationNode = this.containedElement; var animations = [].concat(this.openAnimationConfig || []).concat(this.closeAnimationConfig || []); for (var i = 0; i < animations.length; i++) { animations[i].node = animationNode; } this.animationConfig = { open: this.openAnimationConfig, close: this.closeAnimationConfig }; }, /** * Updates the overlay position based on configured horizontal * and vertical alignment. */ _updateOverlayPosition: function() { if (this.isAttached) { // This triggers iron-resize, and iron-overlay-behavior will call refit if needed. this.notifyResize(); } }, /** * Apply focus to focusTarget or containedElement */ _applyFocus: function () { var focusTarget = this.focusTarget || this.containedElement; if (focusTarget && this.opened && !this.noAutoFocus) { focusTarget.focus(); } else { Polymer.IronOverlayBehaviorImpl._applyFocus.apply(this, arguments); } } }); })(); </script> </dom-module>