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

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

Issue 1846383002: MD Downloads: fix vulcanize issues by excluding higher up in the dependency tree (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@master
Patch Set: just polymer.html Created 4 years, 8 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/vulcanize.py » ('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..efa2abf0e1294cc9664a6e53acb14ad99bb8278f 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,2583 +1531,2592 @@ 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;
+ _onDescendantIronResize: function(event) {
+ if (this._notifyingDescendant) {
+ event.stopPropagation();
+ return;
+ }
- for (var i = 0; sameTerms && i < searchTerms.length; ++i) {
- if (searchTerms[i] != this.searchTerms_[i])
- sameTerms = false;
+ // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
+ // otherwise non-bubbling event "just work." We do it manually here for
+ // the case where Polymer is not using shadow roots for whatever reason:
+ if (!Polymer.Settings.useShadow) {
+ this._fireResize();
}
+ },
- if (sameTerms)
+ _fireResize: function() {
+ this.fire('iron-resize', null, {
+ node: this,
+ bubbles: false
+ });
+ },
+
+ _onIronRequestResizeNotifications: function(event) {
+ var target = event.path ? event.path[0] : event.target;
+
+ if (target === this) {
return;
+ }
- this.searchTerms_ = searchTerms;
- this.loadMore();
+ if (this._interestedResizables.indexOf(target) === -1) {
+ this._interestedResizables.push(target);
+ this.listen(target, 'iron-resize', '_onDescendantIronResize');
+ }
+
+ target.assignParentResizable(this);
+ this._notifyDescendant(target);
+
+ event.stopPropagation();
+ },
+
+ _parentResizableChanged: function(parentResizable) {
+ if (parentResizable) {
+ window.removeEventListener('resize', this._boundNotifyResize);
+ }
},
+ _notifyDescendant: function(descendant) {
+ // NOTE(cdata): In IE10, attached is fired on children first, so it's
+ // important not to notify them if the parent is not attached yet (or
+ // else they will get redundantly notified when the parent attaches).
+ if (!this.isAttached) {
+ return;
+ }
+
+ this._notifyingDescendant = true;
+ descendant.notifyResize();
+ this._notifyingDescendant = false;
+ }
+ };
+(function() {
+ 'use strict';
+
/**
- * Shows the local folder a finished download resides in.
- * @param {string} id ID of the download to show.
+ * Chrome uses an older version of DOM Level 3 Keyboard Events
+ *
+ * Most keys are labeled as text, but some are Unicode codepoints.
+ * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
*/
- show: chromeSendWithId('show'),
+ var KEY_IDENTIFIER = {
+ 'U+0008': 'backspace',
+ 'U+0009': 'tab',
+ 'U+001B': 'esc',
+ 'U+0020': 'space',
+ 'U+007F': 'del'
+ };
- /** Undo download removal. */
- undo: chrome.send.bind(chrome, 'undo'),
- };
+ /**
+ * Special table for KeyboardEvent.keyCode.
+ * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better
+ * than that.
+ *
+ * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
+ */
+ var KEY_CODE = {
+ 8: 'backspace',
+ 9: 'tab',
+ 13: 'enter',
+ 27: 'esc',
+ 33: 'pageup',
+ 34: 'pagedown',
+ 35: 'end',
+ 36: 'home',
+ 32: 'space',
+ 37: 'left',
+ 38: 'up',
+ 39: 'right',
+ 40: 'down',
+ 46: 'del',
+ 106: '*'
+ };
- cr.addSingletonGetter(ActionService);
+ /**
+ * MODIFIER_KEYS maps the short name for modifier keys used in a key
+ * combo string to the property name that references those same keys
+ * in a KeyboardEvent instance.
+ */
+ var MODIFIER_KEYS = {
+ 'shift': 'shiftKey',
+ 'ctrl': 'ctrlKey',
+ 'alt': 'altKey',
+ 'meta': 'metaKey'
+ };
- 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.
+ /**
+ * KeyboardEvent.key is mostly represented by printable character made by
+ * the keyboard, with unprintable keys labeled nicely.
+ *
+ * However, on OS X, Alt+char can make a Unicode character that follows an
+ * Apple-specific mapping. In this case, we fall back to .keyCode.
+ */
+ var KEY_CHAR = /[a-z0-9*]/;
-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',
- };
+ /**
+ * Matches a keyIdentifier string.
+ */
+ var IDENT_CHAR = /U\+/;
- /**
- * 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',
- };
+ /**
+ * Matches arrow keys in Gecko 27.0+
+ */
+ var ARROW_KEY = /^arrow/;
- 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.
+ /**
+ * Matches space keys everywhere (notably including IE10's exceptional name
+ * `spacebar`).
+ */
+ var SPACE_KEY = /^space(bar)?/;
-// 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.
+ /**
+ * Matches ESC key.
+ *
+ * Value from: http://w3c.github.io/uievents-key/#key-Escape
+ */
+ var ESC_KEY = /^escape$/;
-/**
- * @constructor
- * @extends {HTMLAnchorElement}
- */
-var ActionLink = document.registerElement('action-link', {
- prototype: {
- __proto__: HTMLAnchorElement.prototype,
+ /**
+ * Transforms the key.
+ * @param {string} key The KeyBoardEvent.key
+ * @param {Boolean} [noSpecialChars] Limits the transformation to
+ * alpha-numeric characters.
+ */
+ function transformKey(key, noSpecialChars) {
+ var validKey = '';
+ if (key) {
+ var lKey = key.toLowerCase();
+ if (lKey === ' ' || SPACE_KEY.test(lKey)) {
+ validKey = 'space';
+ } else if (ESC_KEY.test(lKey)) {
+ validKey = 'esc';
+ } else if (lKey.length == 1) {
+ if (!noSpecialChars || KEY_CHAR.test(lKey)) {
+ validKey = lKey;
+ }
+ } else if (ARROW_KEY.test(lKey)) {
+ validKey = lKey.replace('arrow', '');
+ } else if (lKey == 'multiply') {
+ // numpad '*' can map to Multiply on IE/Windows
+ validKey = '*';
+ } else {
+ validKey = lKey;
+ }
+ }
+ return validKey;
+ }
- /** @this {ActionLink} */
- createdCallback: function() {
- // Action links can start disabled (e.g. <a is="action-link" disabled>).
- this.tabIndex = this.disabled ? -1 : 0;
+ function transformKeyIdentifier(keyIdent) {
+ var validKey = '';
+ if (keyIdent) {
+ if (keyIdent in KEY_IDENTIFIER) {
+ validKey = KEY_IDENTIFIER[keyIdent];
+ } else if (IDENT_CHAR.test(keyIdent)) {
+ keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
+ validKey = String.fromCharCode(keyIdent).toLowerCase();
+ } else {
+ validKey = keyIdent.toLowerCase();
+ }
+ }
+ return validKey;
+ }
+
+ function transformKeyCode(keyCode) {
+ var validKey = '';
+ if (Number(keyCode)) {
+ if (keyCode >= 65 && keyCode <= 90) {
+ // ascii a-z
+ // lowercase is 32 offset from uppercase
+ validKey = String.fromCharCode(32 + keyCode);
+ } else if (keyCode >= 112 && keyCode <= 123) {
+ // function keys f1-f12
+ validKey = 'f' + (keyCode - 112);
+ } else if (keyCode >= 48 && keyCode <= 57) {
+ // top 0-9 keys
+ validKey = String(keyCode - 48);
+ } else if (keyCode >= 96 && keyCode <= 105) {
+ // num pad 0-9
+ validKey = String(keyCode - 96);
+ } else {
+ validKey = KEY_CODE[keyCode];
+ }
+ }
+ return validKey;
+ }
+
+ /**
+ * Calculates the normalized key for a KeyboardEvent.
+ * @param {KeyboardEvent} keyEvent
+ * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
+ * transformation to alpha-numeric chars. This is useful with key
+ * combinations like shift + 2, which on FF for MacOS produces
+ * keyEvent.key = @
+ * To get 2 returned, set noSpecialChars = true
+ * To get @ returned, set noSpecialChars = false
+ */
+ function normalizedKeyForEvent(keyEvent, noSpecialChars) {
+ // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
+ // .detail.key to support artificial keyboard events.
+ return transformKey(keyEvent.key, noSpecialChars) ||
+ transformKeyIdentifier(keyEvent.keyIdentifier) ||
+ transformKeyCode(keyEvent.keyCode) ||
+ transformKey(keyEvent.detail.key, noSpecialChars) || '';
+ }
- if (!this.hasAttribute('role'))
- this.setAttribute('role', 'link');
+ function keyComboMatchesEvent(keyCombo, event) {
+ // For combos with modifiers we support only alpha-numeric keys
+ var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
+ return keyEvent === keyCombo.key &&
+ (!keyCombo.hasModifiers || (
+ !!event.shiftKey === !!keyCombo.shiftKey &&
+ !!event.ctrlKey === !!keyCombo.ctrlKey &&
+ !!event.altKey === !!keyCombo.altKey &&
+ !!event.metaKey === !!keyCombo.metaKey)
+ );
+ }
- 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 parseKeyComboString(keyComboString) {
+ if (keyComboString.length === 1) {
+ return {
+ combo: keyComboString,
+ key: keyComboString,
+ event: 'keydown'
+ };
+ }
+ return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
+ var eventParts = keyComboPart.split(':');
+ var keyName = eventParts[0];
+ var event = eventParts[1];
+
+ if (keyName in MODIFIER_KEYS) {
+ parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
+ parsedKeyCombo.hasModifiers = true;
+ } else {
+ parsedKeyCombo.key = keyName;
+ parsedKeyCombo.event = event || 'keydown';
}
+
+ return parsedKeyCombo;
+ }, {
+ combo: keyComboString.split(':').shift()
});
+ }
- function preventDefault(e) {
- e.preventDefault();
- }
+ function parseEventString(eventString) {
+ return eventString.trim().split(' ').map(function(keyComboString) {
+ return parseKeyComboString(keyComboString);
+ });
+ }
- function removePreventDefault() {
- document.removeEventListener('selectstart', preventDefault);
- document.removeEventListener('mouseup', removePreventDefault);
- }
+ /**
+ * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
+ * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
+ * The element takes care of browser differences with respect to Keyboard events
+ * and uses an expressive syntax to filter key presses.
+ *
+ * Use the `keyBindings` prototype property to express what combination of keys
+ * will trigger the event to fire.
+ *
+ * Use the `key-event-target` attribute to set up event handlers on a specific
+ * node.
+ * The `keys-pressed` event will fire when one of the key combinations set with the
+ * `keys` property is pressed.
+ *
+ * @demo demo/index.html
+ * @polymerBehavior
+ */
+ Polymer.IronA11yKeysBehavior = {
+ properties: {
+ /**
+ * The HTMLElement that will be firing relevant KeyboardEvents.
+ */
+ keyEventTarget: {
+ type: Object,
+ value: function() {
+ return this;
+ }
+ },
- this.addEventListener('mousedown', function() {
- // This handlers strives to match the behavior of <a href="...">.
+ /**
+ * If true, this property will cause the implementing element to
+ * automatically stop propagation on any handled KeyboardEvents.
+ */
+ stopKeyboardEventPropagation: {
+ type: Boolean,
+ value: false
+ },
- // While the mouse is down, prevent text selection from dragging.
- document.addEventListener('selectstart', preventDefault);
- document.addEventListener('mouseup', removePreventDefault);
+ _boundKeyHandlers: {
+ type: Array,
+ value: function() {
+ return [];
+ }
+ },
- // If focus started via mouse press, don't show an outline.
- if (document.activeElement != this)
- this.classList.add('no-outline');
- });
+ // We use this due to a limitation in IE10 where instances will have
+ // own properties of everything on the "prototype".
+ _imperativeKeyBindings: {
+ type: Object,
+ value: function() {
+ return {};
+ }
+ }
+ },
- this.addEventListener('blur', function() {
- this.classList.remove('no-outline');
- });
- },
+ observers: [
+ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
+ ],
- /** @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');
- },
+ keyBindings: {},
- /** @override */
- setAttribute: function(attr, val) {
- if (attr.toLowerCase() == 'disabled')
- this.disabled = true;
- else
- HTMLAnchorElement.prototype.setAttribute.apply(this, arguments);
- },
+ registered: function() {
+ this._prepKeyBindings();
+ },
- /** @override */
- removeAttribute: function(attr) {
- if (attr.toLowerCase() == 'disabled')
- this.disabled = false;
- else
- HTMLAnchorElement.prototype.removeAttribute.apply(this, arguments);
- },
- },
+ attached: function() {
+ this._listenKeyEventListeners();
+ },
- 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.
+ detached: function() {
+ this._unlistenKeyEventListeners();
+ },
-// <include src="../../../../ui/webui/resources/js/i18n_template_no_process.js">
+ /**
+ * Can be used to imperatively add a key binding to the implementing
+ * element. This is the imperative equivalent of declaring a keybinding
+ * in the `keyBindings` prototype property.
+ */
+ addOwnKeyBinding: function(eventString, handlerName) {
+ this._imperativeKeyBindings[eventString] = handlerName;
+ this._prepKeyBindings();
+ this._resetKeyEventListeners();
+ },
-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`.
+ * When called, will remove all imperatively-added key bindings.
*/
- _parentResizable: {
- type: Object,
- observer: '_parentResizableChanged'
+ removeOwnKeyBindings: function() {
+ this._imperativeKeyBindings = {};
+ this._prepKeyBindings();
+ this._resetKeyEventListeners();
},
/**
- * True if this element is currently notifying its descedant elements of
- * resize.
+ * Returns true if a keyboard event matches `eventString`.
+ *
+ * @param {KeyboardEvent} event
+ * @param {string} eventString
+ * @return {boolean}
*/
- _notifyingDescendant: {
- type: Boolean,
- value: false
- }
- },
+ keyboardEventMatchesKeys: function(event, eventString) {
+ var keyCombos = parseEventString(eventString);
+ for (var i = 0; i < keyCombos.length; ++i) {
+ if (keyComboMatchesEvent(keyCombos[i], event)) {
+ return true;
+ }
+ }
+ return false;
+ },
- listeners: {
- 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
- },
+ _collectKeyBindings: function() {
+ var keyBindings = this.behaviors.map(function(behavior) {
+ return behavior.keyBindings;
+ });
- 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);
- },
+ if (keyBindings.indexOf(this.keyBindings) === -1) {
+ keyBindings.push(this.keyBindings);
+ }
- attached: function() {
- this.fire('iron-request-resize-notifications', null, {
- node: this,
- bubbles: true,
- cancelable: true
- });
+ return keyBindings;
+ },
+
+ _prepKeyBindings: function() {
+ this._keyBindings = {};
+
+ this._collectKeyBindings().forEach(function(keyBindings) {
+ for (var eventString in keyBindings) {
+ this._addKeyBinding(eventString, keyBindings[eventString]);
+ }
+ }, this);
- if (!this._parentResizable) {
- window.addEventListener('resize', this._boundNotifyResize);
- this.notifyResize();
- }
- },
+ for (var eventString in this._imperativeKeyBindings) {
+ this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
+ }
- detached: function() {
- if (this._parentResizable) {
- this._parentResizable.stopResizeNotificationsFor(this);
- } else {
- window.removeEventListener('resize', this._boundNotifyResize);
- }
+ // Give precedence to combos with modifiers to be checked first.
+ for (var eventName in this._keyBindings) {
+ this._keyBindings[eventName].sort(function (kb1, kb2) {
+ var b1 = kb1[0].hasModifiers;
+ var b2 = kb2[0].hasModifiers;
+ return (b1 === b2) ? 0 : b1 ? -1 : 1;
+ })
+ }
+ },
- this._parentResizable = null;
- },
+ _addKeyBinding: function(eventString, handlerName) {
+ parseEventString(eventString).forEach(function(keyCombo) {
+ this._keyBindings[keyCombo.event] =
+ this._keyBindings[keyCombo.event] || [];
- /**
- * Can be called to manually notify a resizable and its descendant
- * resizables of a resize change.
- */
- notifyResize: function() {
- if (!this.isAttached) {
- return;
- }
+ this._keyBindings[keyCombo.event].push([
+ keyCombo,
+ handlerName
+ ]);
+ }, this);
+ },
- this._interestedResizables.forEach(function(resizable) {
- if (this.resizerShouldNotify(resizable)) {
- this._notifyDescendant(resizable);
- }
- }, this);
+ _resetKeyEventListeners: function() {
+ this._unlistenKeyEventListeners();
- this._fireResize();
- },
+ if (this.isAttached) {
+ this._listenKeyEventListeners();
+ }
+ },
- /**
- * Used to assign the closest resizable ancestor to this resizable
- * if the ancestor detects a request for notifications.
- */
- assignParentResizable: function(parentResizable) {
- this._parentResizable = parentResizable;
- },
+ _listenKeyEventListeners: function() {
+ Object.keys(this._keyBindings).forEach(function(eventName) {
+ var keyBindings = this._keyBindings[eventName];
+ var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
- /**
- * 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);
+ this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
- if (index > -1) {
- this._interestedResizables.splice(index, 1);
- this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
- }
- },
+ this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
+ }, this);
+ },
- /**
- * 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; },
+ _unlistenKeyEventListeners: function() {
+ var keyHandlerTuple;
+ var keyEventTarget;
+ var eventName;
+ var boundKeyHandler;
- _onDescendantIronResize: function(event) {
- if (this._notifyingDescendant) {
- event.stopPropagation();
- return;
- }
+ while (this._boundKeyHandlers.length) {
+ // My kingdom for block-scope binding and destructuring assignment..
+ keyHandlerTuple = this._boundKeyHandlers.pop();
+ keyEventTarget = keyHandlerTuple[0];
+ eventName = keyHandlerTuple[1];
+ boundKeyHandler = keyHandlerTuple[2];
- // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
- // otherwise non-bubbling event "just work." We do it manually here for
- // the case where Polymer is not using shadow roots for whatever reason:
- if (!Polymer.Settings.useShadow) {
- this._fireResize();
- }
- },
+ keyEventTarget.removeEventListener(eventName, boundKeyHandler);
+ }
+ },
- _fireResize: function() {
- this.fire('iron-resize', null, {
- node: this,
- bubbles: false
- });
- },
+ _onKeyBindingEvent: function(keyBindings, event) {
+ if (this.stopKeyboardEventPropagation) {
+ event.stopPropagation();
+ }
- _onIronRequestResizeNotifications: function(event) {
- var target = event.path ? event.path[0] : event.target;
+ // if event has been already prevented, don't do anything
+ if (event.defaultPrevented) {
+ return;
+ }
- if (target === this) {
- return;
- }
+ for (var i = 0; i < keyBindings.length; i++) {
+ var keyCombo = keyBindings[i][0];
+ var handlerName = keyBindings[i][1];
+ if (keyComboMatchesEvent(keyCombo, event)) {
+ this._triggerKeyHandler(keyCombo, handlerName, event);
+ // exit the loop if eventDefault was prevented
+ if (event.defaultPrevented) {
+ return;
+ }
+ }
+ }
+ },
- if (this._interestedResizables.indexOf(target) === -1) {
- this._interestedResizables.push(target);
- this.listen(target, 'iron-resize', '_onDescendantIronResize');
+ _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
+ var detail = Object.create(keyCombo);
+ detail.keyboardEvent = keyboardEvent;
+ var event = new CustomEvent(keyCombo.event, {
+ detail: detail,
+ cancelable: true
+ });
+ this[handlerName].call(this, event);
+ if (event.defaultPrevented) {
+ keyboardEvent.preventDefault();
+ }
}
+ };
+ })();
+/**
+ * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll events from a
+ * designated scroll target.
+ *
+ * Elements that consume this behavior can override the `_scrollHandler`
+ * method to add logic on the scroll event.
+ *
+ * @demo demo/scrolling-region.html Scrolling Region
+ * @demo demo/document.html Document Element
+ * @polymerBehavior
+ */
+ Polymer.IronScrollTargetBehavior = {
- target.assignParentResizable(this);
- this._notifyDescendant(target);
-
- event.stopPropagation();
- },
+ properties: {
- _parentResizableChanged: function(parentResizable) {
- if (parentResizable) {
- window.removeEventListener('resize', this._boundNotifyResize);
+ /**
+ * Specifies the element that will handle the scroll event
+ * on the behalf of the current element. This is typically a reference to an `Element`,
+ * but there are a few more posibilities:
+ *
+ * ### Elements id
+ *
+ *```html
+ * <div id="scrollable-element" style="overflow-y: auto;">
+ * <x-element scroll-target="scrollable-element">
+ * Content
+ * </x-element>
+ * </div>
+ *```
+ * In this case, `scrollTarget` will point to the outer div element. Alternatively,
+ * you can set the property programatically:
+ *
+ *```js
+ * appHeader.scrollTarget = document.querySelector('#scrollable-element');
+ *```
+ *
+ * @type {HTMLElement}
+ */
+ scrollTarget: {
+ type: HTMLElement,
+ value: function() {
+ return this._defaultScrollTarget;
+ }
}
},
- _notifyDescendant: function(descendant) {
- // NOTE(cdata): In IE10, attached is fired on children first, so it's
- // important not to notify them if the parent is not attached yet (or
- // else they will get redundantly notified when the parent attaches).
- if (!this.isAttached) {
- return;
+ observers: [
+ '_scrollTargetChanged(scrollTarget, isAttached)'
+ ],
+
+ _scrollTargetChanged: function(scrollTarget, isAttached) {
+ // Remove lister to the current scroll target
+ if (this._oldScrollTarget) {
+ if (this._oldScrollTarget === this._doc) {
+ window.removeEventListener('scroll', this._boundScrollHandler);
+ } else if (this._oldScrollTarget.removeEventListener) {
+ this._oldScrollTarget.removeEventListener('scroll', this._boundScrollHandler);
+ }
+ this._oldScrollTarget = null;
}
+ if (isAttached) {
+ // Support element id references
+ if (typeof scrollTarget === 'string') {
- this._notifyingDescendant = true;
- descendant.notifyResize();
- this._notifyingDescendant = false;
- }
- };
-(function() {
- 'use strict';
+ var host = this.domHost;
+ this.scrollTarget = host && host.$ ? host.$[scrollTarget] :
+ Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
+
+ } else if (this._scrollHandler) {
+
+ this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler.bind(this);
+ // Add a new listener
+ if (scrollTarget === this._doc) {
+ window.addEventListener('scroll', this._boundScrollHandler);
+ if (this._scrollTop !== 0 || this._scrollLeft !== 0) {
+ this._scrollHandler();
+ }
+ } else if (scrollTarget && scrollTarget.addEventListener) {
+ scrollTarget.addEventListener('scroll', this._boundScrollHandler);
+ }
+ this._oldScrollTarget = scrollTarget;
+ }
+ }
+ },
/**
- * Chrome uses an older version of DOM Level 3 Keyboard Events
+ * Runs on every scroll event. Consumer of this behavior may want to override this method.
*
- * Most keys are labeled as text, but some are Unicode codepoints.
- * Values taken from: http://www.w3.org/TR/2007/WD-DOM-Level-3-Events-20071221/keyset.html#KeySet-Set
+ * @protected
*/
- var KEY_IDENTIFIER = {
- 'U+0008': 'backspace',
- 'U+0009': 'tab',
- 'U+001B': 'esc',
- 'U+0020': 'space',
- 'U+007F': 'del'
- };
+ _scrollHandler: function scrollHandler() {},
/**
- * Special table for KeyboardEvent.keyCode.
- * KeyboardEvent.keyIdentifier is better, and KeyBoardEvent.key is even better
- * than that.
+ * The default scroll target. Consumers of this behavior may want to customize
+ * the default scroll target.
*
- * Values from: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent.keyCode#Value_of_keyCode
+ * @type {Element}
*/
- var KEY_CODE = {
- 8: 'backspace',
- 9: 'tab',
- 13: 'enter',
- 27: 'esc',
- 33: 'pageup',
- 34: 'pagedown',
- 35: 'end',
- 36: 'home',
- 32: 'space',
- 37: 'left',
- 38: 'up',
- 39: 'right',
- 40: 'down',
- 46: 'del',
- 106: '*'
- };
+ get _defaultScrollTarget() {
+ return this._doc;
+ },
/**
- * MODIFIER_KEYS maps the short name for modifier keys used in a key
- * combo string to the property name that references those same keys
- * in a KeyboardEvent instance.
+ * Shortcut for the document element
+ *
+ * @type {Element}
*/
- var MODIFIER_KEYS = {
- 'shift': 'shiftKey',
- 'ctrl': 'ctrlKey',
- 'alt': 'altKey',
- 'meta': 'metaKey'
- };
+ get _doc() {
+ return this.ownerDocument.documentElement;
+ },
/**
- * KeyboardEvent.key is mostly represented by printable character made by
- * the keyboard, with unprintable keys labeled nicely.
+ * Gets the number of pixels that the content of an element is scrolled upward.
*
- * However, on OS X, Alt+char can make a Unicode character that follows an
- * Apple-specific mapping. In this case, we fall back to .keyCode.
+ * @type {number}
*/
- var KEY_CHAR = /[a-z0-9*]/;
+ get _scrollTop() {
+ if (this._isValidScrollTarget()) {
+ return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollTarget.scrollTop;
+ }
+ return 0;
+ },
/**
- * Matches a keyIdentifier string.
+ * Gets the number of pixels that the content of an element is scrolled to the left.
+ *
+ * @type {number}
*/
- var IDENT_CHAR = /U\+/;
+ get _scrollLeft() {
+ if (this._isValidScrollTarget()) {
+ return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollTarget.scrollLeft;
+ }
+ return 0;
+ },
/**
- * Matches arrow keys in Gecko 27.0+
+ * Sets the number of pixels that the content of an element is scrolled upward.
+ *
+ * @type {number}
*/
- var ARROW_KEY = /^arrow/;
+ set _scrollTop(top) {
+ if (this.scrollTarget === this._doc) {
+ window.scrollTo(window.pageXOffset, top);
+ } else if (this._isValidScrollTarget()) {
+ this.scrollTarget.scrollTop = top;
+ }
+ },
/**
- * Matches space keys everywhere (notably including IE10's exceptional name
- * `spacebar`).
+ * Sets the number of pixels that the content of an element is scrolled to the left.
+ *
+ * @type {number}
*/
- var SPACE_KEY = /^space(bar)?/;
+ set _scrollLeft(left) {
+ if (this.scrollTarget === this._doc) {
+ window.scrollTo(left, window.pageYOffset);
+ } else if (this._isValidScrollTarget()) {
+ this.scrollTarget.scrollLeft = left;
+ }
+ },
/**
- * Transforms the key.
- * @param {string} key The KeyBoardEvent.key
- * @param {Boolean} [noSpecialChars] Limits the transformation to
- * alpha-numeric characters.
+ * Scrolls the content to a particular place.
+ *
+ * @method scroll
+ * @param {number} left The left position
+ * @param {number} top The top position
*/
- function transformKey(key, noSpecialChars) {
- var validKey = '';
- if (key) {
- var lKey = key.toLowerCase();
- if (lKey === ' ' || SPACE_KEY.test(lKey)) {
- validKey = 'space';
- } else if (lKey.length == 1) {
- if (!noSpecialChars || KEY_CHAR.test(lKey)) {
- validKey = lKey;
- }
- } else if (ARROW_KEY.test(lKey)) {
- validKey = lKey.replace('arrow', '');
- } else if (lKey == 'multiply') {
- // numpad '*' can map to Multiply on IE/Windows
- validKey = '*';
- } else {
- validKey = lKey;
- }
- }
- return validKey;
- }
-
- function transformKeyIdentifier(keyIdent) {
- var validKey = '';
- if (keyIdent) {
- if (keyIdent in KEY_IDENTIFIER) {
- validKey = KEY_IDENTIFIER[keyIdent];
- } else if (IDENT_CHAR.test(keyIdent)) {
- keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
- validKey = String.fromCharCode(keyIdent).toLowerCase();
- } else {
- validKey = keyIdent.toLowerCase();
- }
- }
- return validKey;
- }
-
- function transformKeyCode(keyCode) {
- var validKey = '';
- if (Number(keyCode)) {
- if (keyCode >= 65 && keyCode <= 90) {
- // ascii a-z
- // lowercase is 32 offset from uppercase
- validKey = String.fromCharCode(32 + keyCode);
- } else if (keyCode >= 112 && keyCode <= 123) {
- // function keys f1-f12
- validKey = 'f' + (keyCode - 112);
- } else if (keyCode >= 48 && keyCode <= 57) {
- // top 0-9 keys
- validKey = String(48 - keyCode);
- } else if (keyCode >= 96 && keyCode <= 105) {
- // num pad 0-9
- validKey = String(96 - keyCode);
- } else {
- validKey = KEY_CODE[keyCode];
- }
+ scroll: function(left, top) {
+ if (this.scrollTarget === this._doc) {
+ window.scrollTo(left, top);
+ } else if (this._isValidScrollTarget()) {
+ this.scrollTarget.scrollLeft = left;
+ this.scrollTarget.scrollTop = top;
}
- return validKey;
- }
+ },
/**
- * Calculates the normalized key for a KeyboardEvent.
- * @param {KeyboardEvent} keyEvent
- * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
- * transformation to alpha-numeric chars. This is useful with key
- * combinations like shift + 2, which on FF for MacOS produces
- * keyEvent.key = @
- * To get 2 returned, set noSpecialChars = true
- * To get @ returned, set noSpecialChars = false
+ * Gets the width of the scroll target.
+ *
+ * @type {number}
*/
- function normalizedKeyForEvent(keyEvent, noSpecialChars) {
- // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
- // .detail.key to support artificial keyboard events.
- return transformKey(keyEvent.key, noSpecialChars) ||
- transformKeyIdentifier(keyEvent.keyIdentifier) ||
- transformKeyCode(keyEvent.keyCode) ||
- transformKey(keyEvent.detail.key, noSpecialChars) || '';
- }
-
- function keyComboMatchesEvent(keyCombo, event) {
- // For combos with modifiers we support only alpha-numeric keys
- var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
- return keyEvent === keyCombo.key &&
- (!keyCombo.hasModifiers || (
- !!event.shiftKey === !!keyCombo.shiftKey &&
- !!event.ctrlKey === !!keyCombo.ctrlKey &&
- !!event.altKey === !!keyCombo.altKey &&
- !!event.metaKey === !!keyCombo.metaKey)
- );
- }
-
- function parseKeyComboString(keyComboString) {
- if (keyComboString.length === 1) {
- return {
- combo: keyComboString,
- key: keyComboString,
- event: 'keydown'
- };
+ get _scrollTargetWidth() {
+ if (this._isValidScrollTarget()) {
+ return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTarget.offsetWidth;
}
- return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
- var eventParts = keyComboPart.split(':');
- var keyName = eventParts[0];
- var event = eventParts[1];
-
- if (keyName in MODIFIER_KEYS) {
- parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
- parsedKeyCombo.hasModifiers = true;
- } else {
- parsedKeyCombo.key = keyName;
- parsedKeyCombo.event = event || 'keydown';
- }
-
- return parsedKeyCombo;
- }, {
- combo: keyComboString.split(':').shift()
- });
- }
+ return 0;
+ },
- function parseEventString(eventString) {
- return eventString.trim().split(' ').map(function(keyComboString) {
- return parseKeyComboString(keyComboString);
- });
- }
+ /**
+ * Gets the height of the scroll target.
+ *
+ * @type {number}
+ */
+ get _scrollTargetHeight() {
+ if (this._isValidScrollTarget()) {
+ return this.scrollTarget === this._doc ? window.innerHeight : this.scrollTarget.offsetHeight;
+ }
+ return 0;
+ },
/**
- * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
- * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
- * The element takes care of browser differences with respect to Keyboard events
- * and uses an expressive syntax to filter key presses.
- *
- * Use the `keyBindings` prototype property to express what combination of keys
- * will trigger the event to fire.
- *
- * Use the `key-event-target` attribute to set up event handlers on a specific
- * node.
- * The `keys-pressed` event will fire when one of the key combinations set with the
- * `keys` property is pressed.
+ * Returns true if the scroll target is a valid HTMLElement.
*
- * @demo demo/index.html
- * @polymerBehavior
+ * @return {boolean}
*/
- Polymer.IronA11yKeysBehavior = {
- properties: {
- /**
- * The HTMLElement that will be firing relevant KeyboardEvents.
- */
- keyEventTarget: {
- type: Object,
- value: function() {
- return this;
- }
- },
-
- /**
- * If true, this property will cause the implementing element to
- * automatically stop propagation on any handled KeyboardEvents.
- */
- stopKeyboardEventPropagation: {
- type: Boolean,
- value: false
- },
+ _isValidScrollTarget: function() {
+ return this.scrollTarget instanceof HTMLElement;
+ }
+ };
+(function() {
- _boundKeyHandlers: {
- type: Array,
- value: function() {
- return [];
- }
- },
+ var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
+ var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
+ var DEFAULT_PHYSICAL_COUNT = 3;
+ var MAX_PHYSICAL_COUNT = 500;
+ var HIDDEN_Y = '-10000px';
- // We use this due to a limitation in IE10 where instances will have
- // own properties of everything on the "prototype".
- _imperativeKeyBindings: {
- type: Object,
- value: function() {
- return {};
- }
- }
- },
+ Polymer({
- observers: [
- '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
- ],
+ is: 'iron-list',
- keyBindings: {},
+ properties: {
- registered: function() {
- this._prepKeyBindings();
+ /**
+ * An array containing items determining how many instances of the template
+ * to stamp and that that each template instance should bind to.
+ */
+ items: {
+ type: Array
},
- attached: function() {
- this._listenKeyEventListeners();
+ /**
+ * The name of the variable to add to the binding scope for the array
+ * element associated with a given template instance.
+ */
+ as: {
+ type: String,
+ value: 'item'
},
- detached: function() {
- this._unlistenKeyEventListeners();
+ /**
+ * The name of the variable to add to the binding scope with the index
+ * for the row.
+ */
+ indexAs: {
+ type: String,
+ value: 'index'
},
/**
- * Can be used to imperatively add a key binding to the implementing
- * element. This is the imperative equivalent of declaring a keybinding
- * in the `keyBindings` prototype property.
+ * The name of the variable to add to the binding scope to indicate
+ * if the row is selected.
*/
- addOwnKeyBinding: function(eventString, handlerName) {
- this._imperativeKeyBindings[eventString] = handlerName;
- this._prepKeyBindings();
- this._resetKeyEventListeners();
+ selectedAs: {
+ type: String,
+ value: 'selected'
},
/**
- * When called, will remove all imperatively-added key bindings.
+ * When true, tapping a row will select the item, placing its data model
+ * in the set of selected items retrievable via the selection property.
+ *
+ * Note that tapping focusable elements within the list item will not
+ * result in selection, since they are presumed to have their * own action.
*/
- removeOwnKeyBindings: function() {
- this._imperativeKeyBindings = {};
- this._prepKeyBindings();
- this._resetKeyEventListeners();
+ selectionEnabled: {
+ type: Boolean,
+ value: false
},
- keyboardEventMatchesKeys: function(event, eventString) {
- var keyCombos = parseEventString(eventString);
- for (var i = 0; i < keyCombos.length; ++i) {
- if (keyComboMatchesEvent(keyCombos[i], event)) {
- return true;
- }
- }
- return false;
+ /**
+ * When `multiSelection` is false, this is the currently selected item, or `null`
+ * if no item is selected.
+ */
+ selectedItem: {
+ type: Object,
+ notify: true
},
- _collectKeyBindings: function() {
- var keyBindings = this.behaviors.map(function(behavior) {
- return behavior.keyBindings;
- });
-
- if (keyBindings.indexOf(this.keyBindings) === -1) {
- keyBindings.push(this.keyBindings);
- }
-
- return keyBindings;
+ /**
+ * When `multiSelection` is true, this is an array that contains the selected items.
+ */
+ selectedItems: {
+ type: Object,
+ notify: true
},
- _prepKeyBindings: function() {
- this._keyBindings = {};
-
- this._collectKeyBindings().forEach(function(keyBindings) {
- for (var eventString in keyBindings) {
- this._addKeyBinding(eventString, keyBindings[eventString]);
- }
- }, this);
-
- for (var eventString in this._imperativeKeyBindings) {
- this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
- }
+ /**
+ * When `true`, multiple items may be selected at once (in this case,
+ * `selected` is an array of currently selected items). When `false`,
+ * only one item may be selected at a time.
+ */
+ multiSelection: {
+ type: Boolean,
+ value: false
+ }
+ },
- // Give precedence to combos with modifiers to be checked first.
- for (var eventName in this._keyBindings) {
- this._keyBindings[eventName].sort(function (kb1, kb2) {
- var b1 = kb1[0].hasModifiers;
- var b2 = kb2[0].hasModifiers;
- return (b1 === b2) ? 0 : b1 ? -1 : 1;
- })
- }
- },
+ observers: [
+ '_itemsChanged(items.*)',
+ '_selectionEnabledChanged(selectionEnabled)',
+ '_multiSelectionChanged(multiSelection)',
+ '_setOverflow(scrollTarget)'
+ ],
- _addKeyBinding: function(eventString, handlerName) {
- parseEventString(eventString).forEach(function(keyCombo) {
- this._keyBindings[keyCombo.event] =
- this._keyBindings[keyCombo.event] || [];
+ behaviors: [
+ Polymer.Templatizer,
+ Polymer.IronResizableBehavior,
+ Polymer.IronA11yKeysBehavior,
+ Polymer.IronScrollTargetBehavior
+ ],
- this._keyBindings[keyCombo.event].push([
- keyCombo,
- handlerName
- ]);
- }, this);
- },
+ listeners: {
+ 'iron-resize': '_resizeHandler'
+ },
- _resetKeyEventListeners: function() {
- this._unlistenKeyEventListeners();
+ keyBindings: {
+ 'up': '_didMoveUp',
+ 'down': '_didMoveDown',
+ 'enter': '_didEnter'
+ },
- if (this.isAttached) {
- this._listenKeyEventListeners();
- }
- },
+ /**
+ * The ratio of hidden tiles that should remain in the scroll direction.
+ * Recommended value ~0.5, so it will distribute tiles evely in both directions.
+ */
+ _ratio: 0.5,
- _listenKeyEventListeners: function() {
- Object.keys(this._keyBindings).forEach(function(eventName) {
- var keyBindings = this._keyBindings[eventName];
- var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
+ /**
+ * The padding-top value for the list.
+ */
+ _scrollerPaddingTop: 0,
- this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
+ /**
+ * This value is the same as `scrollTop`.
+ */
+ _scrollPosition: 0,
- this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
- }, this);
- },
+ /**
+ * The sum of the heights of all the tiles in the DOM.
+ */
+ _physicalSize: 0,
- _unlistenKeyEventListeners: function() {
- var keyHandlerTuple;
- var keyEventTarget;
- var eventName;
- var boundKeyHandler;
+ /**
+ * The average `F` of the tiles observed till now.
+ */
+ _physicalAverage: 0,
- while (this._boundKeyHandlers.length) {
- // My kingdom for block-scope binding and destructuring assignment..
- keyHandlerTuple = this._boundKeyHandlers.pop();
- keyEventTarget = keyHandlerTuple[0];
- eventName = keyHandlerTuple[1];
- boundKeyHandler = keyHandlerTuple[2];
+ /**
+ * The number of tiles which `offsetHeight` > 0 observed until now.
+ */
+ _physicalAverageCount: 0,
- keyEventTarget.removeEventListener(eventName, boundKeyHandler);
- }
- },
+ /**
+ * The Y position of the item rendered in the `_physicalStart`
+ * tile relative to the scrolling list.
+ */
+ _physicalTop: 0,
- _onKeyBindingEvent: function(keyBindings, event) {
- if (this.stopKeyboardEventPropagation) {
- event.stopPropagation();
- }
+ /**
+ * The number of items in the list.
+ */
+ _virtualCount: 0,
- // if event has been already prevented, don't do anything
- if (event.defaultPrevented) {
- return;
- }
+ /**
+ * A map between an item key and its physical item index
+ */
+ _physicalIndexForKey: null,
- for (var i = 0; i < keyBindings.length; i++) {
- var keyCombo = keyBindings[i][0];
- var handlerName = keyBindings[i][1];
- if (keyComboMatchesEvent(keyCombo, event)) {
- this._triggerKeyHandler(keyCombo, handlerName, event);
- // exit the loop if eventDefault was prevented
- if (event.defaultPrevented) {
- return;
- }
- }
- }
- },
+ /**
+ * The estimated scroll height based on `_physicalAverage`
+ */
+ _estScrollHeight: 0,
- _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
- var detail = Object.create(keyCombo);
- detail.keyboardEvent = keyboardEvent;
- var event = new CustomEvent(keyCombo.event, {
- detail: detail,
- cancelable: true
- });
- this[handlerName].call(this, event);
- if (event.defaultPrevented) {
- keyboardEvent.preventDefault();
- }
- }
- };
- })();
-/**
- * `Polymer.IronScrollTargetBehavior` allows an element to respond to scroll events from a
- * designated scroll target.
- *
- * Elements that consume this behavior can override the `_scrollHandler`
- * method to add logic on the scroll event.
- *
- * @demo demo/scrolling-region.html Scrolling Region
- * @demo demo/document.html Document Element
- * @polymerBehavior
- */
- Polymer.IronScrollTargetBehavior = {
+ /**
+ * The scroll height of the dom node
+ */
+ _scrollHeight: 0,
- properties: {
+ /**
+ * The height of the list. This is referred as the viewport in the context of list.
+ */
+ _viewportSize: 0,
- /**
- * Specifies the element that will handle the scroll event
- * on the behalf of the current element. This is typically a reference to an `Element`,
- * but there are a few more posibilities:
- *
- * ### Elements id
- *
- *```html
- * <div id="scrollable-element" style="overflow-y: auto;">
- * <x-element scroll-target="scrollable-element">
- * Content
- * </x-element>
- * </div>
- *```
- * In this case, `scrollTarget` will point to the outer div element. Alternatively,
- * you can set the property programatically:
- *
- *```js
- * appHeader.scrollTarget = document.querySelector('#scrollable-element');
- *```
- *
- * @type {HTMLElement}
- */
- scrollTarget: {
- type: HTMLElement,
- value: function() {
- return this._defaultScrollTarget;
- }
- }
- },
+ /**
+ * An array of DOM nodes that are currently in the tree
+ * @type {?Array<!TemplatizerNode>}
+ */
+ _physicalItems: null,
- observers: [
- '_scrollTargetChanged(scrollTarget, isAttached)'
- ],
+ /**
+ * An array of heights for each item in `_physicalItems`
+ * @type {?Array<number>}
+ */
+ _physicalSizes: null,
- _scrollTargetChanged: function(scrollTarget, isAttached) {
- // Remove lister to the current scroll target
- if (this._oldScrollTarget) {
- if (this._oldScrollTarget === this._doc) {
- window.removeEventListener('scroll', this._boundScrollHandler);
- } else if (this._oldScrollTarget.removeEventListener) {
- this._oldScrollTarget.removeEventListener('scroll', this._boundScrollHandler);
- }
- this._oldScrollTarget = null;
- }
- if (isAttached) {
- // Support element id references
- if (typeof scrollTarget === 'string') {
+ /**
+ * A cached value for the first visible index.
+ * See `firstVisibleIndex`
+ * @type {?number}
+ */
+ _firstVisibleIndexVal: null,
- var host = this.domHost;
- this.scrollTarget = host && host.$ ? host.$[scrollTarget] :
- Polymer.dom(this.ownerDocument).querySelector('#' + scrollTarget);
+ /**
+ * A cached value for the last visible index.
+ * See `lastVisibleIndex`
+ * @type {?number}
+ */
+ _lastVisibleIndexVal: null,
- } else if (this._scrollHandler) {
+ /**
+ * A Polymer collection for the items.
+ * @type {?Polymer.Collection}
+ */
+ _collection: null,
- this._boundScrollHandler = this._boundScrollHandler || this._scrollHandler.bind(this);
- // Add a new listener
- if (scrollTarget === this._doc) {
- window.addEventListener('scroll', this._boundScrollHandler);
- if (this._scrollTop !== 0 || this._scrollLeft !== 0) {
- this._scrollHandler();
- }
- } else if (scrollTarget && scrollTarget.addEventListener) {
- scrollTarget.addEventListener('scroll', this._boundScrollHandler);
- }
- this._oldScrollTarget = scrollTarget;
- }
- }
- },
+ /**
+ * True if the current item list was rendered for the first time
+ * after attached.
+ */
+ _itemsRendered: false,
/**
- * Runs on every scroll event. Consumer of this behavior may want to override this method.
- *
- * @protected
+ * The page that is currently rendered.
*/
- _scrollHandler: function scrollHandler() {},
+ _lastPage: null,
/**
- * The default scroll target. Consumers of this behavior may want to customize
- * the default scroll target.
- *
- * @type {Element}
+ * The max number of pages to render. One page is equivalent to the height of the list.
*/
- get _defaultScrollTarget() {
- return this._doc;
- },
+ _maxPages: 3,
/**
- * Shortcut for the document element
- *
- * @type {Element}
+ * The currently focused physical item.
*/
- get _doc() {
- return this.ownerDocument.documentElement;
- },
+ _focusedItem: null,
/**
- * Gets the number of pixels that the content of an element is scrolled upward.
- *
- * @type {number}
+ * The index of the `_focusedItem`.
*/
- get _scrollTop() {
- if (this._isValidScrollTarget()) {
- return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollTarget.scrollTop;
- }
- return 0;
- },
+ _focusedIndex: -1,
/**
- * Gets the number of pixels that the content of an element is scrolled to the left.
- *
- * @type {number}
+ * The the item that is focused if it is moved offscreen.
+ * @private {?TemplatizerNode}
*/
- get _scrollLeft() {
- if (this._isValidScrollTarget()) {
- return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollTarget.scrollLeft;
- }
- return 0;
- },
+ _offscreenFocusedItem: null,
/**
- * Sets the number of pixels that the content of an element is scrolled upward.
- *
- * @type {number}
+ * The item that backfills the `_offscreenFocusedItem` in the physical items
+ * list when that item is moved offscreen.
*/
- set _scrollTop(top) {
- if (this.scrollTarget === this._doc) {
- window.scrollTo(window.pageXOffset, top);
- } else if (this._isValidScrollTarget()) {
- this.scrollTarget.scrollTop = top;
- }
- },
+ _focusBackfillItem: null,
/**
- * Sets the number of pixels that the content of an element is scrolled to the left.
- *
- * @type {number}
+ * The bottom of the physical content.
*/
- set _scrollLeft(left) {
- if (this.scrollTarget === this._doc) {
- window.scrollTo(left, window.pageYOffset);
- } else if (this._isValidScrollTarget()) {
- this.scrollTarget.scrollLeft = left;
- }
+ get _physicalBottom() {
+ return this._physicalTop + this._physicalSize;
+ },
+
+ /**
+ * The bottom of the scroll.
+ */
+ get _scrollBottom() {
+ return this._scrollPosition + this._viewportSize;
},
/**
- * Scrolls the content to a particular place.
- *
- * @method scroll
- * @param {number} left The left position
- * @param {number} top The top position
+ * The n-th item rendered in the last physical item.
*/
- scroll: function(left, top) {
- if (this.scrollTarget === this._doc) {
- window.scrollTo(left, top);
- } else if (this._isValidScrollTarget()) {
- this.scrollTarget.scrollLeft = left;
- this.scrollTarget.scrollTop = top;
- }
+ get _virtualEnd() {
+ return this._virtualStart + this._physicalCount - 1;
},
/**
- * Gets the width of the scroll target.
- *
- * @type {number}
+ * The height of the physical content that isn't on the screen.
*/
- get _scrollTargetWidth() {
- if (this._isValidScrollTarget()) {
- return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTarget.offsetWidth;
- }
- return 0;
+ get _hiddenContentSize() {
+ return this._physicalSize - this._viewportSize;
},
/**
- * Gets the height of the scroll target.
- *
- * @type {number}
+ * The maximum scroll top value.
*/
- get _scrollTargetHeight() {
- if (this._isValidScrollTarget()) {
- return this.scrollTarget === this._doc ? window.innerHeight : this.scrollTarget.offsetHeight;
- }
- return 0;
+ get _maxScrollTop() {
+ return this._estScrollHeight - this._viewportSize + this._scrollerPaddingTop;
},
/**
- * Returns true if the scroll target is a valid HTMLElement.
- *
- * @return {boolean}
+ * The lowest n-th value for an item such that it can be rendered in `_physicalStart`.
*/
- _isValidScrollTarget: function() {
- return this.scrollTarget instanceof HTMLElement;
- }
- };
-(function() {
-
- var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
- var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
- var DEFAULT_PHYSICAL_COUNT = 3;
- var MAX_PHYSICAL_COUNT = 500;
- var HIDDEN_Y = '-10000px';
-
- Polymer({
-
- is: 'iron-list',
-
- properties: {
-
- /**
- * An array containing items determining how many instances of the template
- * to stamp and that that each template instance should bind to.
- */
- items: {
- type: Array
- },
-
- /**
- * The name of the variable to add to the binding scope for the array
- * element associated with a given template instance.
- */
- as: {
- type: String,
- value: 'item'
- },
+ _minVirtualStart: 0,
- /**
- * The name of the variable to add to the binding scope with the index
- * for the row.
- */
- indexAs: {
- type: String,
- value: 'index'
- },
+ /**
+ * The largest n-th value for an item such that it can be rendered in `_physicalStart`.
+ */
+ get _maxVirtualStart() {
+ return Math.max(0, this._virtualCount - this._physicalCount);
+ },
- /**
- * The name of the variable to add to the binding scope to indicate
- * if the row is selected.
- */
- selectedAs: {
- type: String,
- value: 'selected'
- },
+ /**
+ * The n-th item rendered in the `_physicalStart` tile.
+ */
+ _virtualStartVal: 0,
- /**
- * When true, tapping a row will select the item, placing its data model
- * in the set of selected items retrievable via the selection property.
- *
- * Note that tapping focusable elements within the list item will not
- * result in selection, since they are presumed to have their * own action.
- */
- selectionEnabled: {
- type: Boolean,
- value: false
- },
+ set _virtualStart(val) {
+ this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
+ },
- /**
- * When `multiSelection` is false, this is the currently selected item, or `null`
- * if no item is selected.
- */
- selectedItem: {
- type: Object,
- notify: true
- },
+ get _virtualStart() {
+ return this._virtualStartVal || 0;
+ },
- /**
- * When `multiSelection` is true, this is an array that contains the selected items.
- */
- selectedItems: {
- type: Object,
- notify: true
- },
+ /**
+ * The k-th tile that is at the top of the scrolling list.
+ */
+ _physicalStartVal: 0,
- /**
- * When `true`, multiple items may be selected at once (in this case,
- * `selected` is an array of currently selected items). When `false`,
- * only one item may be selected at a time.
- */
- multiSelection: {
- type: Boolean,
- value: false
+ 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;
},
- observers: [
- '_itemsChanged(items.*)',
- '_selectionEnabledChanged(selectionEnabled)',
- '_multiSelectionChanged(multiSelection)',
- '_setOverflow(scrollTarget)'
- ],
+ get _physicalStart() {
+ return this._physicalStartVal || 0;
+ },
- behaviors: [
- Polymer.Templatizer,
- Polymer.IronResizableBehavior,
- Polymer.IronA11yKeysBehavior,
- Polymer.IronScrollTargetBehavior
- ],
+ /**
+ * The number of tiles in the DOM.
+ */
+ _physicalCountVal: 0,
- listeners: {
- 'iron-resize': '_resizeHandler'
+ set _physicalCount(val) {
+ this._physicalCountVal = val;
+ this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
},
- keyBindings: {
- 'up': '_didMoveUp',
- 'down': '_didMoveDown',
- 'enter': '_didEnter'
+ get _physicalCount() {
+ return this._physicalCountVal;
},
/**
- * The ratio of hidden tiles that should remain in the scroll direction.
- * Recommended value ~0.5, so it will distribute tiles evely in both directions.
+ * The k-th tile that is at the bottom of the scrolling list.
*/
- _ratio: 0.5,
+ _physicalEnd: 0,
/**
- * The padding-top value for the list.
+ * An optimal physical size such that we will have enough physical items
+ * to fill up the viewport and recycle when the user scrolls.
+ *
+ * This default value assumes that we will at least have the equivalent
+ * to a viewport of physical items above and below the user's viewport.
*/
- _scrollerPaddingTop: 0,
+ get _optPhysicalSize() {
+ return this._viewportSize * this._maxPages;
+ },
- /**
- * This value is the same as `scrollTop`.
- */
- _scrollPosition: 0,
+ /**
+ * True if the current list is visible.
+ */
+ get _isVisible() {
+ return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.scrollTarget.offsetHeight);
+ },
/**
- * The sum of the heights of all the tiles in the DOM.
+ * Gets the index of the first visible item in the viewport.
+ *
+ * @type {number}
*/
- _physicalSize: 0,
+ get firstVisibleIndex() {
+ if (this._firstVisibleIndexVal === null) {
+ var physicalOffset = this._physicalTop + this._scrollerPaddingTop;
+
+ this._firstVisibleIndexVal = this._iterateItems(
+ function(pidx, vidx) {
+ physicalOffset += this._physicalSizes[pidx];
+ if (physicalOffset > this._scrollPosition) {
+ return vidx;
+ }
+ }) || 0;
+ }
+ return this._firstVisibleIndexVal;
+ },
/**
- * The average `F` of the tiles observed till now.
+ * Gets the index of the last visible item in the viewport.
+ *
+ * @type {number}
*/
- _physicalAverage: 0,
+ get lastVisibleIndex() {
+ if (this._lastVisibleIndexVal === null) {
+ var physicalOffset = this._physicalTop;
+
+ this._iterateItems(function(pidx, vidx) {
+ physicalOffset += this._physicalSizes[pidx];
+
+ if (physicalOffset <= this._scrollBottom) {
+ this._lastVisibleIndexVal = vidx;
+ }
+ });
+ }
+ return this._lastVisibleIndexVal;
+ },
+
+ get _defaultScrollTarget() {
+ return this;
+ },
+
+ ready: function() {
+ this.addEventListener('focus', this._didFocus.bind(this), true);
+ },
+
+ attached: function() {
+ this.updateViewportBoundaries();
+ this._render();
+ },
- /**
- * The number of tiles which `offsetHeight` > 0 observed until now.
- */
- _physicalAverageCount: 0,
+ detached: function() {
+ this._itemsRendered = false;
+ },
/**
- * The Y position of the item rendered in the `_physicalStart`
- * tile relative to the scrolling list.
+ * Set the overflow property if this element has its own scrolling region
*/
- _physicalTop: 0,
+ _setOverflow: function(scrollTarget) {
+ this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
+ this.style.overflow = scrollTarget === this ? 'auto' : '';
+ },
/**
- * The number of items in the list.
+ * Invoke this method if you dynamically update the viewport's
+ * size or CSS padding.
+ *
+ * @method updateViewportBoundaries
*/
- _virtualCount: 0,
+ updateViewportBoundaries: function() {
+ this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
+ parseInt(window.getComputedStyle(this)['padding-top'], 10);
- /**
- * A map between an item key and its physical item index
- */
- _physicalIndexForKey: null,
+ this._viewportSize = this._scrollTargetHeight;
+ },
/**
- * The estimated scroll height based on `_physicalAverage`
+ * Update the models, the position of the
+ * items in the viewport and recycle tiles as needed.
*/
- _estScrollHeight: 0,
+ _scrollHandler: function() {
+ // clamp the `scrollTop` value
+ 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 recycledTiles = 0;
+ var hiddenContentSize = this._hiddenContentSize;
+ var currentRatio = ratio;
+ var movingUp = [];
- /**
- * The scroll height of the dom node
- */
- _scrollHeight: 0,
+ // track the last `scrollTop`
+ this._scrollPosition = scrollTop;
- /**
- * The height of the list. This is referred as the viewport in the context of list.
- */
- _viewportSize: 0,
+ // clear cached visible indexes
+ this._firstVisibleIndexVal = null;
+ this._lastVisibleIndexVal = null;
- /**
- * An array of DOM nodes that are currently in the tree
- * @type {?Array<!TemplatizerNode>}
- */
- _physicalItems: null,
+ scrollBottom = this._scrollBottom;
+ physicalBottom = this._physicalBottom;
- /**
- * An array of heights for each item in `_physicalItems`
- * @type {?Array<number>}
- */
- _physicalSizes: null,
+ // random access
+ if (Math.abs(delta) > this._physicalSize) {
+ this._physicalTop += delta;
+ recycledTiles = Math.round(delta / this._physicalAverage);
+ }
+ // scroll up
+ else if (delta < 0) {
+ var topSpace = scrollTop - this._physicalTop;
+ var virtualStart = this._virtualStart;
- /**
- * A cached value for the first visible index.
- * See `firstVisibleIndex`
- * @type {?number}
- */
- _firstVisibleIndexVal: null,
+ recycledTileSet = [];
- /**
- * A cached value for the last visible index.
- * See `lastVisibleIndex`
- * @type {?number}
- */
- _lastVisibleIndexVal: null,
+ kth = this._physicalEnd;
+ currentRatio = topSpace / hiddenContentSize;
- /**
- * A Polymer collection for the items.
- * @type {?Polymer.Collection}
- */
- _collection: null,
+ // move tiles from bottom to top
+ while (
+ // approximate `currentRatio` to `ratio`
+ currentRatio < ratio &&
+ // recycle less physical items than the total
+ recycledTiles < this._physicalCount &&
+ // ensure that these recycled tiles are needed
+ virtualStart - recycledTiles > 0 &&
+ // ensure that the tile is not visible
+ physicalBottom - this._physicalSizes[kth] > scrollBottom
+ ) {
- /**
- * True if the current item list was rendered for the first time
- * after attached.
- */
- _itemsRendered: false,
+ tileHeight = this._physicalSizes[kth];
+ currentRatio += tileHeight / hiddenContentSize;
+ physicalBottom -= tileHeight;
+ recycledTileSet.push(kth);
+ recycledTiles++;
+ kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
+ }
- /**
- * The page that is currently rendered.
- */
- _lastPage: null,
+ movingUp = recycledTileSet;
+ recycledTiles = -recycledTiles;
+ }
+ // scroll down
+ else if (delta > 0) {
+ var bottomSpace = physicalBottom - scrollBottom;
+ var virtualEnd = this._virtualEnd;
+ var lastVirtualItemIndex = this._virtualCount-1;
- /**
- * The max number of pages to render. One page is equivalent to the height of the list.
- */
- _maxPages: 3,
+ recycledTileSet = [];
- /**
- * The currently focused physical item.
- */
- _focusedItem: null,
+ kth = this._physicalStart;
+ currentRatio = bottomSpace / hiddenContentSize;
- /**
- * The index of the `_focusedItem`.
- */
- _focusedIndex: -1,
+ // move tiles from top to bottom
+ while (
+ // approximate `currentRatio` to `ratio`
+ currentRatio < ratio &&
+ // recycle less physical items than the total
+ recycledTiles < this._physicalCount &&
+ // ensure that these recycled tiles are needed
+ virtualEnd + recycledTiles < lastVirtualItemIndex &&
+ // ensure that the tile is not visible
+ this._physicalTop + this._physicalSizes[kth] < scrollTop
+ ) {
- /**
- * The the item that is focused if it is moved offscreen.
- * @private {?TemplatizerNode}
- */
- _offscreenFocusedItem: null,
+ tileHeight = this._physicalSizes[kth];
+ currentRatio += tileHeight / hiddenContentSize;
- /**
- * The item that backfills the `_offscreenFocusedItem` in the physical items
- * list when that item is moved offscreen.
- */
- _focusBackfillItem: null,
+ this._physicalTop += tileHeight;
+ recycledTileSet.push(kth);
+ recycledTiles++;
+ kth = (kth + 1) % this._physicalCount;
+ }
+ }
- /**
- * The bottom of the physical content.
- */
- get _physicalBottom() {
- return this._physicalTop + this._physicalSize;
+ if (recycledTiles === 0) {
+ // If the list ever reach this case, the physical average is not significant enough
+ // to create all the items needed to cover the entire viewport.
+ // e.g. A few items have a height that differs from the average by serveral order of magnitude.
+ if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
+ this.async(this._increasePool.bind(this, 1));
+ }
+ } else {
+ this._virtualStart = this._virtualStart + recycledTiles;
+ this._physicalStart = this._physicalStart + recycledTiles;
+ this._update(recycledTileSet, movingUp);
+ }
},
/**
- * The bottom of the scroll.
+ * Update the list of items, starting from the `_virtualStart` item.
+ * @param {!Array<number>=} itemSet
+ * @param {!Array<number>=} movingUp
*/
- get _scrollBottom() {
- return this._scrollPosition + this._viewportSize;
+ _update: function(itemSet, movingUp) {
+ // manage focus
+ this._manageFocus();
+ // update models
+ this._assignModels(itemSet);
+ // measure heights
+ this._updateMetrics(itemSet);
+ // adjust offset after measuring
+ if (movingUp) {
+ while (movingUp.length) {
+ this._physicalTop -= this._physicalSizes[movingUp.pop()];
+ }
+ }
+ // update the position of the items
+ this._positionItems();
+ // set the scroller size
+ this._updateScrollerSize();
+ // increase the pool of physical items
+ this._increasePoolIfNeeded();
},
/**
- * The n-th item rendered in the last physical item.
+ * Creates a pool of DOM elements and attaches them to the local dom.
*/
- get _virtualEnd() {
- return this._virtualStart + this._physicalCount - 1;
- },
+ _createPool: function(size) {
+ var physicalItems = new Array(size);
- /**
- * The height of the physical content that isn't on the screen.
- */
- get _hiddenContentSize() {
- return this._physicalSize - this._viewportSize;
+ this._ensureTemplatized();
+
+ for (var i = 0; i < size; i++) {
+ var inst = this.stamp(null);
+ // First element child is item; Safari doesn't support children[0]
+ // on a doc fragment
+ physicalItems[i] = inst.root.querySelector('*');
+ Polymer.dom(this).appendChild(inst.root);
+ }
+ return physicalItems;
},
/**
- * The maximum scroll top value.
+ * Increases the pool of physical items only if needed.
+ * This function will allocate additional physical items
+ * if the physical size is shorter than `_optPhysicalSize`
*/
- get _maxScrollTop() {
- return this._estScrollHeight - this._viewportSize + this._scrollerPaddingTop;
+ _increasePoolIfNeeded: function() {
+ if (this._viewportSize === 0 || this._physicalSize >= this._optPhysicalSize) {
+ return false;
+ }
+ // 0 <= `currentPage` <= `_maxPages`
+ var currentPage = Math.floor(this._physicalSize / this._viewportSize);
+ if (currentPage === 0) {
+ // fill the first page
+ this._debounceTemplate(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5)));
+ } else if (this._lastPage !== currentPage) {
+ // paint the page and defer the next increase
+ // wait 16ms which is rough enough to get paint cycle.
+ Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increasePool.bind(this, 1), 16));
+ } else {
+ // fill the rest of the pages
+ this._debounceTemplate(this._increasePool.bind(this, 1));
+ }
+ this._lastPage = currentPage;
+ return true;
},
/**
- * The lowest n-th value for an item such that it can be rendered in `_physicalStart`.
+ * Increases the pool size.
*/
- _minVirtualStart: 0,
+ _increasePool: function(missingItems) {
+ var nextPhysicalCount = Math.min(
+ this._physicalCount + missingItems,
+ this._virtualCount - this._virtualStart,
+ MAX_PHYSICAL_COUNT
+ );
+ var prevPhysicalCount = this._physicalCount;
+ var delta = nextPhysicalCount - prevPhysicalCount;
- /**
- * The largest n-th value for an item such that it can be rendered in `_physicalStart`.
- */
- get _maxVirtualStart() {
- return Math.max(0, this._virtualCount - this._physicalCount);
- },
+ if (delta <= 0) {
+ return;
+ }
- /**
- * The n-th item rendered in the `_physicalStart` tile.
- */
- _virtualStartVal: 0,
+ [].push.apply(this._physicalItems, this._createPool(delta));
+ [].push.apply(this._physicalSizes, new Array(delta));
- set _virtualStart(val) {
- this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
- },
+ this._physicalCount = prevPhysicalCount + delta;
- get _virtualStart() {
- return this._virtualStartVal || 0;
+ // 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();
},
/**
- * The k-th tile that is at the top of the scrolling list.
+ * Render a new list of items. This method does exactly the same as `update`,
+ * but it also ensures that only one `update` cycle is created.
*/
- _physicalStartVal: 0,
+ _render: function() {
+ var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
- set _physicalStart(val) {
- this._physicalStartVal = val % this._physicalCount;
- if (this._physicalStartVal < 0) {
- this._physicalStartVal = this._physicalCount + this._physicalStartVal;
+ if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) {
+ this._lastPage = 0;
+ this._update();
+ this._scrollHandler();
+ this._itemsRendered = true;
}
- this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
- },
-
- get _physicalStart() {
- return this._physicalStartVal || 0;
},
/**
- * The number of tiles in the DOM.
+ * Templetizes the user template.
*/
- _physicalCountVal: 0,
+ _ensureTemplatized: function() {
+ if (!this.ctor) {
+ // Template instance props that should be excluded from forwarding
+ var props = {};
+ props.__key__ = true;
+ props[this.as] = true;
+ props[this.indexAs] = true;
+ props[this.selectedAs] = true;
+ props.tabIndex = true;
- set _physicalCount(val) {
- this._physicalCountVal = val;
- this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
- },
+ this._instanceProps = props;
+ this._userTemplate = Polymer.dom(this).querySelector('template');
- get _physicalCount() {
- return this._physicalCountVal;
+ if (this._userTemplate) {
+ this.templatize(this._userTemplate);
+ } else {
+ console.warn('iron-list requires a template to be provided in light-dom');
+ }
+ }
},
/**
- * 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.
- *
- * This default value assumes that we will at least have the equivalent
- * to a viewport of physical items above and below the user's viewport.
+ * Implements extension point from Templatizer mixin.
*/
- get _optPhysicalSize() {
- return this._viewportSize * this._maxPages;
- },
-
- /**
- * True if the current list is visible.
- */
- get _isVisible() {
- return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.scrollTarget.offsetHeight);
+ _getStampedChildren: function() {
+ return this._physicalItems;
},
/**
- * Gets the index of the first visible item in the viewport.
- *
- * @type {number}
+ * Implements extension point from Templatizer
+ * Called as a side effect of a template instance path change, responsible
+ * for notifying items.<key-for-instance>.<path> change up to host.
*/
- get firstVisibleIndex() {
- if (this._firstVisibleIndexVal === null) {
- var physicalOffset = this._physicalTop + this._scrollerPaddingTop;
-
- this._firstVisibleIndexVal = this._iterateItems(
- function(pidx, vidx) {
- physicalOffset += this._physicalSizes[pidx];
- if (physicalOffset > this._scrollPosition) {
- return vidx;
- }
- }) || 0;
+ _forwardInstancePath: function(inst, path, value) {
+ if (path.indexOf(this.as + '.') === 0) {
+ this.notifyPath('items.' + inst.__key__ + '.' +
+ path.slice(this.as.length + 1), value);
}
- return this._firstVisibleIndexVal;
},
/**
- * Gets the index of the last visible item in the viewport.
- *
- * @type {number}
+ * Implements extension point from Templatizer mixin
+ * Called as side-effect of a host property change, responsible for
+ * notifying parent path change on each row.
*/
- get lastVisibleIndex() {
- if (this._lastVisibleIndexVal === null) {
- var physicalOffset = this._physicalTop;
-
- this._iterateItems(function(pidx, vidx) {
- physicalOffset += this._physicalSizes[pidx];
-
- if (physicalOffset <= this._scrollBottom) {
- this._lastVisibleIndexVal = vidx;
- }
- });
+ _forwardParentProp: function(prop, value) {
+ if (this._physicalItems) {
+ this._physicalItems.forEach(function(item) {
+ item._templateInstance[prop] = value;
+ }, this);
}
- return this._lastVisibleIndexVal;
- },
-
- get _defaultScrollTarget() {
- return this;
- },
-
- ready: function() {
- this.addEventListener('focus', this._didFocus.bind(this), true);
- },
-
- attached: function() {
- this.updateViewportBoundaries();
- this._render();
- },
-
- detached: function() {
- this._itemsRendered = false;
},
/**
- * Set the overflow property if this element has its own scrolling region
+ * Implements extension point from Templatizer
+ * Called as side-effect of a host path change, responsible for
+ * notifying parent.<path> path change on each row.
*/
- _setOverflow: function(scrollTarget) {
- this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
- this.style.overflow = scrollTarget === this ? 'auto' : '';
+ _forwardParentPath: function(path, value) {
+ if (this._physicalItems) {
+ this._physicalItems.forEach(function(item) {
+ item._templateInstance.notifyPath(path, value, true);
+ }, this);
+ }
},
/**
- * Invoke this method if you dynamically update the viewport's
- * size or CSS padding.
- *
- * @method updateViewportBoundaries
+ * Called as a side effect of a host items.<key>.<path> path change,
+ * responsible for notifying item.<path> changes.
*/
- updateViewportBoundaries: function() {
- this._scrollerPaddingTop = this.scrollTarget === this ? 0 :
- parseInt(window.getComputedStyle(this)['padding-top'], 10);
+ _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];
- this._viewportSize = this._scrollTargetHeight;
+
+ 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;
+ }
},
/**
- * Update the models, the position of the
- * items in the viewport and recycle tiles as needed.
+ * Called when the items have changed. That is, ressignments
+ * to `items`, splices or updates to a single item.
*/
- _scrollHandler: function() {
- // clamp the `scrollTop` value
- 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 recycledTiles = 0;
- var hiddenContentSize = this._hiddenContentSize;
- var currentRatio = ratio;
- var movingUp = [];
+ _itemsChanged: function(change) {
+ if (change.path === 'items') {
+ // reset items
+ this._virtualStart = 0;
+ this._physicalTop = 0;
+ this._virtualCount = this.items ? this.items.length : 0;
+ this._collection = this.items ? Polymer.Collection.get(this.items) : null;
+ this._physicalIndexForKey = {};
- // track the last `scrollTop`
- this._scrollPosition = scrollTop;
+ this._resetScrollPosition(0);
+ this._removeFocusedItem();
- // clear cached visible indexes
- this._firstVisibleIndexVal = null;
- this._lastVisibleIndexVal = null;
+ // create the initial physical items
+ if (!this._physicalItems) {
+ this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, this._virtualCount));
+ this._physicalItems = this._createPool(this._physicalCount);
+ this._physicalSizes = new Array(this._physicalCount);
+ }
- scrollBottom = this._scrollBottom;
- physicalBottom = this._physicalBottom;
+ this._physicalStart = 0;
- // random access
- if (Math.abs(delta) > this._physicalSize) {
- this._physicalTop += delta;
- recycledTiles = Math.round(delta / this._physicalAverage);
+ } else if (change.path === 'items.splices') {
+ this._adjustVirtualIndex(change.value.indexSplices);
+ this._virtualCount = this.items ? this.items.length : 0;
+
+ } else {
+ // update a single item
+ this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.value);
+ return;
}
- // scroll up
- else if (delta < 0) {
- var topSpace = scrollTop - this._physicalTop;
- var virtualStart = this._virtualStart;
- recycledTileSet = [];
+ this._itemsRendered = false;
+ this._debounceTemplate(this._render);
+ },
- kth = this._physicalEnd;
- currentRatio = topSpace / hiddenContentSize;
+ /**
+ * @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);
- // move tiles from bottom to top
- while (
- // approximate `currentRatio` to `ratio`
- currentRatio < ratio &&
- // recycle less physical items than the total
- recycledTiles < this._physicalCount &&
- // ensure that these recycled tiles are needed
- virtualStart - recycledTiles > 0 &&
- // ensure that the tile is not visible
- physicalBottom - this._physicalSizes[kth] > scrollBottom
- ) {
+ this._virtualStart = this._virtualStart + delta;
- tileHeight = this._physicalSizes[kth];
- currentRatio += tileHeight / hiddenContentSize;
- physicalBottom -= tileHeight;
- recycledTileSet.push(kth);
- recycledTiles++;
- kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
+ if (this._focusedIndex >= 0) {
+ this._focusedIndex = this._focusedIndex + delta;
+ }
}
+ }, this);
+ },
- movingUp = recycledTileSet;
- recycledTiles = -recycledTiles;
+ _removeItem: function(item) {
+ this.$.selector.deselect(item);
+ // remove the current focused item
+ if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
+ this._removeFocusedItem();
}
- // scroll down
- else if (delta > 0) {
- var bottomSpace = physicalBottom - scrollBottom;
- var virtualEnd = this._virtualEnd;
- var lastVirtualItemIndex = this._virtualCount-1;
-
- recycledTileSet = [];
-
- kth = this._physicalStart;
- currentRatio = bottomSpace / hiddenContentSize;
-
- // move tiles from top to bottom
- while (
- // approximate `currentRatio` to `ratio`
- currentRatio < ratio &&
- // recycle less physical items than the total
- recycledTiles < this._physicalCount &&
- // ensure that these recycled tiles are needed
- virtualEnd + recycledTiles < lastVirtualItemIndex &&
- // ensure that the tile is not visible
- this._physicalTop + this._physicalSizes[kth] < scrollTop
- ) {
+ },
- tileHeight = this._physicalSizes[kth];
- currentRatio += tileHeight / hiddenContentSize;
+ /**
+ * Executes a provided function per every physical index in `itemSet`
+ * `itemSet` default value is equivalent to the entire set of physical indexes.
+ *
+ * @param {!function(number, number)} fn
+ * @param {!Array<number>=} itemSet
+ */
+ _iterateItems: function(fn, itemSet) {
+ var pidx, vidx, rtn, i;
- this._physicalTop += tileHeight;
- recycledTileSet.push(kth);
- recycledTiles++;
- kth = (kth + 1) % this._physicalCount;
+ if (arguments.length === 2 && itemSet) {
+ for (i = 0; i < itemSet.length; i++) {
+ pidx = itemSet[i];
+ if (pidx >= this._physicalStart) {
+ vidx = this._virtualStart + (pidx - this._physicalStart);
+ } else {
+ vidx = this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
+ }
+ if ((rtn = fn.call(this, pidx, vidx)) != null) {
+ return rtn;
+ }
}
- }
+ } else {
+ pidx = this._physicalStart;
+ vidx = this._virtualStart;
- if (recycledTiles === 0) {
- // If the list ever reach this case, the physical average is not significant enough
- // to create all the items needed to cover the entire viewport.
- // e.g. A few items have a height that differs from the average by serveral order of magnitude.
- if (physicalBottom < scrollBottom || this._physicalTop > scrollTop) {
- this.async(this._increasePool.bind(this, 1));
+ for (; pidx < this._physicalCount; pidx++, vidx++) {
+ if ((rtn = fn.call(this, pidx, vidx)) != null) {
+ return rtn;
+ }
+ }
+ for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
+ if ((rtn = fn.call(this, pidx, vidx)) != null) {
+ return rtn;
+ }
}
- } else {
- this._virtualStart = this._virtualStart + recycledTiles;
- this._physicalStart = this._physicalStart + recycledTiles;
- this._update(recycledTileSet, movingUp);
}
},
/**
- * Update the list of items, starting from the `_virtualStart` item.
+ * Assigns the data models to a given set of items.
* @param {!Array<number>=} itemSet
- * @param {!Array<number>=} movingUp
*/
- _update: function(itemSet, movingUp) {
- // manage focus
- this._manageFocus();
- // update models
- this._assignModels(itemSet);
- // measure heights
- this._updateMetrics(itemSet);
- // adjust offset after measuring
- if (movingUp) {
- while (movingUp.length) {
- this._physicalTop -= this._physicalSizes[movingUp.pop()];
+ _assignModels: function(itemSet) {
+ this._iterateItems(function(pidx, vidx) {
+ var el = this._physicalItems[pidx];
+ var inst = el._templateInstance;
+ var item = this.items && this.items[vidx];
+
+ 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 = this._focusedIndex === vidx ? 0 : -1;
+ this._physicalIndexForKey[inst.__key__] = pidx;
+ el.removeAttribute('hidden');
+ } else {
+ inst.__key__ = null;
+ el.setAttribute('hidden', '');
}
- }
- // update the position of the items
- this._positionItems();
- // set the scroller size
- this._updateScrollerSize();
- // increase the pool of physical items
- this._increasePoolIfNeeded();
+ }, itemSet);
},
/**
- * Creates a pool of DOM elements and attaches them to the local dom.
+ * Updates the height for a given set of items.
+ *
+ * @param {!Array<number>=} itemSet
*/
- _createPool: function(size) {
- var physicalItems = new Array(size);
+ _updateMetrics: function(itemSet) {
+ // Make sure we distributed all the physical items
+ // so we can measure them
+ Polymer.dom.flush();
- this._ensureTemplatized();
+ var newPhysicalSize = 0;
+ var oldPhysicalSize = 0;
+ var prevAvgCount = this._physicalAverageCount;
+ var prevPhysicalAvg = this._physicalAverage;
- for (var i = 0; i < size; i++) {
- var inst = this.stamp(null);
- // First element child is item; Safari doesn't support children[0]
- // on a doc fragment
- physicalItems[i] = inst.root.querySelector('*');
- Polymer.dom(this).appendChild(inst.root);
+ this._iterateItems(function(pidx, vidx) {
+
+ oldPhysicalSize += this._physicalSizes[pidx] || 0;
+ this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
+ newPhysicalSize += this._physicalSizes[pidx];
+ this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
+
+ }, itemSet);
+
+ this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize;
+ this._viewportSize = this._scrollTargetHeight;
+
+ // update the average if we measured something
+ if (this._physicalAverageCount !== prevAvgCount) {
+ this._physicalAverage = Math.round(
+ ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
+ this._physicalAverageCount);
}
- return physicalItems;
},
/**
- * Increases the pool of physical items only if needed.
- * This function will allocate additional physical items
- * if the physical size is shorter than `_optPhysicalSize`
+ * Updates the position of the physical items.
*/
- _increasePoolIfNeeded: function() {
- if (this._viewportSize === 0 || this._physicalSize >= this._optPhysicalSize) {
- return false;
- }
- // 0 <= `currentPage` <= `_maxPages`
- var currentPage = Math.floor(this._physicalSize / this._viewportSize);
- if (currentPage === 0) {
- // fill the first page
- this._debounceTemplate(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5)));
- } else if (this._lastPage !== currentPage) {
- // paint the page and defer the next increase
- // wait 16ms which is rough enough to get paint cycle.
- Polymer.dom.addDebouncer(this.debounce('_debounceTemplate', this._increasePool.bind(this, 1), 16));
- } else {
- // fill the rest of the pages
- this._debounceTemplate(this._increasePool.bind(this, 1));
- }
- this._lastPage = currentPage;
- return true;
+ _positionItems: function() {
+ this._adjustScrollPosition();
+
+ var y = this._physicalTop;
+
+ this._iterateItems(function(pidx) {
+ this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
+ y += this._physicalSizes[pidx];
+ });
},
/**
- * Increases the pool size.
+ * Adjusts the scroll position when it was overestimated.
*/
- _increasePool: function(missingItems) {
- var nextPhysicalCount = Math.min(
- this._physicalCount + missingItems,
- this._virtualCount - this._virtualStart,
- MAX_PHYSICAL_COUNT
- );
- var prevPhysicalCount = this._physicalCount;
- var delta = nextPhysicalCount - prevPhysicalCount;
+ _adjustScrollPosition: function() {
+ var deltaHeight = this._virtualStart === 0 ? this._physicalTop :
+ Math.min(this._scrollPosition + this._physicalTop, 0);
- if (delta <= 0) {
- return;
+ if (deltaHeight) {
+ this._physicalTop = this._physicalTop - deltaHeight;
+ // juking scroll position during interial scrolling on iOS is no bueno
+ if (!IOS_TOUCH_SCROLLING) {
+ this._resetScrollPosition(this._scrollTop - deltaHeight);
+ }
}
+ },
- [].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;
+ /**
+ * Sets the position of the scroll.
+ */
+ _resetScrollPosition: function(pos) {
+ if (this.scrollTarget) {
+ this._scrollTop = pos;
+ this._scrollPosition = this._scrollTop;
}
- this._update();
},
/**
- * Render a new list of items. This method does exactly the same as `update`,
- * but it also ensures that only one `update` cycle is created.
+ * Sets the scroll height, that's the height of the content,
+ *
+ * @param {boolean=} forceUpdate If true, updates the height no matter what.
*/
- _render: function() {
- var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
+ _updateScrollerSize: function(forceUpdate) {
+ this._estScrollHeight = (this._physicalBottom +
+ Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage);
- if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) {
- this._lastPage = 0;
- this._update();
- this._scrollHandler();
- this._itemsRendered = true;
+ forceUpdate = forceUpdate || this._scrollHeight === 0;
+ forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
+
+ // amortize height adjustment, so it won't trigger repaints very often
+ if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
+ this.$.items.style.height = this._estScrollHeight + 'px';
+ this._scrollHeight = this._estScrollHeight;
}
},
-
/**
- * Templetizes the user template.
+ * Scroll to a specific item in the virtual list regardless
+ * of the physical items in the DOM tree.
+ *
+ * @method scrollToIndex
+ * @param {number} idx The index of the item
*/
- _ensureTemplatized: function() {
- if (!this.ctor) {
- // Template instance props that should be excluded from forwarding
- var props = {};
- props.__key__ = true;
- props[this.as] = true;
- props[this.indexAs] = true;
- props[this.selectedAs] = true;
- props.tabIndex = true;
+ scrollToIndex: function(idx) {
+ if (typeof idx !== 'number') {
+ return;
+ }
- this._instanceProps = props;
- this._userTemplate = Polymer.dom(this).querySelector('template');
+ Polymer.dom.flush();
- if (this._userTemplate) {
- this.templatize(this._userTemplate);
- } else {
- console.warn('iron-list requires a template to be provided in light-dom');
- }
+ 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;
+ }
+ // 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;
+
+ 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;
},
/**
- * Implements extension point from Templatizer mixin.
+ * Reset the physical average and the average count.
*/
- _getStampedChildren: function() {
- return this._physicalItems;
+ _resetAverage: function() {
+ this._physicalAverage = 0;
+ this._physicalAverageCount = 0;
},
/**
- * Implements extension point from Templatizer
- * Called as a side effect of a template instance path change, responsible
- * for notifying items.<key-for-instance>.<path> change up to host.
+ * A handler for the `iron-resize` event triggered by `IronResizableBehavior`
+ * when the element is resized.
*/
- _forwardInstancePath: function(inst, path, value) {
- if (path.indexOf(this.as + '.') === 0) {
- this.notifyPath('items.' + inst.__key__ + '.' +
- path.slice(this.as.length + 1), value);
+ _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);
+ }
+ });
},
- /**
- * Implements extension point from Templatizer mixin
- * Called as side-effect of a host property change, responsible for
- * notifying parent path change on each row.
- */
- _forwardParentProp: function(prop, value) {
- if (this._physicalItems) {
- this._physicalItems.forEach(function(item) {
- item._templateInstance[prop] = value;
- }, this);
+ _getModelFromItem: function(item) {
+ var key = this._collection.getKey(item);
+ var pidx = this._physicalIndexForKey[key];
+
+ if (pidx != null) {
+ return this._physicalItems[pidx]._templateInstance;
}
+ return null;
},
/**
- * Implements extension point from Templatizer
- * Called as side-effect of a host path change, responsible for
- * notifying parent.<path> path change on each row.
+ * Gets a valid item instance from its index or the object value.
+ *
+ * @param {(Object|number)} item The item object or its index
*/
- _forwardParentPath: function(path, value) {
- if (this._physicalItems) {
- this._physicalItems.forEach(function(item) {
- item._templateInstance.notifyPath(path, value, true);
- }, this);
+ _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;
},
/**
- * Called as a side effect of a host items.<key>.<path> path change,
- * responsible for notifying item.<path> changes.
+ * Select the list item at the given index.
+ *
+ * @method selectItem
+ * @param {(Object|number)} item The item object or its index
*/
- _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];
-
+ selectItem: function(item) {
+ item = this._getNormalizedItem(item);
+ var model = this._getModelFromItem(item);
- if (idx === this._focusedIndex && this._offscreenFocusedItem) {
- el = this._offscreenFocusedItem;
+ if (!this.multiSelection && this.selectedItem) {
+ this.deselectItem(this.selectedItem);
}
- if (!el) {
- return;
+ if (model) {
+ model[this.selectedAs] = true;
}
-
- inst = el._templateInstance;
+ this.$.selector.select(item);
+ this.updateSizeForItem(item);
+ },
- if (inst.__key__ !== key) {
- return;
+ /**
+ * 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;
}
- if (dot >= 0) {
- path = this.as + '.' + path.substring(dot+1);
- inst.notifyPath(path, value, true);
+ 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 {
- inst[this.as] = value;
+ this.selectItem(item);
}
},
/**
- * Called when the items have changed. That is, ressignments
- * to `items`, splices or updates to a single item.
+ * Clears the current selection state of the list.
+ *
+ * @method clearSelection
*/
- _itemsChanged: function(change) {
- if (change.path === 'items') {
- // reset items
- this._virtualStart = 0;
- this._physicalTop = 0;
- this._virtualCount = this.items ? this.items.length : 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) {
- this._physicalCount = Math.max(1, Math.min(DEFAULT_PHYSICAL_COUNT, this._virtualCount));
- this._physicalItems = this._createPool(this._physicalCount);
- this._physicalSizes = new Array(this._physicalCount);
+ clearSelection: function() {
+ function unselect(item) {
+ var model = this._getModelFromItem(item);
+ if (model) {
+ model[this.selectedAs] = false;
}
+ }
- this._physicalStart = 0;
-
- } else if (change.path === 'items.splices') {
- this._adjustVirtualIndex(change.value.indexSplices);
- this._virtualCount = this.items ? this.items.length : 0;
-
- } else {
- // update a single item
- this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.value);
- return;
+ if (Array.isArray(this.selectedItems)) {
+ this.selectedItems.forEach(unselect, this);
+ } else if (this.selectedItem) {
+ unselect.call(this, this.selectedItem);
}
- this._itemsRendered = false;
- this._debounceTemplate(this._render);
+ /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
},
/**
- * @param {!Array<!PolymerSplice>} splices
+ * Add an event listener to `tap` if `selectionEnabled` is true,
+ * it will remove the listener otherwise.
*/
- _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);
-
- this._virtualStart = this._virtualStart + delta;
+ _selectionEnabledChanged: function(selectionEnabled) {
+ var handler = selectionEnabled ? this.listen : this.unlisten;
+ handler.call(this, this, 'tap', '_selectionHandler');
+ },
- if (this._focusedIndex >= 0) {
- this._focusedIndex = this._focusedIndex + delta;
- }
+ /**
+ * 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]);
}
- }, this);
+ }
},
- _removeItem: function(item) {
- this.$.selector.deselect(item);
- // remove the current focused item
- if (this._focusedItem && this._focusedItem._templateInstance[this.as] === item) {
- this._removeFocusedItem();
- }
+ _multiSelectionChanged: function(multiSelection) {
+ this.clearSelection();
+ this.$.selector.multi = multiSelection;
},
/**
- * Executes a provided function per every physical index in `itemSet`
- * `itemSet` default value is equivalent to the entire set of physical indexes.
+ * Updates the size of an item.
*
- * @param {!function(number, number)} fn
- * @param {!Array<number>=} itemSet
+ * @method updateSizeForItem
+ * @param {(Object|number)} item The item object or its index
*/
- _iterateItems: function(fn, itemSet) {
- var pidx, vidx, rtn, i;
-
- if (arguments.length === 2 && itemSet) {
- for (i = 0; i < itemSet.length; i++) {
- pidx = itemSet[i];
- if (pidx >= this._physicalStart) {
- vidx = this._virtualStart + (pidx - this._physicalStart);
- } else {
- vidx = this._virtualStart + (this._physicalCount - this._physicalStart) + pidx;
- }
- if ((rtn = fn.call(this, pidx, vidx)) != null) {
- return rtn;
- }
- }
- } else {
- pidx = this._physicalStart;
- vidx = this._virtualStart;
+ updateSizeForItem: function(item) {
+ item = this._getNormalizedItem(item);
+ var key = this._collection.getKey(item);
+ var pidx = this._physicalIndexForKey[key];
- for (; pidx < this._physicalCount; pidx++, vidx++) {
- if ((rtn = fn.call(this, pidx, vidx)) != null) {
- return rtn;
- }
- }
- for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
- if ((rtn = fn.call(this, pidx, vidx)) != null) {
- return rtn;
- }
- }
+ if (pidx != null) {
+ this._updateMetrics([pidx]);
+ this._positionItems();
}
},
/**
- * Assigns the data models to a given set of items.
- * @param {!Array<number>=} itemSet
+ * 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.
*/
- _assignModels: function(itemSet) {
- this._iterateItems(function(pidx, vidx) {
- var el = this._physicalItems[pidx];
- var inst = el._templateInstance;
- var item = this.items && this.items[vidx];
+ _manageFocus: function() {
+ var fidx = this._focusedIndex;
- 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 = this._focusedIndex === vidx ? 0 : -1;
- this._physicalIndexForKey[inst.__key__] = pidx;
- el.removeAttribute('hidden');
+ 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 {
- inst.__key__ = null;
- el.setAttribute('hidden', '');
+ this._createFocusBackfillItem();
}
- }, itemSet);
+ } else if (this._virtualCount > 0 && this._physicalCount > 0) {
+ // otherwise, assign the initial focused index.
+ this._focusedIndex = this._virtualStart;
+ this._focusedItem = this._physicalItems[this._physicalStart];
+ }
},
- /**
- * Updates the height for a given set of items.
- *
- * @param {!Array<number>=} itemSet
- */
- _updateMetrics: function(itemSet) {
- // Make sure we distributed all the physical items
- // so we can measure them
- Polymer.dom.flush();
+ _isIndexRendered: function(idx) {
+ return idx >= this._virtualStart && idx <= this._virtualEnd;
+ },
- var newPhysicalSize = 0;
- var oldPhysicalSize = 0;
- var prevAvgCount = this._physicalAverageCount;
- var prevPhysicalAvg = this._physicalAverage;
+ _isIndexVisible: function(idx) {
+ return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
+ },
- this._iterateItems(function(pidx, vidx) {
+ _getPhysicalIndex: function(idx) {
+ return this._physicalIndexForKey[this._collection.getKey(this._getNormalizedItem(idx))];
+ },
- oldPhysicalSize += this._physicalSizes[pidx] || 0;
- this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
- newPhysicalSize += this._physicalSizes[pidx];
- this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
+ _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);
+ }
- }, itemSet);
+ var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
+ var SECRET = ~(Math.random() * 100);
+ var model = physicalItem._templateInstance;
+ var focusable;
- this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize;
- this._viewportSize = this._scrollTargetHeight;
+ // 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();
+ },
+
+ _removeFocusedItem: function() {
+ if (this._offscreenFocusedItem) {
+ Polymer.dom(this).removeChild(this._offscreenFocusedItem);
+ }
+ this._offscreenFocusedItem = null;
+ this._focusBackfillItem = null;
+ this._focusedItem = null;
+ this._focusedIndex = -1;
+ },
+
+ _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);
- // update the average if we measured something
- if (this._physicalAverageCount !== prevAvgCount) {
- this._physicalAverage = Math.round(
- ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
- this._physicalAverageCount);
+ 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);
}
},
- /**
- * Updates the position of the physical items.
- */
- _positionItems: function() {
- this._adjustScrollPosition();
+ _restoreFocusedItem: function() {
+ var pidx, fidx = this._focusedIndex;
- var y = this._physicalTop;
+ 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);
- this._iterateItems(function(pidx) {
- this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
- y += this._physicalSizes[pidx];
- });
+ 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);
+ }
},
- /**
- * Adjusts the scroll position when it was overestimated.
- */
- _adjustScrollPosition: function() {
- var deltaHeight = this._virtualStart === 0 ? this._physicalTop :
- Math.min(this._scrollPosition + this._physicalTop, 0);
+ _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 (deltaHeight) {
- this._physicalTop = this._physicalTop - deltaHeight;
- // juking scroll position during interial scrolling on iOS is no bueno
- if (!IOS_TOUCH_SCROLLING) {
- this._resetScrollPosition(this._scrollTop - deltaHeight);
+ 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)];
+
+ if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
+ this._update();
}
}
},
- /**
- * Sets the position of the scroll.
- */
- _resetScrollPosition: function(pos) {
- if (this.scrollTarget) {
- this._scrollTop = pos;
- this._scrollPosition = this._scrollTop;
- }
+ _didMoveUp: function() {
+ this._focusPhysicalItem(this._focusedIndex - 1);
},
- /**
- * Sets the scroll height, that's the height of the content,
- *
- * @param {boolean=} forceUpdate If true, updates the height no matter what.
- */
- _updateScrollerSize: function(forceUpdate) {
- this._estScrollHeight = (this._physicalBottom +
- Math.max(this._virtualCount - this._physicalCount - this._virtualStart, 0) * this._physicalAverage);
+ _didMoveDown: function() {
+ this._focusPhysicalItem(this._focusedIndex + 1);
+ },
- forceUpdate = forceUpdate || this._scrollHeight === 0;
- forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
+ _didEnter: function(e) {
+ this._focusPhysicalItem(this._focusedIndex);
+ this._selectionHandler(e.detail.keyboardEvent);
+ }
+ });
- // amortize height adjustment, so it won't trigger repaints very often
- if (forceUpdate || Math.abs(this._estScrollHeight - this._scrollHeight) >= this._optPhysicalSize) {
- this.$.items.style.height = this._estScrollHeight + 'px';
- this._scrollHeight = this._estScrollHeight;
- }
- },
- /**
- * Scroll to a specific item in the virtual list regardless
- * of the physical items in the DOM tree.
- *
- * @method scrollToIndex
- * @param {number} idx The index of the item
- */
- scrollToIndex: function(idx) {
- if (typeof idx !== 'number') {
- return;
- }
+})();
+// 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.
- Polymer.dom.flush();
+/**
+ * @fileoverview Assertion support.
+ */
- 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;
- }
- // 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;
+/**
+ * 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;
+}
- var currentTopItem = this._physicalStart;
- var currentVirtualItem = this._virtualStart;
- var targetOffsetTop = 0;
- var hiddenContentSize = this._hiddenContentSize;
+/**
+ * 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');
+}
- // 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;
- },
+/**
+ * @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.
- /**
- * Reset the physical average and the average count.
- */
- _resetAverage: function() {
- this._physicalAverage = 0;
- this._physicalAverageCount = 0;
- },
+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(); }
- /**
- * 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);
- }
- });
- },
+ /**
+ * @param {string|undefined} value
+ * @return {boolean} Whether |value| is truthy.
+ */
+ function truthy(value) { return !!value; }
- _getModelFromItem: function(item) {
- var key = this._collection.getKey(item);
- var pidx = this._physicalIndexForKey[key];
+ /**
+ * @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);
+ };
- if (pidx != null) {
- return this._physicalItems[pidx]._templateInstance;
- }
- return null;
- },
+ ActionService.prototype = {
+ /** @param {string} id ID of the download to cancel. */
+ cancel: chromeSendWithId('cancel'),
- /**
- * 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');
+ /** Instructs the browser to clear all finished downloads. */
+ clearAll: function() {
+ if (loadTimeData.getBoolean('allowDeletingHistory')) {
+ chrome.send('clearAll');
+ this.search('');
}
- 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);
+ /** @param {string} id ID of the dangerous download to discard. */
+ discardDangerous: chromeSendWithId('discardDangerous'),
- if (!this.multiSelection && this.selectedItem) {
- this.deselectItem(this.selectedItem);
- }
- if (model) {
- model[this.selectedAs] = true;
- }
- this.$.selector.select(item);
- this.updateSizeForItem(item);
+ /** @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();
},
- /**
- * 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);
+ /** @param {string} id ID of the download that the user started dragging. */
+ drag: chromeSendWithId('drag'),
- if (model) {
- model[this.selectedAs] = false;
- }
- this.$.selector.deselect(item);
- this.updateSizeForItem(item);
+ /** Loads more downloads with the current search terms. */
+ loadMore: function() {
+ chrome.send('getDownloads', this.searchTerms_);
},
/**
- * 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
+ * @return {boolean} Whether the user is currently searching for downloads
+ * (i.e. has a non-empty search term).
*/
- toggleSelectionForItem: function(item) {
- item = this._getNormalizedItem(item);
- if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item)) {
- this.deselectItem(item);
- } else {
- this.selectItem(item);
- }
+ isSearching: function() {
+ return this.searchTerms_.length > 0;
},
+ /** Opens the current local destination for downloads. */
+ openDownloadsFolder: chrome.send.bind(chrome, 'openDownloadsFolder'),
+
/**
- * Clears the current selection state of the list.
- *
- * @method clearSelection
+ * @param {string} id ID of the download to run locally on the user's box.
*/
- clearSelection: function() {
- function unselect(item) {
- var model = this._getModelFromItem(item);
- if (model) {
- model[this.selectedAs] = false;
- }
- }
+ openFile: chromeSendWithId('openFile'),
- if (Array.isArray(this.selectedItems)) {
- this.selectedItems.forEach(unselect, this);
- } else if (this.selectedItem) {
- unselect.call(this, this.selectedItem);
- }
+ /** @param {string} id ID the of the progressing download to pause. */
+ pause: chromeSendWithId('pause'),
- /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
- },
+ /** @param {string} id ID of the finished download to remove. */
+ remove: chromeSendWithId('remove'),
- /**
- * 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');
- },
+ /** @param {string} id ID of the paused download to resume. */
+ resume: chromeSendWithId('resume'),
/**
- * Select an item from an event object.
+ * @param {string} id ID of the dangerous download to save despite
+ * warnings.
*/
- _selectionHandler: function(e) {
- if (this.selectionEnabled) {
- var model = this.modelForElement(e.target);
- if (model) {
- this.toggleSelectionForItem(model[this.as]);
- }
+ saveDangerous: chromeSendWithId('saveDangerous'),
+
+ /** @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;
}
- },
- _multiSelectionChanged: function(multiSelection) {
- this.clearSelection();
- this.$.selector.multi = multiSelection;
+ if (sameTerms)
+ return;
+
+ this.searchTerms_ = searchTerms;
+ this.loadMore();
},
/**
- * Updates the size of an item.
- *
- * @method updateSizeForItem
- * @param {(Object|number)} item The item object or its index
+ * Shows the local folder a finished download resides in.
+ * @param {string} id ID of the download to show.
*/
- updateSizeForItem: function(item) {
- item = this._getNormalizedItem(item);
- var key = this._collection.getKey(item);
- var pidx = this._physicalIndexForKey[key];
+ show: chromeSendWithId('show'),
- if (pidx != null) {
- this._updateMetrics([pidx]);
- this._positionItems();
- }
- },
+ /** Undo download removal. */
+ undo: chrome.send.bind(chrome, 'undo'),
+ };
- /**
- * 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;
+ cr.addSingletonGetter(ActionService);
- 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];
- }
- },
+ 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.
- _isIndexRendered: function(idx) {
- return idx >= this._virtualStart && idx <= this._virtualEnd;
- },
+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',
+ };
- _isIndexVisible: function(idx) {
- return idx >= this.firstVisibleIndex && idx <= this.lastVisibleIndex;
- },
+ /**
+ * 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',
+ };
- _getPhysicalIndex: function(idx) {
- return this._physicalIndexForKey[this._collection.getKey(this._getNormalizedItem(idx))];
- },
+ 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.
- _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);
- }
+// 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.
- var physicalItem = this._physicalItems[this._getPhysicalIndex(idx)];
- var SECRET = ~(Math.random() * 100);
- var model = physicalItem._templateInstance;
- var focusable;
+/**
+ * @constructor
+ * @extends {HTMLAnchorElement}
+ */
+var ActionLink = document.registerElement('action-link', {
+ prototype: {
+ __proto__: HTMLAnchorElement.prototype,
- // 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();
- },
+ /** @this {ActionLink} */
+ createdCallback: function() {
+ // Action links can start disabled (e.g. <a is="action-link" disabled>).
+ this.tabIndex = this.disabled ? -1 : 0;
- _removeFocusedItem: function() {
- if (this._offscreenFocusedItem) {
- Polymer.dom(this).removeChild(this._offscreenFocusedItem);
- }
- this._offscreenFocusedItem = null;
- this._focusBackfillItem = null;
- this._focusedItem = null;
- this._focusedIndex = -1;
- },
+ if (!this.hasAttribute('role'))
+ this.setAttribute('role', 'link');
- _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.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 (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);
+ function preventDefault(e) {
+ e.preventDefault();
}
- },
- _restoreFocusedItem: function() {
- var pidx, fidx = this._focusedIndex;
-
- if (!this._offscreenFocusedItem || this._focusedIndex < 0) {
- return;
+ function removePreventDefault() {
+ document.removeEventListener('selectstart', preventDefault);
+ document.removeEventListener('mouseup', removePreventDefault);
}
- // 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);
- }
- },
+ this.addEventListener('mousedown', function() {
+ // This handlers strives to match the behavior of <a href="...">.
- _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;
+ // While the mouse is down, prevent text selection from dragging.
+ document.addEventListener('selectstart', preventDefault);
+ document.addEventListener('mouseup', removePreventDefault);
- 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)];
+ // If focus started via mouse press, don't show an outline.
+ if (document.activeElement != this)
+ this.classList.add('no-outline');
+ });
- if (hasOffscreenFocusedItem && !this._offscreenFocusedItem) {
- this._update();
- }
- }
+ 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
@@ -9947,7 +9965,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 +10495,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 +10857,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/vulcanize.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698