| 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 9ff07deae87c16151f4ad3c13d97404a42d4fa20..352b5bf7f50cd2f7d5ad20653ec41e7da06002c0 100644
|
| --- a/chrome/browser/resources/md_downloads/crisper.js
|
| +++ b/chrome/browser/resources/md_downloads/crisper.js
|
| @@ -1,3 +1,38 @@
|
| +// 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.
|
| +
|
| +/**
|
| + * @fileoverview PromiseResolver is a helper class that allows creating a
|
| + * Promise that will be fulfilled (resolved or rejected) some time later.
|
| + *
|
| + * Example:
|
| + * var resolver = new PromiseResolver();
|
| + * resolver.promise.then(function(result) {
|
| + * console.log('resolved with', result);
|
| + * });
|
| + * ...
|
| + * ...
|
| + * resolver.resolve({hello: 'world'});
|
| + */
|
| +
|
| +/**
|
| + * @constructor @struct
|
| + * @template T
|
| + */
|
| +function PromiseResolver() {
|
| + /** @type {function(T): void} */
|
| + this.resolve;
|
| +
|
| + /** @type {function(*=): void} */
|
| + this.reject;
|
| +
|
| + /** @type {!Promise<T>} */
|
| + this.promise = new Promise(function(resolve, reject) {
|
| + this.resolve = resolve;
|
| + this.reject = reject;
|
| + }.bind(this));
|
| +};
|
| // Copyright (c) 2012 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.
|
| @@ -13,7 +48,7 @@ var global = this;
|
| var WebUIListener;
|
|
|
| /** Platform, package, object property, and Event support. **/
|
| -var cr = function() {
|
| +var cr = cr || function() {
|
| 'use strict';
|
|
|
| /**
|
| @@ -25,6 +60,8 @@ var cr = function() {
|
| * @param {*=} opt_object The object to expose at the end of the path.
|
| * @param {Object=} opt_objectToExportTo The object to add the path to;
|
| * default is {@code global}.
|
| + * @return {!Object} The last object exported (i.e. exportPath('cr.ui')
|
| + * returns a reference to the ui property of window.cr).
|
| * @private
|
| */
|
| function exportPath(name, opt_object, opt_objectToExportTo) {
|
| @@ -42,7 +79,7 @@ var cr = function() {
|
| }
|
| }
|
| return cur;
|
| - };
|
| + }
|
|
|
| /**
|
| * Fires a property change event on the target.
|
| @@ -318,9 +355,9 @@ var cr = function() {
|
| /**
|
| * The mapping used by the sendWithPromise mechanism to tie the Promise
|
| * returned to callers with the corresponding WebUI response. The mapping is
|
| - * from ID to the Promise resolver function; the ID is generated by
|
| + * from ID to the PromiseResolver helper; the ID is generated by
|
| * sendWithPromise and is unique across all invocations of said method.
|
| - * @type {!Object<!Function>}
|
| + * @type {!Object<!PromiseResolver>}
|
| */
|
| var chromeSendResolverMap = {};
|
|
|
| @@ -334,12 +371,17 @@ var cr = function() {
|
| * supply any number of other arguments that will be included in the response.
|
| * @param {string} id The unique ID identifying the Promise this response is
|
| * tied to.
|
| + * @param {boolean} isSuccess Whether the request was successful.
|
| * @param {*} response The response as sent from C++.
|
| */
|
| - function webUIResponse(id, response) {
|
| - var resolverFn = chromeSendResolverMap[id];
|
| + function webUIResponse(id, isSuccess, response) {
|
| + var resolver = chromeSendResolverMap[id];
|
| delete chromeSendResolverMap[id];
|
| - resolverFn(response);
|
| +
|
| + if (isSuccess)
|
| + resolver.resolve(response);
|
| + else
|
| + resolver.reject(response);
|
| }
|
|
|
| /**
|
| @@ -352,11 +394,11 @@ var cr = function() {
|
| */
|
| function sendWithPromise(methodName, var_args) {
|
| var args = Array.prototype.slice.call(arguments, 1);
|
| - return new Promise(function(resolve, reject) {
|
| - var id = methodName + '_' + createUid();
|
| - chromeSendResolverMap[id] = resolve;
|
| - chrome.send(methodName, [id].concat(args));
|
| - });
|
| + var promiseResolver = new PromiseResolver();
|
| + var id = methodName + '_' + createUid();
|
| + chromeSendResolverMap[id] = promiseResolver;
|
| + chrome.send(methodName, [id].concat(args));
|
| + return promiseResolver.promise;
|
| }
|
|
|
| /**
|
| @@ -1470,6 +1512,15 @@ function elide(original, maxLength) {
|
| if (original.length <= maxLength)
|
| return original;
|
| return original.substring(0, maxLength - 1) + '\u2026';
|
| +}
|
| +
|
| +/**
|
| + * Quote a string so it can be used in a regular expression.
|
| + * @param {string} str The source string.
|
| + * @return {string} The escaped string.
|
| + */
|
| +function quoteString(str) {
|
| + return str.replace(/([\\\.\+\*\?\[\^\]\$\(\)\{\}\=\!\<\>\|\:])/g, '\\$1');
|
| };
|
| // Copyright (c) 2013 The Chromium Authors. All rights reserved.
|
| // Use of this source code is governed by a BSD-style license that can be
|
| @@ -1840,11 +1891,13 @@ i18nTemplate.process(document, loadTimeData);
|
| * size or hidden state of their children) and "resizables" (elements that need to be
|
| * notified when they are resized or un-hidden by their parents in order to take
|
| * action on their new measurements).
|
| + *
|
| * Elements that perform measurement should add the `IronResizableBehavior` behavior to
|
| * their element definition and listen for the `iron-resize` event on themselves.
|
| * This event will be fired when they become showing after having been hidden,
|
| * when they are resized explicitly by another resizable, or when the window has been
|
| * resized.
|
| + *
|
| * Note, the `iron-resize` event is non-bubbling.
|
| *
|
| * @polymerBehavior Polymer.IronResizableBehavior
|
| @@ -2508,9 +2561,9 @@ i18nTemplate.process(document, loadTimeData);
|
| // Support element id references
|
| if (typeof scrollTarget === 'string') {
|
|
|
| - var ownerRoot = Polymer.dom(this).getOwnerRoot();
|
| - this.scrollTarget = (ownerRoot && ownerRoot.$) ?
|
| - ownerRoot.$[scrollTarget] : Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
|
| + var host = this.domHost;
|
| + this.scrollTarget = host && host.$ ? host.$[scrollTarget] :
|
| + Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
|
|
|
| } else if (this._scrollHandler) {
|
|
|
| @@ -2609,15 +2662,15 @@ i18nTemplate.process(document, loadTimeData);
|
| * Scrolls the content to a particular place.
|
| *
|
| * @method scroll
|
| - * @param {number} top The top position
|
| * @param {number} left The left position
|
| + * @param {number} top The top position
|
| */
|
| - scroll: function(top, left) {
|
| + scroll: function(left, top) {
|
| if (this.scrollTarget === this._doc) {
|
| - window.scrollTo(top, left);
|
| + window.scrollTo(left, top);
|
| } else if (this._isValidScrollTarget()) {
|
| - this.scrollTarget.scrollTop = top;
|
| this.scrollTarget.scrollLeft = left;
|
| + this.scrollTarget.scrollTop = top;
|
| }
|
| },
|
|
|
| @@ -2774,7 +2827,7 @@ i18nTemplate.process(document, loadTimeData);
|
| _ratio: 0.5,
|
|
|
| /**
|
| - * The padding-top value of the `scroller` element
|
| + * The padding-top value for the list.
|
| */
|
| _scrollerPaddingTop: 0,
|
|
|
| @@ -2784,21 +2837,6 @@ i18nTemplate.process(document, loadTimeData);
|
| _scrollPosition: 0,
|
|
|
| /**
|
| - * The number of tiles in the DOM.
|
| - */
|
| - _physicalCount: 0,
|
| -
|
| - /**
|
| - * The k-th tile that is at the top of the scrolling list.
|
| - */
|
| - _physicalStart: 0,
|
| -
|
| - /**
|
| - * The k-th tile that is at the bottom of the scrolling list.
|
| - */
|
| - _physicalEnd: 0,
|
| -
|
| - /**
|
| * The sum of the heights of all the tiles in the DOM.
|
| */
|
| _physicalSize: 0,
|
| @@ -2825,11 +2863,6 @@ i18nTemplate.process(document, loadTimeData);
|
| _virtualCount: 0,
|
|
|
| /**
|
| - * The n-th item rendered in the `_physicalStart` tile.
|
| - */
|
| - _virtualStartVal: 0,
|
| -
|
| - /**
|
| * A map between an item key and its physical item index
|
| */
|
| _physicalIndexForKey: null,
|
| @@ -2875,7 +2908,6 @@ i18nTemplate.process(document, loadTimeData);
|
| */
|
| _lastVisibleIndexVal: null,
|
|
|
| -
|
| /**
|
| * A Polymer collection for the items.
|
| * @type {?Polymer.Collection}
|
| @@ -2899,9 +2931,14 @@ i18nTemplate.process(document, loadTimeData);
|
| _maxPages: 3,
|
|
|
| /**
|
| - * The currently focused item index.
|
| + * The currently focused physical item.
|
| */
|
| - _focusedIndex: 0,
|
| + _focusedItem: null,
|
| +
|
| + /**
|
| + * The index of the `_focusedItem`.
|
| + */
|
| + _focusedIndex: -1,
|
|
|
| /**
|
| * The the item that is focused if it is moved offscreen.
|
| @@ -2937,6 +2974,20 @@ i18nTemplate.process(document, loadTimeData);
|
| },
|
|
|
| /**
|
| + * The height of the physical content that isn't on the screen.
|
| + */
|
| + get _hiddenContentSize() {
|
| + return this._physicalSize - this._viewportSize;
|
| + },
|
| +
|
| + /**
|
| + * The maximum scroll top value.
|
| + */
|
| + get _maxScrollTop() {
|
| + return this._estScrollHeight - this._viewportSize + this._scrollerPaddingTop;
|
| + },
|
| +
|
| + /**
|
| * The lowest n-th value for an item such that it can be rendered in `_physicalStart`.
|
| */
|
| _minVirtualStart: 0,
|
| @@ -2949,41 +3000,54 @@ i18nTemplate.process(document, loadTimeData);
|
| },
|
|
|
| /**
|
| - * The height of the physical content that isn't on the screen.
|
| + * The n-th item rendered in the `_physicalStart` tile.
|
| */
|
| - get _hiddenContentSize() {
|
| - return this._physicalSize - this._viewportSize;
|
| + _virtualStartVal: 0,
|
| +
|
| + set _virtualStart(val) {
|
| + this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
|
| },
|
|
|
| - /**
|
| - * The maximum scroll top value.
|
| - */
|
| - get _maxScrollTop() {
|
| - return this._estScrollHeight - this._viewportSize;
|
| + get _virtualStart() {
|
| + return this._virtualStartVal || 0;
|
| },
|
|
|
| /**
|
| - * Sets the n-th item rendered in `_physicalStart`
|
| + * The k-th tile that is at the top of the scrolling list.
|
| */
|
| - set _virtualStart(val) {
|
| - // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart
|
| - this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
|
| - if (this._physicalCount === 0) {
|
| - this._physicalStart = 0;
|
| - this._physicalEnd = 0;
|
| - } else {
|
| - this._physicalStart = this._virtualStartVal % this._physicalCount;
|
| - this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
|
| + _physicalStartVal: 0,
|
| +
|
| + set _physicalStart(val) {
|
| + this._physicalStartVal = val % this._physicalCount;
|
| + if (this._physicalStartVal < 0) {
|
| + this._physicalStartVal = this._physicalCount + this._physicalStartVal;
|
| }
|
| + this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
|
| + },
|
| +
|
| + get _physicalStart() {
|
| + return this._physicalStartVal || 0;
|
| },
|
|
|
| /**
|
| - * Gets the n-th item rendered in `_physicalStart`
|
| + * The number of tiles in the DOM.
|
| */
|
| - get _virtualStart() {
|
| - return this._virtualStartVal;
|
| + _physicalCountVal: 0,
|
| +
|
| + set _physicalCount(val) {
|
| + this._physicalCountVal = val;
|
| + this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
|
| },
|
|
|
| + get _physicalCount() {
|
| + return this._physicalCountVal;
|
| + },
|
| +
|
| + /**
|
| + * The k-th tile that is at the bottom of the scrolling list.
|
| + */
|
| + _physicalEnd: 0,
|
| +
|
| /**
|
| * An optimal physical size such that we will have enough physical items
|
| * to fill up the viewport and recycle when the user scrolls.
|
| @@ -3009,12 +3073,11 @@ i18nTemplate.process(document, loadTimeData);
|
| */
|
| get firstVisibleIndex() {
|
| if (this._firstVisibleIndexVal === null) {
|
| - var physicalOffset = this._physicalTop;
|
| + var physicalOffset = this._physicalTop + this._scrollerPaddingTop;
|
|
|
| this._firstVisibleIndexVal = this._iterateItems(
|
| function(pidx, vidx) {
|
| physicalOffset += this._physicalSizes[pidx];
|
| -
|
| if (physicalOffset > this._scrollPosition) {
|
| return vidx;
|
| }
|
| @@ -3035,14 +3098,18 @@ i18nTemplate.process(document, loadTimeData);
|
| this._iterateItems(function(pidx, vidx) {
|
| physicalOffset += this._physicalSizes[pidx];
|
|
|
| - if(physicalOffset <= this._scrollBottom) {
|
| - this._lastVisibleIndexVal = vidx;
|
| + if (physicalOffset <= this._scrollBottom) {
|
| + this._lastVisibleIndexVal = vidx;
|
| }
|
| });
|
| }
|
| return this._lastVisibleIndexVal;
|
| },
|
|
|
| + get _defaultScrollTarget() {
|
| + return this;
|
| + },
|
| +
|
| ready: function() {
|
| this.addEventListener('focus', this._didFocus.bind(this), true);
|
| },
|
| @@ -3056,10 +3123,6 @@ i18nTemplate.process(document, loadTimeData);
|
| this._itemsRendered = false;
|
| },
|
|
|
| - get _defaultScrollTarget() {
|
| - return this;
|
| - },
|
| -
|
| /**
|
| * Set the overflow property if this element has its own scrolling region
|
| */
|
| @@ -3075,8 +3138,9 @@ i18nTemplate.process(document, loadTimeData);
|
| * @method updateViewportBoundaries
|
| */
|
| updateViewportBoundaries: function() {
|
| - var scrollerStyle = window.getComputedStyle(this.scrollTarget);
|
| - this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10);
|
| + this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
|
| + parseInt(window.getComputedStyle(this)['padding-top'], 10);
|
| +
|
| this._viewportSize = this._scrollTargetHeight;
|
| },
|
|
|
| @@ -3086,12 +3150,10 @@ i18nTemplate.process(document, loadTimeData);
|
| */
|
| _scrollHandler: function() {
|
| // clamp the `scrollTop` value
|
| - // IE 10|11 scrollTop may go above `_maxScrollTop`
|
| - // iOS `scrollTop` may go below 0 and above `_maxScrollTop`
|
| var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop));
|
| + var delta = scrollTop - this._scrollPosition;
|
| var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBottom;
|
| var ratio = this._ratio;
|
| - var delta = scrollTop - this._scrollPosition;
|
| var recycledTiles = 0;
|
| var hiddenContentSize = this._hiddenContentSize;
|
| var currentRatio = ratio;
|
| @@ -3100,7 +3162,7 @@ i18nTemplate.process(document, loadTimeData);
|
| // track the last `scrollTop`
|
| this._scrollPosition = scrollTop;
|
|
|
| - // clear cached visible index
|
| + // clear cached visible indexes
|
| this._firstVisibleIndexVal = null;
|
| this._lastVisibleIndexVal = null;
|
|
|
| @@ -3187,6 +3249,7 @@ i18nTemplate.process(document, loadTimeData);
|
| }
|
| } else {
|
| this._virtualStart = this._virtualStart + recycledTiles;
|
| + this._physicalStart = this._physicalStart + recycledTiles;
|
| this._update(recycledTileSet, movingUp);
|
| }
|
| },
|
| @@ -3198,11 +3261,7 @@ i18nTemplate.process(document, loadTimeData);
|
| */
|
| _update: function(itemSet, movingUp) {
|
| // manage focus
|
| - if (this._isIndexRendered(this._focusedIndex)) {
|
| - this._restoreFocusedItem();
|
| - } else {
|
| - this._createFocusBackfillItem();
|
| - }
|
| + this._manageFocus();
|
| // update models
|
| this._assignModels(itemSet);
|
| // measure heights
|
| @@ -3269,7 +3328,6 @@ i18nTemplate.process(document, loadTimeData);
|
| * Increases the pool size.
|
| */
|
| _increasePool: function(missingItems) {
|
| - // limit the size
|
| var nextPhysicalCount = Math.min(
|
| this._physicalCount + missingItems,
|
| this._virtualCount - this._virtualStart,
|
| @@ -3278,14 +3336,24 @@ i18nTemplate.process(document, loadTimeData);
|
| var prevPhysicalCount = this._physicalCount;
|
| var delta = nextPhysicalCount - prevPhysicalCount;
|
|
|
| - if (delta > 0) {
|
| - [].push.apply(this._physicalItems, this._createPool(delta));
|
| - [].push.apply(this._physicalSizes, new Array(delta));
|
| + if (delta <= 0) {
|
| + return;
|
| + }
|
| +
|
| + [].push.apply(this._physicalItems, this._createPool(delta));
|
| + [].push.apply(this._physicalSizes, new Array(delta));
|
|
|
| - this._physicalCount = prevPhysicalCount + delta;
|
| - // tail call
|
| - return this._update();
|
| + this._physicalCount = prevPhysicalCount + delta;
|
| +
|
| + // update the physical start if we need to preserve the model of the focused item.
|
| + // In this situation, the focused item is currently rendered and its model would
|
| + // have changed after increasing the pool if the physical start remained unchanged.
|
| + if (this._physicalStart > this._physicalEnd &&
|
| + this._isIndexRendered(this._focusedIndex) &&
|
| + this._getPhysicalIndex(this._focusedIndex) < this._physicalEnd) {
|
| + this._physicalStart = this._physicalStart + delta;
|
| }
|
| + this._update();
|
| },
|
|
|
| /**
|
| @@ -3374,27 +3442,36 @@ i18nTemplate.process(document, loadTimeData);
|
|
|
| /**
|
| * Called as a side effect of a host items.<key>.<path> path change,
|
| - * responsible for notifying item.<path> changes to row for key.
|
| + * responsible for notifying item.<path> changes.
|
| */
|
| _forwardItemPath: function(path, value) {
|
| - if (this._physicalIndexForKey) {
|
| - var dot = path.indexOf('.');
|
| - var key = path.substring(0, dot < 0 ? path.length : dot);
|
| - var idx = this._physicalIndexForKey[key];
|
| - var row = this._physicalItems[idx];
|
| -
|
| - if (idx === this._focusedIndex && this._offscreenFocusedItem) {
|
| - row = this._offscreenFocusedItem;
|
| - }
|
| - if (row) {
|
| - var inst = row._templateInstance;
|
| - if (dot >= 0) {
|
| - path = this.as + '.' + path.substring(dot+1);
|
| - inst.notifyPath(path, value, true);
|
| - } else {
|
| - inst[this.as] = value;
|
| - }
|
| - }
|
| + if (!this._physicalIndexForKey) {
|
| + return;
|
| + }
|
| + var inst;
|
| + var dot = path.indexOf('.');
|
| + var key = path.substring(0, dot < 0 ? path.length : dot);
|
| + var idx = this._physicalIndexForKey[key];
|
| + var el = this._physicalItems[idx];
|
| +
|
| +
|
| + if (idx === this._focusedIndex && this._offscreenFocusedItem) {
|
| + el = this._offscreenFocusedItem;
|
| + }
|
| + if (!el) {
|
| + return;
|
| + }
|
| +
|
| + inst = el._templateInstance;
|
| +
|
| + if (inst.__key__ !== key) {
|
| + return;
|
| + }
|
| + if (dot >= 0) {
|
| + path = this.as + '.' + path.substring(dot+1);
|
| + inst.notifyPath(path, value, true);
|
| + } else {
|
| + inst[this.as] = value;
|
| }
|
| },
|
|
|
| @@ -3404,19 +3481,15 @@ i18nTemplate.process(document, loadTimeData);
|
| */
|
| _itemsChanged: function(change) {
|
| if (change.path === 'items') {
|
| -
|
| - this._restoreFocusedItem();
|
| - // render the new set
|
| - this._itemsRendered = false;
|
| - // update the whole set
|
| + // reset items
|
| this._virtualStart = 0;
|
| this._physicalTop = 0;
|
| this._virtualCount = this.items ? this.items.length : 0;
|
| - this._focusedIndex = 0;
|
| this._collection = this.items ? Polymer.Collection.get(this.items) : null;
|
| this._physicalIndexForKey = {};
|
|
|
| this._resetScrollPosition(0);
|
| + this._removeFocusedItem();
|
|
|
| // create the initial physical items
|
| if (!this._physicalItems) {
|
| @@ -3424,47 +3497,50 @@ i18nTemplate.process(document, loadTimeData);
|
| this._physicalItems = this._createPool(this._physicalCount);
|
| this._physicalSizes = new Array(this._physicalCount);
|
| }
|
| - this._debounceTemplate(this._render);
|
| +
|
| + this._physicalStart = 0;
|
|
|
| } else if (change.path === 'items.splices') {
|
| - // render the new set
|
| - this._itemsRendered = false;
|
| this._adjustVirtualIndex(change.value.indexSplices);
|
| this._virtualCount = this.items ? this.items.length : 0;
|
|
|
| - this._debounceTemplate(this._render);
|
| -
|
| - if (this._focusedIndex < 0 || this._focusedIndex >= this._virtualCount) {
|
| - this._focusedIndex = 0;
|
| - }
|
| - this._debounceTemplate(this._render);
|
| -
|
| } else {
|
| // update a single item
|
| this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.value);
|
| + return;
|
| }
|
| +
|
| + this._itemsRendered = false;
|
| + this._debounceTemplate(this._render);
|
| },
|
|
|
| /**
|
| * @param {!Array<!PolymerSplice>} splices
|
| */
|
| _adjustVirtualIndex: function(splices) {
|
| - var i, splice, idx;
|
| -
|
| - for (i = 0; i < splices.length; i++) {
|
| - splice = splices[i];
|
| -
|
| + splices.forEach(function(splice) {
|
| // deselect removed items
|
| - splice.removed.forEach(this.$.selector.deselect, this.$.selector);
|
| -
|
| - idx = splice.index;
|
| + splice.removed.forEach(this._removeItem, this);
|
| // We only need to care about changes happening above the current position
|
| - if (idx >= this._virtualStart) {
|
| - break;
|
| + if (splice.index < this._virtualStart) {
|
| + var delta = Math.max(
|
| + splice.addedCount - splice.removed.length,
|
| + splice.index - this._virtualStart);
|
| +
|
| + this._virtualStart = this._virtualStart + delta;
|
| +
|
| + if (this._focusedIndex >= 0) {
|
| + this._focusedIndex = this._focusedIndex + delta;
|
| + }
|
| }
|
| + }, this);
|
| + },
|
|
|
| - this._virtualStart = this._virtualStart +
|
| - Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStart);
|
| + _removeItem: function(item) {
|
| + this.$.selector.deselect(item);
|
| + // remove the current focused item
|
| + if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
|
| + this._removeFocusedItem();
|
| }
|
| },
|
|
|
| @@ -3517,19 +3593,18 @@ i18nTemplate.process(document, loadTimeData);
|
| var inst = el._templateInstance;
|
| var item = this.items && this.items[vidx];
|
|
|
| - if (item !== undefined && item !== null) {
|
| + if (item != null) {
|
| inst[this.as] = item;
|
| inst.__key__ = this._collection.getKey(item);
|
| inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item);
|
| inst[this.indexAs] = vidx;
|
| - inst.tabIndex = vidx === this._focusedIndex ? 0 : -1;
|
| - el.removeAttribute('hidden');
|
| + inst.tabIndex = this._focusedIndex === vidx ? 0 : -1;
|
| this._physicalIndexForKey[inst.__key__] = pidx;
|
| + el.removeAttribute('hidden');
|
| } else {
|
| inst.__key__ = null;
|
| el.setAttribute('hidden', '');
|
| }
|
| -
|
| }, itemSet);
|
| },
|
|
|
| @@ -3577,10 +3652,8 @@ i18nTemplate.process(document, loadTimeData);
|
| var y = this._physicalTop;
|
|
|
| this._iterateItems(function(pidx) {
|
| -
|
| this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
|
| y += this._physicalSizes[pidx];
|
| -
|
| });
|
| },
|
|
|
| @@ -3628,7 +3701,6 @@ i18nTemplate.process(document, loadTimeData);
|
| this._scrollHeight = this._estScrollHeight;
|
| }
|
| },
|
| -
|
| /**
|
| * Scroll to a specific item in the virtual list regardless
|
| * of the physical items in the DOM tree.
|
| @@ -3643,12 +3715,13 @@ i18nTemplate.process(document, loadTimeData);
|
|
|
| Polymer.dom.flush();
|
|
|
| - var firstVisible = this.firstVisibleIndex;
|
| idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
|
| -
|
| - // start at the previous virtual item
|
| - // so we have a item above the first visible item
|
| - this._virtualStart = idx - 1;
|
| + // update the virtual start only when needed
|
| + if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
|
| + this._virtualStart = idx - 1;
|
| + }
|
| + // manage focus
|
| + this._manageFocus();
|
| // assign new models
|
| this._assignModels();
|
| // measure the new sizes
|
| @@ -3711,7 +3784,7 @@ i18nTemplate.process(document, loadTimeData);
|
| var key = this._collection.getKey(item);
|
| var pidx = this._physicalIndexForKey[key];
|
|
|
| - if (pidx !== undefined) {
|
| + if (pidx != null) {
|
| return this._physicalItems[pidx]._templateInstance;
|
| }
|
| return null;
|
| @@ -3849,139 +3922,179 @@ i18nTemplate.process(document, loadTimeData);
|
| var key = this._collection.getKey(item);
|
| var pidx = this._physicalIndexForKey[key];
|
|
|
| - if (pidx !== undefined) {
|
| + if (pidx != null) {
|
| this._updateMetrics([pidx]);
|
| this._positionItems();
|
| }
|
| },
|
|
|
| + /**
|
| + * Creates a temporary backfill item in the rendered pool of physical items
|
| + * to replace the main focused item. The focused item has tabIndex = 0
|
| + * and might be currently focused by the user.
|
| + *
|
| + * This dynamic replacement helps to preserve the focus state.
|
| + */
|
| + _manageFocus: function() {
|
| + var fidx = this._focusedIndex;
|
| +
|
| + if (fidx >= 0 && fidx < this._virtualCount) {
|
| + // if it's a valid index, check if that index is rendered
|
| + // in a physical item.
|
| + if (this._isIndexRendered(fidx)) {
|
| + this._restoreFocusedItem();
|
| + } else {
|
| + this._createFocusBackfillItem();
|
| + }
|
| + } else if (this._virtualCount > 0 && this._physicalCount > 0) {
|
| + // otherwise, assign the initial focused index.
|
| + this._focusedIndex = this._virtualStart;
|
| + this._focusedItem = this._physicalItems[this._physicalStart];
|
| + }
|
| + },
|
| +
|
| _isIndexRendered: function(idx) {
|
| return idx >= this._virtualStart && idx <= this._virtualEnd;
|
| },
|
|
|
| - _getPhysicalItemForIndex: function(idx, force) {
|
| - if (!this._collection) {
|
| - return null;
|
| - }
|
| - if (!this._isIndexRendered(idx)) {
|
| - if (force) {
|
| - this.scrollToIndex(idx);
|
| - return this._getPhysicalItemForIndex(idx, false);
|
| - }
|
| - return null;
|
| - }
|
| - var item = this._getNormalizedItem(idx);
|
| - var physicalItem = this._physicalItems[this._physicalIndexForKey[this._collection.getKey(item)]];
|
| + _isIndexVisible: function(idx) {
|
| + return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
|
| + },
|
|
|
| - return physicalItem || null;
|
| + _getPhysicalIndex: function(idx) {
|
| + return this._physicalIndexForKey[this._collection.getKey(this._getNormalizedItem(idx))];
|
| },
|
|
|
| _focusPhysicalItem: function(idx) {
|
| - this._restoreFocusedItem();
|
| -
|
| - var physicalItem = this._getPhysicalItemForIndex(idx, true);
|
| - if (!physicalItem) {
|
| + if (idx < 0 || idx >= this._virtualCount) {
|
| return;
|
| }
|
| + this._restoreFocusedItem();
|
| + // scroll to index to make sure it's rendered
|
| + if (!this._isIndexRendered(idx)) {
|
| + this.scrollToIndex(idx);
|
| + }
|
| +
|
| + var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
|
| var SECRET = ~(Math.random() * 100);
|
| var model = physicalItem._templateInstance;
|
| var focusable;
|
|
|
| + // set a secret tab index
|
| model.tabIndex = SECRET;
|
| - // the focusable element could be the entire physical item
|
| + // check if focusable element is the physical item
|
| if (physicalItem.tabIndex === SECRET) {
|
| focusable = physicalItem;
|
| }
|
| - // the focusable element could be somewhere within the physical item
|
| + // search for the element which tabindex is bound to the secret tab index
|
| if (!focusable) {
|
| focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECRET + '"]');
|
| }
|
| // restore the tab index
|
| model.tabIndex = 0;
|
| + // focus the focusable element
|
| + this._focusedIndex = idx;
|
| focusable && focusable.focus();
|
| },
|
|
|
| - _restoreFocusedItem: function() {
|
| - if (!this._offscreenFocusedItem) {
|
| - return;
|
| - }
|
| - var item = this._getNormalizedItem(this._focusedIndex);
|
| - var pidx = this._physicalIndexForKey[this._collection.getKey(item)];
|
| -
|
| - if (pidx !== undefined) {
|
| - this.translate3d(0, HIDDEN_Y, 0, this._physicalItems[pidx]);
|
| - this._physicalItems[pidx] = this._offscreenFocusedItem;
|
| - }
|
| - this._offscreenFocusedItem = null;
|
| - },
|
| -
|
| _removeFocusedItem: function() {
|
| - if (!this._offscreenFocusedItem) {
|
| - return;
|
| + if (this._offscreenFocusedItem) {
|
| + Polymer.dom(this).removeChild(this._offscreenFocusedItem);
|
| }
|
| - Polymer.dom(this).removeChild(this._offscreenFocusedItem);
|
| this._offscreenFocusedItem = null;
|
| this._focusBackfillItem = null;
|
| + this._focusedItem = null;
|
| + this._focusedIndex = -1;
|
| },
|
|
|
| _createFocusBackfillItem: function() {
|
| - if (this._offscreenFocusedItem) {
|
| + var pidx, fidx = this._focusedIndex;
|
| + if (this._offscreenFocusedItem || fidx < 0) {
|
| return;
|
| }
|
| - var item = this._getNormalizedItem(this._focusedIndex);
|
| - var pidx = this._physicalIndexForKey[this._collection.getKey(item)];
|
| -
|
| - this._offscreenFocusedItem = this._physicalItems[pidx];
|
| - this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
|
| -
|
| if (!this._focusBackfillItem) {
|
| + // create a physical item, so that it backfills the focused item.
|
| var stampedTemplate = this.stamp(null);
|
| this._focusBackfillItem = stampedTemplate.root.querySelector('*');
|
| Polymer.dom(this).appendChild(stampedTemplate.root);
|
| }
|
| - this._physicalItems[pidx] = this._focusBackfillItem;
|
| + // get the physical index for the focused index
|
| + pidx = this._getPhysicalIndex(fidx);
|
| +
|
| + if (pidx != null) {
|
| + // set the offcreen focused physical item
|
| + this._offscreenFocusedItem = this._physicalItems[pidx];
|
| + // backfill the focused physical item
|
| + this._physicalItems[pidx] = this._focusBackfillItem;
|
| + // hide the focused physical
|
| + this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
|
| + }
|
| + },
|
| +
|
| + _restoreFocusedItem: function() {
|
| + var pidx, fidx = this._focusedIndex;
|
| +
|
| + if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
|
| + return;
|
| + }
|
| + // assign models to the focused index
|
| + this._assignModels();
|
| + // get the new physical index for the focused index
|
| + pidx = this._getPhysicalIndex(fidx);
|
| +
|
| + if (pidx != null) {
|
| + // flip the focus backfill
|
| + this._focusBackfillItem = this._physicalItems[pidx];
|
| + // restore the focused physical item
|
| + this._physicalItems[pidx] = this._offscreenFocusedItem;
|
| + // reset the offscreen focused item
|
| + this._offscreenFocusedItem = null;
|
| + // hide the physical item that backfills
|
| + this.translate3d(0, HIDDEN_Y, 0, this._focusBackfillItem);
|
| + }
|
| },
|
|
|
| _didFocus: function(e) {
|
| var targetModel = this.modelForElement(e.target);
|
| + var focusedModel = this._focusedItem ? this._focusedItem._templateInstance : null;
|
| + var hasOffscreenFocusedItem = this._offscreenFocusedItem !== null;
|
| var fidx = this._focusedIndex;
|
|
|
| - if (!targetModel) {
|
| + if (!targetModel || !focusedModel) {
|
| return;
|
| }
|
| - this._restoreFocusedItem();
|
| -
|
| - if (this.modelForElement(this._offscreenFocusedItem) === targetModel) {
|
| - this.scrollToIndex(fidx);
|
| + if (focusedModel === targetModel) {
|
| + // if the user focused the same item, then bring it into view if it's not visible
|
| + if (!this._isIndexVisible(fidx)) {
|
| + this.scrollToIndex(fidx);
|
| + }
|
| } else {
|
| + this._restoreFocusedItem();
|
| // restore tabIndex for the currently focused item
|
| - this._getModelFromItem(this._getNormalizedItem(fidx)).tabIndex = -1;
|
| + focusedModel.tabIndex = -1;
|
| // set the tabIndex for the next focused item
|
| targetModel.tabIndex = 0;
|
| - fidx = /** @type {{index: number}} */(targetModel).index;
|
| + fidx = targetModel[this.indexAs];
|
| this._focusedIndex = fidx;
|
| - // bring the item into view
|
| - if (fidx < this.firstVisibleIndex || fidx > this.lastVisibleIndex) {
|
| - this.scrollToIndex(fidx);
|
| - } else {
|
| + this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
|
| +
|
| + if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
|
| this._update();
|
| }
|
| }
|
| },
|
|
|
| _didMoveUp: function() {
|
| - this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1));
|
| + this._focusPhysicalItem(this._focusedIndex - 1);
|
| },
|
|
|
| _didMoveDown: function() {
|
| - this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex + 1));
|
| + this._focusPhysicalItem(this._focusedIndex + 1);
|
| },
|
|
|
| _didEnter: function(e) {
|
| - // focus the currently focused physical item
|
| this._focusPhysicalItem(this._focusedIndex);
|
| - // toggle selection
|
| - this._selectionHandler(/** @type {{keyboardEvent: Event}} */(e.detail).keyboardEvent);
|
| + this._selectionHandler(e.detail.keyboardEvent);
|
| }
|
| });
|
|
|
| @@ -4301,7 +4414,8 @@ Polymer({
|
| * @type {!Polymer.IronMeta}
|
| */
|
| _meta: {
|
| - value: Polymer.Base.create('iron-meta', {type: 'iconset'})
|
| + value: Polymer.Base.create('iron-meta', {type: 'iconset'}),
|
| + observer: '_updateIcon'
|
| }
|
|
|
| },
|
| @@ -4326,7 +4440,14 @@ Polymer({
|
| /** @suppress {visibility} */
|
| _updateIcon: function() {
|
| if (this._usesIconset()) {
|
| - if (this._iconsetName) {
|
| + if (this._img && this._img.parentNode) {
|
| + Polymer.dom(this.root).removeChild(this._img);
|
| + }
|
| + if (this._iconName === "") {
|
| + if (this._iconset) {
|
| + this._iconset.removeIcon(this);
|
| + }
|
| + } else if (this._iconsetName && this._meta) {
|
| this._iconset = /** @type {?Polymer.Iconset} */ (
|
| this._meta.byKey(this._iconsetName));
|
| if (this._iconset) {
|
| @@ -4337,6 +4458,9 @@ Polymer({
|
| }
|
| }
|
| } else {
|
| + if (this._iconset) {
|
| + this._iconset.removeIcon(this);
|
| + }
|
| if (!this._img) {
|
| this._img = document.createElement('img');
|
| this._img.style.width = '100%';
|
| @@ -4605,6 +4729,7 @@ Polymer({
|
| this._oldTabIndex = this.tabIndex;
|
| this.focused = false;
|
| this.tabIndex = -1;
|
| + this.blur();
|
| } else if (this._oldTabIndex !== undefined) {
|
| this.tabIndex = this._oldTabIndex;
|
| }
|
| @@ -6335,16 +6460,19 @@ Polymer({
|
| */
|
| setItemSelected: function(item, isSelected) {
|
| if (item != null) {
|
| - if (isSelected) {
|
| - this.selection.push(item);
|
| - } else {
|
| - var i = this.selection.indexOf(item);
|
| - if (i >= 0) {
|
| - this.selection.splice(i, 1);
|
| + if (isSelected !== this.isSelected(item)) {
|
| + // proceed to update selection only if requested state differs from current
|
| + if (isSelected) {
|
| + this.selection.push(item);
|
| + } else {
|
| + var i = this.selection.indexOf(item);
|
| + if (i >= 0) {
|
| + this.selection.splice(i, 1);
|
| + }
|
| + }
|
| + if (this.selectCallback) {
|
| + this.selectCallback(item, isSelected);
|
| }
|
| - }
|
| - if (this.selectCallback) {
|
| - this.selectCallback(item, isSelected);
|
| }
|
| }
|
| },
|
| @@ -6501,7 +6629,8 @@ Polymer({
|
| },
|
|
|
| observers: [
|
| - '_updateSelected(attrForSelected, selected)'
|
| + '_updateAttrForSelected(attrForSelected)',
|
| + '_updateSelected(selected)'
|
| ],
|
|
|
| created: function() {
|
| @@ -6513,7 +6642,7 @@ Polymer({
|
| this._observer = this._observeItems(this);
|
| this._updateItems();
|
| if (!this._shouldUpdateSelection) {
|
| - this._updateSelected(this.attrForSelected,this.selected)
|
| + this._updateSelected();
|
| }
|
| this._addListener(this.activateEvent);
|
| },
|
| @@ -6606,6 +6735,12 @@ Polymer({
|
| this._setItems(nodes);
|
| },
|
|
|
| + _updateAttrForSelected: function() {
|
| + if (this._shouldUpdateSelection) {
|
| + this.selected = this._indexToValue(this.indexOf(this.selectedItem));
|
| + }
|
| + },
|
| +
|
| _updateSelected: function() {
|
| this._selectSelected(this.selected);
|
| },
|
| @@ -6675,7 +6810,7 @@ Polymer({
|
| }
|
|
|
| // Let other interested parties know about the change so that
|
| - // we don't have to recreate mutation observers everywher.
|
| + // we don't have to recreate mutation observers everywhere.
|
| this.fire('iron-items-changed', mutations, {
|
| bubbles: false,
|
| cancelable: false
|
| @@ -6739,7 +6874,7 @@ Polymer({
|
| },
|
|
|
| observers: [
|
| - '_updateSelected(attrForSelected, selectedValues)'
|
| + '_updateSelected(selectedValues)'
|
| ],
|
|
|
| /**
|
| @@ -6770,6 +6905,18 @@ Polymer({
|
| (this.selectedValues != null && this.selectedValues.length);
|
| },
|
|
|
| + _updateAttrForSelected: function() {
|
| + if (!this.multi) {
|
| + Polymer.IronSelectableBehavior._updateAttrForSelected.apply(this);
|
| + } else if (this._shouldUpdateSelection) {
|
| + this.selectedValues = this.selectedItems.map(function(selectedItem) {
|
| + return this._indexToValue(this.indexOf(selectedItem));
|
| + }, this).filter(function(unfilteredValue) {
|
| + return unfilteredValue != null;
|
| + }, this);
|
| + }
|
| + },
|
| +
|
| _updateSelected: function() {
|
| if (this.multi) {
|
| this._selectMulti(this.selectedValues);
|
| @@ -6779,11 +6926,16 @@ Polymer({
|
| },
|
|
|
| _selectMulti: function(values) {
|
| - this._selection.clear();
|
| if (values) {
|
| - for (var i = 0; i < values.length; i++) {
|
| - this._selection.setItemSelected(this._valueToItem(values[i]), true);
|
| + var selectedItems = this._valuesToItems(values);
|
| + // clear all but the current selected items
|
| + this._selection.clear(selectedItems);
|
| + // select only those not selected yet
|
| + for (var i = 0; i < selectedItems.length; i++) {
|
| + this._selection.setItemSelected(selectedItems[i], true);
|
| }
|
| + } else {
|
| + this._selection.clear();
|
| }
|
| },
|
|
|
| @@ -6806,6 +6958,12 @@ Polymer({
|
| this.splice('selectedValues',i,1);
|
| }
|
| this._selection.setItemSelected(this._valueToItem(value), unselected);
|
| + },
|
| +
|
| + _valuesToItems: function(values) {
|
| + return (values == null) ? null : values.map(function(value) {
|
| + return this._valueToItem(value);
|
| + }, this);
|
| }
|
| };
|
|
|
| @@ -7039,6 +7197,14 @@ Polymer({
|
| return;
|
| }
|
|
|
| + // Do not focus the selected tab if the deepest target is part of the
|
| + // menu element's local DOM and is focusable.
|
| + var rootTarget = /** @type {?HTMLElement} */(
|
| + Polymer.dom(event).rootTarget);
|
| + if (rootTarget !== this && typeof rootTarget.tabIndex !== "undefined" && !this.isLightDescendant(rootTarget)) {
|
| + return;
|
| + }
|
| +
|
| this.blur();
|
|
|
| // clear the cached focus item
|
| @@ -7066,6 +7232,7 @@ Polymer({
|
| _onUpKey: function(event) {
|
| // up and down arrows moves the focus
|
| this._focusPrevious();
|
| + event.detail.keyboardEvent.preventDefault();
|
| },
|
|
|
| /**
|
| @@ -7075,6 +7242,7 @@ Polymer({
|
| */
|
| _onDownKey: function(event) {
|
| this._focusNext();
|
| + event.detail.keyboardEvent.preventDefault();
|
| },
|
|
|
| /**
|
| @@ -7283,14 +7451,14 @@ CSS properties | Action
|
| * the memoized data.
|
| */
|
| resetFit: function() {
|
| - if (!this._fitInfo || !this._fitInfo.sizedBy.height) {
|
| - this.sizingTarget.style.maxHeight = '';
|
| - this.style.top = this._fitInfo ? this._fitInfo.inlineStyle.top : '';
|
| - }
|
| if (!this._fitInfo || !this._fitInfo.sizedBy.width) {
|
| this.sizingTarget.style.maxWidth = '';
|
| - this.style.left = this._fitInfo ? this._fitInfo.inlineStyle.left : '';
|
| }
|
| + if (!this._fitInfo || !this._fitInfo.sizedBy.height) {
|
| + this.sizingTarget.style.maxHeight = '';
|
| + }
|
| + this.style.top = this._fitInfo ? this._fitInfo.inlineStyle.top : '';
|
| + this.style.left = this._fitInfo ? this._fitInfo.inlineStyle.left : '';
|
| if (this._fitInfo) {
|
| this.style.position = this._fitInfo.positionedBy.css;
|
| }
|
| @@ -7351,18 +7519,30 @@ CSS properties | Action
|
| * `position:fixed`.
|
| */
|
| center: function() {
|
| - if (!this._fitInfo.positionedBy.vertically || !this._fitInfo.positionedBy.horizontally) {
|
| - // need position:fixed to center
|
| - this.style.position = 'fixed';
|
| + var positionedBy = this._fitInfo.positionedBy;
|
| + if (positionedBy.vertically && positionedBy.horizontally) {
|
| + // Already positioned.
|
| + return;
|
| }
|
| - if (!this._fitInfo.positionedBy.vertically) {
|
| - var top = (this._fitHeight - this.offsetHeight) / 2 + this._fitTop;
|
| - top -= this._fitInfo.margin.top;
|
| + // Need position:fixed to center
|
| + this.style.position = 'fixed';
|
| + // Take into account the offset caused by parents that create stacking
|
| + // contexts (e.g. with transform: translate3d). Translate to 0,0 and
|
| + // measure the bounding rect.
|
| + if (!positionedBy.vertically) {
|
| + this.style.top = '0px';
|
| + }
|
| + if (!positionedBy.horizontally) {
|
| + this.style.left = '0px';
|
| + }
|
| + // It will take in consideration margins and transforms
|
| + var rect = this.getBoundingClientRect();
|
| + if (!positionedBy.vertically) {
|
| + var top = this._fitTop - rect.top + (this._fitHeight - rect.height) / 2;
|
| this.style.top = top + 'px';
|
| }
|
| - if (!this._fitInfo.positionedBy.horizontally) {
|
| - var left = (this._fitWidth - this.offsetWidth) / 2 + this._fitLeft;
|
| - left -= this._fitInfo.margin.left;
|
| + if (!positionedBy.horizontally) {
|
| + var left = this._fitLeft - rect.left + (this._fitWidth - rect.width) / 2;
|
| this.style.left = left + 'px';
|
| }
|
| }
|
| @@ -7374,6 +7554,9 @@ CSS properties | Action
|
| */
|
| Polymer.IronOverlayManagerClass = function() {
|
| 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.
|
| @@ -7382,7 +7565,52 @@ CSS properties | Action
|
| this._minimumZ = 101;
|
|
|
| this._backdrops = [];
|
| - }
|
| +
|
| + this._backdropElement = null;
|
| + Object.defineProperty(this, 'backdropElement', {
|
| + get: function() {
|
| + if (!this._backdropElement) {
|
| + this._backdropElement = document.createElement('iron-overlay-backdrop');
|
| + }
|
| + return this._backdropElement;
|
| + }.bind(this)
|
| + });
|
| +
|
| + /**
|
| + * 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)
|
| + });
|
| + };
|
| +
|
| + /**
|
| + * 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;
|
| + }
|
| + }
|
| + return false;
|
| + };
|
|
|
| Polymer.IronOverlayManagerClass.prototype._applyOverlayZ = function(overlay, aboveZ) {
|
| this._setZ(overlay, aboveZ + 2);
|
| @@ -7402,6 +7630,12 @@ CSS properties | Action
|
| 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);
|
| };
|
|
|
| Polymer.IronOverlayManagerClass.prototype.removeOverlay = function(overlay) {
|
| @@ -7409,6 +7643,13 @@ CSS properties | Action
|
| if (i >= 0) {
|
| this._overlays.splice(i, 1);
|
| this._setZ(overlay, '');
|
| +
|
| + 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();
|
| + }
|
| + this._lastFocusedNodes.splice(i, 1);
|
| }
|
| };
|
|
|
| @@ -7463,15 +7704,6 @@ CSS properties | Action
|
| }
|
| };
|
|
|
| - Object.defineProperty(Polymer.IronOverlayManagerClass.prototype, "backdropElement", {
|
| - get: function() {
|
| - if (!this._backdropElement) {
|
| - this._backdropElement = document.createElement('iron-overlay-backdrop');
|
| - }
|
| - return this._backdropElement;
|
| - }
|
| - });
|
| -
|
| Polymer.IronOverlayManagerClass.prototype.getBackdrops = function() {
|
| return this._backdrops;
|
| };
|
| @@ -7701,6 +7933,23 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| type: Object
|
| },
|
|
|
| + /**
|
| + * 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: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| _manager: {
|
| type: Object,
|
| value: Polymer.IronOverlayManager
|
| @@ -7713,13 +7962,6 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| }
|
| },
|
|
|
| - _boundOnCaptureKeydown: {
|
| - type: Function,
|
| - value: function() {
|
| - return this._onCaptureKeydown.bind(this);
|
| - }
|
| - },
|
| -
|
| _boundOnCaptureFocus: {
|
| type: Function,
|
| value: function() {
|
| @@ -7727,44 +7969,113 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| }
|
| },
|
|
|
| - /** @type {?Node} */
|
| + /**
|
| + * The node being focused.
|
| + * @type {?Node}
|
| + */
|
| _focusedChild: {
|
| type: Object
|
| }
|
|
|
| },
|
|
|
| + keyBindings: {
|
| + 'esc': '__onEsc',
|
| + 'tab': '__onTab'
|
| + },
|
| +
|
| listeners: {
|
| 'iron-resize': '_onIronResize'
|
| },
|
|
|
| /**
|
| * The backdrop element.
|
| - * @type Node
|
| + * @type {Node}
|
| */
|
| get backdropElement() {
|
| return this._manager.backdropElement;
|
| },
|
|
|
| + /**
|
| + * Returns the node to give focus to.
|
| + * @type {Node}
|
| + */
|
| get _focusNode() {
|
| return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]') || this;
|
| },
|
|
|
| + /**
|
| + * Array of nodes that can receive focus (overlay included), ordered by `tabindex`.
|
| + * This is used to retrieve which is the first and last focusable nodes in order
|
| + * to wrap the focus for overlays `with-backdrop`.
|
| + *
|
| + * 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]}
|
| + * @protected
|
| + */
|
| + get _focusableNodes() {
|
| + // Elements that can be focused even if they have [disabled] attribute.
|
| + var FOCUSABLE_WITH_DISABLED = [
|
| + 'a[href]',
|
| + 'area[href]',
|
| + 'iframe',
|
| + '[tabindex]',
|
| + '[contentEditable=true]'
|
| + ];
|
| +
|
| + // Elements that cannot be focused if they have [disabled] attribute.
|
| + var FOCUSABLE_WITHOUT_DISABLED = [
|
| + 'input',
|
| + 'select',
|
| + 'textarea',
|
| + 'button'
|
| + ];
|
| +
|
| + // Discard elements with tabindex=-1 (makes them not focusable).
|
| + var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') +
|
| + ':not([tabindex="-1"]),' +
|
| + FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),') +
|
| + ':not([disabled]):not([tabindex="-1"])';
|
| +
|
| + var focusables = Polymer.dom(this).querySelectorAll(selector);
|
| + if (this.tabIndex >= 0) {
|
| + // Insert at the beginning because we might have all elements with tabIndex = 0,
|
| + // and the overlay should be the first of the list.
|
| + focusables.splice(0, 0, this);
|
| + }
|
| + // Sort by tabindex.
|
| + return focusables.sort(function (a, b) {
|
| + if (a.tabIndex === b.tabIndex) {
|
| + return 0;
|
| + }
|
| + if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) {
|
| + return 1;
|
| + }
|
| + return -1;
|
| + });
|
| + },
|
| +
|
| ready: function() {
|
| - // with-backdrop need tabindex to be set in order to trap the focus.
|
| + // 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;
|
| this._ensureSetup();
|
| },
|
|
|
| attached: function() {
|
| // Call _openedChanged here so that position can be computed correctly.
|
| - if (this._callOpenedWhenReady) {
|
| + if (this.opened) {
|
| this._openedChanged();
|
| }
|
| + this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
|
| },
|
|
|
| detached: function() {
|
| + Polymer.dom(this).unobserveNodes(this._observer);
|
| + this._observer = null;
|
| this.opened = false;
|
| this._manager.trackBackdrop(this);
|
| this._manager.removeOverlay(this);
|
| @@ -7796,9 +8107,10 @@ context. You should place this element as a child of `<body>` whenever possible.
|
|
|
| /**
|
| * Cancels the overlay.
|
| + * @param {?Event} event The original event
|
| */
|
| - cancel: function() {
|
| - var cancelEvent = this.fire('iron-overlay-canceled', undefined, {cancelable: true});
|
| + cancel: function(event) {
|
| + var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
|
| if (cancelEvent.defaultPrevented) {
|
| return;
|
| }
|
| @@ -7821,12 +8133,10 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| this.removeAttribute('aria-hidden');
|
| } else {
|
| this.setAttribute('aria-hidden', 'true');
|
| - Polymer.dom(this).unobserveNodes(this._observer);
|
| }
|
|
|
| // wait to call after ready only if we're initially open
|
| if (!this._overlaySetup) {
|
| - this._callOpenedWhenReady = this.opened;
|
| return;
|
| }
|
|
|
| @@ -7901,16 +8211,18 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| }
|
| },
|
|
|
| - _toggleListeners: function () {
|
| + _toggleListeners: function() {
|
| this._toggleListener(this.opened, document, 'tap', this._boundOnCaptureClick, true);
|
| - this._toggleListener(this.opened, document, 'keydown', this._boundOnCaptureKeydown, true);
|
| this._toggleListener(this.opened, document, 'focus', this._boundOnCaptureFocus, true);
|
| },
|
|
|
| // tasks which must occur before opening; e.g. making the element visible
|
| _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._finishPositioning();
|
| @@ -7918,6 +8230,12 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| 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) {
|
| + this._focusNode.blur();
|
| + }
|
| },
|
|
|
| // tasks which cause the overlay to actually open; typically play an
|
| @@ -7937,23 +8255,24 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| },
|
|
|
| _finishRenderOpened: function() {
|
| - // focus the child node with [autofocus]
|
| + // 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._observer = Polymer.dom(this).observeNodes(this.notifyResize);
|
| this.fire('iron-overlay-opened');
|
| },
|
|
|
| _finishRenderClosed: function() {
|
| - // hide the overlay and remove the backdrop
|
| + // Hide the overlay and remove the backdrop.
|
| this.resetFit();
|
| this.style.display = 'none';
|
| this._manager.removeOverlay(this);
|
|
|
| - this._focusedChild = null;
|
| this._applyFocus();
|
| -
|
| this.notifyResize();
|
| +
|
| this.fire('iron-overlay-closed', this.closingReason);
|
| },
|
|
|
| @@ -7966,8 +8285,9 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| _finishPositioning: function() {
|
| this.style.display = 'none';
|
| this.style.transform = this.style.webkitTransform = '';
|
| - // force layout to avoid application of transform
|
| - /** @suppress {suspiciousCode} */ this.offsetWidth;
|
| + // Force layout layout to avoid application of transform.
|
| + // Set offsetWidth to itself so that compilers won't remove it.
|
| + this.offsetWidth = this.offsetWidth;
|
| this.style.transition = this.style.webkitTransition = '';
|
| },
|
|
|
| @@ -7978,6 +8298,7 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| }
|
| } else {
|
| this._focusNode.blur();
|
| + this._focusedChild = null;
|
| this._manager.focusOverlay();
|
| }
|
| },
|
| @@ -7988,23 +8309,13 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| if (this.noCancelOnOutsideClick) {
|
| this._applyFocus();
|
| } else {
|
| - this.cancel();
|
| + this.cancel(event);
|
| }
|
| }
|
| },
|
|
|
| - _onCaptureKeydown: function(event) {
|
| - var ESC = 27;
|
| - if (this._manager.currentOverlay() === this &&
|
| - !this.noCancelOnEscKey &&
|
| - event.keyCode === ESC) {
|
| - this.cancel();
|
| - }
|
| - },
|
| -
|
| _onCaptureFocus: function (event) {
|
| - if (this._manager.currentOverlay() === this &&
|
| - this.withBackdrop) {
|
| + if (this._manager.currentOverlay() === this && this.withBackdrop) {
|
| var path = Polymer.dom(event).path;
|
| if (path.indexOf(this) === -1) {
|
| event.stopPropagation();
|
| @@ -8019,28 +8330,73 @@ context. You should place this element as a child of `<body>` whenever possible.
|
| if (this.opened) {
|
| this.refit();
|
| }
|
| - }
|
| + },
|
|
|
| -/**
|
| - * Fired after the `iron-overlay` opens.
|
| - * @event iron-overlay-opened
|
| - */
|
| + /**
|
| + * @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];
|
| + },
|
|
|
| -/**
|
| - * Fired when the `iron-overlay` is canceled, but before it is closed.
|
| - * Cancel the event to prevent the `iron-overlay` from closing.
|
| - * @event iron-overlay-canceled
|
| - */
|
| + __onEsc: function(event) {
|
| + // Not opened or not on top, so return.
|
| + if (this._manager.currentOverlay() !== this) {
|
| + return;
|
| + }
|
| + if (!this.noCancelOnEscKey) {
|
| + this.cancel(event);
|
| + }
|
| + },
|
|
|
| -/**
|
| - * Fired after the `iron-overlay` closes.
|
| - * @event iron-overlay-closed
|
| - * @param {{canceled: (boolean|undefined)}} set to the `closingReason` attribute
|
| - */
|
| + __onTab: function(event) {
|
| + // Not opened or not on top, so return.
|
| + if (this._manager.currentOverlay() !== this) {
|
| + return;
|
| + }
|
| + // TAB wraps from last to first focusable.
|
| + // Shift + TAB wraps from first to last focusable.
|
| + var shift = event.detail.keyboardEvent.shiftKey;
|
| + var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusableNode;
|
| + var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNode;
|
| + if (this.withBackdrop && this._focusedChild === nodeToCheck) {
|
| + // We set here the _focusedChild so that _onCaptureFocus will handle the
|
| + // wrapping of the focus (the next event after tab is focus).
|
| + this._focusedChild = nodeToSet;
|
| + }
|
| + }
|
| };
|
|
|
| /** @polymerBehavior */
|
| - Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
|
| + Polymer.IronOverlayBehavior = [Polymer.IronA11yKeysBehavior, Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
|
| +
|
| + /**
|
| + * Fired after the `iron-overlay` opens.
|
| + * @event iron-overlay-opened
|
| + */
|
| +
|
| + /**
|
| + * Fired when the `iron-overlay` is canceled, but before it is closed.
|
| + * Cancel the event to prevent the `iron-overlay` from closing.
|
| + * @event iron-overlay-canceled
|
| + * @param {Event} event The closing of the `iron-overlay` can be prevented
|
| + * by calling `event.preventDefault()`. The `event.detail` is the original event that originated
|
| + * the canceling (e.g. ESC keyboard event or click event outside the `iron-overlay`).
|
| + */
|
| +
|
| + /**
|
| + * Fired after the `iron-overlay` closes.
|
| + * @event iron-overlay-closed
|
| + * @param {{canceled: (boolean|undefined)}} closingReason Contains `canceled` (whether the overlay was canceled).
|
| + */
|
| /**
|
| * Use `Polymer.NeonAnimationBehavior` to implement an animation.
|
| * @polymerBehavior
|
|
|