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

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

Issue 1681053002: Unrestrict version of PolymerElements/iron-list and update it (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@fix-closure
Patch Set: and vulcanize Created 4 years, 10 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « no previous file | chrome/browser/resources/md_downloads/vulcanized.html » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: chrome/browser/resources/md_downloads/crisper.js
diff --git a/chrome/browser/resources/md_downloads/crisper.js b/chrome/browser/resources/md_downloads/crisper.js
index 1c61c9f3b1c42ff5511a3214684a5ebaa8835875..0c4f256ec3ce12f648de0f3422812bf0baea1277 100644
--- a/chrome/browser/resources/md_downloads/crisper.js
+++ b/chrome/browser/resources/md_downloads/crisper.js
@@ -9,6 +9,9 @@
*/
var global = this;
+/** @typedef {{eventName: string, uid: number}} */
+var WebUIListener;
+
/** Platform, package, object property, and Event support. **/
var cr = function() {
'use strict';
@@ -357,11 +360,13 @@ var cr = function() {
}
/**
- * A registry of callbacks keyed by event name. Used by addWebUIListener to
- * register listeners.
- * @type {!Object<Array<Function>>}
+ * A map of maps associating event names with listeners. The 2nd level map
+ * associates a listener ID with the callback function, such that individual
+ * listeners can be removed from an event without affecting other listeners of
+ * the same event.
+ * @type {!Object<!Object<!Function>>}
*/
- var webUIListenerMap = Object.create(null);
+ var webUIListenerMap = {};
/**
* The named method the WebUI handler calls directly when an event occurs.
@@ -369,26 +374,52 @@ var cr = function() {
* of the JS invocation; additionally, the handler may supply any number of
* other arguments that will be forwarded to the listener callbacks.
* @param {string} event The name of the event that has occurred.
+ * @param {...*} var_args Additional arguments passed from C++.
*/
- function webUIListenerCallback(event) {
- var listenerCallbacks = webUIListenerMap[event];
- for (var i = 0; i < listenerCallbacks.length; i++) {
- var callback = listenerCallbacks[i];
- callback.apply(null, Array.prototype.slice.call(arguments, 1));
+ function webUIListenerCallback(event, var_args) {
+ var eventListenersMap = webUIListenerMap[event];
+ if (!eventListenersMap) {
+ // C++ event sent for an event that has no listeners.
+ // TODO(dpapad): Should a warning be displayed here?
+ return;
+ }
+
+ var args = Array.prototype.slice.call(arguments, 1);
+ for (var listenerId in eventListenersMap) {
+ eventListenersMap[listenerId].apply(null, args);
}
}
/**
* Registers a listener for an event fired from WebUI handlers. Any number of
* listeners may register for a single event.
- * @param {string} event The event to listen to.
- * @param {Function} callback The callback run when the event is fired.
+ * @param {string} eventName The event to listen to.
+ * @param {!Function} callback The callback run when the event is fired.
+ * @return {!WebUIListener} An object to be used for removing a listener via
+ * cr.removeWebUIListener. Should be treated as read-only.
*/
- function addWebUIListener(event, callback) {
- if (event in webUIListenerMap)
- webUIListenerMap[event].push(callback);
- else
- webUIListenerMap[event] = [callback];
+ function addWebUIListener(eventName, callback) {
+ webUIListenerMap[eventName] = webUIListenerMap[eventName] || {};
+ var uid = createUid();
+ webUIListenerMap[eventName][uid] = callback;
+ return {eventName: eventName, uid: uid};
+ }
+
+ /**
+ * Removes a listener. Does nothing if the specified listener is not found.
+ * @param {!WebUIListener} listener The listener to be removed (as returned by
+ * addWebUIListener).
+ * @return {boolean} Whether the given listener was found and actually
+ * removed.
+ */
+ function removeWebUIListener(listener) {
+ var listenerExists = webUIListenerMap[listener.eventName] &&
+ webUIListenerMap[listener.eventName][listener.uid];
+ if (listenerExists) {
+ delete webUIListenerMap[listener.eventName][listener.uid];
+ return true;
+ }
+ return false;
}
return {
@@ -401,11 +432,14 @@ var cr = function() {
exportPath: exportPath,
getUid: getUid,
makePublic: makePublic,
- webUIResponse: webUIResponse,
+ PropertyKind: PropertyKind,
+
+ // C++ <-> JS communication related methods.
+ addWebUIListener: addWebUIListener,
+ removeWebUIListener: removeWebUIListener,
sendWithPromise: sendWithPromise,
webUIListenerCallback: webUIListenerCallback,
- addWebUIListener: addWebUIListener,
- PropertyKind: PropertyKind,
+ webUIResponse: webUIResponse,
get doc() {
return document;
@@ -430,6 +464,11 @@ var cr = function() {
get isLinux() {
return /Linux/.test(navigator.userAgent);
},
+
+ /** Whether this is on Android. */
+ get isAndroid() {
+ return /Android/.test(navigator.userAgent);
+ }
};
}();
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
@@ -987,9 +1026,19 @@ cr.define('cr.ui', function() {
*/
function $(id) {
var el = document.getElementById(id);
- var message =
- 'Element ' + el + ' with id "' + id + '" is not an HTMLElement.';
- return el ? assertInstanceof(el, HTMLElement, message) : null;
+ return el ? assertInstanceof(el, HTMLElement) : null;
+}
+
+// TODO(devlin): This should return SVGElement, but closure compiler is missing
+// those externs.
+/**
+ * Alias for document.getElementById. Found elements must be SVGElements.
+ * @param {string} id The ID of the element to find.
+ * @return {Element} The found element or null if not found.
+ */
+function getSVGElement(id) {
+ var el = document.getElementById(id);
+ return el ? assertInstanceof(el, Element) : null;
}
/**
@@ -1505,8 +1554,12 @@ function assertNotReached(opt_message) {
* @template T
*/
function assertInstanceof(value, type, opt_message) {
- assert(value instanceof type,
- opt_message || value + ' is not a[n] ' + (type.name || typeof type));
+ // 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.
@@ -1979,2149 +2032,2521 @@ i18nTemplate.process(document, loadTimeData);
}
};
(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;
-
- 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'
- },
-
- /**
- * The name of the variable to add to the binding scope with the index
- * for the row.
- */
- indexAs: {
- type: String,
- value: 'index'
- },
-
- /**
- * The name of the variable to add to the binding scope to indicate
- * if the row is selected.
- */
- selectedAs: {
- type: String,
- value: 'selected'
- },
-
- /**
- * 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
- },
-
- /**
- * When `multiSelection` is false, this is the currently selected item, or `null`
- * if no item is selected.
- */
- selectedItem: {
- type: Object,
- notify: true
- },
-
- /**
- * When `multiSelection` is true, this is an array that contains the selected items.
- */
- selectedItems: {
- type: Object,
- notify: true
- },
-
- /**
- * 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
- }
- },
-
- observers: [
- '_itemsChanged(items.*)',
- '_selectionEnabledChanged(selectionEnabled)',
- '_multiSelectionChanged(multiSelection)'
- ],
-
- behaviors: [
- Polymer.Templatizer,
- Polymer.IronResizableBehavior
- ],
-
- listeners: {
- 'iron-resize': '_resizeHandler'
- },
-
- /**
- * 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,
-
- /**
- * The element that controls the scroll
- * @type {?Element}
- */
- _scroller: null,
-
- /**
- * The padding-top value of the `scroller` element
- */
- _scrollerPaddingTop: 0,
+ 'use strict';
/**
- * This value is the same as `scrollTop`.
+ * 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
*/
- _scrollPosition: 0,
+ var KEY_IDENTIFIER = {
+ 'U+0008': 'backspace',
+ 'U+0009': 'tab',
+ 'U+001B': 'esc',
+ 'U+0020': 'space',
+ 'U+007F': 'del'
+ };
/**
- * The number of tiles in the DOM.
+ * 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
*/
- _physicalCount: 0,
+ 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: '*'
+ };
/**
- * The k-th tile that is at the top of the scrolling list.
+ * 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.
*/
- _physicalStart: 0,
+ var MODIFIER_KEYS = {
+ 'shift': 'shiftKey',
+ 'ctrl': 'ctrlKey',
+ 'alt': 'altKey',
+ 'meta': 'metaKey'
+ };
/**
- * The k-th tile that is at the bottom of the scrolling list.
+ * 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.
*/
- _physicalEnd: 0,
+ var KEY_CHAR = /[a-z0-9*]/;
/**
- * The sum of the heights of all the tiles in the DOM.
+ * Matches a keyIdentifier string.
*/
- _physicalSize: 0,
+ var IDENT_CHAR = /U\+/;
/**
- * The average `offsetHeight` of the tiles observed till now.
+ * Matches arrow keys in Gecko 27.0+
*/
- _physicalAverage: 0,
+ var ARROW_KEY = /^arrow/;
/**
- * The number of tiles which `offsetHeight` > 0 observed until now.
+ * Matches space keys everywhere (notably including IE10's exceptional name
+ * `spacebar`).
*/
- _physicalAverageCount: 0,
+ var SPACE_KEY = /^space(bar)?/;
/**
- * The Y position of the item rendered in the `_physicalStart`
- * tile relative to the scrolling list.
+ * Transforms the key.
+ * @param {string} key The KeyBoardEvent.key
+ * @param {Boolean} [noSpecialChars] Limits the transformation to
+ * alpha-numeric characters.
*/
- _physicalTop: 0,
+ 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;
+ }
- /**
- * The number of items in the list.
- */
- _virtualCount: 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;
+ }
- /**
- * The n-th item rendered in the `_physicalStart` tile.
- */
- _virtualStartVal: 0,
+ 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];
+ }
+ }
+ return validKey;
+ }
/**
- * A map between an item key and its physical item index
+ * 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
*/
- _physicalIndexForKey: null,
+ 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) || '';
+ }
- /**
- * The estimated scroll height based on `_physicalAverage`
- */
- _estScrollHeight: 0,
+ 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)
+ );
+ }
- /**
- * The scroll height of the dom node
- */
- _scrollHeight: 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];
- /**
- * The height of the list. This is referred as the viewport in the context of list.
- */
- _viewportSize: 0,
+ if (keyName in MODIFIER_KEYS) {
+ parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
+ parsedKeyCombo.hasModifiers = true;
+ } else {
+ parsedKeyCombo.key = keyName;
+ parsedKeyCombo.event = event || 'keydown';
+ }
- /**
- * An array of DOM nodes that are currently in the tree
- * @type {?Array<!TemplatizerNode>}
- */
- _physicalItems: null,
+ return parsedKeyCombo;
+ }, {
+ combo: keyComboString.split(':').shift()
+ });
+ }
- /**
- * An array of heights for each item in `_physicalItems`
- * @type {?Array<number>}
- */
- _physicalSizes: null,
+ function parseEventString(eventString) {
+ return eventString.trim().split(' ').map(function(keyComboString) {
+ return parseKeyComboString(keyComboString);
+ });
+ }
/**
- * A cached value for the visible index.
- * See `firstVisibleIndex`
- * @type {?number}
+ * `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
*/
- _firstVisibleIndexVal: null,
+ Polymer.IronA11yKeysBehavior = {
+ properties: {
+ /**
+ * The HTMLElement that will be firing relevant KeyboardEvents.
+ */
+ keyEventTarget: {
+ type: Object,
+ value: function() {
+ return this;
+ }
+ },
- /**
- * A Polymer collection for the items.
- * @type {?Polymer.Collection}
- */
- _collection: null,
+ /**
+ * If true, this property will cause the implementing element to
+ * automatically stop propagation on any handled KeyboardEvents.
+ */
+ stopKeyboardEventPropagation: {
+ type: Boolean,
+ value: false
+ },
- /**
- * True if the current item list was rendered for the first time
- * after attached.
- */
- _itemsRendered: false,
+ _boundKeyHandlers: {
+ type: Array,
+ value: function() {
+ return [];
+ }
+ },
- /**
- * The page that is currently rendered.
- */
- _lastPage: null,
+ // 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 {};
+ }
+ }
+ },
- /**
- * The max number of pages to render. One page is equivalent to the height of the list.
- */
- _maxPages: 3,
+ observers: [
+ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
+ ],
- /**
- * The bottom of the physical content.
- */
- get _physicalBottom() {
- return this._physicalTop + this._physicalSize;
- },
+ keyBindings: {},
- /**
- * The bottom of the scroll.
- */
- get _scrollBottom() {
- return this._scrollPosition + this._viewportSize;
- },
+ registered: function() {
+ this._prepKeyBindings();
+ },
- /**
- * The n-th item rendered in the last physical item.
- */
- get _virtualEnd() {
- return this._virtualStartVal + this._physicalCount - 1;
- },
+ attached: function() {
+ this._listenKeyEventListeners();
+ },
- /**
- * The lowest n-th value for an item such that it can be rendered in `_physicalStart`.
- */
- _minVirtualStart: 0,
+ detached: function() {
+ this._unlistenKeyEventListeners();
+ },
- /**
- * 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);
- },
+ /**
+ * 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();
+ },
- /**
- * The height of the physical content that isn't on the screen.
- */
- get _hiddenContentSize() {
- return this._physicalSize - this._viewportSize;
- },
+ /**
+ * When called, will remove all imperatively-added key bindings.
+ */
+ removeOwnKeyBindings: function() {
+ this._imperativeKeyBindings = {};
+ this._prepKeyBindings();
+ this._resetKeyEventListeners();
+ },
- /**
- * The maximum scroll top value.
- */
- get _maxScrollTop() {
- return this._estScrollHeight - this._viewportSize;
- },
+ 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;
+ },
- /**
- * Sets the n-th item rendered in `_physicalStart`
- */
- set _virtualStart(val) {
- // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart
- this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
- this._physicalStart = this._virtualStartVal % this._physicalCount;
- this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
+ _collectKeyBindings: function() {
+ var keyBindings = this.behaviors.map(function(behavior) {
+ return behavior.keyBindings;
+ });
+
+ if (keyBindings.indexOf(this.keyBindings) === -1) {
+ keyBindings.push(this.keyBindings);
+ }
+
+ return keyBindings;
+ },
+
+ _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]);
+ }
+
+ // 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;
+ })
+ }
+ },
+
+ _addKeyBinding: function(eventString, handlerName) {
+ parseEventString(eventString).forEach(function(keyCombo) {
+ this._keyBindings[keyCombo.event] =
+ this._keyBindings[keyCombo.event] || [];
+
+ this._keyBindings[keyCombo.event].push([
+ keyCombo,
+ handlerName
+ ]);
+ }, this);
+ },
+
+ _resetKeyEventListeners: function() {
+ this._unlistenKeyEventListeners();
+
+ if (this.isAttached) {
+ this._listenKeyEventListeners();
+ }
+ },
+
+ _listenKeyEventListeners: function() {
+ Object.keys(this._keyBindings).forEach(function(eventName) {
+ var keyBindings = this._keyBindings[eventName];
+ var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
+
+ this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
+
+ this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
+ }, this);
+ },
+
+ _unlistenKeyEventListeners: function() {
+ var keyHandlerTuple;
+ var keyEventTarget;
+ var eventName;
+ var boundKeyHandler;
+
+ 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];
+
+ keyEventTarget.removeEventListener(eventName, boundKeyHandler);
+ }
+ },
+
+ _onKeyBindingEvent: function(keyBindings, event) {
+ if (this.stopKeyboardEventPropagation) {
+ event.stopPropagation();
+ }
+
+ // if event has been already prevented, don't do anything
+ if (event.defaultPrevented) {
+ 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;
+ }
+ }
+ }
+ },
+
+ _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 = {
+
+ properties: {
+
+ /**
+ * 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;
+ }
+ }
+ },
+
+ 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') {
+
+ var ownerRoot = Polymer.dom(this).getOwnerRoot();
+ this.scrollTarget = (ownerRoot && ownerRoot.$) ?
+ ownerRoot.$[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;
+ }
+ }
},
/**
- * Gets the n-th item rendered in `_physicalStart`
+ * Runs on every scroll event. Consumer of this behavior may want to override this method.
+ *
+ * @protected
*/
- get _virtualStart() {
- return this._virtualStartVal;
- },
+ _scrollHandler: function scrollHandler() {},
/**
- * An optimal physical size such that we will have enough physical items
- * to fill up the viewport and recycle when the user scrolls.
+ * The default scroll target. Consumers of this behavior may want to customize
+ * the default scroll target.
*
- * 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.
+ * @type {Element}
*/
- get _optPhysicalSize() {
- return this._viewportSize * this._maxPages;
+ get _defaultScrollTarget() {
+ return this._doc;
},
- /**
- * True if the current list is visible.
- */
- get _isVisible() {
- return this._scroller && Boolean(this._scroller.offsetWidth || this._scroller.offsetHeight);
+ /**
+ * Shortcut for the document element
+ *
+ * @type {Element}
+ */
+ get _doc() {
+ return this.ownerDocument.documentElement;
},
/**
- * Gets the index of the first visible item in the viewport.
+ * Gets the number of pixels that the content of an element is scrolled upward.
*
* @type {number}
*/
- get firstVisibleIndex() {
- var physicalOffset;
-
- if (this._firstVisibleIndexVal === null) {
- physicalOffset = this._physicalTop;
-
- this._firstVisibleIndexVal = this._iterateItems(
- function(pidx, vidx) {
- physicalOffset += this._physicalSizes[pidx];
-
- if (physicalOffset > this._scrollPosition) {
- return vidx;
- }
- }) || 0;
+ get _scrollTop() {
+ if (this._isValidScrollTarget()) {
+ return this.scrollTarget === this._doc ? window.pageYOffset : this.scrollTarget.scrollTop;
}
-
- return this._firstVisibleIndexVal;
+ return 0;
},
- ready: function() {
- if (IOS_TOUCH_SCROLLING) {
- this._scrollListener = function() {
- requestAnimationFrame(this._scrollHandler.bind(this));
- }.bind(this);
- } else {
- this._scrollListener = this._scrollHandler.bind(this);
+ /**
+ * Gets the number of pixels that the content of an element is scrolled to the left.
+ *
+ * @type {number}
+ */
+ get _scrollLeft() {
+ if (this._isValidScrollTarget()) {
+ return this.scrollTarget === this._doc ? window.pageXOffset : this.scrollTarget.scrollLeft;
}
+ return 0;
},
/**
- * When the element has been attached to the DOM tree.
+ * Sets the number of pixels that the content of an element is scrolled upward.
+ *
+ * @type {number}
*/
- attached: function() {
- // delegate to the parent's scroller
- // e.g. paper-scroll-header-panel
- var el = Polymer.dom(this);
-
- var parentNode = /** @type {?{scroller: ?Element}} */ (el.parentNode);
- if (parentNode && parentNode.scroller) {
- this._scroller = parentNode.scroller;
- } else {
- this._scroller = this;
- this.classList.add('has-scroller');
+ set _scrollTop(top) {
+ if (this.scrollTarget === this._doc) {
+ window.scrollTo(window.pageXOffset, top);
+ } else if (this._isValidScrollTarget()) {
+ this.scrollTarget.scrollTop = top;
}
+ },
- if (IOS_TOUCH_SCROLLING) {
- this._scroller.style.webkitOverflowScrolling = 'touch';
+ /**
+ * Sets the number of pixels that the content of an element is scrolled to the left.
+ *
+ * @type {number}
+ */
+ set _scrollLeft(left) {
+ if (this.scrollTarget === this._doc) {
+ window.scrollTo(left, window.pageYOffset);
+ } else if (this._isValidScrollTarget()) {
+ this.scrollTarget.scrollLeft = left;
}
+ },
- this._scroller.addEventListener('scroll', this._scrollListener);
-
- this.updateViewportBoundaries();
- this._render();
+ /**
+ * Scrolls the content to a particular place.
+ *
+ * @method scroll
+ * @param {number} top The top position
+ * @param {number} left The left position
+ */
+ scroll: function(top, left) {
+ if (this.scrollTarget === this._doc) {
+ window.scrollTo(top, left);
+ } else if (this._isValidScrollTarget()) {
+ this.scrollTarget.scrollTop = top;
+ this.scrollTarget.scrollLeft = left;
+ }
},
/**
- * When the element has been removed from the DOM tree.
+ * Gets the width of the scroll target.
+ *
+ * @type {number}
*/
- detached: function() {
- this._itemsRendered = false;
- if (this._scroller) {
- this._scroller.removeEventListener('scroll', this._scrollListener);
+ get _scrollTargetWidth() {
+ if (this._isValidScrollTarget()) {
+ return this.scrollTarget === this._doc ? window.innerWidth : this.scrollTarget.offsetWidth;
}
+ return 0;
},
/**
- * Invoke this method if you dynamically update the viewport's
- * size or CSS padding.
+ * Gets the height of the scroll target.
*
- * @method updateViewportBoundaries
+ * @type {number}
*/
- updateViewportBoundaries: function() {
- var scrollerStyle = window.getComputedStyle(this._scroller);
- this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10);
- this._viewportSize = this._scroller.offsetHeight;
+ get _scrollTargetHeight() {
+ if (this._isValidScrollTarget()) {
+ return this.scrollTarget === this._doc ? window.innerHeight : this.scrollTarget.offsetHeight;
+ }
+ return 0;
},
/**
- * Update the models, the position of the
- * items in the viewport and recycle tiles as needed.
+ * Returns true if the scroll target is a valid HTMLElement.
+ *
+ * @return {boolean}
*/
- _refresh: function() {
- // clamp the `scrollTop` value
- // IE 10|11 scrollTop may go above `_maxScrollTop`
- // iOS `scrollTop` may go below 0 and above `_maxScrollTop`
- var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scroller.scrollTop));
- var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBottom;
- var ratio = this._ratio;
- var delta = scrollTop - this._scrollPosition;
- var recycledTiles = 0;
- var hiddenContentSize = this._hiddenContentSize;
- var currentRatio = ratio;
- var movingUp = [];
+ _isValidScrollTarget: function() {
+ return this.scrollTarget instanceof HTMLElement;
+ }
+ };
+(function() {
- // track the last `scrollTop`
- this._scrollPosition = scrollTop;
+ 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';
- // clear cached visible index
- this._firstVisibleIndexVal = null;
+ Polymer({
- scrollBottom = this._scrollBottom;
- physicalBottom = this._physicalBottom;
+ is: 'iron-list',
- // 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;
-
- recycledTileSet = [];
-
- kth = this._physicalEnd;
- currentRatio = topSpace / hiddenContentSize;
-
- // 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
- ) {
+ properties: {
- tileHeight = this._physicalSizes[kth];
- currentRatio += tileHeight / hiddenContentSize;
- physicalBottom -= tileHeight;
- recycledTileSet.push(kth);
- recycledTiles++;
- kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
- }
+ /**
+ * 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
+ },
- movingUp = recycledTileSet;
- recycledTiles = -recycledTiles;
- }
- // scroll down
- else if (delta > 0) {
- var bottomSpace = physicalBottom - scrollBottom;
- var virtualEnd = this._virtualEnd;
- var lastVirtualItemIndex = this._virtualCount-1;
+ /**
+ * 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'
+ },
- recycledTileSet = [];
+ /**
+ * The name of the variable to add to the binding scope with the index
+ * for the row.
+ */
+ indexAs: {
+ type: String,
+ value: 'index'
+ },
- kth = this._physicalStart;
- currentRatio = bottomSpace / hiddenContentSize;
+ /**
+ * The name of the variable to add to the binding scope to indicate
+ * if the row is selected.
+ */
+ selectedAs: {
+ type: String,
+ value: 'selected'
+ },
- // 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
- ) {
+ /**
+ * 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
+ },
- tileHeight = this._physicalSizes[kth];
- currentRatio += tileHeight / hiddenContentSize;
+ /**
+ * When `multiSelection` is false, this is the currently selected item, or `null`
+ * if no item is selected.
+ */
+ selectedItem: {
+ type: Object,
+ notify: true
+ },
- this._physicalTop += tileHeight;
- recycledTileSet.push(kth);
- recycledTiles++;
- kth = (kth + 1) % this._physicalCount;
- }
- }
+ /**
+ * When `multiSelection` is true, this is an array that contains the selected items.
+ */
+ selectedItems: {
+ type: Object,
+ notify: true
+ },
- 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._update(recycledTileSet, movingUp);
+ /**
+ * 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
}
},
- /**
- * Update the list of items, starting from the `_virtualStartVal` item.
- * @param {!Array<number>=} itemSet
- * @param {!Array<number>=} movingUp
- */
- _update: function(itemSet, movingUp) {
- // update models
- this._assignModels(itemSet);
-
- // measure heights
- this._updateMetrics(itemSet);
+ observers: [
+ '_itemsChanged(items.*)',
+ '_selectionEnabledChanged(selectionEnabled)',
+ '_multiSelectionChanged(multiSelection)',
+ '_setOverflow(scrollTarget)'
+ ],
- // adjust offset after measuring
- if (movingUp) {
- while (movingUp.length) {
- this._physicalTop -= this._physicalSizes[movingUp.pop()];
- }
- }
- // update the position of the items
- this._positionItems();
+ behaviors: [
+ Polymer.Templatizer,
+ Polymer.IronResizableBehavior,
+ Polymer.IronA11yKeysBehavior,
+ Polymer.IronScrollTargetBehavior
+ ],
- // set the scroller size
- this._updateScrollerSize();
+ listeners: {
+ 'iron-resize': '_resizeHandler'
+ },
- // increase the pool of physical items
- this._increasePoolIfNeeded();
+ keyBindings: {
+ 'up': '_didMoveUp',
+ 'down': '_didMoveDown',
+ 'enter': '_didEnter'
},
/**
- * Creates a pool of DOM elements and attaches them to the local dom.
+ * 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.
*/
- _createPool: function(size) {
- var physicalItems = new Array(size);
+ _ratio: 0.5,
- this._ensureTemplatized();
+ /**
+ * The padding-top value of the `scroller` element
+ */
+ _scrollerPaddingTop: 0,
- 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 value is the same as `scrollTop`.
+ */
+ _scrollPosition: 0,
- return physicalItems;
- },
+ /**
+ * The number of tiles in the DOM.
+ */
+ _physicalCount: 0,
/**
- * Increases the pool of physical items only if needed.
- * This function will allocate additional physical items
- * if the physical size is shorter than `_optPhysicalSize`
+ * The k-th tile that is at the top of the scrolling list.
*/
- _increasePoolIfNeeded: function() {
- if (this._viewportSize !== 0 && this._physicalSize < this._optPhysicalSize) {
- // 0 <= `currentPage` <= `_maxPages`
- var currentPage = Math.floor(this._physicalSize / this._viewportSize);
-
- if (currentPage === 0) {
- // fill the first page
- this.async(this._increasePool.bind(this, Math.round(this._physicalCount * 0.5)));
- } else if (this._lastPage !== currentPage) {
- // once a page is filled up, paint it and defer the next increase
- requestAnimationFrame(this._increasePool.bind(this, 1));
- } else {
- // fill the rest of the pages
- this.async(this._increasePool.bind(this, 1));
- }
- this._lastPage = currentPage;
- return true;
- }
- return false;
- },
+ _physicalStart: 0,
/**
- * Increases the pool size.
+ * The k-th tile that is at the bottom of the scrolling list.
*/
- _increasePool: function(missingItems) {
- // limit the size
- var nextPhysicalCount = Math.min(
- this._physicalCount + missingItems,
- this._virtualCount,
- MAX_PHYSICAL_COUNT
- );
- var prevPhysicalCount = this._physicalCount;
- var delta = nextPhysicalCount - prevPhysicalCount;
+ _physicalEnd: 0,
- if (delta > 0) {
- [].push.apply(this._physicalItems, this._createPool(delta));
- [].push.apply(this._physicalSizes, new Array(delta));
+ /**
+ * The sum of the heights of all the tiles in the DOM.
+ */
+ _physicalSize: 0,
- this._physicalCount = prevPhysicalCount + delta;
- // tail call
- return this._update();
- }
- },
+ /**
+ * The average `F` of the tiles observed till now.
+ */
+ _physicalAverage: 0,
/**
- * 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.
+ * The number of tiles which `offsetHeight` > 0 observed until now.
*/
- _render: function() {
- var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
+ _physicalAverageCount: 0,
- if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) {
- this._lastPage = 0;
- this._update();
- this._itemsRendered = true;
- }
- },
+ /**
+ * The Y position of the item rendered in the `_physicalStart`
+ * tile relative to the scrolling list.
+ */
+ _physicalTop: 0,
/**
- * Templetizes the user template.
+ * The number of items in the list.
*/
- _ensureTemplatized: function() {
- if (!this.ctor) {
- // Template instance props that should be excluded from forwarding
- var props = {};
+ _virtualCount: 0,
- props.__key__ = true;
- props[this.as] = true;
- props[this.indexAs] = true;
- props[this.selectedAs] = true;
+ /**
+ * The n-th item rendered in the `_physicalStart` tile.
+ */
+ _virtualStartVal: 0,
- this._instanceProps = props;
- this._userTemplate = Polymer.dom(this).querySelector('template');
+ /**
+ * A map between an item key and its physical item index
+ */
+ _physicalIndexForKey: null,
- if (this._userTemplate) {
- this.templatize(this._userTemplate);
- } else {
- console.warn('iron-list requires a template to be provided in light-dom');
- }
- }
- },
+ /**
+ * The estimated scroll height based on `_physicalAverage`
+ */
+ _estScrollHeight: 0,
/**
- * Implements extension point from Templatizer mixin.
+ * The scroll height of the dom node
*/
- _getStampedChildren: function() {
- return this._physicalItems;
- },
+ _scrollHeight: 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.
+ * The height of the list. This is referred as the viewport in the context of list.
*/
- _forwardInstancePath: function(inst, path, value) {
- if (path.indexOf(this.as + '.') === 0) {
- this.notifyPath('items.' + inst.__key__ + '.' +
- path.slice(this.as.length + 1), value);
- }
- },
+ _viewportSize: 0,
/**
- * Implements extension point from Templatizer mixin
- * Called as side-effect of a host property change, responsible for
- * notifying parent path change on each row.
+ * An array of DOM nodes that are currently in the tree
+ * @type {?Array<!TemplatizerNode>}
*/
- _forwardParentProp: function(prop, value) {
- if (this._physicalItems) {
- this._physicalItems.forEach(function(item) {
- item._templateInstance[prop] = value;
- }, this);
- }
- },
+ _physicalItems: 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.
+ * An array of heights for each item in `_physicalItems`
+ * @type {?Array<number>}
*/
- _forwardParentPath: function(path, value) {
- if (this._physicalItems) {
- this._physicalItems.forEach(function(item) {
- item._templateInstance.notifyPath(path, value, true);
- }, this);
- }
- },
+ _physicalSizes: null,
/**
- * Called as a side effect of a host items.<key>.<path> path change,
- * responsible for notifying item.<path> changes to row for key.
+ * A cached value for the first visible index.
+ * See `firstVisibleIndex`
+ * @type {?number}
*/
- _forwardItemPath: function(path, value) {
- if (this._physicalIndexForKey) {
- var dot = path.indexOf('.');
- var key = path.substring(0, dot < 0 ? path.length : dot);
- var idx = this._physicalIndexForKey[key];
- var row = this._physicalItems[idx];
- if (row) {
- var inst = row._templateInstance;
- if (dot >= 0) {
- path = this.as + '.' + path.substring(dot+1);
- inst.notifyPath(path, value, true);
- } else {
- inst[this.as] = value;
- }
- }
- }
- },
+ _firstVisibleIndexVal: null,
/**
- * Called when the items have changed. That is, ressignments
- * to `items`, splices or updates to a single item.
+ * A cached value for the last visible index.
+ * See `lastVisibleIndex`
+ * @type {?number}
*/
- _itemsChanged: function(change) {
- if (change.path === 'items') {
- // render the new set
- this._itemsRendered = false;
+ _lastVisibleIndexVal: null,
- // update the whole set
- this._virtualStartVal = 0;
- this._physicalTop = 0;
- this._virtualCount = this.items ? this.items.length : 0;
- this._collection = this.items ? Polymer.Collection.get(this.items) : null;
- this._physicalIndexForKey = {};
- // scroll to the top
- this._resetScrollPosition(0);
+ /**
+ * A Polymer collection for the items.
+ * @type {?Polymer.Collection}
+ */
+ _collection: 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);
- }
+ /**
+ * True if the current item list was rendered for the first time
+ * after attached.
+ */
+ _itemsRendered: false,
- this.debounce('refresh', this._render);
+ /**
+ * The page that is currently rendered.
+ */
+ _lastPage: null,
- } else if (change.path === 'items.splices') {
- // render the new set
- this._itemsRendered = false;
+ /**
+ * The max number of pages to render. One page is equivalent to the height of the list.
+ */
+ _maxPages: 3,
- this._adjustVirtualIndex(change.value.indexSplices);
- this._virtualCount = this.items ? this.items.length : 0;
+ /**
+ * The currently focused item index.
+ */
+ _focusedIndex: 0,
- this.debounce('refresh', this._render);
+ /**
+ * The the item that is focused if it is moved offscreen.
+ * @private {?TemplatizerNode}
+ */
+ _offscreenFocusedItem: null,
- } else {
- // update a single item
- this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.value);
- }
- },
+ /**
+ * The item that backfills the `_offscreenFocusedItem` in the physical items
+ * list when that item is moved offscreen.
+ */
+ _focusBackfillItem: null,
/**
- * @param {!Array<!PolymerSplice>} splices
+ * The bottom of the physical content.
*/
- _adjustVirtualIndex: function(splices) {
- var i, splice, idx;
+ get _physicalBottom() {
+ return this._physicalTop + this._physicalSize;
+ },
- for (i = 0; i < splices.length; i++) {
- splice = splices[i];
+ /**
+ * The bottom of the scroll.
+ */
+ get _scrollBottom() {
+ return this._scrollPosition + this._viewportSize;
+ },
- // deselect removed items
- splice.removed.forEach(this.$.selector.deselect, this.$.selector);
+ /**
+ * The n-th item rendered in the last physical item.
+ */
+ get _virtualEnd() {
+ return this._virtualStart + this._physicalCount - 1;
+ },
- idx = splice.index;
- // We only need to care about changes happening above the current position
- if (idx >= this._virtualStartVal) {
- break;
- }
+ /**
+ * The lowest n-th value for an item such that it can be rendered in `_physicalStart`.
+ */
+ _minVirtualStart: 0,
- this._virtualStart = this._virtualStart +
- Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStartVal);
- }
+ /**
+ * 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);
},
- _scrollHandler: function() {
- this._refresh();
+ /**
+ * The height of the physical content that isn't on the screen.
+ */
+ get _hiddenContentSize() {
+ return this._physicalSize - this._viewportSize;
},
/**
- * 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
+ * The maximum scroll top value.
*/
- _iterateItems: function(fn, itemSet) {
- var pidx, vidx, rtn, i;
+ get _maxScrollTop() {
+ return this._estScrollHeight - this._viewportSize;
+ },
- if (arguments.length === 2 && itemSet) {
- for (i = 0; i < itemSet.length; i++) {
- pidx = itemSet[i];
- if (pidx >= this._physicalStart) {
- vidx = this._virtualStartVal + (pidx - this._physicalStart);
- } else {
- vidx = this._virtualStartVal + (this._physicalCount - this._physicalStart) + pidx;
- }
- if ((rtn = fn.call(this, pidx, vidx)) != null) {
- return rtn;
- }
- }
+ /**
+ * Sets the n-th item rendered in `_physicalStart`
+ */
+ set _virtualStart(val) {
+ // clamp the value so that _minVirtualStart <= val <= _maxVirtualStart
+ this._virtualStartVal = Math.min(this._maxVirtualStart, Math.max(this._minVirtualStart, val));
+ if (this._physicalCount === 0) {
+ this._physicalStart = 0;
+ this._physicalEnd = 0;
} else {
- pidx = this._physicalStart;
- vidx = this._virtualStartVal;
-
- for (; pidx < this._physicalCount; pidx++, vidx++) {
- if ((rtn = fn.call(this, pidx, vidx)) != null) {
- return rtn;
- }
- }
-
- pidx = 0;
-
- for (; pidx < this._physicalStart; pidx++, vidx++) {
- if ((rtn = fn.call(this, pidx, vidx)) != null) {
- return rtn;
- }
- }
+ this._physicalStart = this._virtualStartVal % this._physicalCount;
+ this._physicalEnd = (this._physicalStart + this._physicalCount - 1) % this._physicalCount;
}
},
/**
- * Assigns the data models to a given set of items.
- * @param {!Array<number>=} itemSet
+ * Gets the n-th item rendered in `_physicalStart`
*/
- _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) {
- inst[this.as] = item;
- inst.__key__ = this._collection.getKey(item);
- inst[this.selectedAs] =
- /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item);
- inst[this.indexAs] = vidx;
- el.removeAttribute('hidden');
- this._physicalIndexForKey[inst.__key__] = pidx;
- } else {
- inst.__key__ = null;
- el.setAttribute('hidden', '');
- }
-
- }, itemSet);
+ get _virtualStart() {
+ return this._virtualStartVal;
},
/**
- * Updates the height for a given set of items.
+ * An optimal physical size such that we will have enough physical items
+ * to fill up the viewport and recycle when the user scrolls.
*
- * @param {!Array<number>=} itemSet
+ * 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.
*/
- _updateMetrics: function(itemSet) {
- var newPhysicalSize = 0;
- var oldPhysicalSize = 0;
- var prevAvgCount = this._physicalAverageCount;
- var prevPhysicalAvg = this._physicalAverage;
- // Make sure we distributed all the physical items
- // so we can measure them
- Polymer.dom.flush();
+ get _optPhysicalSize() {
+ return this._viewportSize * this._maxPages;
+ },
- 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);
+ /**
+ * True if the current list is visible.
+ */
+ get _isVisible() {
+ return this.scrollTarget && Boolean(this.scrollTarget.offsetWidth || this.scrollTarget.offsetHeight);
+ },
- this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize;
- this._viewportSize = this._scroller.offsetHeight;
+ /**
+ * Gets the index of the first visible item in the viewport.
+ *
+ * @type {number}
+ */
+ get firstVisibleIndex() {
+ if (this._firstVisibleIndexVal === null) {
+ var physicalOffset = this._physicalTop;
- // update the average if we measured something
- if (this._physicalAverageCount !== prevAvgCount) {
- this._physicalAverage = Math.round(
- ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
- this._physicalAverageCount);
+ this._firstVisibleIndexVal = this._iterateItems(
+ function(pidx, vidx) {
+ physicalOffset += this._physicalSizes[pidx];
+
+ if (physicalOffset > this._scrollPosition) {
+ return vidx;
+ }
+ }) || 0;
}
+ return this._firstVisibleIndexVal;
},
/**
- * Updates the position of the physical items.
+ * Gets the index of the last visible item in the viewport.
+ *
+ * @type {number}
*/
- _positionItems: function() {
- this._adjustScrollPosition();
+ get lastVisibleIndex() {
+ if (this._lastVisibleIndexVal === null) {
+ var physicalOffset = this._physicalTop;
- var y = this._physicalTop;
-
- this._iterateItems(function(pidx) {
+ this._iterateItems(function(pidx, vidx) {
+ physicalOffset += this._physicalSizes[pidx];
- this.transform('translate3d(0, ' + y + 'px, 0)', this._physicalItems[pidx]);
- y += this._physicalSizes[pidx];
+ if(physicalOffset <= this._scrollBottom) {
+ this._lastVisibleIndexVal = vidx;
+ }
+ });
+ }
+ return this._lastVisibleIndexVal;
+ },
- });
+ ready: function() {
+ this.addEventListener('focus', this._didFocus.bind(this), true);
},
- /**
- * Adjusts the scroll position when it was overestimated.
- */
- _adjustScrollPosition: function() {
- var deltaHeight = this._virtualStartVal === 0 ? this._physicalTop :
- Math.min(this._scrollPosition + this._physicalTop, 0);
+ attached: function() {
+ this.updateViewportBoundaries();
+ this._render();
+ },
- if (deltaHeight) {
- this._physicalTop = this._physicalTop - deltaHeight;
+ detached: function() {
+ this._itemsRendered = false;
+ },
- // juking scroll position during interial scrolling on iOS is no bueno
- if (!IOS_TOUCH_SCROLLING) {
- this._resetScrollPosition(this._scroller.scrollTop - deltaHeight);
- }
- }
+ get _defaultScrollTarget() {
+ return this;
},
/**
- * Sets the position of the scroll.
+ * Set the overflow property if this element has its own scrolling region
*/
- _resetScrollPosition: function(pos) {
- if (this._scroller) {
- this._scroller.scrollTop = pos;
- this._scrollPosition = this._scroller.scrollTop;
- }
+ _setOverflow: function(scrollTarget) {
+ this.style.webkitOverflowScrolling = scrollTarget === this ? 'touch' : '';
+ this.style.overflow = scrollTarget === this ? 'auto' : '';
},
/**
- * Sets the scroll height, that's the height of the content,
+ * Invoke this method if you dynamically update the viewport's
+ * size or CSS padding.
*
- * @param {boolean=} forceUpdate If true, updates the height no matter what.
+ * @method updateViewportBoundaries
*/
- _updateScrollerSize: function(forceUpdate) {
- this._estScrollHeight = (this._physicalBottom +
- Math.max(this._virtualCount - this._physicalCount - this._virtualStartVal, 0) * this._physicalAverage);
-
- 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;
- }
+ updateViewportBoundaries: function() {
+ var scrollerStyle = window.getComputedStyle(this.scrollTarget);
+ this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10);
+ this._viewportSize = this._scrollTargetHeight;
},
/**
- * 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
+ * Update the models, the position of the
+ * items in the viewport and recycle tiles as needed.
*/
- scrollToIndex: function(idx) {
- if (typeof idx !== 'number') {
- return;
- }
+ _scrollHandler: function() {
+ // clamp the `scrollTop` value
+ // IE 10|11 scrollTop may go above `_maxScrollTop`
+ // iOS `scrollTop` may go below 0 and above `_maxScrollTop`
+ var scrollTop = Math.max(0, Math.min(this._maxScrollTop, this._scrollTop));
+ var tileHeight, tileTop, kth, recycledTileSet, scrollBottom, physicalBottom;
+ var ratio = this._ratio;
+ var delta = scrollTop - this._scrollPosition;
+ var recycledTiles = 0;
+ var hiddenContentSize = this._hiddenContentSize;
+ var currentRatio = ratio;
+ var movingUp = [];
- var firstVisible = this.firstVisibleIndex;
+ // track the last `scrollTop`
+ this._scrollPosition = scrollTop;
- idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
+ // clear cached visible index
+ this._firstVisibleIndexVal = null;
+ this._lastVisibleIndexVal = null;
- // start at the previous virtual item
- // so we have a item above the first visible item
- this._virtualStart = idx - 1;
+ scrollBottom = this._scrollBottom;
+ physicalBottom = this._physicalBottom;
- // assign new models
- this._assignModels();
+ // 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;
- // measure the new sizes
- this._updateMetrics();
+ recycledTileSet = [];
- // estimate new physical offset
- this._physicalTop = this._virtualStart * this._physicalAverage;
+ kth = this._physicalEnd;
+ currentRatio = topSpace / hiddenContentSize;
- var currentTopItem = this._physicalStart;
- var currentVirtualItem = this._virtualStart;
- var targetOffsetTop = 0;
- var hiddenContentSize = this._hiddenContentSize;
+ // 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
+ ) {
- // 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++;
+ tileHeight = this._physicalSizes[kth];
+ currentRatio += tileHeight / hiddenContentSize;
+ physicalBottom -= tileHeight;
+ recycledTileSet.push(kth);
+ recycledTiles++;
+ kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
+ }
+
+ movingUp = recycledTileSet;
+ recycledTiles = -recycledTiles;
}
+ // scroll down
+ else if (delta > 0) {
+ var bottomSpace = physicalBottom - scrollBottom;
+ var virtualEnd = this._virtualEnd;
+ var lastVirtualItemIndex = this._virtualCount-1;
- // update the scroller size
- this._updateScrollerSize(true);
+ recycledTileSet = [];
- // update the position of the items
- this._positionItems();
+ kth = this._physicalStart;
+ currentRatio = bottomSpace / hiddenContentSize;
- // set the new scroll position
- this._resetScrollPosition(this._physicalTop + targetOffsetTop + 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
+ ) {
- // increase the pool of physical items if needed
- this._increasePoolIfNeeded();
+ tileHeight = this._physicalSizes[kth];
+ currentRatio += tileHeight / hiddenContentSize;
- // clear cached visible index
- this._firstVisibleIndexVal = null;
+ this._physicalTop += tileHeight;
+ recycledTileSet.push(kth);
+ recycledTiles++;
+ kth = (kth + 1) % this._physicalCount;
+ }
+ }
+
+ 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._update(recycledTileSet, movingUp);
+ }
},
/**
- * Reset the physical average and the average count.
+ * Update the list of items, starting from the `_virtualStart` item.
+ * @param {!Array<number>=} itemSet
+ * @param {!Array<number>=} movingUp
*/
- _resetAverage: function() {
- this._physicalAverage = 0;
- this._physicalAverageCount = 0;
+ _update: function(itemSet, movingUp) {
+ // manage focus
+ if (this._isIndexRendered(this._focusedIndex)) {
+ this._restoreFocusedItem();
+ } else {
+ this._createFocusBackfillItem();
+ }
+ // 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();
},
/**
- * A handler for the `iron-resize` event triggered by `IronResizableBehavior`
- * when the element is resized.
+ * Creates a pool of DOM elements and attaches them to the local dom.
*/
- _resizeHandler: function() {
- this.debounce('resize', function() {
- this._render();
- if (this._itemsRendered && this._physicalItems && this._isVisible) {
- this._resetAverage();
- this.updateViewportBoundaries();
- this.scrollToIndex(this.firstVisibleIndex);
- }
- });
- },
+ _createPool: function(size) {
+ var physicalItems = new Array(size);
- _getModelFromItem: function(item) {
- var key = this._collection.getKey(item);
- var pidx = this._physicalIndexForKey[key];
+ this._ensureTemplatized();
- if (pidx !== undefined) {
- return this._physicalItems[pidx]._templateInstance;
+ 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 null;
+ return physicalItems;
},
/**
- * Gets a valid item instance from its index or the object value.
- *
- * @param {(Object|number)} item The item object or its index
+ * Increases the pool of physical items only if needed.
+ * This function will allocate additional physical items
+ * if the physical size is shorter than `_optPhysicalSize`
*/
- _getNormalizedItem: function(item) {
- if (typeof item === 'number') {
- item = this.items[item];
- if (!item) {
- throw new RangeError('<item> not found');
- }
- } else if (this._collection.getKey(item) === undefined) {
- throw new TypeError('<item> should be a valid item');
+ _increasePoolIfNeeded: function() {
+ if (this._viewportSize === 0 || this._physicalSize >= this._optPhysicalSize) {
+ return false;
}
- return item;
+ // 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;
},
/**
- * Select the list item at the given index.
- *
- * @method selectItem
- * @param {(Object|number)} item The item object or its index
+ * Increases the pool size.
*/
- selectItem: function(item) {
- item = this._getNormalizedItem(item);
- var model = this._getModelFromItem(item);
+ _increasePool: function(missingItems) {
+ // limit the size
+ var nextPhysicalCount = Math.min(
+ this._physicalCount + missingItems,
+ this._virtualCount - this._virtualStart,
+ MAX_PHYSICAL_COUNT
+ );
+ var prevPhysicalCount = this._physicalCount;
+ var delta = nextPhysicalCount - prevPhysicalCount;
- if (!this.multiSelection && this.selectedItem) {
- this.deselectItem(this.selectedItem);
- }
- if (model) {
- model[this.selectedAs] = true;
+ if (delta > 0) {
+ [].push.apply(this._physicalItems, this._createPool(delta));
+ [].push.apply(this._physicalSizes, new Array(delta));
+
+ this._physicalCount = prevPhysicalCount + delta;
+ // tail call
+ return this._update();
}
- this.$.selector.select(item);
},
/**
- * Deselects the given item list if it is already selected.
- *
-
- * @method deselect
- * @param {(Object|number)} item The item object or its index
+ * 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.
*/
- deselectItem: function(item) {
- item = this._getNormalizedItem(item);
- var model = this._getModelFromItem(item);
+ _render: function() {
+ var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
- if (model) {
- model[this.selectedAs] = false;
+ if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) {
+ this._lastPage = 0;
+ this._update();
+ this._scrollHandler();
+ this._itemsRendered = true;
}
- this.$.selector.deselect(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
+ * Templetizes the user template.
*/
- toggleSelectionForItem: function(item) {
- item = this._getNormalizedItem(item);
- if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item)) {
- this.deselectItem(item);
- } else {
- this.selectItem(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;
+
+ this._instanceProps = props;
+ this._userTemplate = Polymer.dom(this).querySelector('template');
+
+ if (this._userTemplate) {
+ this.templatize(this._userTemplate);
+ } else {
+ console.warn('iron-list requires a template to be provided in light-dom');
+ }
}
},
/**
- * Clears the current selection state of the list.
- *
- * @method clearSelection
+ * Implements extension point from Templatizer mixin.
*/
- clearSelection: function() {
- function unselect(item) {
- var model = this._getModelFromItem(item);
- if (model) {
- model[this.selectedAs] = false;
- }
- }
-
- if (Array.isArray(this.selectedItems)) {
- this.selectedItems.forEach(unselect, this);
- } else if (this.selectedItem) {
- unselect.call(this, this.selectedItem);
- }
-
- /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
+ _getStampedChildren: function() {
+ return this._physicalItems;
},
/**
- * Add an event listener to `tap` if `selectionEnabled` is true,
- * it will remove the listener otherwise.
+ * 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.
*/
- _selectionEnabledChanged: function(selectionEnabled) {
- if (selectionEnabled) {
- this.listen(this, 'tap', '_selectionHandler');
- this.listen(this, 'keypress', '_selectionHandler');
- } else {
- this.unlisten(this, 'tap', '_selectionHandler');
- this.unlisten(this, 'keypress', '_selectionHandler');
+ _forwardInstancePath: function(inst, path, value) {
+ if (path.indexOf(this.as + '.') === 0) {
+ this.notifyPath('items.' + inst.__key__ + '.' +
+ path.slice(this.as.length + 1), value);
}
},
/**
- * Select an item from an event object.
+ * Implements extension point from Templatizer mixin
+ * Called as side-effect of a host property change, responsible for
+ * notifying parent path change on each row.
*/
- _selectionHandler: function(e) {
- if (e.type !== 'keypress' || e.keyCode === 13) {
- var model = this.modelForElement(e.target);
- if (model) {
- this.toggleSelectionForItem(model[this.as]);
- }
+ _forwardParentProp: function(prop, value) {
+ if (this._physicalItems) {
+ this._physicalItems.forEach(function(item) {
+ item._templateInstance[prop] = value;
+ }, this);
}
},
- _multiSelectionChanged: function(multiSelection) {
- this.clearSelection();
- this.$.selector.multi = multiSelection;
+ /**
+ * Implements extension point from Templatizer
+ * Called as side-effect of a host path change, responsible for
+ * notifying parent.<path> path change on each row.
+ */
+ _forwardParentPath: function(path, value) {
+ if (this._physicalItems) {
+ this._physicalItems.forEach(function(item) {
+ item._templateInstance.notifyPath(path, value, true);
+ }, this);
+ }
},
/**
- * Updates the size of an item.
- *
- * @method updateSizeForItem
- * @param {(Object|number)} item The item object or its index
+ * Called as a side effect of a host items.<key>.<path> path change,
+ * responsible for notifying item.<path> changes to row for key.
*/
- updateSizeForItem: function(item) {
- item = this._getNormalizedItem(item);
- var key = this._collection.getKey(item);
- var pidx = this._physicalIndexForKey[key];
+ _forwardItemPath: function(path, value) {
+ if (this._physicalIndexForKey) {
+ var dot = path.indexOf('.');
+ var key = path.substring(0, dot < 0 ? path.length : dot);
+ var idx = this._physicalIndexForKey[key];
+ var row = this._physicalItems[idx];
- if (pidx !== undefined) {
- this._updateMetrics([pidx]);
- this._positionItems();
+ if (idx === this._focusedIndex && this._offscreenFocusedItem) {
+ row = this._offscreenFocusedItem;
+ }
+ if (row) {
+ var inst = row._templateInstance;
+ if (dot >= 0) {
+ path = this.as + '.' + path.substring(dot+1);
+ inst.notifyPath(path, value, true);
+ } else {
+ inst[this.as] = value;
+ }
+ }
}
- }
- });
+ },
-})();
-(function() {
+ /**
+ * Called when the items have changed. That is, ressignments
+ * to `items`, splices or updates to a single item.
+ */
+ _itemsChanged: function(change) {
+ if (change.path === 'items') {
- // monostate data
- var metaDatas = {};
- var metaArrays = {};
- var singleton = null;
+ this._restoreFocusedItem();
+ // render the new set
+ this._itemsRendered = false;
+ // update the whole set
+ this._virtualStart = 0;
+ this._physicalTop = 0;
+ this._virtualCount = this.items ? this.items.length : 0;
+ this._focusedIndex = 0;
+ this._collection = this.items ? Polymer.Collection.get(this.items) : null;
+ this._physicalIndexForKey = {};
- Polymer.IronMeta = Polymer({
+ this._resetScrollPosition(0);
- is: 'iron-meta',
+ // 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);
+ }
+ this._debounceTemplate(this._render);
- properties: {
+ } else if (change.path === 'items.splices') {
+ // render the new set
+ this._itemsRendered = false;
+ this._adjustVirtualIndex(change.value.indexSplices);
+ this._virtualCount = this.items ? this.items.length : 0;
- /**
- * The type of meta-data. All meta-data of the same type is stored
- * together.
- */
- type: {
- type: String,
- value: 'default',
- observer: '_typeChanged'
- },
+ this._debounceTemplate(this._render);
- /**
- * The key used to store `value` under the `type` namespace.
- */
- key: {
- type: String,
- observer: '_keyChanged'
- },
-
- /**
- * The meta-data to store or retrieve.
- */
- value: {
- type: Object,
- notify: true,
- observer: '_valueChanged'
- },
+ if (this._focusedIndex < 0 || this._focusedIndex >= this._virtualCount) {
+ this._focusedIndex = 0;
+ }
+ this._debounceTemplate(this._render);
- /**
- * If true, `value` is set to the iron-meta instance itself.
- */
- self: {
- type: Boolean,
- observer: '_selfChanged'
- },
+ } else {
+ // update a single item
+ this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.value);
+ }
+ },
- /**
- * Array of all meta-data values for the given type.
- */
- list: {
- type: Array,
- notify: true
- }
+ /**
+ * @param {!Array<!PolymerSplice>} splices
+ */
+ _adjustVirtualIndex: function(splices) {
+ var i, splice, idx;
- },
+ for (i = 0; i < splices.length; i++) {
+ splice = splices[i];
- hostAttributes: {
- hidden: true
- },
+ // deselect removed items
+ splice.removed.forEach(this.$.selector.deselect, this.$.selector);
- /**
- * Only runs if someone invokes the factory/constructor directly
- * e.g. `new Polymer.IronMeta()`
- *
- * @param {{type: (string|undefined), key: (string|undefined), value}=} config
- */
- factoryImpl: function(config) {
- if (config) {
- for (var n in config) {
- switch(n) {
- case 'type':
- case 'key':
- case 'value':
- this[n] = config[n];
- break;
- }
- }
+ idx = splice.index;
+ // We only need to care about changes happening above the current position
+ if (idx >= this._virtualStart) {
+ break;
}
- },
-
- created: function() {
- // TODO(sjmiles): good for debugging?
- this._metaDatas = metaDatas;
- this._metaArrays = metaArrays;
- },
- _keyChanged: function(key, old) {
- this._resetRegistration(old);
- },
+ this._virtualStart = this._virtualStart +
+ Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStart);
+ }
+ },
- _valueChanged: function(value) {
- this._resetRegistration(this.key);
- },
+ /**
+ * 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;
- _selfChanged: function(self) {
- if (self) {
- this.value = this;
+ 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;
- _typeChanged: function(type) {
- this._unregisterKey(this.key);
- if (!metaDatas[type]) {
- metaDatas[type] = {};
+ for (; pidx < this._physicalCount; pidx++, vidx++) {
+ if ((rtn = fn.call(this, pidx, vidx)) != null) {
+ return rtn;
+ }
}
- this._metaData = metaDatas[type];
- if (!metaArrays[type]) {
- metaArrays[type] = [];
+ for (pidx = 0; pidx < this._physicalStart; pidx++, vidx++) {
+ if ((rtn = fn.call(this, pidx, vidx)) != null) {
+ return rtn;
+ }
}
- this.list = metaArrays[type];
- this._registerKeyValue(this.key, this.value);
- },
-
- /**
- * Retrieves meta data value by key.
- *
- * @method byKey
- * @param {string} key The key of the meta-data to be returned.
- * @return {*}
- */
- byKey: function(key) {
- return this._metaData && this._metaData[key];
- },
-
- _resetRegistration: function(oldKey) {
- this._unregisterKey(oldKey);
- this._registerKeyValue(this.key, this.value);
- },
-
- _unregisterKey: function(key) {
- this._unregister(key, this._metaData, this.list);
- },
+ }
+ },
- _registerKeyValue: function(key, value) {
- this._register(key, value, this._metaData, this.list);
- },
+ /**
+ * Assigns the data models to a given set of items.
+ * @param {!Array<number>=} itemSet
+ */
+ _assignModels: function(itemSet) {
+ this._iterateItems(function(pidx, vidx) {
+ var el = this._physicalItems[pidx];
+ var inst = el._templateInstance;
+ var item = this.items && this.items[vidx];
- _register: function(key, value, data, list) {
- if (key && data && value !== undefined) {
- data[key] = value;
- list.push(value);
+ if (item !== undefined && item !== null) {
+ inst[this.as] = item;
+ inst.__key__ = this._collection.getKey(item);
+ inst[this.selectedAs] = /** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item);
+ inst[this.indexAs] = vidx;
+ inst.tabIndex = vidx === this._focusedIndex ? 0 : -1;
+ el.removeAttribute('hidden');
+ this._physicalIndexForKey[inst.__key__] = pidx;
+ } else {
+ inst.__key__ = null;
+ el.setAttribute('hidden', '');
}
- },
- _unregister: function(key, data, list) {
- if (key && data) {
- if (key in data) {
- var value = data[key];
- delete data[key];
- this.arrayDelete(list, value);
- }
- }
- }
+ }, itemSet);
+ },
- });
+ /**
+ * 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();
- Polymer.IronMeta.getIronMeta = function getIronMeta() {
- if (singleton === null) {
- singleton = new Polymer.IronMeta();
- }
- return singleton;
- };
+ var newPhysicalSize = 0;
+ var oldPhysicalSize = 0;
+ var prevAvgCount = this._physicalAverageCount;
+ var prevPhysicalAvg = this._physicalAverage;
- /**
- `iron-meta-query` can be used to access infomation stored in `iron-meta`.
+ this._iterateItems(function(pidx, vidx) {
- Examples:
+ oldPhysicalSize += this._physicalSizes[pidx] || 0;
+ this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
+ newPhysicalSize += this._physicalSizes[pidx];
+ this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
- If I create an instance like this:
+ }, itemSet);
- <iron-meta key="info" value="foo/bar"></iron-meta>
+ this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize;
+ this._viewportSize = this._scrollTargetHeight;
- Note that value="foo/bar" is the metadata I've defined. I could define more
- attributes or use child nodes to define additional metadata.
+ // update the average if we measured something
+ if (this._physicalAverageCount !== prevAvgCount) {
+ this._physicalAverage = Math.round(
+ ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
+ this._physicalAverageCount);
+ }
+ },
- Now I can access that element (and it's metadata) from any `iron-meta-query` instance:
+ /**
+ * Updates the position of the physical items.
+ */
+ _positionItems: function() {
+ this._adjustScrollPosition();
- var value = new Polymer.IronMetaQuery({key: 'info'}).value;
+ var y = this._physicalTop;
- @group Polymer Iron Elements
- @element iron-meta-query
- */
- Polymer.IronMetaQuery = Polymer({
+ this._iterateItems(function(pidx) {
- is: 'iron-meta-query',
+ this.translate3d(0, y + 'px', 0, this._physicalItems[pidx]);
+ y += this._physicalSizes[pidx];
- properties: {
+ });
+ },
- /**
- * The type of meta-data. All meta-data of the same type is stored
- * together.
- */
- type: {
- type: String,
- value: 'default',
- observer: '_typeChanged'
- },
+ /**
+ * Adjusts the scroll position when it was overestimated.
+ */
+ _adjustScrollPosition: function() {
+ var deltaHeight = this._virtualStart === 0 ? this._physicalTop :
+ Math.min(this._scrollPosition + this._physicalTop, 0);
- /**
- * Specifies a key to use for retrieving `value` from the `type`
- * namespace.
- */
- key: {
- type: String,
- observer: '_keyChanged'
- },
-
- /**
- * The meta-data to store or retrieve.
- */
- value: {
- type: Object,
- notify: true,
- readOnly: true
- },
-
- /**
- * Array of all meta-data values for the given type.
- */
- list: {
- type: Array,
- notify: true
- }
-
- },
-
- /**
- * Actually a factory method, not a true constructor. Only runs if
- * someone invokes it directly (via `new Polymer.IronMeta()`);
- *
- * @param {{type: (string|undefined), key: (string|undefined)}=} config
- */
- factoryImpl: function(config) {
- if (config) {
- for (var n in config) {
- switch(n) {
- case 'type':
- case 'key':
- this[n] = config[n];
- break;
- }
- }
+ 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);
}
- },
+ }
+ },
- created: function() {
- // TODO(sjmiles): good for debugging?
- this._metaDatas = metaDatas;
- this._metaArrays = metaArrays;
- },
+ /**
+ * Sets the position of the scroll.
+ */
+ _resetScrollPosition: function(pos) {
+ if (this.scrollTarget) {
+ this._scrollTop = pos;
+ this._scrollPosition = this._scrollTop;
+ }
+ },
- _keyChanged: function(key) {
- this._setValue(this._metaData && this._metaData[key]);
- },
+ /**
+ * 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);
- _typeChanged: function(type) {
- this._metaData = metaDatas[type];
- this.list = metaArrays[type];
- if (this.key) {
- this._keyChanged(this.key);
- }
- },
+ forceUpdate = forceUpdate || this._scrollHeight === 0;
+ forceUpdate = forceUpdate || this._scrollPosition >= this._estScrollHeight - this._physicalSize;
- /**
- * Retrieves meta data value by key.
- * @param {string} key The key of the meta-data to be returned.
- * @return {*}
- */
- byKey: function(key) {
- return this._metaData && this._metaData[key];
+ // 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;
+ }
- })();
-Polymer({
+ Polymer.dom.flush();
- is: 'iron-icon',
+ var firstVisible = this.firstVisibleIndex;
+ idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
- properties: {
+ // start at the previous virtual item
+ // so we have a item above the first visible item
+ this._virtualStart = idx - 1;
+ // assign new models
+ this._assignModels();
+ // measure the new sizes
+ this._updateMetrics();
+ // estimate new physical offset
+ this._physicalTop = this._virtualStart * this._physicalAverage;
- /**
- * The name of the icon to use. The name should be of the form:
- * `iconset_name:icon_name`.
- */
- icon: {
- type: String,
- observer: '_iconChanged'
- },
+ var currentTopItem = this._physicalStart;
+ var currentVirtualItem = this._virtualStart;
+ var targetOffsetTop = 0;
+ var hiddenContentSize = this._hiddenContentSize;
- /**
- * The name of the theme to used, if one is specified by the
- * iconset.
- */
- theme: {
- type: String,
- observer: '_updateIcon'
- },
+ // 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;
+ },
- /**
- * If using iron-icon without an iconset, you can set the src to be
- * the URL of an individual icon image file. Note that this will take
- * precedence over a given icon attribute.
- */
- src: {
- type: String,
- observer: '_srcChanged'
- },
+ /**
+ * Reset the physical average and the average count.
+ */
+ _resetAverage: function() {
+ this._physicalAverage = 0;
+ this._physicalAverageCount = 0;
+ },
- /**
- * @type {!Polymer.IronMeta}
- */
- _meta: {
- value: Polymer.Base.create('iron-meta', {type: 'iconset'})
+ /**
+ * 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);
}
+ });
+ },
- },
-
- _DEFAULT_ICONSET: 'icons',
-
- _iconChanged: function(icon) {
- var parts = (icon || '').split(':');
- this._iconName = parts.pop();
- this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
- this._updateIcon();
- },
-
- _srcChanged: function(src) {
- this._updateIcon();
- },
+ _getModelFromItem: function(item) {
+ var key = this._collection.getKey(item);
+ var pidx = this._physicalIndexForKey[key];
- _usesIconset: function() {
- return this.icon || !this.src;
- },
+ if (pidx !== undefined) {
+ return this._physicalItems[pidx]._templateInstance;
+ }
+ return null;
+ },
- /** @suppress {visibility} */
- _updateIcon: function() {
- if (this._usesIconset()) {
- if (this._iconsetName) {
- this._iconset = /** @type {?Polymer.Iconset} */ (
- this._meta.byKey(this._iconsetName));
- if (this._iconset) {
- this._iconset.applyIcon(this, this._iconName, this.theme);
- this.unlisten(window, 'iron-iconset-added', '_updateIcon');
- } else {
- this.listen(window, 'iron-iconset-added', '_updateIcon');
- }
- }
- } else {
- if (!this._img) {
- this._img = document.createElement('img');
- this._img.style.width = '100%';
- this._img.style.height = '100%';
- this._img.draggable = false;
+ /**
+ * 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');
}
- this._img.src = this.src;
- Polymer.dom(this.root).appendChild(this._img);
+ return item;
}
+ throw new TypeError('<item> should be a valid item');
}
+ return item;
+ },
- });
-/**
- * The `iron-iconset-svg` element allows users to define their own icon sets
- * that contain svg icons. The svg icon elements should be children of the
- * `iron-iconset-svg` element. Multiple icons should be given distinct id's.
- *
- * Using svg elements to create icons has a few advantages over traditional
- * bitmap graphics like jpg or png. Icons that use svg are vector based so
- * they are resolution independent and should look good on any device. They
- * are stylable via css. Icons can be themed, colorized, and even animated.
- *
- * Example:
- *
- * <iron-iconset-svg name="my-svg-icons" size="24">
- * <svg>
- * <defs>
- * <g id="shape">
- * <rect x="12" y="0" width="12" height="24" />
- * <circle cx="12" cy="12" r="12" />
- * </g>
- * </defs>
- * </svg>
- * </iron-iconset-svg>
- *
- * This will automatically register the icon set "my-svg-icons" to the iconset
- * database. To use these icons from within another element, make a
- * `iron-iconset` element and call the `byId` method
- * to retrieve a given iconset. To apply a particular icon inside an
- * element use the `applyIcon` method. For example:
- *
- * iconset.applyIcon(iconNode, 'car');
- *
- * @element iron-iconset-svg
- * @demo demo/index.html
- * @implements {Polymer.Iconset}
- */
- Polymer({
- is: 'iron-iconset-svg',
-
- properties: {
-
- /**
- * The name of the iconset.
- */
- name: {
- type: String,
- observer: '_nameChanged'
- },
+ /**
+ * 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);
- /**
- * The size of an individual icon. Note that icons must be square.
- */
- size: {
- type: Number,
- value: 24
+ if (!this.multiSelection && this.selectedItem) {
+ this.deselectItem(this.selectedItem);
}
-
- },
-
- attached: function() {
- this.style.display = 'none';
+ if (model) {
+ model[this.selectedAs] = true;
+ }
+ this.$.selector.select(item);
+ this.updateSizeForItem(item);
},
/**
- * Construct an array of all icon names in this iconset.
+ * Deselects the given item list if it is already selected.
*
- * @return {!Array} Array of icon names.
+
+ * @method deselect
+ * @param {(Object|number)} item The item object or its index
*/
- getIconNames: function() {
- this._icons = this._createIconMap();
- return Object.keys(this._icons).map(function(n) {
- return this.name + ':' + n;
- }, this);
+ deselectItem: function(item) {
+ item = this._getNormalizedItem(item);
+ var model = this._getModelFromItem(item);
+
+ if (model) {
+ model[this.selectedAs] = false;
+ }
+ this.$.selector.deselect(item);
+ this.updateSizeForItem(item);
},
/**
- * Applies an icon to the given element.
- *
- * An svg icon is prepended to the element's shadowRoot if it exists,
- * otherwise to the element itself.
+ * Select or deselect a given item depending on whether the item
+ * has already been selected.
*
- * @method applyIcon
- * @param {Element} element Element to which the icon is applied.
- * @param {string} iconName Name of the icon to apply.
- * @return {?Element} The svg element which renders the icon.
+ * @method toggleSelectionForItem
+ * @param {(Object|number)} item The item object or its index
*/
- applyIcon: function(element, iconName) {
- // insert svg element into shadow root, if it exists
- element = element.root || element;
- // Remove old svg element
- this.removeIcon(element);
- // install new svg element
- var svg = this._cloneIcon(iconName);
- if (svg) {
- var pde = Polymer.dom(element);
- pde.insertBefore(svg, pde.childNodes[0]);
- return element._svgIcon = svg;
+ toggleSelectionForItem: function(item) {
+ item = this._getNormalizedItem(item);
+ if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item)) {
+ this.deselectItem(item);
+ } else {
+ this.selectItem(item);
}
- return null;
},
/**
- * Remove an icon from the given element by undoing the changes effected
- * by `applyIcon`.
+ * Clears the current selection state of the list.
*
- * @param {Element} element The element from which the icon is removed.
+ * @method clearSelection
*/
- removeIcon: function(element) {
- // Remove old svg element
- if (element._svgIcon) {
- Polymer.dom(element).removeChild(element._svgIcon);
- element._svgIcon = null;
+ clearSelection: function() {
+ function unselect(item) {
+ var model = this._getModelFromItem(item);
+ if (model) {
+ model[this.selectedAs] = false;
+ }
+ }
+
+ if (Array.isArray(this.selectedItems)) {
+ this.selectedItems.forEach(unselect, this);
+ } else if (this.selectedItem) {
+ unselect.call(this, this.selectedItem);
}
+
+ /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
},
/**
- *
- * When name is changed, register iconset metadata
- *
+ * Add an event listener to `tap` if `selectionEnabled` is true,
+ * it will remove the listener otherwise.
*/
- _nameChanged: function() {
- new Polymer.IronMeta({type: 'iconset', key: this.name, value: this});
- this.async(function() {
- this.fire('iron-iconset-added', this, {node: window});
- });
+ _selectionEnabledChanged: function(selectionEnabled) {
+ var handler = selectionEnabled ? this.listen : this.unlisten;
+ handler.call(this, this, 'tap', '_selectionHandler');
},
/**
- * Create a map of child SVG elements by id.
- *
- * @return {!Object} Map of id's to SVG elements.
+ * Select an item from an event object.
*/
- _createIconMap: function() {
- // Objects chained to Object.prototype (`{}`) have members. Specifically,
- // on FF there is a `watch` method that confuses the icon map, so we
- // need to use a null-based object here.
- var icons = Object.create(null);
- Polymer.dom(this).querySelectorAll('[id]')
- .forEach(function(icon) {
- icons[icon.id] = icon;
- });
- return icons;
+ _selectionHandler: function(e) {
+ if (this.selectionEnabled) {
+ var model = this.modelForElement(e.target);
+ if (model) {
+ this.toggleSelectionForItem(model[this.as]);
+ }
+ }
+ },
+
+ _multiSelectionChanged: function(multiSelection) {
+ this.clearSelection();
+ this.$.selector.multi = multiSelection;
},
/**
- * Produce installable clone of the SVG element matching `id` in this
- * iconset, or `undefined` if there is no matching element.
+ * Updates the size of an item.
*
- * @return {Element} Returns an installable clone of the SVG element
- * matching `id`.
+ * @method updateSizeForItem
+ * @param {(Object|number)} item The item object or its index
*/
- _cloneIcon: function(id) {
- // create the icon map on-demand, since the iconset itself has no discrete
- // signal to know when it's children are fully parsed
- this._icons = this._icons || this._createIconMap();
- return this._prepareSvgClone(this._icons[id], this.size);
+ updateSizeForItem: function(item) {
+ item = this._getNormalizedItem(item);
+ var key = this._collection.getKey(item);
+ var pidx = this._physicalIndexForKey[key];
+
+ if (pidx !== undefined) {
+ this._updateMetrics([pidx]);
+ this._positionItems();
+ }
},
- /**
- * @param {Element} sourceSvg
- * @param {number} size
- * @return {Element}
- */
- _prepareSvgClone: function(sourceSvg, size) {
- if (sourceSvg) {
- var content = sourceSvg.cloneNode(true),
- svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
- viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + size;
- svg.setAttribute('viewBox', viewBox);
- svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
- // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/370136
- // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root
- svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
- svg.appendChild(content).removeAttribute('id');
- return svg;
+ _isIndexRendered: function(idx) {
+ return idx >= this._virtualStart && idx <= this._virtualEnd;
+ },
+
+ _getPhysicalItemForIndex: function(idx, force) {
+ if (!this._collection) {
+ return null;
}
- return null;
- }
+ if (!this._isIndexRendered(idx)) {
+ if (force) {
+ this.scrollToIndex(idx);
+ return this._getPhysicalItemForIndex(idx, false);
+ }
+ return null;
+ }
+ var item = this._getNormalizedItem(idx);
+ var physicalItem = this._physicalItems[this._physicalIndexForKey[this._collection.getKey(item)]];
- });
-(function() {
- 'use strict';
+ return physicalItem || null;
+ },
- /**
- * 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
- */
- var KEY_IDENTIFIER = {
- 'U+0008': 'backspace',
- 'U+0009': 'tab',
- 'U+001B': 'esc',
- 'U+0020': 'space',
- 'U+007F': 'del'
- };
+ _focusPhysicalItem: function(idx) {
+ this._restoreFocusedItem();
- /**
- * 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: '*'
- };
+ var physicalItem = this._getPhysicalItemForIndex(idx, true);
+ if (!physicalItem) {
+ return;
+ }
+ var SECRET = ~(Math.random() * 100);
+ var model = physicalItem._templateInstance;
+ var focusable;
- /**
- * 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'
- };
+ model.tabIndex = SECRET;
+ // the focusable element could be the entire physical item
+ if (physicalItem.tabIndex === SECRET) {
+ focusable = physicalItem;
+ }
+ // the focusable element could be somewhere within the physical item
+ if (!focusable) {
+ focusable = Polymer.dom(physicalItem).querySelector('[tabindex="' + SECRET + '"]');
+ }
+ // restore the tab index
+ model.tabIndex = 0;
+ focusable && focusable.focus();
+ },
- /**
- * 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*]/;
+ _restoreFocusedItem: function() {
+ if (!this._offscreenFocusedItem) {
+ return;
+ }
+ var item = this._getNormalizedItem(this._focusedIndex);
+ var pidx = this._physicalIndexForKey[this._collection.getKey(item)];
- /**
- * Matches a keyIdentifier string.
- */
- var IDENT_CHAR = /U\+/;
+ if (pidx !== undefined) {
+ this.translate3d(0, HIDDEN_Y, 0, this._physicalItems[pidx]);
+ this._physicalItems[pidx] = this._offscreenFocusedItem;
+ }
+ this._offscreenFocusedItem = null;
+ },
- /**
- * Matches arrow keys in Gecko 27.0+
- */
- var ARROW_KEY = /^arrow/;
+ _removeFocusedItem: function() {
+ if (!this._offscreenFocusedItem) {
+ return;
+ }
+ Polymer.dom(this).removeChild(this._offscreenFocusedItem);
+ this._offscreenFocusedItem = null;
+ this._focusBackfillItem = null;
+ },
- /**
- * Matches space keys everywhere (notably including IE10's exceptional name
- * `spacebar`).
- */
- var SPACE_KEY = /^space(bar)?/;
+ _createFocusBackfillItem: function() {
+ if (this._offscreenFocusedItem) {
+ return;
+ }
+ var item = this._getNormalizedItem(this._focusedIndex);
+ var pidx = this._physicalIndexForKey[this._collection.getKey(item)];
- /**
- * 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 (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;
- }
+ this._offscreenFocusedItem = this._physicalItems[pidx];
+ this.translate3d(0, HIDDEN_Y, 0, this._offscreenFocusedItem);
+
+ if (!this._focusBackfillItem) {
+ var stampedTemplate = this.stamp(null);
+ this._focusBackfillItem = stampedTemplate.root.querySelector('*');
+ Polymer.dom(this).appendChild(stampedTemplate.root);
}
- return validKey;
- }
+ this._physicalItems[pidx] = this._focusBackfillItem;
+ },
- 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();
- }
+ _didFocus: function(e) {
+ var targetModel = this.modelForElement(e.target);
+ var fidx = this._focusedIndex;
+
+ if (!targetModel) {
+ return;
}
- return validKey;
- }
+ this._restoreFocusedItem();
- 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);
+ if (this.modelForElement(this._offscreenFocusedItem) === targetModel) {
+ this.scrollToIndex(fidx);
+ } else {
+ // restore tabIndex for the currently focused item
+ this._getModelFromItem(this._getNormalizedItem(fidx)).tabIndex = -1;
+ // set the tabIndex for the next focused item
+ targetModel.tabIndex = 0;
+ fidx = /** @type {{index: number}} */(targetModel).index;
+ this._focusedIndex = fidx;
+ // bring the item into view
+ if (fidx < this.firstVisibleIndex || fidx > this.lastVisibleIndex) {
+ this.scrollToIndex(fidx);
} else {
- validKey = KEY_CODE[keyCode];
+ this._update();
}
}
- 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) || '';
- }
+ _didMoveUp: function() {
+ this._focusPhysicalItem(Math.max(0, this._focusedIndex - 1));
+ },
- 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)
- );
+ _didMoveDown: function() {
+ this._focusPhysicalItem(Math.min(this._virtualCount, this._focusedIndex + 1));
+ },
+
+ _didEnter: function(e) {
+ // focus the currently focused physical item
+ this._focusPhysicalItem(this._focusedIndex);
+ // toggle selection
+ this._selectionHandler(/** @type {{keyboardEvent: Event}} */(e.detail).keyboardEvent);
}
+ });
- 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];
+})();
+(function() {
- if (keyName in MODIFIER_KEYS) {
- parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
- parsedKeyCombo.hasModifiers = true;
- } else {
- parsedKeyCombo.key = keyName;
- parsedKeyCombo.event = event || 'keydown';
- }
+ // monostate data
+ var metaDatas = {};
+ var metaArrays = {};
+ var singleton = null;
- return parsedKeyCombo;
- }, {
- combo: keyComboString.split(':').shift()
- });
- }
+ Polymer.IronMeta = Polymer({
- function parseEventString(eventString) {
- return eventString.trim().split(' ').map(function(keyComboString) {
- return parseKeyComboString(keyComboString);
- });
- }
+ is: 'iron-meta',
- /**
- * `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.
+ * The type of meta-data. All meta-data of the same type is stored
+ * together.
*/
- keyEventTarget: {
+ type: {
+ type: String,
+ value: 'default',
+ observer: '_typeChanged'
+ },
+
+ /**
+ * The key used to store `value` under the `type` namespace.
+ */
+ key: {
+ type: String,
+ observer: '_keyChanged'
+ },
+
+ /**
+ * The meta-data to store or retrieve.
+ */
+ value: {
type: Object,
- value: function() {
- return this;
- }
+ notify: true,
+ observer: '_valueChanged'
},
/**
- * If true, this property will cause the implementing element to
- * automatically stop propagation on any handled KeyboardEvents.
+ * If true, `value` is set to the iron-meta instance itself.
*/
- stopKeyboardEventPropagation: {
+ self: {
type: Boolean,
- value: false
+ observer: '_selfChanged'
},
- _boundKeyHandlers: {
+ /**
+ * Array of all meta-data values for the given type.
+ */
+ list: {
type: Array,
- value: function() {
- return [];
- }
- },
-
- // 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 {};
- }
+ notify: true
}
+
},
- observers: [
- '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
- ],
+ hostAttributes: {
+ hidden: true
+ },
- keyBindings: {},
+ /**
+ * Only runs if someone invokes the factory/constructor directly
+ * e.g. `new Polymer.IronMeta()`
+ *
+ * @param {{type: (string|undefined), key: (string|undefined), value}=} config
+ */
+ factoryImpl: function(config) {
+ if (config) {
+ for (var n in config) {
+ switch(n) {
+ case 'type':
+ case 'key':
+ case 'value':
+ this[n] = config[n];
+ break;
+ }
+ }
+ }
+ },
- registered: function() {
- this._prepKeyBindings();
+ created: function() {
+ // TODO(sjmiles): good for debugging?
+ this._metaDatas = metaDatas;
+ this._metaArrays = metaArrays;
},
- attached: function() {
- this._listenKeyEventListeners();
+ _keyChanged: function(key, old) {
+ this._resetRegistration(old);
},
- detached: function() {
- this._unlistenKeyEventListeners();
+ _valueChanged: function(value) {
+ this._resetRegistration(this.key);
},
- /**
- * 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();
+ _selfChanged: function(self) {
+ if (self) {
+ this.value = this;
+ }
+ },
+
+ _typeChanged: function(type) {
+ this._unregisterKey(this.key);
+ if (!metaDatas[type]) {
+ metaDatas[type] = {};
+ }
+ this._metaData = metaDatas[type];
+ if (!metaArrays[type]) {
+ metaArrays[type] = [];
+ }
+ this.list = metaArrays[type];
+ this._registerKeyValue(this.key, this.value);
},
/**
- * When called, will remove all imperatively-added key bindings.
+ * Retrieves meta data value by key.
+ *
+ * @method byKey
+ * @param {string} key The key of the meta-data to be returned.
+ * @return {*}
*/
- removeOwnKeyBindings: function() {
- this._imperativeKeyBindings = {};
- this._prepKeyBindings();
- this._resetKeyEventListeners();
+ byKey: function(key) {
+ return this._metaData && this._metaData[key];
},
- 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;
+ _resetRegistration: function(oldKey) {
+ this._unregisterKey(oldKey);
+ this._registerKeyValue(this.key, this.value);
},
- _collectKeyBindings: function() {
- var keyBindings = this.behaviors.map(function(behavior) {
- return behavior.keyBindings;
- });
-
- if (keyBindings.indexOf(this.keyBindings) === -1) {
- keyBindings.push(this.keyBindings);
- }
+ _unregisterKey: function(key) {
+ this._unregister(key, this._metaData, this.list);
+ },
- return keyBindings;
+ _registerKeyValue: function(key, value) {
+ this._register(key, value, this._metaData, this.list);
},
- _prepKeyBindings: function() {
- this._keyBindings = {};
+ _register: function(key, value, data, list) {
+ if (key && data && value !== undefined) {
+ data[key] = value;
+ list.push(value);
+ }
+ },
- this._collectKeyBindings().forEach(function(keyBindings) {
- for (var eventString in keyBindings) {
- this._addKeyBinding(eventString, keyBindings[eventString]);
+ _unregister: function(key, data, list) {
+ if (key && data) {
+ if (key in data) {
+ var value = data[key];
+ delete data[key];
+ this.arrayDelete(list, value);
}
- }, this);
-
- for (var eventString in this._imperativeKeyBindings) {
- this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
}
+ }
- // 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;
- })
- }
- },
+ });
- _addKeyBinding: function(eventString, handlerName) {
- parseEventString(eventString).forEach(function(keyCombo) {
- this._keyBindings[keyCombo.event] =
- this._keyBindings[keyCombo.event] || [];
+ Polymer.IronMeta.getIronMeta = function getIronMeta() {
+ if (singleton === null) {
+ singleton = new Polymer.IronMeta();
+ }
+ return singleton;
+ };
- this._keyBindings[keyCombo.event].push([
- keyCombo,
- handlerName
- ]);
- }, this);
- },
+ /**
+ `iron-meta-query` can be used to access infomation stored in `iron-meta`.
- _resetKeyEventListeners: function() {
- this._unlistenKeyEventListeners();
+ Examples:
- if (this.isAttached) {
- this._listenKeyEventListeners();
- }
- },
+ If I create an instance like this:
- _listenKeyEventListeners: function() {
- Object.keys(this._keyBindings).forEach(function(eventName) {
- var keyBindings = this._keyBindings[eventName];
- var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
+ <iron-meta key="info" value="foo/bar"></iron-meta>
- this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
+ Note that value="foo/bar" is the metadata I've defined. I could define more
+ attributes or use child nodes to define additional metadata.
- this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
- }, this);
- },
+ Now I can access that element (and it's metadata) from any `iron-meta-query` instance:
- _unlistenKeyEventListeners: function() {
- var keyHandlerTuple;
- var keyEventTarget;
- var eventName;
- var boundKeyHandler;
+ var value = new Polymer.IronMetaQuery({key: 'info'}).value;
- 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];
+ @group Polymer Iron Elements
+ @element iron-meta-query
+ */
+ Polymer.IronMetaQuery = Polymer({
- keyEventTarget.removeEventListener(eventName, boundKeyHandler);
- }
- },
+ is: 'iron-meta-query',
- _onKeyBindingEvent: function(keyBindings, event) {
- if (this.stopKeyboardEventPropagation) {
- event.stopPropagation();
- }
+ properties: {
- // if event has been already prevented, don't do anything
- if (event.defaultPrevented) {
- return;
- }
+ /**
+ * The type of meta-data. All meta-data of the same type is stored
+ * together.
+ */
+ type: {
+ type: String,
+ value: 'default',
+ observer: '_typeChanged'
+ },
- 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;
- }
- }
+ /**
+ * Specifies a key to use for retrieving `value` from the `type`
+ * namespace.
+ */
+ key: {
+ type: String,
+ observer: '_keyChanged'
+ },
+
+ /**
+ * The meta-data to store or retrieve.
+ */
+ value: {
+ type: Object,
+ notify: true,
+ readOnly: true
+ },
+
+ /**
+ * Array of all meta-data values for the given type.
+ */
+ list: {
+ type: Array,
+ notify: true
}
+
},
- _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();
- }
- }
- };
+ /**
+ * Actually a factory method, not a true constructor. Only runs if
+ * someone invokes it directly (via `new Polymer.IronMeta()`);
+ *
+ * @param {{type: (string|undefined), key: (string|undefined)}=} config
+ */
+ factoryImpl: function(config) {
+ if (config) {
+ for (var n in config) {
+ switch(n) {
+ case 'type':
+ case 'key':
+ this[n] = config[n];
+ break;
+ }
+ }
+ }
+ },
+
+ created: function() {
+ // TODO(sjmiles): good for debugging?
+ this._metaDatas = metaDatas;
+ this._metaArrays = metaArrays;
+ },
+
+ _keyChanged: function(key) {
+ this._setValue(this._metaData && this._metaData[key]);
+ },
+
+ _typeChanged: function(type) {
+ this._metaData = metaDatas[type];
+ this.list = metaArrays[type];
+ if (this.key) {
+ this._keyChanged(this.key);
+ }
+ },
+
+ /**
+ * Retrieves meta data value by key.
+ * @param {string} key The key of the meta-data to be returned.
+ * @return {*}
+ */
+ byKey: function(key) {
+ return this._metaData && this._metaData[key];
+ }
+
+ });
+
})();
+Polymer({
+
+ is: 'iron-icon',
+
+ properties: {
+
+ /**
+ * The name of the icon to use. The name should be of the form:
+ * `iconset_name:icon_name`.
+ */
+ icon: {
+ type: String,
+ observer: '_iconChanged'
+ },
+
+ /**
+ * The name of the theme to used, if one is specified by the
+ * iconset.
+ */
+ theme: {
+ type: String,
+ observer: '_updateIcon'
+ },
+
+ /**
+ * If using iron-icon without an iconset, you can set the src to be
+ * the URL of an individual icon image file. Note that this will take
+ * precedence over a given icon attribute.
+ */
+ src: {
+ type: String,
+ observer: '_srcChanged'
+ },
+
+ /**
+ * @type {!Polymer.IronMeta}
+ */
+ _meta: {
+ value: Polymer.Base.create('iron-meta', {type: 'iconset'})
+ }
+
+ },
+
+ _DEFAULT_ICONSET: 'icons',
+
+ _iconChanged: function(icon) {
+ var parts = (icon || '').split(':');
+ this._iconName = parts.pop();
+ this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
+ this._updateIcon();
+ },
+
+ _srcChanged: function(src) {
+ this._updateIcon();
+ },
+
+ _usesIconset: function() {
+ return this.icon || !this.src;
+ },
+
+ /** @suppress {visibility} */
+ _updateIcon: function() {
+ if (this._usesIconset()) {
+ if (this._iconsetName) {
+ this._iconset = /** @type {?Polymer.Iconset} */ (
+ this._meta.byKey(this._iconsetName));
+ if (this._iconset) {
+ this._iconset.applyIcon(this, this._iconName, this.theme);
+ this.unlisten(window, 'iron-iconset-added', '_updateIcon');
+ } else {
+ this.listen(window, 'iron-iconset-added', '_updateIcon');
+ }
+ }
+ } else {
+ if (!this._img) {
+ this._img = document.createElement('img');
+ this._img.style.width = '100%';
+ this._img.style.height = '100%';
+ this._img.draggable = false;
+ }
+ this._img.src = this.src;
+ Polymer.dom(this.root).appendChild(this._img);
+ }
+ }
+
+ });
+/**
+ * The `iron-iconset-svg` element allows users to define their own icon sets
+ * that contain svg icons. The svg icon elements should be children of the
+ * `iron-iconset-svg` element. Multiple icons should be given distinct id's.
+ *
+ * Using svg elements to create icons has a few advantages over traditional
+ * bitmap graphics like jpg or png. Icons that use svg are vector based so
+ * they are resolution independent and should look good on any device. They
+ * are stylable via css. Icons can be themed, colorized, and even animated.
+ *
+ * Example:
+ *
+ * <iron-iconset-svg name="my-svg-icons" size="24">
+ * <svg>
+ * <defs>
+ * <g id="shape">
+ * <rect x="12" y="0" width="12" height="24" />
+ * <circle cx="12" cy="12" r="12" />
+ * </g>
+ * </defs>
+ * </svg>
+ * </iron-iconset-svg>
+ *
+ * This will automatically register the icon set "my-svg-icons" to the iconset
+ * database. To use these icons from within another element, make a
+ * `iron-iconset` element and call the `byId` method
+ * to retrieve a given iconset. To apply a particular icon inside an
+ * element use the `applyIcon` method. For example:
+ *
+ * iconset.applyIcon(iconNode, 'car');
+ *
+ * @element iron-iconset-svg
+ * @demo demo/index.html
+ * @implements {Polymer.Iconset}
+ */
+ Polymer({
+ is: 'iron-iconset-svg',
+
+ properties: {
+
+ /**
+ * The name of the iconset.
+ */
+ name: {
+ type: String,
+ observer: '_nameChanged'
+ },
+
+ /**
+ * The size of an individual icon. Note that icons must be square.
+ */
+ size: {
+ type: Number,
+ value: 24
+ }
+
+ },
+
+ attached: function() {
+ this.style.display = 'none';
+ },
+
+ /**
+ * Construct an array of all icon names in this iconset.
+ *
+ * @return {!Array} Array of icon names.
+ */
+ getIconNames: function() {
+ this._icons = this._createIconMap();
+ return Object.keys(this._icons).map(function(n) {
+ return this.name + ':' + n;
+ }, this);
+ },
+
+ /**
+ * Applies an icon to the given element.
+ *
+ * An svg icon is prepended to the element's shadowRoot if it exists,
+ * otherwise to the element itself.
+ *
+ * @method applyIcon
+ * @param {Element} element Element to which the icon is applied.
+ * @param {string} iconName Name of the icon to apply.
+ * @return {?Element} The svg element which renders the icon.
+ */
+ applyIcon: function(element, iconName) {
+ // insert svg element into shadow root, if it exists
+ element = element.root || element;
+ // Remove old svg element
+ this.removeIcon(element);
+ // install new svg element
+ var svg = this._cloneIcon(iconName);
+ if (svg) {
+ var pde = Polymer.dom(element);
+ pde.insertBefore(svg, pde.childNodes[0]);
+ return element._svgIcon = svg;
+ }
+ return null;
+ },
+
+ /**
+ * Remove an icon from the given element by undoing the changes effected
+ * by `applyIcon`.
+ *
+ * @param {Element} element The element from which the icon is removed.
+ */
+ removeIcon: function(element) {
+ // Remove old svg element
+ if (element._svgIcon) {
+ Polymer.dom(element).removeChild(element._svgIcon);
+ element._svgIcon = null;
+ }
+ },
+
+ /**
+ *
+ * When name is changed, register iconset metadata
+ *
+ */
+ _nameChanged: function() {
+ new Polymer.IronMeta({type: 'iconset', key: this.name, value: this});
+ this.async(function() {
+ this.fire('iron-iconset-added', this, {node: window});
+ });
+ },
+
+ /**
+ * Create a map of child SVG elements by id.
+ *
+ * @return {!Object} Map of id's to SVG elements.
+ */
+ _createIconMap: function() {
+ // Objects chained to Object.prototype (`{}`) have members. Specifically,
+ // on FF there is a `watch` method that confuses the icon map, so we
+ // need to use a null-based object here.
+ var icons = Object.create(null);
+ Polymer.dom(this).querySelectorAll('[id]')
+ .forEach(function(icon) {
+ icons[icon.id] = icon;
+ });
+ return icons;
+ },
+
+ /**
+ * Produce installable clone of the SVG element matching `id` in this
+ * iconset, or `undefined` if there is no matching element.
+ *
+ * @return {Element} Returns an installable clone of the SVG element
+ * matching `id`.
+ */
+ _cloneIcon: function(id) {
+ // create the icon map on-demand, since the iconset itself has no discrete
+ // signal to know when it's children are fully parsed
+ this._icons = this._icons || this._createIconMap();
+ return this._prepareSvgClone(this._icons[id], this.size);
+ },
+
+ /**
+ * @param {Element} sourceSvg
+ * @param {number} size
+ * @return {Element}
+ */
+ _prepareSvgClone: function(sourceSvg, size) {
+ if (sourceSvg) {
+ var content = sourceSvg.cloneNode(true),
+ svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'),
+ viewBox = content.getAttribute('viewBox') || '0 0 ' + size + ' ' + size;
+ svg.setAttribute('viewBox', viewBox);
+ svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
+ // TODO(dfreedm): `pointer-events: none` works around https://crbug.com/370136
+ // TODO(sjmiles): inline style may not be ideal, but avoids requiring a shadow-root
+ svg.style.cssText = 'pointer-events: none; display: block; width: 100%; height: 100%;';
+ svg.appendChild(content).removeAttribute('id');
+ return svg;
+ }
+ return null;
+ }
+
+ });
/**
* @demo demo/index.html
* @polymerBehavior
« no previous file with comments | « no previous file | chrome/browser/resources/md_downloads/vulcanized.html » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698