| Index: chrome/browser/resources/md_downloads/crisper.js
|
| diff --git a/chrome/browser/resources/md_downloads/crisper.js b/chrome/browser/resources/md_downloads/crisper.js
|
| index efa2abf0e1294cc9664a6e53acb14ad99bb8278f..f720148cd7cce7b70f2b0063994c07beb45aa865 100644
|
| --- a/chrome/browser/resources/md_downloads/crisper.js
|
| +++ b/chrome/browser/resources/md_downloads/crisper.js
|
| @@ -1,3 +1,75 @@
|
| +// Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +/**
|
| + * @fileoverview Assertion support.
|
| + */
|
| +
|
| +/**
|
| + * Verify |condition| is truthy and return |condition| if so.
|
| + * @template T
|
| + * @param {T} condition A condition to check for truthiness. Note that this
|
| + * may be used to test whether a value is defined or not, and we don't want
|
| + * to force a cast to Boolean.
|
| + * @param {string=} opt_message A message to show on failure.
|
| + * @return {T} A non-null |condition|.
|
| + */
|
| +function assert(condition, opt_message) {
|
| + if (!condition) {
|
| + var message = 'Assertion failed';
|
| + if (opt_message)
|
| + message = message + ': ' + opt_message;
|
| + var error = new Error(message);
|
| + var global = function() { return this; }();
|
| + if (global.traceAssertionsForTesting)
|
| + console.warn(error.stack);
|
| + throw error;
|
| + }
|
| + return condition;
|
| +}
|
| +
|
| +/**
|
| + * Call this from places in the code that should never be reached.
|
| + *
|
| + * For example, handling all the values of enum with a switch() like this:
|
| + *
|
| + * function getValueFromEnum(enum) {
|
| + * switch (enum) {
|
| + * case ENUM_FIRST_OF_TWO:
|
| + * return first
|
| + * case ENUM_LAST_OF_TWO:
|
| + * return last;
|
| + * }
|
| + * assertNotReached();
|
| + * return document;
|
| + * }
|
| + *
|
| + * This code should only be hit in the case of serious programmer error or
|
| + * unexpected input.
|
| + *
|
| + * @param {string=} opt_message A message to show when this is hit.
|
| + */
|
| +function assertNotReached(opt_message) {
|
| + assert(false, opt_message || 'Unreachable code hit');
|
| +}
|
| +
|
| +/**
|
| + * @param {*} value The value to check.
|
| + * @param {function(new: T, ...)} type A user-defined constructor.
|
| + * @param {string=} opt_message A message to show when this is hit.
|
| + * @return {T}
|
| + * @template T
|
| + */
|
| +function assertInstanceof(value, type, opt_message) {
|
| + // We don't use assert immediately here so that we avoid constructing an error
|
| + // message if we don't have to.
|
| + if (!(value instanceof type)) {
|
| + assertNotReached(opt_message || 'Value ' + value +
|
| + ' is not a[n] ' + (type.name || typeof type));
|
| + }
|
| + return value;
|
| +};
|
| // Copyright 2016 The Chromium Authors. All rights reserved.
|
| // Use of this source code is governed by a BSD-style license that can be
|
| // found in the LICENSE file.
|
| @@ -21,17 +93,31 @@
|
| * @template T
|
| */
|
| function PromiseResolver() {
|
| - /** @type {function(T): void} */
|
| - this.resolve;
|
| + /** @private {function(T): void} */
|
| + this.resolve_;
|
|
|
| - /** @type {function(*=): void} */
|
| - this.reject;
|
| + /** @private {function(*=): void} */
|
| + this.reject_;
|
|
|
| - /** @type {!Promise<T>} */
|
| - this.promise = new Promise(function(resolve, reject) {
|
| - this.resolve = resolve;
|
| - this.reject = reject;
|
| + /** @private {!Promise<T>} */
|
| + this.promise_ = new Promise(function(resolve, reject) {
|
| + this.resolve_ = resolve;
|
| + this.reject_ = reject;
|
| }.bind(this));
|
| +}
|
| +
|
| +PromiseResolver.prototype = {
|
| + /** @return {!Promise<T>} */
|
| + get promise() { return this.promise_; },
|
| + set promise(p) { assertNotReached(); },
|
| +
|
| + /** @return {function(T): void} */
|
| + get resolve() { return this.resolve_; },
|
| + set resolve(r) { assertNotReached(); },
|
| +
|
| + /** @return {function(*=): void} */
|
| + get reject() { return this.reject_; },
|
| + set reject(s) { assertNotReached(); },
|
| };
|
| // Copyright (c) 2012 The Chromium Authors. All rights reserved.
|
| // Use of this source code is governed by a BSD-style license that can be
|
| @@ -2176,20 +2262,31 @@ function quoteString(str) {
|
|
|
| /**
|
| * Specifies the element that will handle the scroll event
|
| - * on the behalf of the current element. This is typically a reference to an `Element`,
|
| + * on the behalf of the current element. This is typically a reference to an element,
|
| * but there are a few more posibilities:
|
| *
|
| * ### Elements id
|
| *
|
| *```html
|
| - * <div id="scrollable-element" style="overflow-y: auto;">
|
| + * <div id="scrollable-element" style="overflow: auto;">
|
| * <x-element scroll-target="scrollable-element">
|
| - * Content
|
| + * \x3c!-- Content--\x3e
|
| * </x-element>
|
| * </div>
|
| *```
|
| - * In this case, `scrollTarget` will point to the outer div element. Alternatively,
|
| - * you can set the property programatically:
|
| + * In this case, the `scrollTarget` will point to the outer div element.
|
| + *
|
| + * ### Document scrolling
|
| + *
|
| + * For document scrolling, you can use the reserved word `document`:
|
| + *
|
| + *```html
|
| + * <x-element scroll-target="document">
|
| + * \x3c!-- Content --\x3e
|
| + * </x-element>
|
| + *```
|
| + *
|
| + * ### Elements reference
|
| *
|
| *```js
|
| * appHeader.scrollTarget = document.querySelector('#scrollable-element');
|
| @@ -2210,42 +2307,39 @@ function quoteString(str) {
|
| ],
|
|
|
| _scrollTargetChanged: function(scrollTarget, isAttached) {
|
| - // Remove lister to the current scroll target
|
| + var eventTarget;
|
| +
|
| if (this._oldScrollTarget) {
|
| - if (this._oldScrollTarget === this._doc) {
|
| - window.removeEventListener('scroll', this._boundScrollHandler);
|
| - } else if (this._oldScrollTarget.removeEventListener) {
|
| - this._oldScrollTarget.removeEventListener('scroll', this._boundScrollHandler);
|
| - }
|
| + eventTarget = this._oldScrollTarget === this._doc ? window : this._oldScrollTarget;
|
| + eventTarget.removeEventListener('scroll', this._boundScrollHandler);
|
| this._oldScrollTarget = null;
|
| }
|
| - if (isAttached) {
|
| - // Support element id references
|
| - if (typeof scrollTarget === 'string') {
|
|
|
| - var host = this.domHost;
|
| - this.scrollTarget = host && host.$ ? host.$[scrollTarget] :
|
| - Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
|
| + if (!isAttached) {
|
| + return;
|
| + }
|
| + // Support element id references
|
| + if (scrollTarget === 'document') {
|
|
|
| - } else if (this._scrollHandler) {
|
| + this.scrollTarget = this._doc;
|
|
|
| - this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler.bind(this);
|
| - // Add a new listener
|
| - if (scrollTarget === this._doc) {
|
| - window.addEventListener('scroll', this._boundScrollHandler);
|
| - if (this._scrollTop !== 0 || this._scrollLeft !== 0) {
|
| - this._scrollHandler();
|
| - }
|
| - } else if (scrollTarget && scrollTarget.addEventListener) {
|
| - scrollTarget.addEventListener('scroll', this._boundScrollHandler);
|
| - }
|
| - this._oldScrollTarget = scrollTarget;
|
| - }
|
| + } else if (typeof scrollTarget === 'string') {
|
| +
|
| + this.scrollTarget = this.domHost ? this.domHost.$[scrollTarget] :
|
| + Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
|
| +
|
| + } else if (this._isValidScrollTarget()) {
|
| +
|
| + eventTarget = scrollTarget === this._doc ? window : scrollTarget;
|
| + this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler.bind(this);
|
| + this._oldScrollTarget = scrollTarget;
|
| +
|
| + eventTarget.addEventListener('scroll', this._boundScrollHandler);
|
| }
|
| },
|
|
|
| /**
|
| - * Runs on every scroll event. Consumer of this behavior may want to override this method.
|
| + * Runs on every scroll event. Consumer of this behavior may override this method.
|
| *
|
| * @protected
|
| */
|
| @@ -2472,10 +2566,6 @@ function quoteString(str) {
|
| Polymer.IronScrollTargetBehavior
|
| ],
|
|
|
| - listeners: {
|
| - 'iron-resize': '_resizeHandler'
|
| - },
|
| -
|
| keyBindings: {
|
| 'up': '_didMoveUp',
|
| 'down': '_didMoveDown',
|
| @@ -2779,10 +2869,14 @@ function quoteString(str) {
|
| attached: function() {
|
| this.updateViewportBoundaries();
|
| this._render();
|
| + // `iron-resize` is fired when the list is attached if the event is added
|
| + // before attached causing unnecessary work.
|
| + this.listen(this, 'iron-resize', '_resizeHandler');
|
| },
|
|
|
| detached: function() {
|
| this._itemsRendered = false;
|
| + this.unlisten(this, 'iron-resize', '_resizeHandler');
|
| },
|
|
|
| /**
|
| @@ -2903,11 +2997,9 @@ function quoteString(str) {
|
| }
|
|
|
| if (recycledTiles === 0) {
|
| - // If the list ever reach this case, the physical average is not significant enough
|
| - // to create all the items needed to cover the entire viewport.
|
| - // e.g. A few items have a height that differs from the average by serveral order of magnitude.
|
| + // Try to increase the pool if the list's client height isn't filled up with physical items
|
| if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
|
| - this.async(this._increasePool.bind(this, 1));
|
| + this._increasePoolIfNeeded();
|
| }
|
| } else {
|
| this._virtualStart = this._virtualStart + recycledTiles;
|
| @@ -2962,19 +3054,27 @@ function quoteString(str) {
|
|
|
| /**
|
| * Increases the pool of physical items only if needed.
|
| - * This function will allocate additional physical items
|
| - * if the physical size is shorter than `_optPhysicalSize`
|
| + *
|
| + * @return {boolean} True if the pool was increased.
|
| */
|
| _increasePoolIfNeeded: function() {
|
| - if (this._viewportSize === 0 || this._physicalSize >= this._optPhysicalSize) {
|
| + // Base case 1: the list has no size.
|
| + if (this._viewportSize === 0) {
|
| + return false;
|
| + }
|
| + // Base case 2: If the physical size is optimal and the list's client height is full
|
| + // with physical items, don't increase the pool.
|
| + var isClientHeightFull = this._physicalBottom >= this._scrollBottom && this._physicalTop <= this._scrollPosition;
|
| + if (this._physicalSize >= this._optPhysicalSize && isClientHeightFull) {
|
| return false;
|
| }
|
| - // 0 <= `currentPage` <= `_maxPages`
|
| + // this value should range between [0 <= `currentPage` <= `_maxPages`]
|
| var currentPage = Math.floor(this._physicalSize / this._viewportSize);
|
| +
|
| if (currentPage === 0) {
|
| // fill the first page
|
| this._debounceTemplate(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5)));
|
| - } else if (this._lastPage !== currentPage) {
|
| + } else if (this._lastPage !== currentPage && isClientHeightFull) {
|
| // paint the page and defer the next increase
|
| // wait 16ms which is rough enough to get paint cycle.
|
| Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increasePool.bind(this, 1), 16));
|
| @@ -2982,7 +3082,9 @@ function quoteString(str) {
|
| // fill the rest of the pages
|
| this._debounceTemplate(this._increasePool.bind(this, 1));
|
| }
|
| +
|
| this._lastPage = currentPage;
|
| +
|
| return true;
|
| },
|
|
|
| @@ -3028,7 +3130,6 @@ function quoteString(str) {
|
| if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) {
|
| this._lastPage = 0;
|
| this._update();
|
| - this._scrollHandler();
|
| this._itemsRendered = true;
|
| }
|
| },
|
| @@ -3123,7 +3224,7 @@ function quoteString(str) {
|
| if (!el) {
|
| return;
|
| }
|
| -
|
| +
|
| inst = el._templateInstance;
|
|
|
| if (inst.__key__ !== key) {
|
| @@ -3407,7 +3508,7 @@ function quoteString(str) {
|
| // update the position of the items
|
| this._positionItems();
|
| // set the new scroll position
|
| - this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop + 1);
|
| + this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop);
|
| // increase the pool of physical items if needed
|
| this._increasePoolIfNeeded();
|
| // clear cached visible index
|
| @@ -3432,14 +3533,19 @@ function quoteString(str) {
|
| if (IOS && Math.abs(this._viewportSize - this._scrollTargetHeight) < 100) {
|
| return;
|
| }
|
| - this._debounceTemplate(function() {
|
| - this._render();
|
| - if (this._itemsRendered && this._physicalItems && this._isVisible) {
|
| - this._resetAverage();
|
| - this.updateViewportBoundaries();
|
| - this.scrollToIndex(this.firstVisibleIndex);
|
| - }
|
| - });
|
| + // In Desktop Safari 9.0.3, if the scroll bars are always shown,
|
| + // changing the scroll position from a resize handler would result in
|
| + // the scroll position being reset. Waiting 1ms fixes the issue.
|
| + Polymer.dom.addDebouncer(this.debounce('_debounceTemplate',
|
| + function() {
|
| + this._render();
|
| +
|
| + if (this._itemsRendered && this._physicalItems && this._isVisible) {
|
| + this._resetAverage();
|
| + this.updateViewportBoundaries();
|
| + this.scrollToIndex(this.firstVisibleIndex);
|
| + }
|
| + }.bind(this), 1));
|
| },
|
|
|
| _getModelFromItem: function(item) {
|
| @@ -3761,78 +3867,6 @@ function quoteString(str) {
|
| });
|
|
|
| })();
|
| -// Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
| -// Use of this source code is governed by a BSD-style license that can be
|
| -// found in the LICENSE file.
|
| -
|
| -/**
|
| - * @fileoverview Assertion support.
|
| - */
|
| -
|
| -/**
|
| - * Verify |condition| is truthy and return |condition| if so.
|
| - * @template T
|
| - * @param {T} condition A condition to check for truthiness. Note that this
|
| - * may be used to test whether a value is defined or not, and we don't want
|
| - * to force a cast to Boolean.
|
| - * @param {string=} opt_message A message to show on failure.
|
| - * @return {T} A non-null |condition|.
|
| - */
|
| -function assert(condition, opt_message) {
|
| - if (!condition) {
|
| - var message = 'Assertion failed';
|
| - if (opt_message)
|
| - message = message + ': ' + opt_message;
|
| - var error = new Error(message);
|
| - var global = function() { return this; }();
|
| - if (global.traceAssertionsForTesting)
|
| - console.warn(error.stack);
|
| - throw error;
|
| - }
|
| - return condition;
|
| -}
|
| -
|
| -/**
|
| - * Call this from places in the code that should never be reached.
|
| - *
|
| - * For example, handling all the values of enum with a switch() like this:
|
| - *
|
| - * function getValueFromEnum(enum) {
|
| - * switch (enum) {
|
| - * case ENUM_FIRST_OF_TWO:
|
| - * return first
|
| - * case ENUM_LAST_OF_TWO:
|
| - * return last;
|
| - * }
|
| - * assertNotReached();
|
| - * return document;
|
| - * }
|
| - *
|
| - * This code should only be hit in the case of serious programmer error or
|
| - * unexpected input.
|
| - *
|
| - * @param {string=} opt_message A message to show when this is hit.
|
| - */
|
| -function assertNotReached(opt_message) {
|
| - assert(false, opt_message || 'Unreachable code hit');
|
| -}
|
| -
|
| -/**
|
| - * @param {*} value The value to check.
|
| - * @param {function(new: T, ...)} type A user-defined constructor.
|
| - * @param {string=} opt_message A message to show when this is hit.
|
| - * @return {T}
|
| - * @template T
|
| - */
|
| -function assertInstanceof(value, type, opt_message) {
|
| - // We don't use assert immediately here so that we avoid constructing an error
|
| - // message if we don't have to.
|
| - if (!(value instanceof type)) {
|
| - assertNotReached(opt_message || 'Value ' + value +
|
| - ' is not a[n] ' + (type.name || typeof type));
|
| - }
|
| - return value;
|
| -};
|
| // Copyright 2015 The Chromium Authors. All rights reserved.
|
| // Use of this source code is governed by a BSD-style license that can be
|
| // found in the LICENSE file.
|
| @@ -6557,8 +6591,14 @@ Polymer({
|
| properties: {
|
|
|
| /**
|
| - * If you want to use the attribute value of an element for `selected` instead of the index,
|
| - * set this to the name of the attribute.
|
| + * If you want to use an attribute value or property of an element for
|
| + * `selected` instead of the index, set this to the name of the attribute
|
| + * or property. Hyphenated values are converted to camel case when used to
|
| + * look up the property of a selectable element. Camel cased values are
|
| + * *not* converted to hyphenated values for attribute lookup. It's
|
| + * recommended that you provide the hyphenated form of the name so that
|
| + * selection works in both cases. (Use `attr-or-property-name` instead of
|
| + * `attrOrPropertyName`.)
|
| */
|
| attrForSelected: {
|
| type: String,
|
| @@ -6619,6 +6659,15 @@ Polymer({
|
| },
|
|
|
| /**
|
| + * Default fallback if the selection based on selected with `attrForSelected`
|
| + * is not found.
|
| + */
|
| + fallbackSelection: {
|
| + type: String,
|
| + value: null
|
| + },
|
| +
|
| + /**
|
| * The list of items from which a selection can be made.
|
| */
|
| items: {
|
| @@ -6648,7 +6697,8 @@ Polymer({
|
|
|
| observers: [
|
| '_updateAttrForSelected(attrForSelected)',
|
| - '_updateSelected(selected)'
|
| + '_updateSelected(selected)',
|
| + '_checkFallback(fallbackSelection)'
|
| ],
|
|
|
| created: function() {
|
| @@ -6734,6 +6784,12 @@ Polymer({
|
| return this.selected != null;
|
| },
|
|
|
| + _checkFallback: function() {
|
| + if (this._shouldUpdateSelection) {
|
| + this._updateSelected();
|
| + }
|
| + },
|
| +
|
| _addListener: function(eventName) {
|
| this.listen(this, eventName, '_activateHandler');
|
| },
|
| @@ -6755,7 +6811,7 @@ Polymer({
|
|
|
| _updateAttrForSelected: function() {
|
| if (this._shouldUpdateSelection) {
|
| - this.selected = this._indexToValue(this.indexOf(this.selectedItem));
|
| + this.selected = this._indexToValue(this.indexOf(this.selectedItem));
|
| }
|
| },
|
|
|
| @@ -6765,6 +6821,11 @@ Polymer({
|
|
|
| _selectSelected: function(selected) {
|
| this._selection.select(this._valueToItem(this.selected));
|
| + // Check for items, since this array is populated only when attached
|
| + // Since Number(0) is falsy, explicitly check for undefined
|
| + if (this.fallbackSelection && this.items.length && (this._selection.get() === undefined)) {
|
| + this.selected = this.fallbackSelection;
|
| + }
|
| },
|
|
|
| _filterItem: function(node) {
|
| @@ -6799,7 +6860,7 @@ Polymer({
|
| },
|
|
|
| _valueForItem: function(item) {
|
| - var propValue = item[this.attrForSelected];
|
| + var propValue = item[Polymer.CaseMap.dashToCamelCase(this.attrForSelected)];
|
| return propValue != undefined ? propValue : item.getAttribute(this.attrForSelected);
|
| },
|
|
|
| @@ -6892,7 +6953,7 @@ Polymer({
|
| },
|
|
|
| observers: [
|
| - '_updateSelected(selectedValues)'
|
| + '_updateSelected(selectedValues.splices)'
|
| ],
|
|
|
| /**
|
| @@ -6928,7 +6989,7 @@ Polymer({
|
| Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
|
| } else if (this._shouldUpdateSelection) {
|
| this.selectedValues = this.selectedItems.map(function(selectedItem) {
|
| - return this._indexToValue(this.indexOf(selectedItem));
|
| + return this._indexToValue(this.indexOf(selectedItem));
|
| }, this).filter(function(unfilteredValue) {
|
| return unfilteredValue != null;
|
| }, this);
|
| @@ -6952,6 +7013,13 @@ Polymer({
|
| for (var i = 0; i < selectedItems.length; i++) {
|
| this._selection.setItemSelected(selectedItems[i], true);
|
| }
|
| + // Check for items, since this array is populated only when attached
|
| + if (this.fallbackSelection && this.items.length && !this._selection.get().length) {
|
| + var fallback = this._valueToItem(this.fallbackSelection);
|
| + if (fallback) {
|
| + this.selectedValues = [this.fallbackSelection];
|
| + }
|
| + }
|
| } else {
|
| this._selection.clear();
|
| }
|
| @@ -6975,7 +7043,6 @@ Polymer({
|
| } else {
|
| this.splice('selectedValues',i,1);
|
| }
|
| - this._selection.setItemSelected(this._valueToItem(value), unselected);
|
| },
|
|
|
| _valuesToItems: function(values) {
|
| @@ -7053,6 +7120,8 @@ Polymer({
|
| * @param {string|number} value the value to select.
|
| */
|
| select: function(value) {
|
| + // Cancel automatically focusing a default item if the menu received focus
|
| + // through a user action selecting a particular item.
|
| if (this._defaultFocusAsync) {
|
| this.cancelAsync(this._defaultFocusAsync);
|
| this._defaultFocusAsync = null;
|
| @@ -7223,8 +7292,6 @@ Polymer({
|
| return;
|
| }
|
|
|
| - this.blur();
|
| -
|
| // clear the cached focus item
|
| this._defaultFocusAsync = this.async(function() {
|
| // focus the selected item when the menu receives focus, or the first item
|
| @@ -7235,11 +7302,10 @@ Polymer({
|
|
|
| if (selectedItem) {
|
| this._setFocusedItem(selectedItem);
|
| - } else {
|
| + } else if (this.items[0]) {
|
| this._setFocusedItem(this.items[0]);
|
| }
|
| - // async 1ms to wait for `select` to get called from `_itemActivate`
|
| - }, 1);
|
| + });
|
| },
|
|
|
| /**
|
| @@ -7569,195 +7635,356 @@ CSS properties | Action
|
| /**
|
| * @struct
|
| * @constructor
|
| + * @private
|
| */
|
| Polymer.IronOverlayManagerClass = function() {
|
| + /**
|
| + * Used to keep track of the opened overlays.
|
| + * @private {Array<Element>}
|
| + */
|
| this._overlays = [];
|
| - // Used to keep track of the last focused node before an overlay gets opened.
|
| - this._lastFocusedNodes = [];
|
|
|
| /**
|
| - * iframes have a default z-index of 100, so this default should be at least
|
| - * that.
|
| + * iframes have a default z-index of 100,
|
| + * so this default should be at least that.
|
| * @private {number}
|
| */
|
| this._minimumZ = 101;
|
|
|
| - this._backdrops = [];
|
| -
|
| + /**
|
| + * Memoized backdrop element.
|
| + * @private {Element|null}
|
| + */
|
| this._backdropElement = null;
|
| - Object.defineProperty(this, 'backdropElement', {
|
| - get: function() {
|
| - if (!this._backdropElement) {
|
| - this._backdropElement = document.createElement('iron-overlay-backdrop');
|
| - }
|
| - return this._backdropElement;
|
| - }.bind(this)
|
| - });
|
| +
|
| + // Listen to mousedown or touchstart to be sure to be the first to capture
|
| + // clicks outside the overlay.
|
| + var clickEvent = ('ontouchstart' in window) ? 'touchstart' : 'mousedown';
|
| + document.addEventListener(clickEvent, this._onCaptureClick.bind(this), true);
|
| + document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
|
| + document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
|
| + };
|
| +
|
| + Polymer.IronOverlayManagerClass.prototype = {
|
| +
|
| + constructor: Polymer.IronOverlayManagerClass,
|
| +
|
| + /**
|
| + * The shared backdrop element.
|
| + * @type {Element} backdropElement
|
| + */
|
| + get backdropElement() {
|
| + if (!this._backdropElement) {
|
| + this._backdropElement = document.createElement('iron-overlay-backdrop');
|
| + }
|
| + return this._backdropElement;
|
| + },
|
|
|
| /**
|
| * The deepest active element.
|
| - * returns {?Node} element the active element
|
| - */
|
| - this.deepActiveElement = null;
|
| - Object.defineProperty(this, 'deepActiveElement', {
|
| - get: function() {
|
| - var active = document.activeElement;
|
| - // document.activeElement can be null
|
| - // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
|
| - while (active && active.root && Polymer.dom(active.root).activeElement) {
|
| - active = Polymer.dom(active.root).activeElement;
|
| - }
|
| - return active;
|
| - }.bind(this)
|
| - });
|
| - };
|
| + * @type {Element} activeElement the active element
|
| + */
|
| + get deepActiveElement() {
|
| + // document.activeElement can be null
|
| + // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
|
| + // In case of null, default it to document.body.
|
| + var active = document.activeElement || document.body;
|
| + while (active.root && Polymer.dom(active.root).activeElement) {
|
| + active = Polymer.dom(active.root).activeElement;
|
| + }
|
| + return active;
|
| + },
|
|
|
| - /**
|
| - * If a node is contained in an overlay.
|
| - * @private
|
| - * @param {Node} node
|
| - * @returns {Boolean}
|
| - */
|
| - Polymer.IronOverlayManagerClass.prototype._isChildOfOverlay = function(node) {
|
| - while (node && node !== document.body) {
|
| - // Use logical parentNode, or native ShadowRoot host.
|
| - node = Polymer.dom(node).parentNode || node.host;
|
| - // Check if it is an overlay.
|
| - if (node && node.behaviors && node.behaviors.indexOf(Polymer.IronOverlayBehaviorImpl) !== -1) {
|
| - return true;
|
| + /**
|
| + * Brings the overlay at the specified index to the front.
|
| + * @param {number} i
|
| + * @private
|
| + */
|
| + _bringOverlayAtIndexToFront: function(i) {
|
| + var overlay = this._overlays[i];
|
| + var lastI = this._overlays.length - 1;
|
| + // Ensure always-on-top overlay stays on top.
|
| + if (!overlay.alwaysOnTop && this._overlays[lastI].alwaysOnTop) {
|
| + lastI--;
|
| + }
|
| + // If already the top element, return.
|
| + if (!overlay || i >= lastI) {
|
| + return;
|
| + }
|
| + // Update z-index to be on top.
|
| + var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
|
| + if (this._getZ(overlay) <= minimumZ) {
|
| + this._applyOverlayZ(overlay, minimumZ);
|
| }
|
| - }
|
| - return false;
|
| - };
|
|
|
| - Polymer.IronOverlayManagerClass.prototype._applyOverlayZ = function(overlay, aboveZ) {
|
| - this._setZ(overlay, aboveZ + 2);
|
| - };
|
| + // Shift other overlays behind the new on top.
|
| + while (i < lastI) {
|
| + this._overlays[i] = this._overlays[i + 1];
|
| + i++;
|
| + }
|
| + this._overlays[lastI] = overlay;
|
| + },
|
|
|
| - Polymer.IronOverlayManagerClass.prototype._setZ = function(element, z) {
|
| - element.style.zIndex = z;
|
| - };
|
| + /**
|
| + * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
|
| + * Also updates the backdrop z-index.
|
| + * @param {Element} overlay
|
| + */
|
| + addOrRemoveOverlay: function(overlay) {
|
| + if (overlay.opened) {
|
| + this.addOverlay(overlay);
|
| + } else {
|
| + this.removeOverlay(overlay);
|
| + }
|
| + this.trackBackdrop();
|
| + },
|
|
|
| - /**
|
| - * track overlays for z-index and focus managemant
|
| - */
|
| - Polymer.IronOverlayManagerClass.prototype.addOverlay = function(overlay) {
|
| - var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
|
| - this._overlays.push(overlay);
|
| - var newZ = this.currentOverlayZ();
|
| - if (newZ <= minimumZ) {
|
| - this._applyOverlayZ(overlay, minimumZ);
|
| - }
|
| - var element = this.deepActiveElement;
|
| - // If already in other overlay, don't reset focus there.
|
| - if (this._isChildOfOverlay(element)) {
|
| - element = null;
|
| - }
|
| - this._lastFocusedNodes.push(element);
|
| - };
|
| + /**
|
| + * Tracks overlays for z-index and focus management.
|
| + * Ensures the last added overlay with always-on-top remains on top.
|
| + * @param {Element} overlay
|
| + */
|
| + addOverlay: function(overlay) {
|
| + var i = this._overlays.indexOf(overlay);
|
| + if (i >= 0) {
|
| + this._bringOverlayAtIndexToFront(i);
|
| + return;
|
| + }
|
| + var insertionIndex = this._overlays.length;
|
| + var currentOverlay = this._overlays[insertionIndex - 1];
|
| + var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
|
| + var newZ = this._getZ(overlay);
|
|
|
| - Polymer.IronOverlayManagerClass.prototype.removeOverlay = function(overlay) {
|
| - var i = this._overlays.indexOf(overlay);
|
| - if (i >= 0) {
|
| - this._overlays.splice(i, 1);
|
| - this._setZ(overlay, '');
|
| + // Ensure always-on-top overlay stays on top.
|
| + if (currentOverlay && currentOverlay.alwaysOnTop && !overlay.alwaysOnTop) {
|
| + // This bumps the z-index of +2.
|
| + this._applyOverlayZ(currentOverlay, minimumZ);
|
| + insertionIndex--;
|
| + // Update minimumZ to match previous overlay's z-index.
|
| + var previousOverlay = this._overlays[insertionIndex - 1];
|
| + minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
|
| + }
|
|
|
| - var node = this._lastFocusedNodes[i];
|
| - // Focus only if still contained in document.body
|
| - if (overlay.restoreFocusOnClose && node && Polymer.dom(document.body).deepContains(node)) {
|
| - node.focus();
|
| + // Update z-index and insert overlay.
|
| + if (newZ <= minimumZ) {
|
| + this._applyOverlayZ(overlay, minimumZ);
|
| }
|
| - this._lastFocusedNodes.splice(i, 1);
|
| - }
|
| - };
|
| + this._overlays.splice(insertionIndex, 0, overlay);
|
|
|
| - Polymer.IronOverlayManagerClass.prototype.currentOverlay = function() {
|
| - var i = this._overlays.length - 1;
|
| - while (this._overlays[i] && !this._overlays[i].opened) {
|
| - --i;
|
| - }
|
| - return this._overlays[i];
|
| - };
|
| + // Get focused node.
|
| + var element = this.deepActiveElement;
|
| + overlay.restoreFocusNode = this._overlayParent(element) ? null : element;
|
| + },
|
|
|
| - Polymer.IronOverlayManagerClass.prototype.currentOverlayZ = function() {
|
| - return this._getOverlayZ(this.currentOverlay());
|
| - };
|
| + /**
|
| + * @param {Element} overlay
|
| + */
|
| + removeOverlay: function(overlay) {
|
| + var i = this._overlays.indexOf(overlay);
|
| + if (i === -1) {
|
| + return;
|
| + }
|
| + this._overlays.splice(i, 1);
|
|
|
| - /**
|
| - * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
|
| - * This does not effect the z-index of any existing overlays.
|
| - *
|
| - * @param {number} minimumZ
|
| - */
|
| - Polymer.IronOverlayManagerClass.prototype.ensureMinimumZ = function(minimumZ) {
|
| - this._minimumZ = Math.max(this._minimumZ, minimumZ);
|
| - };
|
| + var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null;
|
| + overlay.restoreFocusNode = null;
|
| + // Focus back only if still contained in document.body
|
| + if (node && Polymer.dom(document.body).deepContains(node)) {
|
| + node.focus();
|
| + }
|
| + },
|
|
|
| - Polymer.IronOverlayManagerClass.prototype.focusOverlay = function() {
|
| - var current = this.currentOverlay();
|
| - // We have to be careful to focus the next overlay _after_ any current
|
| - // transitions are complete (due to the state being toggled prior to the
|
| - // transition). Otherwise, we risk infinite recursion when a transitioning
|
| - // (closed) overlay becomes the current overlay.
|
| - //
|
| - // NOTE: We make the assumption that any overlay that completes a transition
|
| - // will call into focusOverlay to kick the process back off. Currently:
|
| - // transitionend -> _applyFocus -> focusOverlay.
|
| - if (current && !current.transitioning) {
|
| - current._applyFocus();
|
| - }
|
| - };
|
| + /**
|
| + * Returns the current overlay.
|
| + * @return {Element|undefined}
|
| + */
|
| + currentOverlay: function() {
|
| + var i = this._overlays.length - 1;
|
| + return this._overlays[i];
|
| + },
|
|
|
| - Polymer.IronOverlayManagerClass.prototype.trackBackdrop = function(element) {
|
| - // backdrops contains the overlays with a backdrop that are currently
|
| - // visible
|
| - var index = this._backdrops.indexOf(element);
|
| - if (element.opened && element.withBackdrop) {
|
| - // no duplicates
|
| - if (index === -1) {
|
| - this._backdrops.push(element);
|
| - }
|
| - } else if (index >= 0) {
|
| - this._backdrops.splice(index, 1);
|
| - }
|
| - };
|
| + /**
|
| + * Returns the current overlay z-index.
|
| + * @return {number}
|
| + */
|
| + currentOverlayZ: function() {
|
| + return this._getZ(this.currentOverlay());
|
| + },
|
|
|
| - Polymer.IronOverlayManagerClass.prototype.getBackdrops = function() {
|
| - return this._backdrops;
|
| - };
|
| + /**
|
| + * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
|
| + * This does not effect the z-index of any existing overlays.
|
| + * @param {number} minimumZ
|
| + */
|
| + ensureMinimumZ: function(minimumZ) {
|
| + this._minimumZ = Math.max(this._minimumZ, minimumZ);
|
| + },
|
|
|
| - /**
|
| - * Returns the z-index for the backdrop.
|
| - */
|
| - Polymer.IronOverlayManagerClass.prototype.backdropZ = function() {
|
| - return this._getOverlayZ(this._overlayWithBackdrop()) - 1;
|
| - };
|
| + focusOverlay: function() {
|
| + var current = /** @type {?} */ (this.currentOverlay());
|
| + // We have to be careful to focus the next overlay _after_ any current
|
| + // transitions are complete (due to the state being toggled prior to the
|
| + // transition). Otherwise, we risk infinite recursion when a transitioning
|
| + // (closed) overlay becomes the current overlay.
|
| + //
|
| + // NOTE: We make the assumption that any overlay that completes a transition
|
| + // will call into focusOverlay to kick the process back off. Currently:
|
| + // transitionend -> _applyFocus -> focusOverlay.
|
| + if (current && !current.transitioning) {
|
| + current._applyFocus();
|
| + }
|
| + },
|
|
|
| - /**
|
| - * Returns the first opened overlay that has a backdrop.
|
| - */
|
| - Polymer.IronOverlayManagerClass.prototype._overlayWithBackdrop = function() {
|
| - for (var i = 0; i < this._overlays.length; i++) {
|
| - if (this._overlays[i].opened && this._overlays[i].withBackdrop) {
|
| - return this._overlays[i];
|
| + /**
|
| + * Updates the backdrop z-index.
|
| + */
|
| + trackBackdrop: function() {
|
| + this.backdropElement.style.zIndex = this.backdropZ();
|
| + },
|
| +
|
| + /**
|
| + * @return {Array<Element>}
|
| + */
|
| + getBackdrops: function() {
|
| + var backdrops = [];
|
| + for (var i = 0; i < this._overlays.length; i++) {
|
| + if (this._overlays[i].withBackdrop) {
|
| + backdrops.push(this._overlays[i]);
|
| + }
|
| }
|
| - }
|
| - };
|
| + return backdrops;
|
| + },
|
|
|
| - /**
|
| - * Calculates the minimum z-index for the overlay.
|
| - */
|
| - Polymer.IronOverlayManagerClass.prototype._getOverlayZ = function(overlay) {
|
| - var z = this._minimumZ;
|
| - if (overlay) {
|
| - var z1 = Number(window.getComputedStyle(overlay).zIndex);
|
| - // Check if is a number
|
| - // Number.isNaN not supported in IE 10+
|
| - if (z1 === z1) {
|
| - z = z1;
|
| + /**
|
| + * Returns the z-index for the backdrop.
|
| + * @return {number}
|
| + */
|
| + backdropZ: function() {
|
| + return this._getZ(this._overlayWithBackdrop()) - 1;
|
| + },
|
| +
|
| + /**
|
| + * Returns the first opened overlay that has a backdrop.
|
| + * @return {Element|undefined}
|
| + * @private
|
| + */
|
| + _overlayWithBackdrop: function() {
|
| + for (var i = 0; i < this._overlays.length; i++) {
|
| + if (this._overlays[i].withBackdrop) {
|
| + return this._overlays[i];
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Calculates the minimum z-index for the overlay.
|
| + * @param {Element=} overlay
|
| + * @private
|
| + */
|
| + _getZ: function(overlay) {
|
| + var z = this._minimumZ;
|
| + if (overlay) {
|
| + var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
|
| + // Check if is a number
|
| + // Number.isNaN not supported in IE 10+
|
| + if (z1 === z1) {
|
| + z = z1;
|
| + }
|
| + }
|
| + return z;
|
| + },
|
| +
|
| + /**
|
| + * @param {Element} element
|
| + * @param {number|string} z
|
| + * @private
|
| + */
|
| + _setZ: function(element, z) {
|
| + element.style.zIndex = z;
|
| + },
|
| +
|
| + /**
|
| + * @param {Element} overlay
|
| + * @param {number} aboveZ
|
| + * @private
|
| + */
|
| + _applyOverlayZ: function(overlay, aboveZ) {
|
| + this._setZ(overlay, aboveZ + 2);
|
| + },
|
| +
|
| + /**
|
| + * Returns the overlay containing the provided node. If the node is an overlay,
|
| + * it returns the node.
|
| + * @param {Element=} node
|
| + * @return {Element|undefined}
|
| + * @private
|
| + */
|
| + _overlayParent: function(node) {
|
| + while (node && node !== document.body) {
|
| + // Check if it is an overlay.
|
| + if (node._manager === this) {
|
| + return node;
|
| + }
|
| + // Use logical parentNode, or native ShadowRoot host.
|
| + node = Polymer.dom(node).parentNode || node.host;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Returns the deepest overlay in the path.
|
| + * @param {Array<Element>=} path
|
| + * @return {Element|undefined}
|
| + * @private
|
| + */
|
| + _overlayInPath: function(path) {
|
| + path = path || [];
|
| + for (var i = 0; i < path.length; i++) {
|
| + if (path[i]._manager === this) {
|
| + return path[i];
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures the click event is delegated to the right overlay.
|
| + * @param {!Event} event
|
| + * @private
|
| + */
|
| + _onCaptureClick: function(event) {
|
| + var overlay = /** @type {?} */ (this.currentOverlay());
|
| + // Check if clicked outside of top overlay.
|
| + if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
|
| + overlay._onCaptureClick(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures the focus event is delegated to the right overlay.
|
| + * @param {!Event} event
|
| + * @private
|
| + */
|
| + _onCaptureFocus: function(event) {
|
| + var overlay = /** @type {?} */ (this.currentOverlay());
|
| + if (overlay) {
|
| + overlay._onCaptureFocus(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures TAB and ESC keyboard events are delegated to the right overlay.
|
| + * @param {!Event} event
|
| + * @private
|
| + */
|
| + _onCaptureKeyDown: function(event) {
|
| + var overlay = /** @type {?} */ (this.currentOverlay());
|
| + if (overlay) {
|
| + if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
|
| + overlay._onCaptureEsc(event);
|
| + } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
|
| + overlay._onCaptureTab(event);
|
| + }
|
| }
|
| }
|
| - return z;
|
| };
|
|
|
| Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
|
| @@ -7794,8 +8021,6 @@ CSS properties | Action
|
| * Appends the backdrop to document body and sets its `z-index` to be below the latest overlay.
|
| */
|
| prepare: function() {
|
| - // Always update z-index
|
| - this.style.zIndex = this._manager.backdropZ();
|
| if (!this.parentNode) {
|
| Polymer.dom(document.body).appendChild(this);
|
| }
|
| @@ -7815,8 +8040,6 @@ CSS properties | Action
|
| * Hides the backdrop if needed.
|
| */
|
| close: function() {
|
| - // Always update z-index
|
| - this.style.zIndex = this._manager.backdropZ();
|
| // close only if no element with backdrop is left
|
| if (this._manager.getBackdrops().length === 0) {
|
| // Read style before setting opened.
|
| @@ -7849,6 +8072,10 @@ CSS properties | Action
|
| });
|
|
|
| })();
|
| +// IIFE to help scripts concatenation.
|
| +(function() {
|
| +'use strict';
|
| +
|
| /**
|
| Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
|
| on top of other content. It includes an optional backdrop, and can be used to implement a variety
|
| @@ -7952,15 +8179,6 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| },
|
|
|
| /**
|
| - * The HTMLElement that will be firing relevant KeyboardEvents.
|
| - * Used for capturing esc and tab. Overridden from `IronA11yKeysBehavior`.
|
| - */
|
| - keyEventTarget: {
|
| - type: Object,
|
| - value: document
|
| - },
|
| -
|
| - /**
|
| * Set to true to enable restoring of focus when overlay is closed.
|
| */
|
| restoreFocusOnClose: {
|
| @@ -7968,25 +8186,23 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| value: false
|
| },
|
|
|
| + /**
|
| + * Set to true to keep overlay always on top.
|
| + */
|
| + alwaysOnTop: {
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * Shortcut to access to the overlay manager.
|
| + * @private
|
| + * @type {Polymer.IronOverlayManagerClass}
|
| + */
|
| _manager: {
|
| type: Object,
|
| value: Polymer.IronOverlayManager
|
| },
|
|
|
| - _boundOnCaptureClick: {
|
| - type: Function,
|
| - value: function() {
|
| - return this._onCaptureClick.bind(this);
|
| - }
|
| - },
|
| -
|
| - _boundOnCaptureFocus: {
|
| - type: Function,
|
| - value: function() {
|
| - return this._onCaptureFocus.bind(this);
|
| - }
|
| - },
|
| -
|
| /**
|
| * The node being focused.
|
| * @type {?Node}
|
| @@ -7997,18 +8213,13 @@ context. You should place this element as a child of `<body>` whenever possible.
|
|
|
| },
|
|
|
| - keyBindings: {
|
| - 'esc': '__onEsc',
|
| - 'tab': '__onTab'
|
| - },
|
| -
|
| listeners: {
|
| 'iron-resize': '_onIronResize'
|
| },
|
|
|
| /**
|
| * The backdrop element.
|
| - * @type {Node}
|
| + * @type {Element}
|
| */
|
| get backdropElement() {
|
| return this._manager.backdropElement;
|
| @@ -8029,7 +8240,7 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| *
|
| * If you know what is your content (specifically the first and last focusable children),
|
| * you can override this method to return only `[firstFocusable, lastFocusable];`
|
| - * @type {[Node]}
|
| + * @type {Array<Node>}
|
| * @protected
|
| */
|
| get _focusableNodes() {
|
| @@ -8075,11 +8286,17 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| },
|
|
|
| ready: function() {
|
| + // Used to skip calls to notifyResize and refit while the overlay is animating.
|
| + this.__isAnimating = false;
|
| // with-backdrop needs tabindex to be set in order to trap the focus.
|
| // If it is not set, IronOverlayBehavior will set it, and remove it if with-backdrop = false.
|
| this.__shouldRemoveTabIndex = false;
|
| // Used for wrapping the focus on TAB / Shift+TAB.
|
| this.__firstFocusableNode = this.__lastFocusableNode = null;
|
| + // Used for requestAnimationFrame when opened changes.
|
| + this.__openChangedAsync = null;
|
| + // Used for requestAnimationFrame when iron-resize is fired.
|
| + this.__onIronResizeAsync = null;
|
| this._ensureSetup();
|
| },
|
|
|
| @@ -8095,8 +8312,10 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| Polymer.dom(this).unobserveNodes(this._observer);
|
| this._observer = null;
|
| this.opened = false;
|
| - this._manager.trackBackdrop(this);
|
| - this._manager.removeOverlay(this);
|
| + if (this.withBackdrop) {
|
| + // Allow user interactions right away.
|
| + this.backdropElement.close();
|
| + }
|
| },
|
|
|
| /**
|
| @@ -8125,7 +8344,7 @@ context. You should place this element as a child of `<body>` whenever possible.
|
|
|
| /**
|
| * Cancels the overlay.
|
| - * @param {?Event} event The original event
|
| + * @param {Event=} event The original event
|
| */
|
| cancel: function(event) {
|
| var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
|
| @@ -8158,31 +8377,26 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| return;
|
| }
|
|
|
| - this._manager.trackBackdrop(this);
|
| + this._manager.addOrRemoveOverlay(this);
|
|
|
| - if (this.opened) {
|
| - this._prepareRenderOpened();
|
| - }
|
| + this.__isAnimating = true;
|
|
|
| - if (this._openChangedAsync) {
|
| - this.cancelAsync(this._openChangedAsync);
|
| + // requestAnimationFrame for non-blocking rendering
|
| + if (this.__openChangedAsync) {
|
| + cancelAnimationFrame(this.__openChangedAsync);
|
| }
|
| - // Async here to allow overlay layer to become visible, and to avoid
|
| - // listeners to immediately close via a click.
|
| - this._openChangedAsync = this.async(function() {
|
| - // overlay becomes visible here
|
| - this.style.display = '';
|
| - // Force layout to ensure transition will go. Set offsetWidth to itself
|
| - // so that compilers won't remove it.
|
| - this.offsetWidth = this.offsetWidth;
|
| - if (this.opened) {
|
| - this._renderOpened();
|
| - } else {
|
| - this._renderClosed();
|
| + if (this.opened) {
|
| + if (this.withBackdrop) {
|
| + this.backdropElement.prepare();
|
| }
|
| - this._toggleListeners();
|
| - this._openChangedAsync = null;
|
| - }, 1);
|
| + this.__openChangedAsync = requestAnimationFrame(function() {
|
| + this.__openChangedAsync = null;
|
| + this._prepareRenderOpened();
|
| + this._renderOpened();
|
| + }.bind(this));
|
| + } else {
|
| + this._renderClosed();
|
| + }
|
| },
|
|
|
| _canceledChanged: function() {
|
| @@ -8200,7 +8414,7 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| this.__shouldRemoveTabIndex = false;
|
| }
|
| if (this.opened) {
|
| - this._manager.trackBackdrop(this);
|
| + this._manager.trackBackdrop();
|
| if (this.withBackdrop) {
|
| this.backdropElement.prepare();
|
| // Give time to be added to document.
|
| @@ -8213,42 +8427,18 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| }
|
| },
|
|
|
| - _toggleListener: function(enable, node, event, boundListener, capture) {
|
| - if (enable) {
|
| - // enable document-wide tap recognizer
|
| - if (event === 'tap') {
|
| - Polymer.Gestures.add(document, 'tap', null);
|
| - }
|
| - node.addEventListener(event, boundListener, capture);
|
| - } else {
|
| - // disable document-wide tap recognizer
|
| - if (event === 'tap') {
|
| - Polymer.Gestures.remove(document, 'tap', null);
|
| - }
|
| - node.removeEventListener(event, boundListener, capture);
|
| - }
|
| - },
|
| -
|
| - _toggleListeners: function() {
|
| - this._toggleListener(this.opened, document, 'tap', this._boundOnCaptureClick, true);
|
| - this._toggleListener(this.opened, document, 'focus', this._boundOnCaptureFocus, true);
|
| - },
|
| -
|
| - // tasks which must occur before opening; e.g. making the element visible
|
| + /**
|
| + * tasks which must occur before opening; e.g. making the element visible.
|
| + * @protected
|
| + */
|
| _prepareRenderOpened: function() {
|
|
|
| - this._manager.addOverlay(this);
|
| -
|
| // Needed to calculate the size of the overlay so that transitions on its size
|
| // will have the correct starting points.
|
| this._preparePositioning();
|
| - this.fit();
|
| + this.refit();
|
| this._finishPositioning();
|
|
|
| - if (this.withBackdrop) {
|
| - this.backdropElement.prepare();
|
| - }
|
| -
|
| // Safari will apply the focus to the autofocus element when displayed for the first time,
|
| // so we blur it. Later, _applyFocus will set the focus if necessary.
|
| if (this.noAutoFocus && document.activeElement === this._focusNode) {
|
| @@ -8256,8 +8446,10 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| }
|
| },
|
|
|
| - // tasks which cause the overlay to actually open; typically play an
|
| - // animation
|
| + /**
|
| + * Tasks which cause the overlay to actually open; typically play an animation.
|
| + * @protected
|
| + */
|
| _renderOpened: function() {
|
| if (this.withBackdrop) {
|
| this.backdropElement.open();
|
| @@ -8265,6 +8457,10 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| this._finishRenderOpened();
|
| },
|
|
|
| + /**
|
| + * Tasks which cause the overlay to actually close; typically play an animation.
|
| + * @protected
|
| + */
|
| _renderClosed: function() {
|
| if (this.withBackdrop) {
|
| this.backdropElement.close();
|
| @@ -8272,25 +8468,33 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| this._finishRenderClosed();
|
| },
|
|
|
| + /**
|
| + * Tasks to be performed at the end of open action. Will fire `iron-overlay-opened`.
|
| + * @protected
|
| + */
|
| _finishRenderOpened: function() {
|
| - // This ensures the overlay is visible before we set the focus
|
| - // (by calling _onIronResize -> refit).
|
| - this.notifyResize();
|
| // Focus the child node with [autofocus]
|
| this._applyFocus();
|
|
|
| + this.notifyResize();
|
| + this.__isAnimating = false;
|
| this.fire('iron-overlay-opened');
|
| },
|
|
|
| + /**
|
| + * Tasks to be performed at the end of close action. Will fire `iron-overlay-closed`.
|
| + * @protected
|
| + */
|
| _finishRenderClosed: function() {
|
| // Hide the overlay and remove the backdrop.
|
| - this.resetFit();
|
| this.style.display = 'none';
|
| - this._manager.removeOverlay(this);
|
| + // Reset z-index only at the end of the animation.
|
| + this.style.zIndex = '';
|
|
|
| this._applyFocus();
|
| - this.notifyResize();
|
|
|
| + this.notifyResize();
|
| + this.__isAnimating = false;
|
| this.fire('iron-overlay-closed', this.closingReason);
|
| },
|
|
|
| @@ -8301,14 +8505,24 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| },
|
|
|
| _finishPositioning: function() {
|
| + // First, make it invisible & reactivate animations.
|
| this.style.display = 'none';
|
| - this.style.transform = this.style.webkitTransform = '';
|
| - // Force layout layout to avoid application of transform.
|
| - // Set offsetWidth to itself so that compilers won't remove it.
|
| - this.offsetWidth = this.offsetWidth;
|
| + // Force reflow before re-enabling animations so that they don't start.
|
| + // Set scrollTop to itself so that Closure Compiler doesn't remove this.
|
| + this.scrollTop = this.scrollTop;
|
| this.style.transition = this.style.webkitTransition = '';
|
| + this.style.transform = this.style.webkitTransform = '';
|
| + // Now that animations are enabled, make it visible again
|
| + this.style.display = '';
|
| + // Force reflow, so that following animations are properly started.
|
| + // Set scrollTop to itself so that Closure Compiler doesn't remove this.
|
| + this.scrollTop = this.scrollTop;
|
| },
|
|
|
| + /**
|
| + * Applies focus according to the opened state.
|
| + * @protected
|
| + */
|
| _applyFocus: function() {
|
| if (this.opened) {
|
| if (!this.noAutoFocus) {
|
| @@ -8321,68 +8535,56 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| }
|
| },
|
|
|
| + /**
|
| + * Cancels (closes) the overlay. Call when click happens outside the overlay.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| _onCaptureClick: function(event) {
|
| - if (this._manager.currentOverlay() === this &&
|
| - Polymer.dom(event).path.indexOf(this) === -1) {
|
| - if (this.noCancelOnOutsideClick) {
|
| - this._applyFocus();
|
| - } else {
|
| - this.cancel(event);
|
| - }
|
| + if (!this.noCancelOnOutsideClick) {
|
| + this.cancel(event);
|
| }
|
| },
|
|
|
| + /**
|
| + * Keeps track of the focused child. If withBackdrop, traps focus within overlay.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| _onCaptureFocus: function (event) {
|
| - if (this._manager.currentOverlay() === this && this.withBackdrop) {
|
| - var path = Polymer.dom(event).path;
|
| - if (path.indexOf(this) === -1) {
|
| - event.stopPropagation();
|
| - this._applyFocus();
|
| - } else {
|
| - this._focusedChild = path[0];
|
| - }
|
| + if (!this.withBackdrop) {
|
| + return;
|
| }
|
| - },
|
| -
|
| - _onIronResize: function() {
|
| - if (this.opened) {
|
| - this.refit();
|
| + var path = Polymer.dom(event).path;
|
| + if (path.indexOf(this) === -1) {
|
| + event.stopPropagation();
|
| + this._applyFocus();
|
| + } else {
|
| + this._focusedChild = path[0];
|
| }
|
| },
|
|
|
| /**
|
| + * Handles the ESC key event and cancels (closes) the overlay.
|
| + * @param {!Event} event
|
| * @protected
|
| - * Will call notifyResize if overlay is opened.
|
| - * Can be overridden in order to avoid multiple observers on the same node.
|
| */
|
| - _onNodesChange: function() {
|
| - if (this.opened) {
|
| - this.notifyResize();
|
| - }
|
| - // Store it so we don't query too much.
|
| - var focusableNodes = this._focusableNodes;
|
| - this.__firstFocusableNode = focusableNodes[0];
|
| - this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
|
| - },
|
| -
|
| - __onEsc: function(event) {
|
| - // Not opened or not on top, so return.
|
| - if (this._manager.currentOverlay() !== this) {
|
| - return;
|
| - }
|
| + _onCaptureEsc: function(event) {
|
| if (!this.noCancelOnEscKey) {
|
| this.cancel(event);
|
| }
|
| },
|
|
|
| - __onTab: function(event) {
|
| - // Not opened or not on top, so return.
|
| - if (this._manager.currentOverlay() !== this) {
|
| - return;
|
| - }
|
| + /**
|
| + * Handles TAB key events to track focus changes.
|
| + * Will wrap focus for overlays withBackdrop.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| + _onCaptureTab: function(event) {
|
| // TAB wraps from last to first focusable.
|
| // Shift + TAB wraps from first to last focusable.
|
| - var shift = event.detail.keyboardEvent.shiftKey;
|
| + var shift = event.shiftKey;
|
| var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusableNode;
|
| var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNode;
|
| if (this.withBackdrop && this._focusedChild === nodeToCheck) {
|
| @@ -8390,11 +8592,43 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| // wrapping of the focus (the next event after tab is focus).
|
| this._focusedChild = nodeToSet;
|
| }
|
| + },
|
| +
|
| + /**
|
| + * Refits if the overlay is opened and not animating.
|
| + * @protected
|
| + */
|
| + _onIronResize: function() {
|
| + if (this.__onIronResizeAsync) {
|
| + cancelAnimationFrame(this.__onIronResizeAsync);
|
| + this.__onIronResizeAsync = null;
|
| + }
|
| + if (this.opened && !this.__isAnimating) {
|
| + this.__onIronResizeAsync = requestAnimationFrame(function() {
|
| + this.__onIronResizeAsync = null;
|
| + this.refit();
|
| + }.bind(this));
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Will call notifyResize if overlay is opened.
|
| + * Can be overridden in order to avoid multiple observers on the same node.
|
| + * @protected
|
| + */
|
| + _onNodesChange: function() {
|
| + if (this.opened && !this.__isAnimating) {
|
| + this.notifyResize();
|
| + }
|
| + // Store it so we don't query too much.
|
| + var focusableNodes = this._focusableNodes;
|
| + this.__firstFocusableNode = focusableNodes[0];
|
| + this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
|
| }
|
| };
|
|
|
| /** @polymerBehavior */
|
| - Polymer.IronOverlayBehavior = [Polymer.IronA11yKeysBehavior, Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
|
| + Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
|
|
|
| /**
|
| * Fired after the `iron-overlay` opens.
|
| @@ -8415,6 +8649,8 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| * @event iron-overlay-closed
|
| * @param {{canceled: (boolean|undefined)}} closingReason Contains `canceled` (whether the overlay was canceled).
|
| */
|
| +
|
| +})();
|
| /**
|
| * Use `Polymer.NeonAnimationBehavior` to implement an animation.
|
| * @polymerBehavior
|
| @@ -8918,9 +9154,11 @@ Polymer({
|
| },
|
|
|
| _scrollInteractionHandler: function(event) {
|
| + var scrolledElement =
|
| + /** @type {HTMLElement} */(Polymer.dom(event).rootTarget);
|
| if (Polymer
|
| .IronDropdownScrollManager
|
| - .elementIsScrollLocked(Polymer.dom(event).rootTarget)) {
|
| + .elementIsScrollLocked(scrolledElement)) {
|
| if (event.type === 'keydown' &&
|
| !Polymer.IronDropdownScrollManager._isScrollingKeypress(event)) {
|
| return;
|
| @@ -9050,8 +9288,7 @@ Polymer({
|
| * it is opened.
|
| */
|
| positionTarget: {
|
| - type: Object,
|
| - observer: '_positionTargetChanged'
|
| + type: Object
|
| },
|
|
|
| /**
|
| @@ -9096,15 +9333,6 @@ Polymer({
|
| allowOutsideScroll: {
|
| type: Boolean,
|
| value: false
|
| - },
|
| -
|
| - /**
|
| - * We memoize the positionTarget bounding rectangle so that we can
|
| - * limit the number of times it is queried per resize / relayout.
|
| - * @type {?Object}
|
| - */
|
| - _positionRectMemo: {
|
| - type: Object
|
| }
|
| },
|
|
|
| @@ -9113,13 +9341,13 @@ Polymer({
|
| },
|
|
|
| observers: [
|
| - '_updateOverlayPosition(verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
|
| + '_updateOverlayPosition(positionTarget, verticalAlign, horizontalAlign, verticalOffset, horizontalOffset)'
|
| ],
|
|
|
| attached: function() {
|
| - if (this.positionTarget === undefined) {
|
| - this.positionTarget = this._defaultPositionTarget;
|
| - }
|
| + this.positionTarget = this.positionTarget || this._defaultPositionTarget;
|
| + // Memoize this to avoid expensive calculations & relayouts.
|
| + this._isRTL = window.getComputedStyle(this).direction == 'rtl';
|
| },
|
|
|
| /**
|
| @@ -9138,13 +9366,6 @@ Polymer({
|
| },
|
|
|
| /**
|
| - * Whether the text direction is RTL
|
| - */
|
| - _isRTL: function() {
|
| - return window.getComputedStyle(this).direction == 'rtl';
|
| - },
|
| -
|
| - /**
|
| * The element that should be used to position the dropdown when
|
| * it opens, if no position target is configured.
|
| */
|
| @@ -9159,31 +9380,32 @@ Polymer({
|
| },
|
|
|
| /**
|
| - * The bounding rect of the position target.
|
| + * The horizontal align value, accounting for the RTL/LTR text direction.
|
| */
|
| - get _positionRect() {
|
| - if (!this._positionRectMemo && this.positionTarget) {
|
| - this._positionRectMemo = this.positionTarget.getBoundingClientRect();
|
| + get _localeHorizontalAlign() {
|
| + // In RTL, "left" becomes "right".
|
| + if (this._isRTL) {
|
| + return this.horizontalAlign === 'right' ? 'left' : 'right';
|
| + } else {
|
| + return this.horizontalAlign;
|
| }
|
| -
|
| - return this._positionRectMemo;
|
| },
|
|
|
| /**
|
| * The horizontal offset value used to position the dropdown.
|
| + * @param {ClientRect} dropdownRect
|
| + * @param {ClientRect} positionRect
|
| + * @param {boolean=false} fromRight
|
| + * @return {number} pixels
|
| + * @private
|
| */
|
| - get _horizontalAlignTargetValue() {
|
| + _horizontalAlignTargetValue: function(dropdownRect, positionRect, fromRight) {
|
| var target;
|
| -
|
| - // In RTL, the direction flips, so what is "right" in LTR becomes "left".
|
| - var isRTL = this._isRTL();
|
| - if ((!isRTL && this.horizontalAlign === 'right') ||
|
| - (isRTL && this.horizontalAlign === 'left')) {
|
| - target = document.documentElement.clientWidth - this._positionRect.right;
|
| + if (fromRight) {
|
| + target = document.documentElement.clientWidth - positionRect.right - (this._fitWidth - dropdownRect.right);
|
| } else {
|
| - target = this._positionRect.left;
|
| + target = positionRect.left - dropdownRect.left;
|
| }
|
| -
|
| target += this.horizontalOffset;
|
|
|
| return Math.max(target, 0);
|
| @@ -9191,34 +9413,25 @@ Polymer({
|
|
|
| /**
|
| * The vertical offset value used to position the dropdown.
|
| + * @param {ClientRect} dropdownRect
|
| + * @param {ClientRect} positionRect
|
| + * @param {boolean=false} fromBottom
|
| + * @return {number} pixels
|
| + * @private
|
| */
|
| - get _verticalAlignTargetValue() {
|
| + _verticalAlignTargetValue: function(dropdownRect, positionRect, fromBottom) {
|
| var target;
|
| -
|
| - if (this.verticalAlign === 'bottom') {
|
| - target = document.documentElement.clientHeight - this._positionRect.bottom;
|
| + if (fromBottom) {
|
| + target = document.documentElement.clientHeight - positionRect.bottom - (this._fitHeight - dropdownRect.bottom);
|
| } else {
|
| - target = this._positionRect.top;
|
| + target = positionRect.top - dropdownRect.top;
|
| }
|
| -
|
| target += this.verticalOffset;
|
|
|
| return Math.max(target, 0);
|
| },
|
|
|
| /**
|
| - * The horizontal align value, accounting for the RTL/LTR text direction.
|
| - */
|
| - get _localeHorizontalAlign() {
|
| - // In RTL, "left" becomes "right".
|
| - if (this._isRTL()) {
|
| - return this.horizontalAlign === 'right' ? 'left' : 'right';
|
| - } else {
|
| - return this.horizontalAlign;
|
| - }
|
| - },
|
| -
|
| - /**
|
| * Called when the value of `opened` changes.
|
| *
|
| * @param {boolean} opened True if the dropdown is opened.
|
| @@ -9228,7 +9441,8 @@ Polymer({
|
| this.cancel();
|
| } else {
|
| this.cancelAnimation();
|
| - this._prepareDropdown();
|
| + this.sizingTarget = this.containedElement || this.sizingTarget;
|
| + this._updateAnimationConfig();
|
| Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
|
| }
|
| },
|
| @@ -9242,6 +9456,9 @@ Polymer({
|
| }
|
|
|
| if (!this.noAnimations && this.animationConfig && this.animationConfig.open) {
|
| + if (this.withBackdrop) {
|
| + this.backdropElement.open();
|
| + }
|
| this.$.contentWrapper.classList.add('animating');
|
| this.playAnimation('open');
|
| } else {
|
| @@ -9255,6 +9472,9 @@ Polymer({
|
| _renderClosed: function() {
|
| Polymer.IronDropdownScrollManager.removeScrollLock(this);
|
| if (!this.noAnimations && this.animationConfig && this.animationConfig.close) {
|
| + if (this.withBackdrop) {
|
| + this.backdropElement.close();
|
| + }
|
| this.$.contentWrapper.classList.add('animating');
|
| this.playAnimation('close');
|
| } else {
|
| @@ -9271,45 +9491,13 @@ Polymer({
|
| _onNeonAnimationFinish: function() {
|
| this.$.contentWrapper.classList.remove('animating');
|
| if (this.opened) {
|
| - Polymer.IronOverlayBehaviorImpl._renderOpened.apply(this);
|
| + Polymer.IronOverlayBehaviorImpl._finishRenderOpened.apply(this);
|
| } else {
|
| - Polymer.IronOverlayBehaviorImpl._renderClosed.apply(this);
|
| + Polymer.IronOverlayBehaviorImpl._finishRenderClosed.apply(this);
|
| }
|
| },
|
|
|
| /**
|
| - * Called when an `iron-resize` event fires.
|
| - */
|
| - _onIronResize: function() {
|
| - var containedElement = this.containedElement;
|
| - var scrollTop;
|
| - var scrollLeft;
|
| -
|
| - if (this.opened && containedElement) {
|
| - scrollTop = containedElement.scrollTop;
|
| - scrollLeft = containedElement.scrollLeft;
|
| - }
|
| -
|
| - if (this.opened) {
|
| - this._updateOverlayPosition();
|
| - }
|
| -
|
| - Polymer.IronOverlayBehaviorImpl._onIronResize.apply(this, arguments);
|
| -
|
| - if (this.opened && containedElement) {
|
| - containedElement.scrollTop = scrollTop;
|
| - containedElement.scrollLeft = scrollLeft;
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * Called when the `positionTarget` property changes.
|
| - */
|
| - _positionTargetChanged: function() {
|
| - this._updateOverlayPosition();
|
| - },
|
| -
|
| - /**
|
| * Constructs the final animation config from different properties used
|
| * to configure specific parts of the opening and closing animations.
|
| */
|
| @@ -9341,43 +9529,73 @@ Polymer({
|
| },
|
|
|
| /**
|
| - * Prepares the dropdown for opening by updating measured layout
|
| - * values.
|
| + * Updates the overlay position based on configured horizontal
|
| + * and vertical alignment.
|
| */
|
| - _prepareDropdown: function() {
|
| - this.sizingTarget = this.containedElement || this.sizingTarget;
|
| - this._updateAnimationConfig();
|
| - this._updateOverlayPosition();
|
| + _updateOverlayPosition: function() {
|
| + if (this.isAttached) {
|
| + // This triggers iron-resize, and iron-overlay-behavior will call refit if needed.
|
| + this.notifyResize();
|
| + }
|
| },
|
|
|
| /**
|
| - * Updates the overlay position based on configured horizontal
|
| - * and vertical alignment, and re-memoizes these values for the sake
|
| - * of behavior in `IronFitBehavior`.
|
| + * Useful to call this after the element, the window, or the `fitInfo`
|
| + * element has been resized. Will maintain the scroll position.
|
| */
|
| - _updateOverlayPosition: function() {
|
| - this._positionRectMemo = null;
|
| + refit: function () {
|
| + if (!this.opened) {
|
| + return
|
| + }
|
| + var containedElement = this.containedElement;
|
| + var scrollTop;
|
| + var scrollLeft;
|
|
|
| - if (!this.positionTarget) {
|
| - return;
|
| + if (containedElement) {
|
| + scrollTop = containedElement.scrollTop;
|
| + scrollLeft = containedElement.scrollLeft;
|
| + }
|
| + Polymer.IronFitBehavior.refit.apply(this, arguments);
|
| +
|
| + if (containedElement) {
|
| + containedElement.scrollTop = scrollTop;
|
| + containedElement.scrollLeft = scrollLeft;
|
| }
|
| + },
|
| +
|
| + /**
|
| + * Resets the target element's position and size constraints, and clear
|
| + * the memoized data.
|
| + */
|
| + resetFit: function() {
|
| + Polymer.IronFitBehavior.resetFit.apply(this, arguments);
|
|
|
| - this.style[this._localeHorizontalAlign] =
|
| - this._horizontalAlignTargetValue + 'px';
|
| + var hAlign = this._localeHorizontalAlign;
|
| + var vAlign = this.verticalAlign;
|
| + // Set to 0, 0 in order to discover any offset caused by parent stacking contexts.
|
| + this.style[hAlign] = this.style[vAlign] = '0px';
|
|
|
| - this.style[this.verticalAlign] =
|
| - this._verticalAlignTargetValue + 'px';
|
| + var dropdownRect = this.getBoundingClientRect();
|
| + var positionRect = this.positionTarget.getBoundingClientRect();
|
| + var horizontalValue = this._horizontalAlignTargetValue(dropdownRect, positionRect, hAlign === 'right');
|
| + var verticalValue = this._verticalAlignTargetValue(dropdownRect, positionRect, vAlign === 'bottom');
|
|
|
| - // NOTE(cdata): We re-memoize inline styles here, otherwise
|
| - // calling `refit` from `IronFitBehavior` will reset inline styles
|
| - // to whatever they were when the dropdown first opened.
|
| - if (this._fitInfo) {
|
| - this._fitInfo.inlineStyle[this.horizontalAlign] =
|
| - this.style[this.horizontalAlign];
|
| + this.style[hAlign] = horizontalValue + 'px';
|
| + this.style[vAlign] = verticalValue + 'px';
|
| + },
|
|
|
| - this._fitInfo.inlineStyle[this.verticalAlign] =
|
| - this.style[this.verticalAlign];
|
| - }
|
| + /**
|
| + * Overridden from `IronFitBehavior`.
|
| + * Ensure positionedBy has correct values for horizontally & vertically.
|
| + */
|
| + _discoverInfo: function() {
|
| + Polymer.IronFitBehavior._discoverInfo.apply(this, arguments);
|
| + // Note(valdrin): in Firefox, an element with style `position: fixed; bottom: 90vh; height: 20vh`
|
| + // would have `getComputedStyle(element).top < 0` (instead of being `auto`) http://jsbin.com/cofired/3/edit?html,output
|
| + // This would cause IronFitBehavior's `constrain` to wrongly calculate sizes
|
| + // (it would use `top` instead of `bottom`), so we ensure we give the correct values.
|
| + this._fitInfo.positionedBy.horizontally = this._localeHorizontalAlign;
|
| + this._fitInfo.positionedBy.vertically = this.verticalAlign;
|
| },
|
|
|
| /**
|
| @@ -9522,257 +9740,278 @@ Polymer({
|
| }
|
| });
|
| (function() {
|
| - 'use strict';
|
| + 'use strict';
|
|
|
| - var PaperMenuButton = Polymer({
|
| - is: 'paper-menu-button',
|
| + var PaperMenuButton = Polymer({
|
| + is: 'paper-menu-button',
|
|
|
| - /**
|
| - * Fired when the dropdown opens.
|
| - *
|
| - * @event paper-dropdown-open
|
| - */
|
| + /**
|
| + * Fired when the dropdown opens.
|
| + *
|
| + * @event paper-dropdown-open
|
| + */
|
|
|
| - /**
|
| - * Fired when the dropdown closes.
|
| - *
|
| - * @event paper-dropdown-close
|
| - */
|
| + /**
|
| + * Fired when the dropdown closes.
|
| + *
|
| + * @event paper-dropdown-close
|
| + */
|
|
|
| - behaviors: [
|
| - Polymer.IronA11yKeysBehavior,
|
| - Polymer.IronControlState
|
| - ],
|
| + behaviors: [
|
| + Polymer.IronA11yKeysBehavior,
|
| + Polymer.IronControlState
|
| + ],
|
|
|
| - properties: {
|
| + properties: {
|
| + /**
|
| + * True if the content is currently displayed.
|
| + */
|
| + opened: {
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true,
|
| + observer: '_openedChanged'
|
| + },
|
|
|
| - /**
|
| - * True if the content is currently displayed.
|
| - */
|
| - opened: {
|
| - type: Boolean,
|
| - value: false,
|
| - notify: true,
|
| - observer: '_openedChanged'
|
| + /**
|
| + * The orientation against which to align the menu dropdown
|
| + * horizontally relative to the dropdown trigger.
|
| + */
|
| + horizontalAlign: {
|
| + type: String,
|
| + value: 'left',
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /**
|
| + * The orientation against which to align the menu dropdown
|
| + * vertically relative to the dropdown trigger.
|
| + */
|
| + verticalAlign: {
|
| + type: String,
|
| + value: 'top',
|
| + reflectToAttribute: true
|
| + },
|
| +
|
| + /**
|
| + * A pixel value that will be added to the position calculated for the
|
| + * given `horizontalAlign`. Use a negative value to offset to the
|
| + * left, or a positive value to offset to the right.
|
| + */
|
| + horizontalOffset: {
|
| + type: Number,
|
| + value: 0,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * A pixel value that will be added to the position calculated for the
|
| + * given `verticalAlign`. Use a negative value to offset towards the
|
| + * top, or a positive value to offset towards the bottom.
|
| + */
|
| + verticalOffset: {
|
| + type: Number,
|
| + value: 0,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * Set to true to disable animations when opening and closing the
|
| + * dropdown.
|
| + */
|
| + noAnimations: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to disable automatically closing the dropdown after
|
| + * a selection has been made.
|
| + */
|
| + ignoreSelect: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * An animation config. If provided, this will be used to animate the
|
| + * opening of the dropdown.
|
| + */
|
| + openAnimationConfig: {
|
| + type: Object,
|
| + value: function() {
|
| + return [{
|
| + name: 'fade-in-animation',
|
| + timing: {
|
| + delay: 100,
|
| + duration: 200
|
| + }
|
| + }, {
|
| + name: 'paper-menu-grow-width-animation',
|
| + timing: {
|
| + delay: 100,
|
| + duration: 150,
|
| + easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
|
| + }
|
| + }, {
|
| + name: 'paper-menu-grow-height-animation',
|
| + timing: {
|
| + delay: 100,
|
| + duration: 275,
|
| + easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
|
| + }
|
| + }];
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * An animation config. If provided, this will be used to animate the
|
| + * closing of the dropdown.
|
| + */
|
| + closeAnimationConfig: {
|
| + type: Object,
|
| + value: function() {
|
| + return [{
|
| + name: 'fade-out-animation',
|
| + timing: {
|
| + duration: 150
|
| + }
|
| + }, {
|
| + name: 'paper-menu-shrink-width-animation',
|
| + timing: {
|
| + delay: 100,
|
| + duration: 50,
|
| + easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
|
| + }
|
| + }, {
|
| + name: 'paper-menu-shrink-height-animation',
|
| + timing: {
|
| + duration: 200,
|
| + easing: 'ease-in'
|
| + }
|
| + }];
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * This is the element intended to be bound as the focus target
|
| + * for the `iron-dropdown` contained by `paper-menu-button`.
|
| + */
|
| + _dropdownContent: {
|
| + type: Object
|
| + }
|
| },
|
|
|
| - /**
|
| - * The orientation against which to align the menu dropdown
|
| - * horizontally relative to the dropdown trigger.
|
| - */
|
| - horizontalAlign: {
|
| - type: String,
|
| - value: 'left',
|
| - reflectToAttribute: true
|
| + hostAttributes: {
|
| + role: 'group',
|
| + 'aria-haspopup': 'true'
|
| },
|
|
|
| - /**
|
| - * The orientation against which to align the menu dropdown
|
| - * vertically relative to the dropdown trigger.
|
| - */
|
| - verticalAlign: {
|
| - type: String,
|
| - value: 'top',
|
| - reflectToAttribute: true
|
| + listeners: {
|
| + 'iron-select': '_onIronSelect'
|
| },
|
|
|
| /**
|
| - * A pixel value that will be added to the position calculated for the
|
| - * given `horizontalAlign`. Use a negative value to offset to the
|
| - * left, or a positive value to offset to the right.
|
| + * The content element that is contained by the menu button, if any.
|
| */
|
| - horizontalOffset: {
|
| - type: Number,
|
| - value: 0,
|
| - notify: true
|
| + get contentElement() {
|
| + return Polymer.dom(this.$.content).getDistributedNodes()[0];
|
| },
|
|
|
| /**
|
| - * A pixel value that will be added to the position calculated for the
|
| - * given `verticalAlign`. Use a negative value to offset towards the
|
| - * top, or a positive value to offset towards the bottom.
|
| + * Toggles the drowpdown content between opened and closed.
|
| */
|
| - verticalOffset: {
|
| - type: Number,
|
| - value: 0,
|
| - notify: true
|
| + toggle: function() {
|
| + if (this.opened) {
|
| + this.close();
|
| + } else {
|
| + this.open();
|
| + }
|
| },
|
|
|
| /**
|
| - * Set to true to disable animations when opening and closing the
|
| - * dropdown.
|
| + * Make the dropdown content appear as an overlay positioned relative
|
| + * to the dropdown trigger.
|
| */
|
| - noAnimations: {
|
| - type: Boolean,
|
| - value: false
|
| + open: function() {
|
| + if (this.disabled) {
|
| + return;
|
| + }
|
| +
|
| + this.$.dropdown.open();
|
| },
|
|
|
| /**
|
| - * Set to true to disable automatically closing the dropdown after
|
| - * a selection has been made.
|
| + * Hide the dropdown content.
|
| */
|
| - ignoreSelect: {
|
| - type: Boolean,
|
| - value: false
|
| + close: function() {
|
| + this.$.dropdown.close();
|
| },
|
|
|
| /**
|
| - * An animation config. If provided, this will be used to animate the
|
| - * opening of the dropdown.
|
| + * When an `iron-select` event is received, the dropdown should
|
| + * automatically close on the assumption that a value has been chosen.
|
| + *
|
| + * @param {CustomEvent} event A CustomEvent instance with type
|
| + * set to `"iron-select"`.
|
| */
|
| - openAnimationConfig: {
|
| - type: Object,
|
| - value: function() {
|
| - return [{
|
| - name: 'fade-in-animation',
|
| - timing: {
|
| - delay: 100,
|
| - duration: 200
|
| - }
|
| - }, {
|
| - name: 'paper-menu-grow-width-animation',
|
| - timing: {
|
| - delay: 100,
|
| - duration: 150,
|
| - easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
|
| - }
|
| - }, {
|
| - name: 'paper-menu-grow-height-animation',
|
| - timing: {
|
| - delay: 100,
|
| - duration: 275,
|
| - easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
|
| - }
|
| - }];
|
| + _onIronSelect: function(event) {
|
| + if (!this.ignoreSelect) {
|
| + this.close();
|
| }
|
| },
|
|
|
| /**
|
| - * An animation config. If provided, this will be used to animate the
|
| - * closing of the dropdown.
|
| + * When the dropdown opens, the `paper-menu-button` fires `paper-open`.
|
| + * When the dropdown closes, the `paper-menu-button` fires `paper-close`.
|
| + *
|
| + * @param {boolean} opened True if the dropdown is opened, otherwise false.
|
| + * @param {boolean} oldOpened The previous value of `opened`.
|
| */
|
| - closeAnimationConfig: {
|
| - type: Object,
|
| - value: function() {
|
| - return [{
|
| - name: 'fade-out-animation',
|
| - timing: {
|
| - duration: 150
|
| - }
|
| - }, {
|
| - name: 'paper-menu-shrink-width-animation',
|
| - timing: {
|
| - delay: 100,
|
| - duration: 50,
|
| - easing: PaperMenuButton.ANIMATION_CUBIC_BEZIER
|
| - }
|
| - }, {
|
| - name: 'paper-menu-shrink-height-animation',
|
| - timing: {
|
| - duration: 200,
|
| - easing: 'ease-in'
|
| - }
|
| - }];
|
| + _openedChanged: function(opened, oldOpened) {
|
| + if (opened) {
|
| + // TODO(cdata): Update this when we can measure changes in distributed
|
| + // children in an idiomatic way.
|
| + // We poke this property in case the element has changed. This will
|
| + // cause the focus target for the `iron-dropdown` to be updated as
|
| + // necessary:
|
| + this._dropdownContent = this.contentElement;
|
| + this.fire('paper-dropdown-open');
|
| + } else if (oldOpened != null) {
|
| + this.fire('paper-dropdown-close');
|
| }
|
| },
|
|
|
| /**
|
| - * This is the element intended to be bound as the focus target
|
| - * for the `iron-dropdown` contained by `paper-menu-button`.
|
| + * If the dropdown is open when disabled becomes true, close the
|
| + * dropdown.
|
| + *
|
| + * @param {boolean} disabled True if disabled, otherwise false.
|
| */
|
| - _dropdownContent: {
|
| - type: Object
|
| - }
|
| - },
|
| -
|
| - hostAttributes: {
|
| - role: 'group',
|
| - 'aria-haspopup': 'true'
|
| - },
|
| -
|
| - listeners: {
|
| - 'iron-select': '_onIronSelect'
|
| - },
|
| -
|
| - /**
|
| - * The content element that is contained by the menu button, if any.
|
| - */
|
| - get contentElement() {
|
| - return Polymer.dom(this.$.content).getDistributedNodes()[0];
|
| - },
|
| -
|
| - /**
|
| - * Make the dropdown content appear as an overlay positioned relative
|
| - * to the dropdown trigger.
|
| - */
|
| - open: function() {
|
| - if (this.disabled) {
|
| - return;
|
| - }
|
| -
|
| - this.$.dropdown.open();
|
| - },
|
| -
|
| - /**
|
| - * Hide the dropdown content.
|
| - */
|
| - close: function() {
|
| - this.$.dropdown.close();
|
| - },
|
| -
|
| - /**
|
| - * When an `iron-select` event is received, the dropdown should
|
| - * automatically close on the assumption that a value has been chosen.
|
| - *
|
| - * @param {CustomEvent} event A CustomEvent instance with type
|
| - * set to `"iron-select"`.
|
| - */
|
| - _onIronSelect: function(event) {
|
| - if (!this.ignoreSelect) {
|
| - this.close();
|
| - }
|
| - },
|
| + _disabledChanged: function(disabled) {
|
| + Polymer.IronControlState._disabledChanged.apply(this, arguments);
|
| + if (disabled && this.opened) {
|
| + this.close();
|
| + }
|
| + },
|
|
|
| - /**
|
| - * When the dropdown opens, the `paper-menu-button` fires `paper-open`.
|
| - * When the dropdown closes, the `paper-menu-button` fires `paper-close`.
|
| - *
|
| - * @param {boolean} opened True if the dropdown is opened, otherwise false.
|
| - * @param {boolean} oldOpened The previous value of `opened`.
|
| - */
|
| - _openedChanged: function(opened, oldOpened) {
|
| - if (opened) {
|
| - // TODO(cdata): Update this when we can measure changes in distributed
|
| - // children in an idiomatic way.
|
| - // We poke this property in case the element has changed. This will
|
| - // cause the focus target for the `iron-dropdown` to be updated as
|
| - // necessary:
|
| - this._dropdownContent = this.contentElement;
|
| - this.fire('paper-dropdown-open');
|
| - } else if (oldOpened != null) {
|
| - this.fire('paper-dropdown-close');
|
| - }
|
| - },
|
| + __onIronOverlayCanceled: function(event) {
|
| + var uiEvent = event.detail;
|
| + var target = Polymer.dom(uiEvent).rootTarget;
|
| + var trigger = this.$.trigger;
|
| + var path = Polymer.dom(uiEvent).path;
|
|
|
| - /**
|
| - * If the dropdown is open when disabled becomes true, close the
|
| - * dropdown.
|
| - *
|
| - * @param {boolean} disabled True if disabled, otherwise false.
|
| - */
|
| - _disabledChanged: function(disabled) {
|
| - Polymer.IronControlState._disabledChanged.apply(this, arguments);
|
| - if (disabled && this.opened) {
|
| - this.close();
|
| + if (path.indexOf(trigger) > -1) {
|
| + event.preventDefault();
|
| + }
|
| }
|
| - }
|
| - });
|
| + });
|
|
|
| - PaperMenuButton.ANIMATION_CUBIC_BEZIER = 'cubic-bezier(.3,.95,.5,1)';
|
| - PaperMenuButton.MAX_ANIMATION_TIME_MS = 400;
|
| + PaperMenuButton.ANIMATION_CUBIC_BEZIER = 'cubic-bezier(.3,.95,.5,1)';
|
| + PaperMenuButton.MAX_ANIMATION_TIME_MS = 400;
|
|
|
| - Polymer.PaperMenuButton = PaperMenuButton;
|
| - })();
|
| + Polymer.PaperMenuButton = PaperMenuButton;
|
| + })();
|
| Polymer({
|
| is: 'paper-icon-button',
|
|
|
| @@ -9821,6 +10060,67 @@ Polymer({
|
| }
|
| }
|
| });
|
| +(function() {
|
| + 'use strict';
|
| +
|
| + Polymer.IronA11yAnnouncer = Polymer({
|
| + is: 'iron-a11y-announcer',
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * The value of mode is used to set the `aria-live` attribute
|
| + * for the element that will be announced. Valid values are: `off`,
|
| + * `polite` and `assertive`.
|
| + */
|
| + mode: {
|
| + type: String,
|
| + value: 'polite'
|
| + },
|
| +
|
| + _text: {
|
| + type: String,
|
| + value: ''
|
| + }
|
| + },
|
| +
|
| + created: function() {
|
| + if (!Polymer.IronA11yAnnouncer.instance) {
|
| + Polymer.IronA11yAnnouncer.instance = this;
|
| + }
|
| +
|
| + document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(this));
|
| + },
|
| +
|
| + /**
|
| + * Cause a text string to be announced by screen readers.
|
| + *
|
| + * @param {string} text The text that should be announced.
|
| + */
|
| + announce: function(text) {
|
| + this._text = '';
|
| + this.async(function() {
|
| + this._text = text;
|
| + }, 100);
|
| + },
|
| +
|
| + _onIronAnnounce: function(event) {
|
| + if (event.detail && event.detail.text) {
|
| + this.announce(event.detail.text);
|
| + }
|
| + }
|
| + });
|
| +
|
| + Polymer.IronA11yAnnouncer.instance = null;
|
| +
|
| + Polymer.IronA11yAnnouncer.requestAvailability = function() {
|
| + if (!Polymer.IronA11yAnnouncer.instance) {
|
| + Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-announcer');
|
| + }
|
| +
|
| + document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
|
| + };
|
| + })();
|
| /**
|
| * `Use Polymer.IronValidatableBehavior` to implement an element that validates user input.
|
| * Use the related `Polymer.IronValidatorBehavior` to add custom validation logic to an iron-input.
|
| @@ -9993,18 +10293,22 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| },
|
|
|
| /**
|
| - * Set to true to prevent the user from entering invalid input. The new input characters are
|
| - * matched with `allowedPattern` if it is set, otherwise it will use the `type` attribute (only
|
| - * supported for `type=number`).
|
| + * Set to true to prevent the user from entering invalid input. If `allowedPattern` is set,
|
| + * any character typed by the user will be matched against that pattern, and rejected if it's not a match.
|
| + * Pasted input will have each character checked individually; if any character
|
| + * doesn't match `allowedPattern`, the entire pasted string will be rejected.
|
| + * If `allowedPattern` is not set, it will use the `type` attribute (only supported for `type=number`).
|
| */
|
| preventInvalidInput: {
|
| type: Boolean
|
| },
|
|
|
| /**
|
| - * Regular expression expressing a set of characters to enforce the validity of input characters.
|
| - * The recommended value should follow this format: `[a-ZA-Z0-9.+-!;:]` that list the characters
|
| - * allowed as input.
|
| + * Regular expression that list the characters allowed as input.
|
| + * This pattern represents the allowed characters for the field; as the user inputs text,
|
| + * each individual character will be checked against the pattern (rather than checking
|
| + * the entire value as a whole). The recommended format should be a list of allowed characters;
|
| + * for example, `[a-zA-Z0-9.+-!;:]`
|
| */
|
| allowedPattern: {
|
| type: String,
|
| @@ -10028,6 +10332,47 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| 'keypress': '_onKeypress'
|
| },
|
|
|
| + /** @suppress {checkTypes} */
|
| + registered: function() {
|
| + // Feature detect whether we need to patch dispatchEvent (i.e. on FF and IE).
|
| + if (!this._canDispatchEventOnDisabled()) {
|
| + this._origDispatchEvent = this.dispatchEvent;
|
| + this.dispatchEvent = this._dispatchEventFirefoxIE;
|
| + }
|
| + },
|
| +
|
| + created: function() {
|
| + Polymer.IronA11yAnnouncer.requestAvailability();
|
| + },
|
| +
|
| + _canDispatchEventOnDisabled: function() {
|
| + var input = document.createElement('input');
|
| + var canDispatch = false;
|
| + input.disabled = true;
|
| +
|
| + input.addEventListener('feature-check-dispatch-event', function() {
|
| + canDispatch = true;
|
| + });
|
| +
|
| + try {
|
| + input.dispatchEvent(new Event('feature-check-dispatch-event'));
|
| + } catch(e) {}
|
| +
|
| + return canDispatch;
|
| + },
|
| +
|
| + _dispatchEventFirefoxIE: function() {
|
| + // Due to Firefox bug, events fired on disabled form controls can throw
|
| + // errors; furthermore, neither IE nor Firefox will actually dispatch
|
| + // events from disabled form controls; as such, we toggle disable around
|
| + // the dispatch to allow notifying properties to notify
|
| + // See issue #47 for details
|
| + var disabled = this.disabled;
|
| + this.disabled = false;
|
| + this._origDispatchEvent.apply(this, arguments);
|
| + this.disabled = disabled;
|
| + },
|
| +
|
| get _patternRegExp() {
|
| var pattern;
|
| if (this.allowedPattern) {
|
| @@ -10068,6 +10413,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| if (this.preventInvalidInput && !this._patternAlreadyChecked) {
|
| var valid = this._checkPatternValidity();
|
| if (!valid) {
|
| + this._announceInvalidCharacter('Invalid string of characters not entered.');
|
| this.value = this._previousValidInput;
|
| }
|
| }
|
| @@ -10128,6 +10474,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| var thisChar = String.fromCharCode(event.charCode);
|
| if (this._isPrintable(event) && !regexp.test(thisChar)) {
|
| event.preventDefault();
|
| + this._announceInvalidCharacter('Invalid character ' + thisChar + ' not entered.');
|
| }
|
| },
|
|
|
| @@ -10150,23 +10497,29 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| * @return {boolean} True if the value is valid.
|
| */
|
| validate: function() {
|
| - // Empty, non-required input is valid.
|
| - if (!this.required && this.value == '') {
|
| - this.invalid = false;
|
| - return true;
|
| - }
|
| + // First, check what the browser thinks. Some inputs (like type=number)
|
| + // behave weirdly and will set the value to "" if something invalid is
|
| + // entered, but will set the validity correctly.
|
| + var valid = this.checkValidity();
|
|
|
| - var valid;
|
| - if (this.hasValidator()) {
|
| - valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
|
| - } else {
|
| - valid = this.checkValidity();
|
| - this.invalid = !valid;
|
| + // Only do extra checking if the browser thought this was valid.
|
| + if (valid) {
|
| + // Empty, required input is invalid
|
| + if (this.required && this.value === '') {
|
| + valid = false;
|
| + } else if (this.hasValidator()) {
|
| + valid = Polymer.IronValidatableBehavior.validate.call(this, this.value);
|
| + }
|
| }
|
| +
|
| + this.invalid = !valid;
|
| this.fire('iron-input-validate');
|
| return valid;
|
| - }
|
| + },
|
|
|
| + _announceInvalidCharacter: function(message) {
|
| + this.fire('iron-announce', { text: message });
|
| + }
|
| });
|
|
|
| /*
|
| @@ -10303,14 +10656,15 @@ Polymer({
|
| }
|
| this.addEventListener('focus', this._boundOnFocus, true);
|
| this.addEventListener('blur', this._boundOnBlur, true);
|
| + },
|
| +
|
| + attached: function() {
|
| if (this.attrForValue) {
|
| this._inputElement.addEventListener(this._valueChangedEvent, this._boundValueChanged);
|
| } else {
|
| this.addEventListener('input', this._onInput);
|
| }
|
| - },
|
|
|
| - attached: function() {
|
| // Only validate when attached if the input already has a value.
|
| if (this._inputElementValue != '') {
|
| this._handleValueAndAutoValidate(this._inputElement);
|
|
|