| 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
|
|
|