Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(4118)

Unified Diff: chrome/browser/resources/md_downloads/crisper.js

Issue 1845223007: MD Downloads: revulcanize in a polymer.html world (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@vulcanize-bug
Patch Set: Created 4 years, 9 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | chrome/browser/resources/md_downloads/vulcanized.html » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 352b5bf7f50cd2f7d5ad20653ec41e7da06002c0..2efb147752cdf7aa59af7985e8de63ffb9472b45 100644
--- a/chrome/browser/resources/md_downloads/crisper.js
+++ b/chrome/browser/resources/md_downloads/crisper.js
@@ -1431,10 +1431,19 @@ function createElementWithClassName(type, className) {
* or when no paint happens during the animation). This function sets up
* a timer and emulate the event if it is not fired when the timer expires.
* @param {!HTMLElement} el The element to watch for webkitTransitionEnd.
- * @param {number} timeOut The maximum wait time in milliseconds for the
- * webkitTransitionEnd to happen.
+ * @param {number=} opt_timeOut The maximum wait time in milliseconds for the
+ * webkitTransitionEnd to happen. If not specified, it is fetched from |el|
+ * using the transitionDuration style value.
*/
-function ensureTransitionEndEvent(el, timeOut) {
+function ensureTransitionEndEvent(el, opt_timeOut) {
+ if (opt_timeOut === undefined) {
+ var style = getComputedStyle(el);
+ opt_timeOut = parseFloat(style.transitionDuration) * 1000;
+
+ // Give an additional 50ms buffer for the animation to complete.
+ opt_timeOut += 50;
+ }
+
var fired = false;
el.addEventListener('webkitTransitionEnd', function f(e) {
el.removeEventListener('webkitTransitionEnd', f);
@@ -1443,7 +1452,7 @@ function ensureTransitionEndEvent(el, timeOut) {
window.setTimeout(function() {
if (!fired)
cr.dispatchSimpleEvent(el, 'webkitTransitionEnd', true);
- }, timeOut);
+ }, opt_timeOut);
}
/**
@@ -1522,496 +1531,131 @@ function elide(original, maxLength) {
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
-// 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.
-
-cr.define('downloads', function() {
- /**
- * @param {string} chromeSendName
- * @return {function(string):void} A chrome.send() callback with curried name.
- */
- function chromeSendWithId(chromeSendName) {
- return function(id) { chrome.send(chromeSendName, [id]); };
- }
-
- /** @constructor */
- function ActionService() {
- /** @private {Array<string>} */
- this.searchTerms_ = [];
- }
+ * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
+ * coordinate the flow of resize events between "resizers" (elements that control the
+ * 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
+ * @demo demo/index.html
+ **/
+ Polymer.IronResizableBehavior = {
+ properties: {
+ /**
+ * The closest ancestor element that implements `IronResizableBehavior`.
+ */
+ _parentResizable: {
+ type: Object,
+ observer: '_parentResizableChanged'
+ },
- /**
- * @param {string} s
- * @return {string} |s| without whitespace at the beginning or end.
- */
- function trim(s) { return s.trim(); }
+ /**
+ * True if this element is currently notifying its descedant elements of
+ * resize.
+ */
+ _notifyingDescendant: {
+ type: Boolean,
+ value: false
+ }
+ },
- /**
- * @param {string|undefined} value
- * @return {boolean} Whether |value| is truthy.
- */
- function truthy(value) { return !!value; }
+ listeners: {
+ 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
+ },
- /**
- * @param {string} searchText Input typed by the user into a search box.
- * @return {Array<string>} A list of terms extracted from |searchText|.
- */
- ActionService.splitTerms = function(searchText) {
- // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']).
- return searchText.split(/"([^"]*)"/).map(trim).filter(truthy);
- };
+ created: function() {
+ // We don't really need property effects on these, and also we want them
+ // to be created before the `_parentResizable` observer fires:
+ this._interestedResizables = [];
+ this._boundNotifyResize = this.notifyResize.bind(this);
+ },
- ActionService.prototype = {
- /** @param {string} id ID of the download to cancel. */
- cancel: chromeSendWithId('cancel'),
+ attached: function() {
+ this.fire('iron-request-resize-notifications', null, {
+ node: this,
+ bubbles: true,
+ cancelable: true
+ });
- /** Instructs the browser to clear all finished downloads. */
- clearAll: function() {
- if (loadTimeData.getBoolean('allowDeletingHistory')) {
- chrome.send('clearAll');
- this.search('');
+ if (!this._parentResizable) {
+ window.addEventListener('resize', this._boundNotifyResize);
+ this.notifyResize();
}
},
- /** @param {string} id ID of the dangerous download to discard. */
- discardDangerous: chromeSendWithId('discardDangerous'),
+ detached: function() {
+ if (this._parentResizable) {
+ this._parentResizable.stopResizeNotificationsFor(this);
+ } else {
+ window.removeEventListener('resize', this._boundNotifyResize);
+ }
- /** @param {string} url URL of a file to download. */
- download: function(url) {
- var a = document.createElement('a');
- a.href = url;
- a.setAttribute('download', '');
- a.click();
+ this._parentResizable = null;
},
- /** @param {string} id ID of the download that the user started dragging. */
- drag: chromeSendWithId('drag'),
+ /**
+ * Can be called to manually notify a resizable and its descendant
+ * resizables of a resize change.
+ */
+ notifyResize: function() {
+ if (!this.isAttached) {
+ return;
+ }
- /** Loads more downloads with the current search terms. */
- loadMore: function() {
- chrome.send('getDownloads', this.searchTerms_);
+ this._interestedResizables.forEach(function(resizable) {
+ if (this.resizerShouldNotify(resizable)) {
+ this._notifyDescendant(resizable);
+ }
+ }, this);
+
+ this._fireResize();
},
/**
- * @return {boolean} Whether the user is currently searching for downloads
- * (i.e. has a non-empty search term).
+ * Used to assign the closest resizable ancestor to this resizable
+ * if the ancestor detects a request for notifications.
*/
- isSearching: function() {
- return this.searchTerms_.length > 0;
+ assignParentResizable: function(parentResizable) {
+ this._parentResizable = parentResizable;
},
- /** Opens the current local destination for downloads. */
- openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'),
-
/**
- * @param {string} id ID of the download to run locally on the user's box.
+ * Used to remove a resizable descendant from the list of descendants
+ * that should be notified of a resize change.
*/
- openFile: chromeSendWithId('openFile'),
-
- /** @param {string} id ID the of the progressing download to pause. */
- pause: chromeSendWithId('pause'),
-
- /** @param {string} id ID of the finished download to remove. */
- remove: chromeSendWithId('remove'),
+ stopResizeNotificationsFor: function(target) {
+ var index = this._interestedResizables.indexOf(target);
- /** @param {string} id ID of the paused download to resume. */
- resume: chromeSendWithId('resume'),
+ if (index > -1) {
+ this._interestedResizables.splice(index, 1);
+ this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
+ }
+ },
/**
- * @param {string} id ID of the dangerous download to save despite
- * warnings.
+ * This method can be overridden to filter nested elements that should or
+ * should not be notified by the current element. Return true if an element
+ * should be notified, or false if it should not be notified.
+ *
+ * @param {HTMLElement} element A candidate descendant element that
+ * implements `IronResizableBehavior`.
+ * @return {boolean} True if the `element` should be notified of resize.
*/
- saveDangerous: chromeSendWithId('saveDangerous'),
+ resizerShouldNotify: function(element) { return true; },
- /** @param {string} searchText What to search for. */
- search: function(searchText) {
- var searchTerms = ActionService.splitTerms(searchText);
- var sameTerms = searchTerms.length == this.searchTerms_.length;
-
- for (var i = 0; sameTerms && i < searchTerms.length; ++i) {
- if (searchTerms[i] != this.searchTerms_[i])
- sameTerms = false;
- }
-
- if (sameTerms)
- return;
-
- this.searchTerms_ = searchTerms;
- this.loadMore();
- },
-
- /**
- * Shows the local folder a finished download resides in.
- * @param {string} id ID of the download to show.
- */
- show: chromeSendWithId('show'),
-
- /** Undo download removal. */
- undo: chrome.send.bind(chrome, 'undo'),
- };
-
- cr.addSingletonGetter(ActionService);
-
- return {ActionService: ActionService};
-});
-// 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.
-
-cr.define('downloads', function() {
- /**
- * Explains why a download is in DANGEROUS state.
- * @enum {string}
- */
- var DangerType = {
- NOT_DANGEROUS: 'NOT_DANGEROUS',
- DANGEROUS_FILE: 'DANGEROUS_FILE',
- DANGEROUS_URL: 'DANGEROUS_URL',
- DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
- UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
- DANGEROUS_HOST: 'DANGEROUS_HOST',
- POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED',
- };
-
- /**
- * The states a download can be in. These correspond to states defined in
- * DownloadsDOMHandler::CreateDownloadItemValue
- * @enum {string}
- */
- var States = {
- IN_PROGRESS: 'IN_PROGRESS',
- CANCELLED: 'CANCELLED',
- COMPLETE: 'COMPLETE',
- PAUSED: 'PAUSED',
- DANGEROUS: 'DANGEROUS',
- INTERRUPTED: 'INTERRUPTED',
- };
-
- return {
- DangerType: DangerType,
- States: States,
- };
-});
-// Copyright 2014 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.
-
-// Action links are elements that are used to perform an in-page navigation or
-// action (e.g. showing a dialog).
-//
-// They look like normal anchor (<a>) tags as their text color is blue. However,
-// they're subtly different as they're not initially underlined (giving users a
-// clue that underlined links navigate while action links don't).
-//
-// Action links look very similar to normal links when hovered (hand cursor,
-// underlined). This gives the user an idea that clicking this link will do
-// something similar to navigation but in the same page.
-//
-// They can be created in JavaScript like this:
-//
-// var link = document.createElement('a', 'action-link'); // Note second arg.
-//
-// or with a constructor like this:
-//
-// var link = new ActionLink();
-//
-// They can be used easily from HTML as well, like so:
-//
-// <a is="action-link">Click me!</a>
-//
-// NOTE: <action-link> and document.createElement('action-link') don't work.
-
-/**
- * @constructor
- * @extends {HTMLAnchorElement}
- */
-var ActionLink = document.registerElement('action-link', {
- prototype: {
- __proto__: HTMLAnchorElement.prototype,
-
- /** @this {ActionLink} */
- createdCallback: function() {
- // Action links can start disabled (e.g. <a is="action-link" disabled>).
- this.tabIndex = this.disabled ? -1 : 0;
-
- if (!this.hasAttribute('role'))
- this.setAttribute('role', 'link');
-
- this.addEventListener('keydown', function(e) {
- if (!this.disabled && e.keyIdentifier == 'Enter' && !this.href) {
- // Schedule a click asynchronously because other 'keydown' handlers
- // may still run later (e.g. document.addEventListener('keydown')).
- // Specifically options dialogs break when this timeout isn't here.
- // NOTE: this affects the "trusted" state of the ensuing click. I
- // haven't found anything that breaks because of this (yet).
- window.setTimeout(this.click.bind(this), 0);
- }
- });
-
- function preventDefault(e) {
- e.preventDefault();
- }
-
- function removePreventDefault() {
- document.removeEventListener('selectstart', preventDefault);
- document.removeEventListener('mouseup', removePreventDefault);
- }
-
- this.addEventListener('mousedown', function() {
- // This handlers strives to match the behavior of <a href="...">.
-
- // While the mouse is down, prevent text selection from dragging.
- document.addEventListener('selectstart', preventDefault);
- document.addEventListener('mouseup', removePreventDefault);
-
- // If focus started via mouse press, don't show an outline.
- if (document.activeElement != this)
- this.classList.add('no-outline');
- });
-
- this.addEventListener('blur', function() {
- this.classList.remove('no-outline');
- });
- },
-
- /** @type {boolean} */
- set disabled(disabled) {
- if (disabled)
- HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', '');
- else
- HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled');
- this.tabIndex = disabled ? -1 : 0;
- },
- get disabled() {
- return this.hasAttribute('disabled');
- },
-
- /** @override */
- setAttribute: function(attr, val) {
- if (attr.toLowerCase() == 'disabled')
- this.disabled = true;
- else
- HTMLAnchorElement.prototype.setAttribute.apply(this, arguments);
- },
-
- /** @override */
- removeAttribute: function(attr) {
- if (attr.toLowerCase() == 'disabled')
- this.disabled = false;
- else
- HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments);
- },
- },
-
- extends: 'a',
-});
-// 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.
-
-// <include src="../../../../ui/webui/resources/js/i18n_template_no_process.js">
-
-i18nTemplate.process(document, loadTimeData);
-/**
- * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
- * coordinate the flow of resize events between "resizers" (elements that control the
- * 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
- * @demo demo/index.html
- **/
- Polymer.IronResizableBehavior = {
- properties: {
- /**
- * The closest ancestor element that implements `IronResizableBehavior`.
- */
- _parentResizable: {
- type: Object,
- observer: '_parentResizableChanged'
- },
-
- /**
- * True if this element is currently notifying its descedant elements of
- * resize.
- */
- _notifyingDescendant: {
- type: Boolean,
- value: false
- }
- },
-
- listeners: {
- 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
- },
-
- created: function() {
- // We don't really need property effects on these, and also we want them
- // to be created before the `_parentResizable` observer fires:
- this._interestedResizables = [];
- this._boundNotifyResize = this.notifyResize.bind(this);
- },
-
- attached: function() {
- this.fire('iron-request-resize-notifications', null, {
- node: this,
- bubbles: true,
- cancelable: true
- });
-
- if (!this._parentResizable) {
- window.addEventListener('resize', this._boundNotifyResize);
- this.notifyResize();
- }
- },
-
- detached: function() {
- if (this._parentResizable) {
- this._parentResizable.stopResizeNotificationsFor(this);
- } else {
- window.removeEventListener('resize', this._boundNotifyResize);
- }
-
- this._parentResizable = null;
- },
-
- /**
- * Can be called to manually notify a resizable and its descendant
- * resizables of a resize change.
- */
- notifyResize: function() {
- if (!this.isAttached) {
- return;
- }
-
- this._interestedResizables.forEach(function(resizable) {
- if (this.resizerShouldNotify(resizable)) {
- this._notifyDescendant(resizable);
- }
- }, this);
-
- this._fireResize();
- },
-
- /**
- * Used to assign the closest resizable ancestor to this resizable
- * if the ancestor detects a request for notifications.
- */
- assignParentResizable: function(parentResizable) {
- this._parentResizable = parentResizable;
- },
-
- /**
- * Used to remove a resizable descendant from the list of descendants
- * that should be notified of a resize change.
- */
- stopResizeNotificationsFor: function(target) {
- var index = this._interestedResizables.indexOf(target);
-
- if (index > -1) {
- this._interestedResizables.splice(index, 1);
- this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
- }
- },
-
- /**
- * This method can be overridden to filter nested elements that should or
- * should not be notified by the current element. Return true if an element
- * should be notified, or false if it should not be notified.
- *
- * @param {HTMLElement} element A candidate descendant element that
- * implements `IronResizableBehavior`.
- * @return {boolean} True if the `element` should be notified of resize.
- */
- resizerShouldNotify: function(element) { return true; },
-
- _onDescendantIronResize: function(event) {
- if (this._notifyingDescendant) {
- event.stopPropagation();
- return;
- }
+ _onDescendantIronResize: function(event) {
+ if (this._notifyingDescendant) {
+ event.stopPropagation();
+ return;
+ }
// NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
// otherwise non-bubbling event "just work." We do it manually here for
@@ -2561,9 +2205,9 @@ i18nTemplate.process(document, loadTimeData);
// Support element id references
if (typeof scrollTarget === 'string') {
- var host = this.domHost;
- this.scrollTarget = host && host.$ ? host.$[scrollTarget] :
- Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
+ var ownerRoot = Polymer.dom(this).getOwnerRoot();
+ this.scrollTarget = (ownerRoot && ownerRoot.$) ?
+ ownerRoot.$[scrollTarget] : Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
} else if (this._scrollHandler) {
@@ -2662,15 +2306,15 @@ i18nTemplate.process(document, loadTimeData);
* Scrolls the content to a particular place.
*
* @method scroll
- * @param {number} left The left position
* @param {number} top The top position
+ * @param {number} left The left position
*/
- scroll: function(left, top) {
+ scroll: function(top, left) {
if (this.scrollTarget === this._doc) {
- window.scrollTo(left, top);
+ window.scrollTo(top, left);
} else if (this._isValidScrollTarget()) {
- this.scrollTarget.scrollLeft = left;
this.scrollTarget.scrollTop = top;
+ this.scrollTarget.scrollLeft = left;
}
},
@@ -2827,7 +2471,7 @@ i18nTemplate.process(document, loadTimeData);
_ratio: 0.5,
/**
- * The padding-top value for the list.
+ * The padding-top value of the `scroller` element
*/
_scrollerPaddingTop: 0,
@@ -2837,6 +2481,21 @@ 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,
@@ -2863,6 +2522,11 @@ 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,
@@ -2908,6 +2572,7 @@ i18nTemplate.process(document, loadTimeData);
*/
_lastVisibleIndexVal: null,
+
/**
* A Polymer collection for the items.
* @type {?Polymer.Collection}
@@ -2931,14 +2596,9 @@ i18nTemplate.process(document, loadTimeData);
_maxPages: 3,
/**
- * The currently focused physical item.
- */
- _focusedItem: null,
-
- /**
- * The index of the `_focusedItem`.
+ * The currently focused item index.
*/
- _focusedIndex: -1,
+ _focusedIndex: 0,
/**
* The the item that is focused if it is moved offscreen.
@@ -2974,20 +2634,6 @@ 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,
@@ -3000,53 +2646,40 @@ i18nTemplate.process(document, loadTimeData);
},
/**
- * The n-th item rendered in the `_physicalStart` tile.
+ * The height of the physical content that isn't on the screen.
*/
- _virtualStartVal: 0,
-
- set _virtualStart(val) {
- this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
- },
-
- get _virtualStart() {
- return this._virtualStartVal || 0;
+ get _hiddenContentSize() {
+ return this._physicalSize - this._viewportSize;
},
/**
- * The k-th tile that is at the top of the scrolling list.
+ * The maximum scroll top value.
*/
- _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;
+ get _maxScrollTop() {
+ return this._estScrollHeight - this._viewportSize;
},
/**
- * The number of tiles in the DOM.
+ * Sets the n-th item rendered in `_physicalStart`
*/
- _physicalCountVal: 0,
-
- set _physicalCount(val) {
- this._physicalCountVal = val;
- this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
- },
-
- get _physicalCount() {
- return this._physicalCountVal;
+ 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;
+ }
},
/**
- * The k-th tile that is at the bottom of the scrolling list.
+ * Gets the n-th item rendered in `_physicalStart`
*/
- _physicalEnd: 0,
+ get _virtualStart() {
+ return this._virtualStartVal;
+ },
/**
* An optimal physical size such that we will have enough physical items
@@ -3073,11 +2706,12 @@ i18nTemplate.process(document, loadTimeData);
*/
get firstVisibleIndex() {
if (this._firstVisibleIndexVal === null) {
- var physicalOffset = this._physicalTop + this._scrollerPaddingTop;
+ var physicalOffset = this._physicalTop;
this._firstVisibleIndexVal = this._iterateItems(
function(pidx, vidx) {
physicalOffset += this._physicalSizes[pidx];
+
if (physicalOffset > this._scrollPosition) {
return vidx;
}
@@ -3098,18 +2732,14 @@ 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);
},
@@ -3123,6 +2753,10 @@ i18nTemplate.process(document, loadTimeData);
this._itemsRendered = false;
},
+ get _defaultScrollTarget() {
+ return this;
+ },
+
/**
* Set the overflow property if this element has its own scrolling region
*/
@@ -3138,9 +2772,8 @@ i18nTemplate.process(document, loadTimeData);
* @method updateViewportBoundaries
*/
updateViewportBoundaries: function() {
- this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
- parseInt(window.getComputedStyle(this)['padding-top'], 10);
-
+ var scrollerStyle = window.getComputedStyle(this.scrollTarget);
+ this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10);
this._viewportSize = this._scrollTargetHeight;
},
@@ -3150,10 +2783,12 @@ 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;
@@ -3162,7 +2797,7 @@ i18nTemplate.process(document, loadTimeData);
// track the last `scrollTop`
this._scrollPosition = scrollTop;
- // clear cached visible indexes
+ // clear cached visible index
this._firstVisibleIndexVal = null;
this._lastVisibleIndexVal = null;
@@ -3249,7 +2884,6 @@ i18nTemplate.process(document, loadTimeData);
}
} else {
this._virtualStart = this._virtualStart + recycledTiles;
- this._physicalStart = this._physicalStart + recycledTiles;
this._update(recycledTileSet, movingUp);
}
},
@@ -3261,7 +2895,11 @@ i18nTemplate.process(document, loadTimeData);
*/
_update: function(itemSet, movingUp) {
// manage focus
- this._manageFocus();
+ if (this._isIndexRendered(this._focusedIndex)) {
+ this._restoreFocusedItem();
+ } else {
+ this._createFocusBackfillItem();
+ }
// update models
this._assignModels(itemSet);
// measure heights
@@ -3328,6 +2966,7 @@ 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,
@@ -3336,24 +2975,14 @@ i18nTemplate.process(document, loadTimeData);
var prevPhysicalCount = this._physicalCount;
var delta = nextPhysicalCount - prevPhysicalCount;
- if (delta <= 0) {
- return;
- }
-
- [].push.apply(this._physicalItems, this._createPool(delta));
- [].push.apply(this._physicalSizes, new Array(delta));
+ if (delta > 0) {
+ [].push.apply(this._physicalItems, this._createPool(delta));
+ [].push.apply(this._physicalSizes, new Array(delta));
- 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._physicalCount = prevPhysicalCount + delta;
+ // tail call
+ return this._update();
}
- this._update();
},
/**
@@ -3441,37 +3070,28 @@ i18nTemplate.process(document, loadTimeData);
},
/**
- * Called as a side effect of a host items.<key>.<path> path change,
- * responsible for notifying item.<path> changes.
- */
- _forwardItemPath: function(path, 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;
+ * Called as a side effect of a host items.<key>.<path> path change,
+ * responsible for notifying item.<path> changes to row for key.
+ */
+ _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;
+ }
+ }
}
},
@@ -3481,15 +3101,19 @@ i18nTemplate.process(document, loadTimeData);
*/
_itemsChanged: function(change) {
if (change.path === 'items') {
- // reset items
+
+ this._restoreFocusedItem();
+ // render the new set
+ this._itemsRendered = false;
+ // update the whole set
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) {
@@ -3497,50 +3121,47 @@ i18nTemplate.process(document, loadTimeData);
this._physicalItems = this._createPool(this._physicalCount);
this._physicalSizes = new Array(this._physicalCount);
}
-
- this._physicalStart = 0;
+ this._debounceTemplate(this._render);
} 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) {
- splices.forEach(function(splice) {
- // deselect removed items
- splice.removed.forEach(this._removeItem, this);
- // We only need to care about changes happening above the current position
- if (splice.index < this._virtualStart) {
- var delta = Math.max(
- splice.addedCount - splice.removed.length,
- splice.index - this._virtualStart);
+ var i, splice, idx;
- this._virtualStart = this._virtualStart + delta;
+ for (i = 0; i < splices.length; i++) {
+ splice = splices[i];
- if (this._focusedIndex >= 0) {
- this._focusedIndex = this._focusedIndex + delta;
- }
+ // deselect removed items
+ splice.removed.forEach(this.$.selector.deselect, this.$.selector);
+
+ idx = splice.index;
+ // We only need to care about changes happening above the current position
+ if (idx >= this._virtualStart) {
+ break;
}
- }, this);
- },
- _removeItem: function(item) {
- this.$.selector.deselect(item);
- // remove the current focused item
- if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
- this._removeFocusedItem();
+ this._virtualStart = this._virtualStart +
+ Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStart);
}
},
@@ -3593,18 +3214,19 @@ i18nTemplate.process(document, loadTimeData);
var inst = el._templateInstance;
var item = this.items && this.items[vidx];
- if (item != null) {
+ if (item !== undefined && 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 = this._focusedIndex === vidx ? 0 : -1;
- this._physicalIndexForKey[inst.__key__] = pidx;
+ inst.tabIndex = vidx === this._focusedIndex ? 0 : -1;
el.removeAttribute('hidden');
+ this._physicalIndexForKey[inst.__key__] = pidx;
} else {
inst.__key__ = null;
el.setAttribute('hidden', '');
}
+
}, itemSet);
},
@@ -3652,8 +3274,10 @@ 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];
+
});
},
@@ -3701,6 +3325,7 @@ 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.
@@ -3713,392 +3338,707 @@ i18nTemplate.process(document, loadTimeData);
return;
}
- Polymer.dom.flush();
-
- idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
- // update the virtual start only when needed
- if (!this._isIndexRendered(idx) || idx >= this._maxVirtualStart) {
- this._virtualStart = idx - 1;
+ 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;
+ // assign new models
+ this._assignModels();
+ // measure the new sizes
+ this._updateMetrics();
+ // estimate new physical offset
+ this._physicalTop = this._virtualStart * this._physicalAverage;
+
+ var currentTopItem = this._physicalStart;
+ var currentVirtualItem = this._virtualStart;
+ var targetOffsetTop = 0;
+ var hiddenContentSize = this._hiddenContentSize;
+
+ // scroll to the item as much as we can
+ while (currentVirtualItem < idx && targetOffsetTop < hiddenContentSize) {
+ targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem];
+ currentTopItem = (currentTopItem + 1) % this._physicalCount;
+ currentVirtualItem++;
+ }
+ // update the scroller size
+ this._updateScrollerSize(true);
+ // update the position of the items
+ this._positionItems();
+ // set the new scroll position
+ this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop + 1);
+ // increase the pool of physical items if needed
+ this._increasePoolIfNeeded();
+ // clear cached visible index
+ this._firstVisibleIndexVal = null;
+ this._lastVisibleIndexVal = null;
+ },
+
+ /**
+ * Reset the physical average and the average count.
+ */
+ _resetAverage: function() {
+ this._physicalAverage = 0;
+ this._physicalAverageCount = 0;
+ },
+
+ /**
+ * A handler for the `iron-resize` event triggered by `IronResizableBehavior`
+ * when the element is resized.
+ */
+ _resizeHandler: function() {
+ // iOS fires the resize event when the address bar slides up
+ 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);
+ }
+ });
+ },
+
+ _getModelFromItem: function(item) {
+ var key = this._collection.getKey(item);
+ var pidx = this._physicalIndexForKey[key];
+
+ if (pidx !== undefined) {
+ return this._physicalItems[pidx]._templateInstance;
+ }
+ return null;
+ },
+
+ /**
+ * Gets a valid item instance from its index or the object value.
+ *
+ * @param {(Object|number)} item The item object or its index
+ */
+ _getNormalizedItem: function(item) {
+ if (this._collection.getKey(item) === undefined) {
+ if (typeof item === 'number') {
+ item = this.items[item];
+ if (!item) {
+ throw new RangeError('<item> not found');
+ }
+ return item;
+ }
+ throw new TypeError('<item> should be a valid item');
+ }
+ return item;
+ },
+
+ /**
+ * Select the list item at the given index.
+ *
+ * @method selectItem
+ * @param {(Object|number)} item The item object or its index
+ */
+ selectItem: function(item) {
+ item = this._getNormalizedItem(item);
+ var model = this._getModelFromItem(item);
+
+ if (!this.multiSelection && this.selectedItem) {
+ this.deselectItem(this.selectedItem);
+ }
+ if (model) {
+ model[this.selectedAs] = true;
+ }
+ this.$.selector.select(item);
+ this.updateSizeForItem(item);
+ },
+
+ /**
+ * Deselects the given item list if it is already selected.
+ *
+
+ * @method deselect
+ * @param {(Object|number)} item The item object or its index
+ */
+ deselectItem: function(item) {
+ item = this._getNormalizedItem(item);
+ var model = this._getModelFromItem(item);
+
+ if (model) {
+ model[this.selectedAs] = false;
+ }
+ this.$.selector.deselect(item);
+ this.updateSizeForItem(item);
+ },
+
+ /**
+ * Select or deselect a given item depending on whether the item
+ * has already been selected.
+ *
+ * @method toggleSelectionForItem
+ * @param {(Object|number)} item The item object or its index
+ */
+ toggleSelectionForItem: function(item) {
+ item = this._getNormalizedItem(item);
+ if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item)) {
+ this.deselectItem(item);
+ } else {
+ this.selectItem(item);
+ }
+ },
+
+ /**
+ * Clears the current selection state of the list.
+ *
+ * @method clearSelection
+ */
+ clearSelection: function() {
+ function unselect(item) {
+ var model = this._getModelFromItem(item);
+ if (model) {
+ model[this.selectedAs] = false;
+ }
+ }
+
+ if (Array.isArray(this.selectedItems)) {
+ this.selectedItems.forEach(unselect, this);
+ } else if (this.selectedItem) {
+ unselect.call(this, this.selectedItem);
+ }
+
+ /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
+ },
+
+ /**
+ * Add an event listener to `tap` if `selectionEnabled` is true,
+ * it will remove the listener otherwise.
+ */
+ _selectionEnabledChanged: function(selectionEnabled) {
+ var handler = selectionEnabled ? this.listen : this.unlisten;
+ handler.call(this, this, 'tap', '_selectionHandler');
+ },
+
+ /**
+ * Select an item from an event object.
+ */
+ _selectionHandler: function(e) {
+ if (this.selectionEnabled) {
+ var model = this.modelForElement(e.target);
+ if (model) {
+ this.toggleSelectionForItem(model[this.as]);
+ }
+ }
+ },
+
+ _multiSelectionChanged: function(multiSelection) {
+ this.clearSelection();
+ this.$.selector.multi = multiSelection;
+ },
+
+ /**
+ * Updates the size of an item.
+ *
+ * @method updateSizeForItem
+ * @param {(Object|number)} item The item object or its index
+ */
+ updateSizeForItem: function(item) {
+ item = this._getNormalizedItem(item);
+ var key = this._collection.getKey(item);
+ var pidx = this._physicalIndexForKey[key];
+
+ if (pidx !== undefined) {
+ this._updateMetrics([pidx]);
+ this._positionItems();
+ }
+ },
+
+ _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)]];
+
+ return physicalItem || null;
+ },
+
+ _focusPhysicalItem: function(idx) {
+ this._restoreFocusedItem();
+
+ var physicalItem = this._getPhysicalItemForIndex(idx, true);
+ if (!physicalItem) {
+ return;
+ }
+ var SECRET = ~(Math.random() * 100);
+ var model = physicalItem._templateInstance;
+ var focusable;
+
+ model.tabIndex = SECRET;
+ // the focusable element could be the entire physical item
+ if (physicalItem.tabIndex === SECRET) {
+ focusable = physicalItem;
}
- // manage focus
- this._manageFocus();
- // assign new models
- this._assignModels();
- // measure the new sizes
- this._updateMetrics();
- // estimate new physical offset
- this._physicalTop = this._virtualStart * this._physicalAverage;
+ // the focusable element could be somewhere within the physical item
+ if (!focusable) {
+ focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECRET + '"]');
+ }
+ // restore the tab index
+ model.tabIndex = 0;
+ focusable && focusable.focus();
+ },
- var currentTopItem = this._physicalStart;
- var currentVirtualItem = this._virtualStart;
- var targetOffsetTop = 0;
- var hiddenContentSize = this._hiddenContentSize;
+ _restoreFocusedItem: function() {
+ if (!this._offscreenFocusedItem) {
+ return;
+ }
+ var item = this._getNormalizedItem(this._focusedIndex);
+ var pidx = this._physicalIndexForKey[this._collection.getKey(item)];
- // scroll to the item as much as we can
- while (currentVirtualItem < idx && targetOffsetTop < hiddenContentSize) {
- targetOffsetTop = targetOffsetTop + this._physicalSizes[currentTopItem];
- currentTopItem = (currentTopItem + 1) % this._physicalCount;
- currentVirtualItem++;
+ if (pidx !== undefined) {
+ this.translate3d(0, HIDDEN_Y, 0, this._physicalItems[pidx]);
+ this._physicalItems[pidx] = this._offscreenFocusedItem;
}
- // update the scroller size
- this._updateScrollerSize(true);
- // update the position of the items
- this._positionItems();
- // set the new scroll position
- this._resetScrollPosition(this._physicalTop + this._scrollerPaddingTop + targetOffsetTop + 1);
- // increase the pool of physical items if needed
- this._increasePoolIfNeeded();
- // clear cached visible index
- this._firstVisibleIndexVal = null;
- this._lastVisibleIndexVal = null;
+ this._offscreenFocusedItem = null;
},
- /**
- * Reset the physical average and the average count.
- */
- _resetAverage: function() {
- this._physicalAverage = 0;
- this._physicalAverageCount = 0;
+ _removeFocusedItem: function() {
+ if (!this._offscreenFocusedItem) {
+ return;
+ }
+ Polymer.dom(this).removeChild(this._offscreenFocusedItem);
+ this._offscreenFocusedItem = null;
+ this._focusBackfillItem = null;
},
- /**
- * A handler for the `iron-resize` event triggered by `IronResizableBehavior`
- * when the element is resized.
- */
- _resizeHandler: function() {
- // iOS fires the resize event when the address bar slides up
- if (IOS && Math.abs(this._viewportSize - this._scrollTargetHeight) < 100) {
+ _createFocusBackfillItem: function() {
+ if (this._offscreenFocusedItem) {
return;
}
- this._debounceTemplate(function() {
- this._render();
- if (this._itemsRendered && this._physicalItems && this._isVisible) {
- this._resetAverage();
- this.updateViewportBoundaries();
- this.scrollToIndex(this.firstVisibleIndex);
- }
- });
- },
+ var item = this._getNormalizedItem(this._focusedIndex);
+ var pidx = this._physicalIndexForKey[this._collection.getKey(item)];
- _getModelFromItem: function(item) {
- var key = this._collection.getKey(item);
- var pidx = this._physicalIndexForKey[key];
+ this._offscreenFocusedItem = this._physicalItems[pidx];
+ this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
- if (pidx != null) {
- return this._physicalItems[pidx]._templateInstance;
+ if (!this._focusBackfillItem) {
+ var stampedTemplate = this.stamp(null);
+ this._focusBackfillItem = stampedTemplate.root.querySelector('*');
+ Polymer.dom(this).appendChild(stampedTemplate.root);
}
- return null;
+ this._physicalItems[pidx] = this._focusBackfillItem;
},
- /**
- * Gets a valid item instance from its index or the object value.
- *
- * @param {(Object|number)} item The item object or its index
- */
- _getNormalizedItem: function(item) {
- if (this._collection.getKey(item) === undefined) {
- if (typeof item === 'number') {
- item = this.items[item];
- if (!item) {
- throw new RangeError('<item> not found');
- }
- return item;
+ _didFocus: function(e) {
+ var targetModel = this.modelForElement(e.target);
+ var fidx = this._focusedIndex;
+
+ if (!targetModel) {
+ return;
+ }
+ this._restoreFocusedItem();
+
+ if (this.modelForElement(this._offscreenFocusedItem) === targetModel) {
+ this.scrollToIndex(fidx);
+ } else {
+ // restore tabIndex for the currently focused item
+ this._getModelFromItem(this._getNormalizedItem(fidx)).tabIndex = -1;
+ // set the tabIndex for the next focused item
+ targetModel.tabIndex = 0;
+ fidx = /** @type {{index: number}} */(targetModel).index;
+ this._focusedIndex = fidx;
+ // bring the item into view
+ if (fidx < this.firstVisibleIndex || fidx > this.lastVisibleIndex) {
+ this.scrollToIndex(fidx);
+ } else {
+ this._update();
}
- throw new TypeError('<item> should be a valid item');
}
- return item;
},
- /**
- * Select the list item at the given index.
- *
- * @method selectItem
- * @param {(Object|number)} item The item object or its index
- */
- selectItem: function(item) {
- item = this._getNormalizedItem(item);
- var model = this._getModelFromItem(item);
+ _didMoveUp: function() {
+ this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1));
+ },
- if (!this.multiSelection && this.selectedItem) {
- this.deselectItem(this.selectedItem);
- }
- if (model) {
- model[this.selectedAs] = true;
- }
- this.$.selector.select(item);
- this.updateSizeForItem(item);
+ _didMoveDown: function() {
+ this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex + 1));
},
- /**
- * Deselects the given item list if it is already selected.
- *
+ _didEnter: function(e) {
+ // focus the currently focused physical item
+ this._focusPhysicalItem(this._focusedIndex);
+ // toggle selection
+ this._selectionHandler(/** @type {{keyboardEvent: Event}} */(e.detail).keyboardEvent);
+ }
+ });
+
+})();
+// 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.
+
+cr.define('downloads', function() {
+ /**
+ * @param {string} chromeSendName
+ * @return {function(string):void} A chrome.send() callback with curried name.
+ */
+ function chromeSendWithId(chromeSendName) {
+ return function(id) { chrome.send(chromeSendName, [id]); };
+ }
+
+ /** @constructor */
+ function ActionService() {
+ /** @private {Array<string>} */
+ this.searchTerms_ = [];
+ }
+
+ /**
+ * @param {string} s
+ * @return {string} |s| without whitespace at the beginning or end.
+ */
+ function trim(s) { return s.trim(); }
+
+ /**
+ * @param {string|undefined} value
+ * @return {boolean} Whether |value| is truthy.
+ */
+ function truthy(value) { return !!value; }
+
+ /**
+ * @param {string} searchText Input typed by the user into a search box.
+ * @return {Array<string>} A list of terms extracted from |searchText|.
+ */
+ ActionService.splitTerms = function(searchText) {
+ // Split quoted terms (e.g., 'The "lazy" dog' => ['The', 'lazy', 'dog']).
+ return searchText.split(/"([^"]*)"/).map(trim).filter(truthy);
+ };
- * @method deselect
- * @param {(Object|number)} item The item object or its index
- */
- deselectItem: function(item) {
- item = this._getNormalizedItem(item);
- var model = this._getModelFromItem(item);
+ ActionService.prototype = {
+ /** @param {string} id ID of the download to cancel. */
+ cancel: chromeSendWithId('cancel'),
- if (model) {
- model[this.selectedAs] = false;
+ /** Instructs the browser to clear all finished downloads. */
+ clearAll: function() {
+ if (loadTimeData.getBoolean('allowDeletingHistory')) {
+ chrome.send('clearAll');
+ this.search('');
}
- this.$.selector.deselect(item);
- this.updateSizeForItem(item);
},
- /**
- * Select or deselect a given item depending on whether the item
- * has already been selected.
- *
- * @method toggleSelectionForItem
- * @param {(Object|number)} item The item object or its index
- */
- toggleSelectionForItem: function(item) {
- item = this._getNormalizedItem(item);
- if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item)) {
- this.deselectItem(item);
- } else {
- this.selectItem(item);
- }
- },
+ /** @param {string} id ID of the dangerous download to discard. */
+ discardDangerous: chromeSendWithId('discardDangerous'),
- /**
- * Clears the current selection state of the list.
- *
- * @method clearSelection
- */
- clearSelection: function() {
- function unselect(item) {
- var model = this._getModelFromItem(item);
- if (model) {
- model[this.selectedAs] = false;
- }
- }
+ /** @param {string} url URL of a file to download. */
+ download: function(url) {
+ var a = document.createElement('a');
+ a.href = url;
+ a.setAttribute('download', '');
+ a.click();
+ },
- if (Array.isArray(this.selectedItems)) {
- this.selectedItems.forEach(unselect, this);
- } else if (this.selectedItem) {
- unselect.call(this, this.selectedItem);
- }
+ /** @param {string} id ID of the download that the user started dragging. */
+ drag: chromeSendWithId('drag'),
- /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
+ /** Loads more downloads with the current search terms. */
+ loadMore: function() {
+ chrome.send('getDownloads', this.searchTerms_);
},
/**
- * Add an event listener to `tap` if `selectionEnabled` is true,
- * it will remove the listener otherwise.
+ * @return {boolean} Whether the user is currently searching for downloads
+ * (i.e. has a non-empty search term).
*/
- _selectionEnabledChanged: function(selectionEnabled) {
- var handler = selectionEnabled ? this.listen : this.unlisten;
- handler.call(this, this, 'tap', '_selectionHandler');
+ isSearching: function() {
+ return this.searchTerms_.length > 0;
},
+ /** Opens the current local destination for downloads. */
+ openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'),
+
/**
- * Select an item from an event object.
+ * @param {string} id ID of the download to run locally on the user's box.
*/
- _selectionHandler: function(e) {
- if (this.selectionEnabled) {
- var model = this.modelForElement(e.target);
- if (model) {
- this.toggleSelectionForItem(model[this.as]);
- }
- }
- },
+ openFile: chromeSendWithId('openFile'),
- _multiSelectionChanged: function(multiSelection) {
- this.clearSelection();
- this.$.selector.multi = multiSelection;
- },
+ /** @param {string} id ID the of the progressing download to pause. */
+ pause: chromeSendWithId('pause'),
+
+ /** @param {string} id ID of the finished download to remove. */
+ remove: chromeSendWithId('remove'),
+
+ /** @param {string} id ID of the paused download to resume. */
+ resume: chromeSendWithId('resume'),
/**
- * Updates the size of an item.
- *
- * @method updateSizeForItem
- * @param {(Object|number)} item The item object or its index
+ * @param {string} id ID of the dangerous download to save despite
+ * warnings.
*/
- updateSizeForItem: function(item) {
- item = this._getNormalizedItem(item);
- var key = this._collection.getKey(item);
- var pidx = this._physicalIndexForKey[key];
+ saveDangerous: chromeSendWithId('saveDangerous'),
- if (pidx != null) {
- this._updateMetrics([pidx]);
- this._positionItems();
+ /** @param {string} searchText What to search for. */
+ search: function(searchText) {
+ var searchTerms = ActionService.splitTerms(searchText);
+ var sameTerms = searchTerms.length == this.searchTerms_.length;
+
+ for (var i = 0; sameTerms && i < searchTerms.length; ++i) {
+ if (searchTerms[i] != this.searchTerms_[i])
+ sameTerms = false;
}
+
+ if (sameTerms)
+ return;
+
+ this.searchTerms_ = searchTerms;
+ this.loadMore();
},
/**
- * 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.
+ * Shows the local folder a finished download resides in.
+ * @param {string} id ID of the download to show.
*/
- _manageFocus: function() {
- var fidx = this._focusedIndex;
+ show: chromeSendWithId('show'),
- 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];
- }
- },
+ /** Undo download removal. */
+ undo: chrome.send.bind(chrome, 'undo'),
+ };
- _isIndexRendered: function(idx) {
- return idx >= this._virtualStart && idx <= this._virtualEnd;
- },
+ cr.addSingletonGetter(ActionService);
- _isIndexVisible: function(idx) {
- return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
- },
+ return {ActionService: ActionService};
+});
+// 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.
- _getPhysicalIndex: function(idx) {
- return this._physicalIndexForKey[this._collection.getKey(this._getNormalizedItem(idx))];
- },
+cr.define('downloads', function() {
+ /**
+ * Explains why a download is in DANGEROUS state.
+ * @enum {string}
+ */
+ var DangerType = {
+ NOT_DANGEROUS: 'NOT_DANGEROUS',
+ DANGEROUS_FILE: 'DANGEROUS_FILE',
+ DANGEROUS_URL: 'DANGEROUS_URL',
+ DANGEROUS_CONTENT: 'DANGEROUS_CONTENT',
+ UNCOMMON_CONTENT: 'UNCOMMON_CONTENT',
+ DANGEROUS_HOST: 'DANGEROUS_HOST',
+ POTENTIALLY_UNWANTED: 'POTENTIALLY_UNWANTED',
+ };
- _focusPhysicalItem: function(idx) {
- 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);
- }
+ /**
+ * The states a download can be in. These correspond to states defined in
+ * DownloadsDOMHandler::CreateDownloadItemValue
+ * @enum {string}
+ */
+ var States = {
+ IN_PROGRESS: 'IN_PROGRESS',
+ CANCELLED: 'CANCELLED',
+ COMPLETE: 'COMPLETE',
+ PAUSED: 'PAUSED',
+ DANGEROUS: 'DANGEROUS',
+ INTERRUPTED: 'INTERRUPTED',
+ };
- var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
- var SECRET = ~(Math.random() * 100);
- var model = physicalItem._templateInstance;
- var focusable;
+ return {
+ DangerType: DangerType,
+ States: States,
+ };
+});
+// Copyright 2014 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.
- // set a secret tab index
- model.tabIndex = SECRET;
- // check if focusable element is the physical item
- if (physicalItem.tabIndex === SECRET) {
- focusable = physicalItem;
- }
- // 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();
- },
+// Action links are elements that are used to perform an in-page navigation or
+// action (e.g. showing a dialog).
+//
+// They look like normal anchor (<a>) tags as their text color is blue. However,
+// they're subtly different as they're not initially underlined (giving users a
+// clue that underlined links navigate while action links don't).
+//
+// Action links look very similar to normal links when hovered (hand cursor,
+// underlined). This gives the user an idea that clicking this link will do
+// something similar to navigation but in the same page.
+//
+// They can be created in JavaScript like this:
+//
+// var link = document.createElement('a', 'action-link'); // Note second arg.
+//
+// or with a constructor like this:
+//
+// var link = new ActionLink();
+//
+// They can be used easily from HTML as well, like so:
+//
+// <a is="action-link">Click me!</a>
+//
+// NOTE: <action-link> and document.createElement('action-link') don't work.
- _removeFocusedItem: function() {
- if (this._offscreenFocusedItem) {
- Polymer.dom(this).removeChild(this._offscreenFocusedItem);
- }
- this._offscreenFocusedItem = null;
- this._focusBackfillItem = null;
- this._focusedItem = null;
- this._focusedIndex = -1;
- },
+/**
+ * @constructor
+ * @extends {HTMLAnchorElement}
+ */
+var ActionLink = document.registerElement('action-link', {
+ prototype: {
+ __proto__: HTMLAnchorElement.prototype,
- _createFocusBackfillItem: function() {
- var pidx, fidx = this._focusedIndex;
- if (this._offscreenFocusedItem || fidx < 0) {
- return;
- }
- 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);
- }
- // get the physical index for the focused index
- pidx = this._getPhysicalIndex(fidx);
+ /** @this {ActionLink} */
+ createdCallback: function() {
+ // Action links can start disabled (e.g. <a is="action-link" disabled>).
+ this.tabIndex = this.disabled ? -1 : 0;
- 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);
- }
- },
+ if (!this.hasAttribute('role'))
+ this.setAttribute('role', 'link');
- _restoreFocusedItem: function() {
- var pidx, fidx = this._focusedIndex;
+ this.addEventListener('keydown', function(e) {
+ if (!this.disabled && e.keyIdentifier == 'Enter' && !this.href) {
+ // Schedule a click asynchronously because other 'keydown' handlers
+ // may still run later (e.g. document.addEventListener('keydown')).
+ // Specifically options dialogs break when this timeout isn't here.
+ // NOTE: this affects the "trusted" state of the ensuing click. I
+ // haven't found anything that breaks because of this (yet).
+ window.setTimeout(this.click.bind(this), 0);
+ }
+ });
- if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
- return;
+ function preventDefault(e) {
+ e.preventDefault();
}
- // 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);
+ function removePreventDefault() {
+ document.removeEventListener('selectstart', preventDefault);
+ document.removeEventListener('mouseup', removePreventDefault);
}
- },
- _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;
+ this.addEventListener('mousedown', function() {
+ // This handlers strives to match the behavior of <a href="...">.
- if (!targetModel || !focusedModel) {
- return;
- }
- 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
- focusedModel.tabIndex = -1;
- // set the tabIndex for the next focused item
- targetModel.tabIndex = 0;
- fidx = targetModel[this.indexAs];
- this._focusedIndex = fidx;
- this._focusedItem = this._physicalItems[this._getPhysicalIndex(fidx)];
+ // While the mouse is down, prevent text selection from dragging.
+ document.addEventListener('selectstart', preventDefault);
+ document.addEventListener('mouseup', removePreventDefault);
- if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
- this._update();
- }
- }
+ // If focus started via mouse press, don't show an outline.
+ if (document.activeElement != this)
+ this.classList.add('no-outline');
+ });
+
+ this.addEventListener('blur', function() {
+ this.classList.remove('no-outline');
+ });
},
- _didMoveUp: function() {
- this._focusPhysicalItem(this._focusedIndex - 1);
+ /** @type {boolean} */
+ set disabled(disabled) {
+ if (disabled)
+ HTMLAnchorElement.prototype.setAttribute.call(this, 'disabled', '');
+ else
+ HTMLAnchorElement.prototype.removeAttribute.call(this, 'disabled');
+ this.tabIndex = disabled ? -1 : 0;
+ },
+ get disabled() {
+ return this.hasAttribute('disabled');
},
- _didMoveDown: function() {
- this._focusPhysicalItem(this._focusedIndex + 1);
+ /** @override */
+ setAttribute: function(attr, val) {
+ if (attr.toLowerCase() == 'disabled')
+ this.disabled = true;
+ else
+ HTMLAnchorElement.prototype.setAttribute.apply(this, arguments);
},
- _didEnter: function(e) {
- this._focusPhysicalItem(this._focusedIndex);
- this._selectionHandler(e.detail.keyboardEvent);
- }
- });
+ /** @override */
+ removeAttribute: function(attr) {
+ if (attr.toLowerCase() == 'disabled')
+ this.disabled = false;
+ else
+ HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments);
+ },
+ },
-})();
+ extends: 'a',
+});
(function() {
// monostate data
@@ -4414,8 +4354,7 @@ Polymer({
* @type {!Polymer.IronMeta}
*/
_meta: {
- value: Polymer.Base.create('iron-meta', {type: 'iconset'}),
- observer: '_updateIcon'
+ value: Polymer.Base.create('iron-meta', {type: 'iconset'})
}
},
@@ -4440,14 +4379,7 @@ Polymer({
/** @suppress {visibility} */
_updateIcon: function() {
if (this._usesIconset()) {
- 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) {
+ if (this._iconsetName) {
this._iconset = /** @type {?Polymer.Iconset} */ (
this._meta.byKey(this._iconsetName));
if (this._iconset) {
@@ -4458,9 +4390,6 @@ Polymer({
}
}
} else {
- if (this._iconset) {
- this._iconset.removeIcon(this);
- }
if (!this._img) {
this._img = document.createElement('img');
this._img.style.width = '100%';
@@ -4729,7 +4658,6 @@ Polymer({
this._oldTabIndex = this.tabIndex;
this.focused = false;
this.tabIndex = -1;
- this.blur();
} else if (this._oldTabIndex !== undefined) {
this.tabIndex = this._oldTabIndex;
}
@@ -6460,20 +6388,17 @@ Polymer({
*/
setItemSelected: function(item, isSelected) {
if (item != null) {
- 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 (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);
+ }
}
},
@@ -6629,8 +6554,7 @@ Polymer({
},
observers: [
- '_updateAttrForSelected(attrForSelected)',
- '_updateSelected(selected)'
+ '_updateSelected(attrForSelected, selected)'
],
created: function() {
@@ -6642,7 +6566,7 @@ Polymer({
this._observer = this._observeItems(this);
this._updateItems();
if (!this._shouldUpdateSelection) {
- this._updateSelected();
+ this._updateSelected(this.attrForSelected,this.selected)
}
this._addListener(this.activateEvent);
},
@@ -6735,12 +6659,6 @@ Polymer({
this._setItems(nodes);
},
- _updateAttrForSelected: function() {
- if (this._shouldUpdateSelection) {
- this.selected = this._indexToValue(this.indexOf(this.selectedItem));
- }
- },
-
_updateSelected: function() {
this._selectSelected(this.selected);
},
@@ -6810,7 +6728,7 @@ Polymer({
}
// Let other interested parties know about the change so that
- // we don't have to recreate mutation observers everywhere.
+ // we don't have to recreate mutation observers everywher.
this.fire('iron-items-changed', mutations, {
bubbles: false,
cancelable: false
@@ -6874,7 +6792,7 @@ Polymer({
},
observers: [
- '_updateSelected(selectedValues)'
+ '_updateSelected(attrForSelected, selectedValues)'
],
/**
@@ -6905,18 +6823,6 @@ 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);
@@ -6926,16 +6832,11 @@ Polymer({
},
_selectMulti: function(values) {
+ this._selection.clear();
if (values) {
- 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);
+ for (var i = 0; i < values.length; i++) {
+ this._selection.setItemSelected(this._valueToItem(values[i]), true);
}
- } else {
- this._selection.clear();
}
},
@@ -6958,12 +6859,6 @@ 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);
}
};
@@ -7197,14 +7092,6 @@ 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
@@ -7232,7 +7119,6 @@ Polymer({
_onUpKey: function(event) {
// up and down arrows moves the focus
this._focusPrevious();
- event.detail.keyboardEvent.preventDefault();
},
/**
@@ -7242,7 +7128,6 @@ Polymer({
*/
_onDownKey: function(event) {
this._focusNext();
- event.detail.keyboardEvent.preventDefault();
},
/**
@@ -7451,14 +7336,14 @@ CSS properties | Action
* the memoized data.
*/
resetFit: function() {
- if (!this._fitInfo || !this._fitInfo.sizedBy.width) {
- this.sizingTarget.style.maxWidth = '';
- }
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 : '';
}
- 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;
}
@@ -7519,30 +7404,18 @@ CSS properties | Action
* `position:fixed`.
*/
center: function() {
- var positionedBy = this._fitInfo.positionedBy;
- if (positionedBy.vertically && positionedBy.horizontally) {
- // Already positioned.
- return;
- }
- // 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';
+ if (!this._fitInfo.positionedBy.vertically || !this._fitInfo.positionedBy.horizontally) {
+ // need position:fixed to center
+ this.style.position = 'fixed';
}
- // 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;
+ if (!this._fitInfo.positionedBy.vertically) {
+ var top = (this._fitHeight - this.offsetHeight) / 2 + this._fitTop;
+ top -= this._fitInfo.margin.top;
this.style.top = top + 'px';
}
- if (!positionedBy.horizontally) {
- var left = this._fitLeft - rect.left + (this._fitWidth - rect.width) / 2;
+ if (!this._fitInfo.positionedBy.horizontally) {
+ var left = (this._fitWidth - this.offsetWidth) / 2 + this._fitLeft;
+ left -= this._fitInfo.margin.left;
this.style.left = left + 'px';
}
}
@@ -7554,9 +7427,6 @@ 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.
@@ -7565,52 +7435,7 @@ 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);
@@ -7630,12 +7455,6 @@ 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) {
@@ -7643,13 +7462,6 @@ 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);
}
};
@@ -7704,6 +7516,15 @@ 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;
};
@@ -7933,23 +7754,6 @@ 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
@@ -7962,6 +7766,13 @@ 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() {
@@ -7969,113 +7780,44 @@ context. You should place this element as a child of `<body>` whenever possible.
}
},
- /**
- * The node being focused.
- * @type {?Node}
- */
+ /** @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 needs tabindex to be set in order to trap the focus.
+ // with-backdrop need 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.opened) {
+ if (this._callOpenedWhenReady) {
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);
@@ -8107,10 +7849,9 @@ context. You should place this element as a child of `<body>` whenever possible.
/**
* Cancels the overlay.
- * @param {?Event} event The original event
*/
- cancel: function(event) {
- var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
+ cancel: function() {
+ var cancelEvent = this.fire('iron-overlay-canceled', undefined, {cancelable: true});
if (cancelEvent.defaultPrevented) {
return;
}
@@ -8133,10 +7874,12 @@ 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;
}
@@ -8211,18 +7954,16 @@ 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();
@@ -8230,12 +7971,6 @@ 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
@@ -8255,24 +7990,23 @@ context. You should place this element as a child of `<body>` whenever possible.
},
_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]
+ // 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.notifyResize();
this.fire('iron-overlay-closed', this.closingReason);
},
@@ -8285,9 +8019,8 @@ 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 layout to avoid application of transform.
- // Set offsetWidth to itself so that compilers won't remove it.
- this.offsetWidth = this.offsetWidth;
+ // force layout to avoid application of transform
+ /** @suppress {suspiciousCode} */ this.offsetWidth;
this.style.transition = this.style.webkitTransition = '';
},
@@ -8298,7 +8031,6 @@ context. You should place this element as a child of `<body>` whenever possible.
}
} else {
this._focusNode.blur();
- this._focusedChild = null;
this._manager.focusOverlay();
}
},
@@ -8309,13 +8041,23 @@ context. You should place this element as a child of `<body>` whenever possible.
if (this.noCancelOnOutsideClick) {
this._applyFocus();
} else {
- this.cancel(event);
+ this.cancel();
}
}
},
+ _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();
@@ -8330,73 +8072,28 @@ context. You should place this element as a child of `<body>` whenever possible.
if (this.opened) {
this.refit();
}
- },
+ }
- /**
- * @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 after the `iron-overlay` opens.
+ * @event iron-overlay-opened
+ */
- __onEsc: function(event) {
- // Not opened or not on top, so return.
- if (this._manager.currentOverlay() !== this) {
- return;
- }
- if (!this.noCancelOnEscKey) {
- this.cancel(event);
- }
- },
+/**
+ * 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
+ */
- __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;
- }
- }
+/**
+ * Fired after the `iron-overlay` closes.
+ * @event iron-overlay-closed
+ * @param {{canceled: (boolean|undefined)}} set to the `closingReason` attribute
+ */
};
/** @polymerBehavior */
- 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).
- */
+ Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
/**
* Use `Polymer.NeonAnimationBehavior` to implement an animation.
* @polymerBehavior
@@ -9947,7 +9644,7 @@ It may be desirable to only allow users to enter certain characters. You can use
`prevent-invalid-input` and `allowed-pattern` attributes together to accomplish this. This feature
is separate from validation, and `allowed-pattern` does not affect how the input is validated.
- <!-- only allow characters that match [0-9] -->
+ \x3c!-- only allow characters that match [0-9] --\x3e
<input is="iron-input" prevent-invalid-input allowed-pattern="[0-9]">
@hero hero.svg
@@ -10477,26 +10174,42 @@ var SearchField = Polymer({
return searchInput ? searchInput.value : '';
},
+ /**
+ * Sets the value of the search field, if it exists.
+ * @param {string} value
+ */
+ setValue: function(value) {
+ var searchInput = this.getSearchInput_();
+ if (searchInput)
+ searchInput.value = value;
+ },
+
/** @param {SearchFieldDelegate} delegate */
setDelegate: function(delegate) {
this.delegate_ = delegate;
},
+ /** @return {Promise<boolean>} */
showAndFocus: function() {
this.showingSearch_ = true;
- this.focus_();
+ return this.focus_();
},
- /** @private */
+ /**
+ * @return {Promise<boolean>}
+ * @private
+ */
focus_: function() {
- this.async(function() {
- if (!this.showingSearch_)
- return;
-
- var searchInput = this.getSearchInput_();
- if (searchInput)
- searchInput.focus();
- });
+ return new Promise(function(resolve) {
+ this.async(function() {
+ if (this.showingSearch_) {
+ var searchInput = this.getSearchInput_();
+ if (searchInput)
+ searchInput.focus();
+ }
+ resolve(this.showingSearch_);
+ });
+ }.bind(this));
},
/**
@@ -10823,6 +10536,13 @@ cr.define('downloads', function() {
return {Manager: Manager};
});
+// 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.
+
+// <include src="../../../../ui/webui/resources/js/i18n_template_no_process.js">
+
+i18nTemplate.process(document, loadTimeData);
// 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.
« no previous file with comments | « no previous file | chrome/browser/resources/md_downloads/vulcanized.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698