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

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

Issue 1375333004: MD Downloads: use <iron-list> to render download items (Closed) Base URL: https://chromium.googlesource.com/chromium/src.git@iron-list2
Patch Set: merge Created 5 years, 2 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
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 d0a64e39015c9e79867c0129b5e98f9ecd0b6360..e3a387f45fafe32692e9c2b8f0365ced4f683bdd 100644
--- a/chrome/browser/resources/md_downloads/crisper.js
+++ b/chrome/browser/resources/md_downloads/crisper.js
@@ -2353,67 +2353,6 @@ var ActionLink = document.registerElement('action-link', {
extends: 'a',
});
-// Copyright 2015 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
-
-/** @typedef {{img: HTMLImageElement, url: string}} */
-var LoadIconRequest;
-
-cr.define('downloads', function() {
- /**
- * @param {number} maxAllowed The maximum number of simultaneous downloads
- * allowed.
- * @constructor
- */
- function ThrottledIconLoader(maxAllowed) {
- assert(maxAllowed > 0);
-
- /** @private {number} */
- this.maxAllowed_ = maxAllowed;
-
- /** @private {!Array<!LoadIconRequest>} */
- this.requests_ = [];
- }
-
- ThrottledIconLoader.prototype = {
- /** @private {number} */
- loading_: 0,
-
- /**
- * Load the provided |url| into |img.src| after appending ?scale=.
- * @param {!HTMLImageElement} img An <img> to show the loaded image in.
- * @param {string} url A remote image URL to load.
- */
- loadScaledIcon: function(img, url) {
- var scaledUrl = url + '?scale=' + window.devicePixelRatio + 'x';
- if (img.src == scaledUrl)
- return;
-
- this.requests_.push({img: img, url: scaledUrl});
- this.loadNextIcon_();
- },
-
- /** @private */
- loadNextIcon_: function() {
- if (this.loading_ > this.maxAllowed_ || !this.requests_.length)
- return;
-
- var request = this.requests_.shift();
- var img = request.img;
-
- img.onabort = img.onerror = img.onload = function() {
- this.loading_--;
- this.loadNextIcon_();
- }.bind(this);
-
- this.loading_++;
- img.src = request.url;
- },
- };
-
- return {ThrottledIconLoader: ThrottledIconLoader};
-});
// Copyright 2014 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
@@ -3007,7 +2946,7 @@ debouncer.stop();
}
}
});
-Polymer.version = '1.1.4';
+Polymer.version = '1.1.5';
Polymer.Base._addFeature({
_registerFeatures: function () {
this._prepIs();
@@ -4884,7 +4823,15 @@ this.listen(node, name, listeners[key]);
}
},
listen: function (node, eventName, methodName) {
-this._listen(node, eventName, this._createEventHandler(node, eventName, methodName));
+var handler = this._recallEventHandler(this, eventName, node, methodName);
+if (!handler) {
+handler = this._createEventHandler(node, eventName, methodName);
+}
+if (handler._listening) {
+return;
+}
+this._listen(node, eventName, handler);
+handler._listening = true;
},
_boundListenerKey: function (eventName, methodName) {
return eventName + ':' + methodName;
@@ -4923,6 +4870,7 @@ host[methodName](e, e.detail);
host._warn(host._logf('_createEventHandler', 'listener method `' + methodName + '` not defined'));
}
};
+handler._listening = false;
this._recordEventHandler(host, eventName, node, methodName, handler);
return handler;
},
@@ -4930,6 +4878,7 @@ unlisten: function (node, eventName, methodName) {
var handler = this._recallEventHandler(this, eventName, node, methodName);
if (handler) {
this._unlisten(node, eventName, handler);
+handler._listening = false;
}
},
_listen: function (node, eventName, handler) {
@@ -5777,6 +5726,12 @@ elt[n] = props[n];
}
}
return elt;
+},
+isLightDescendant: function (node) {
+return this.contains(node) && Polymer.dom(this).getOwnerRoot() === Polymer.dom(node).getOwnerRoot();
+},
+isLocalDescendant: function (node) {
+return this.root === Polymer.dom(node).getOwnerRoot();
}
});
Polymer.Bind = {
@@ -6578,6 +6533,22 @@ if (args.length) {
this._notifySplice(array, path, 0, args.length, []);
}
return ret;
+},
+prepareModelNotifyPath: function (model) {
+this.mixin(model, {
+fire: Polymer.Base.fire,
+notifyPath: Polymer.Base.notifyPath,
+_EVENT_CHANGED: Polymer.Base._EVENT_CHANGED,
+_notifyPath: Polymer.Base._notifyPath,
+_pathEffector: Polymer.Base._pathEffector,
+_annotationPathEffect: Polymer.Base._annotationPathEffect,
+_complexObserverPathEffect: Polymer.Base._complexObserverPathEffect,
+_annotatedComputationPathEffect: Polymer.Base._annotatedComputationPathEffect,
+_computePathEffect: Polymer.Base._computePathEffect,
+_modelForPath: Polymer.Base._modelForPath,
+_pathMatchesEffect: Polymer.Base._pathMatchesEffect,
+_notifyBoundPaths: Polymer.Base._notifyBoundPaths
+});
}
});
}());
@@ -7784,6 +7755,7 @@ properties: { __hideTemplateChildren__: { observer: '_showHideChildren' } },
_instanceProps: Polymer.nob,
_parentPropPrefix: '_parent_',
templatize: function (template) {
+this._templatized = template;
if (!template._content) {
template._content = template.content;
}
@@ -7794,11 +7766,11 @@ return;
}
var archetype = Object.create(Polymer.Base);
this._customPrepAnnotations(archetype, template);
+this._prepParentProperties(archetype, template);
archetype._prepEffects();
this._customPrepEffects(archetype);
archetype._prepBehaviors();
archetype._prepBindings();
-this._prepParentProperties(archetype, template);
archetype._notifyPath = this._notifyPathImpl;
archetype._scopeElementClass = this._scopeElementClassImpl;
archetype.listen = this._listenImpl;
@@ -7881,6 +7853,7 @@ delete parentProps[prop];
proto = archetype._parentPropProto = Object.create(null);
if (template != this) {
Polymer.Bind.prepareModel(proto);
+Polymer.Base.prepareModelNotifyPath(proto);
}
for (prop in parentProps) {
var parentProp = this._parentPropPrefix + prop;
@@ -7899,6 +7872,7 @@ Polymer.Bind.prepareInstance(template);
template._forwardParentProp = this._forwardParentProp.bind(this);
}
this._extendTemplate(template, proto);
+template._pathEffector = this._pathEffectorImpl.bind(this);
}
},
_createForwardPropEffector: function (prop) {
@@ -7909,7 +7883,7 @@ this._forwardParentProp(prop, value);
_createHostPropEffector: function (prop) {
var prefix = this._parentPropPrefix;
return function (source, value) {
-this.dataHost[prefix + prop] = value;
+this.dataHost._templatized[prefix + prop] = value;
};
},
_createInstancePropEffector: function (prop) {
@@ -7941,16 +7915,17 @@ var dot = path.indexOf('.');
var root = dot < 0 ? path : path.slice(0, dot);
dataHost._forwardInstancePath.call(dataHost, this, path, value);
if (root in dataHost._parentProps) {
-dataHost.notifyPath(dataHost._parentPropPrefix + path, value);
+dataHost._templatized.notifyPath(dataHost._parentPropPrefix + path, value);
}
},
-_pathEffector: function (path, value, fromAbove) {
+_pathEffectorImpl: function (path, value, fromAbove) {
if (this._forwardParentPath) {
if (path.indexOf(this._parentPropPrefix) === 0) {
-this._forwardParentPath(path.substring(8), value);
+var subPath = path.substring(this._parentPropPrefix.length);
+this._forwardParentPath(subPath, value);
}
}
-Polymer.Base._pathEffector.apply(this, arguments);
+Polymer.Base._pathEffector.call(this._templatized, path, value, fromAbove);
},
_constructorImpl: function (model, host) {
this._rootDataHost = host._getRootDataHost();
@@ -7993,8 +7968,9 @@ return host._scopeElementClass(node, value);
stamp: function (model) {
model = model || {};
if (this._parentProps) {
+var templatized = this._templatized;
for (var prop in this._parentProps) {
-model[prop] = this[this._parentPropPrefix + prop];
+model[prop] = templatized[this._parentPropPrefix + prop];
}
}
return new this.ctor(model, this);
@@ -8602,7 +8578,7 @@ this.deselect(item);
}
} else {
this.push('selected', item);
-skey = this._selectedColl.getKey(item);
+var skey = this._selectedColl.getKey(item);
this.linkPaths('selected.' + skey, 'items.' + key);
}
} else {
@@ -8771,2666 +8747,4161 @@ this._insertChildren();
this.fire('dom-change');
}
});
-(function() {
-
- 'use strict';
-
- var SHADOW_WHEN_SCROLLING = 1;
- var SHADOW_ALWAYS = 2;
-
-
- var MODE_CONFIGS = {
-
- outerScroll: {
- 'scroll': true
- },
-
- shadowMode: {
- 'standard': SHADOW_ALWAYS,
- 'waterfall': SHADOW_WHEN_SCROLLING,
- 'waterfall-tall': SHADOW_WHEN_SCROLLING
+/**
+ * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
+ * coordinate the flow of resize events between "resizers" (elements that control the
+ * size or hidden state of their children) and "resizables" (elements that need to be
+ * notified when they are resized or un-hidden by their parents in order to take
+ * action on their new measurements).
+ * Elements that perform measurement should add the `IronResizableBehavior` behavior to
+ * their element definition and listen for the `iron-resize` event on themselves.
+ * This event will be fired when they become showing after having been hidden,
+ * when they are resized explicitly by another resizable, or when the window has been
+ * resized.
+ * Note, the `iron-resize` event is non-bubbling.
+ *
+ * @polymerBehavior Polymer.IronResizableBehavior
+ * @demo demo/index.html
+ **/
+ Polymer.IronResizableBehavior = {
+ properties: {
+ /**
+ * The closest ancestor element that implements `IronResizableBehavior`.
+ */
+ _parentResizable: {
+ type: Object,
+ observer: '_parentResizableChanged'
},
- tallMode: {
- 'waterfall-tall': true
+ /**
+ * True if this element is currently notifying its descedant elements of
+ * resize.
+ */
+ _notifyingDescendant: {
+ type: Boolean,
+ value: false
}
- };
+ },
- Polymer({
+ listeners: {
+ 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
+ },
- is: 'paper-header-panel',
+ created: function() {
+ // We don't really need property effects on these, and also we want them
+ // to be created before the `_parentResizable` observer fires:
+ this._interestedResizables = [];
+ this._boundNotifyResize = this.notifyResize.bind(this);
+ },
- /**
- * Fired when the content has been scrolled. `event.detail.target` returns
- * the scrollable element which you can use to access scroll info such as
- * `scrollTop`.
- *
- * <paper-header-panel on-content-scroll="scrollHandler">
- * ...
- * </paper-header-panel>
- *
- *
- * scrollHandler: function(event) {
- * var scroller = event.detail.target;
- * console.log(scroller.scrollTop);
- * }
- *
- * @event content-scroll
- */
+ attached: function() {
+ this.fire('iron-request-resize-notifications', null, {
+ node: this,
+ bubbles: true,
+ cancelable: true
+ });
- properties: {
+ if (!this._parentResizable) {
+ window.addEventListener('resize', this._boundNotifyResize);
+ this.notifyResize();
+ }
+ },
- /**
- * Controls header and scrolling behavior. Options are
- * `standard`, `seamed`, `waterfall`, `waterfall-tall`, `scroll` and
- * `cover`. Default is `standard`.
- *
- * `standard`: The header is a step above the panel. The header will consume the
- * panel at the point of entry, preventing it from passing through to the
- * opposite side.
- *
- * `seamed`: The header is presented as seamed with the panel.
- *
- * `waterfall`: Similar to standard mode, but header is initially presented as
- * seamed with panel, but then separates to form the step.
- *
- * `waterfall-tall`: The header is initially taller (`tall` class is added to
- * the header). As the user scrolls, the header separates (forming an edge)
- * while condensing (`tall` class is removed from the header).
- *
- * `scroll`: The header keeps its seam with the panel, and is pushed off screen.
- *
- * `cover`: The panel covers the whole `paper-header-panel` including the
- * header. This allows user to style the panel in such a way that the panel is
- * partially covering the header.
- *
- * <paper-header-panel mode="cover">
- * <paper-toolbar class="tall">
- * <core-icon-button icon="menu"></core-icon-button>
- * </paper-toolbar>
- * <div class="content"></div>
- * </paper-header-panel>
- */
- mode: {
- type: String,
- value: 'standard',
- observer: '_modeChanged',
- reflectToAttribute: true
- },
+ detached: function() {
+ if (this._parentResizable) {
+ this._parentResizable.stopResizeNotificationsFor(this);
+ } else {
+ window.removeEventListener('resize', this._boundNotifyResize);
+ }
- /**
- * If true, the drop-shadow is always shown no matter what mode is set to.
- */
- shadow: {
- type: Boolean,
- value: false
- },
+ this._parentResizable = null;
+ },
- /**
- * The class used in waterfall-tall mode. Change this if the header
- * accepts a different class for toggling height, e.g. "medium-tall"
- */
- tallClass: {
- type: String,
- value: 'tall'
- },
+ /**
+ * Can be called to manually notify a resizable and its descendant
+ * resizables of a resize change.
+ */
+ notifyResize: function() {
+ if (!this.isAttached) {
+ return;
+ }
- /**
- * If true, the scroller is at the top
- */
- atTop: {
- type: Boolean,
- value: true,
- readOnly: true
+ this._interestedResizables.forEach(function(resizable) {
+ if (this.resizerShouldNotify(resizable)) {
+ this._notifyDescendant(resizable);
}
- },
-
- observers: [
- '_computeDropShadowHidden(atTop, mode, shadow)'
- ],
+ }, this);
- ready: function() {
- this.scrollHandler = this._scroll.bind(this);
- this._addListener();
+ this._fireResize();
+ },
- // Run `scroll` logic once to initialze class names, etc.
- this._keepScrollingState();
- },
+ /**
+ * Used to assign the closest resizable ancestor to this resizable
+ * if the ancestor detects a request for notifications.
+ */
+ assignParentResizable: function(parentResizable) {
+ this._parentResizable = parentResizable;
+ },
- detached: function() {
- this._removeListener();
- },
+ /**
+ * Used to remove a resizable descendant from the list of descendants
+ * that should be notified of a resize change.
+ */
+ stopResizeNotificationsFor: function(target) {
+ var index = this._interestedResizables.indexOf(target);
- /**
- * Returns the header element
- *
- * @property header
- * @type Object
- */
- get header() {
- return Polymer.dom(this.$.headerContent).getDistributedNodes()[0];
- },
+ if (index > -1) {
+ this._interestedResizables.splice(index, 1);
+ this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
+ }
+ },
- /**
- * Returns the scrollable element.
- *
- * @property scroller
- * @type Object
- */
- get scroller() {
- return this._getScrollerForMode(this.mode);
- },
+ /**
+ * This method can be overridden to filter nested elements that should or
+ * should not be notified by the current element. Return true if an element
+ * should be notified, or false if it should not be notified.
+ *
+ * @param {HTMLElement} element A candidate descendant element that
+ * implements `IronResizableBehavior`.
+ * @return {boolean} True if the `element` should be notified of resize.
+ */
+ resizerShouldNotify: function(element) { return true; },
- /**
- * Returns true if the scroller has a visible shadow.
- *
- * @property visibleShadow
- * @type Boolean
- */
- get visibleShadow() {
- return this.$.dropShadow.classList.contains('has-shadow');
- },
+ _onDescendantIronResize: function(event) {
+ if (this._notifyingDescendant) {
+ event.stopPropagation();
+ return;
+ }
- _computeDropShadowHidden: function(atTop, mode, shadow) {
+ // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
+ // otherwise non-bubbling event "just work." We do it manually here for
+ // the case where Polymer is not using shadow roots for whatever reason:
+ if (!Polymer.Settings.useShadow) {
+ this._fireResize();
+ }
+ },
- var shadowMode = MODE_CONFIGS.shadowMode[mode];
+ _fireResize: function() {
+ this.fire('iron-resize', null, {
+ node: this,
+ bubbles: false
+ });
+ },
- if (this.shadow) {
- this.toggleClass('has-shadow', true, this.$.dropShadow);
+ _onIronRequestResizeNotifications: function(event) {
+ var target = event.path ? event.path[0] : event.target;
- } else if (shadowMode === SHADOW_ALWAYS) {
- this.toggleClass('has-shadow', true, this.$.dropShadow);
+ if (target === this) {
+ return;
+ }
- } else if (shadowMode === SHADOW_WHEN_SCROLLING && !atTop) {
- this.toggleClass('has-shadow', true, this.$.dropShadow);
+ if (this._interestedResizables.indexOf(target) === -1) {
+ this._interestedResizables.push(target);
+ this.listen(target, 'iron-resize', '_onDescendantIronResize');
+ }
- } else {
- this.toggleClass('has-shadow', false, this.$.dropShadow);
+ target.assignParentResizable(this);
+ this._notifyDescendant(target);
- }
- },
+ event.stopPropagation();
+ },
- _computeMainContainerClass: function(mode) {
- // TODO: It will be useful to have a utility for classes
- // e.g. Polymer.Utils.classes({ foo: true });
+ _parentResizableChanged: function(parentResizable) {
+ if (parentResizable) {
+ window.removeEventListener('resize', this._boundNotifyResize);
+ }
+ },
- var classes = {};
+ _notifyDescendant: function(descendant) {
+ // NOTE(cdata): In IE10, attached is fired on children first, so it's
+ // important not to notify them if the parent is not attached yet (or
+ // else they will get redundantly notified when the parent attaches).
+ if (!this.isAttached) {
+ return;
+ }
- classes['flex'] = mode !== 'cover';
+ this._notifyingDescendant = true;
+ descendant.notifyResize();
+ this._notifyingDescendant = false;
+ }
+ };
+(function() {
- return Object.keys(classes).filter(
- function(className) {
- return classes[className];
- }).join(' ');
- },
+ var IOS = navigator.userAgent.match(/iP(?:hone|ad;(?: U;)? CPU) OS (\d+)/);
+ var IOS_TOUCH_SCROLLING = IOS && IOS[1] >= 8;
+ var DEFAULT_PHYSICAL_COUNT = 20;
+ var MAX_PHYSICAL_COUNT = 500;
- _addListener: function() {
- this.scroller.addEventListener('scroll', this.scrollHandler, false);
- },
+ Polymer({
- _removeListener: function() {
- this.scroller.removeEventListener('scroll', this.scrollHandler);
- },
+ is: 'iron-list',
- _modeChanged: function(newMode, oldMode) {
- var configs = MODE_CONFIGS;
- var header = this.header;
- var animateDuration = 200;
+ properties: {
- if (header) {
- // in tallMode it may add tallClass to the header; so do the cleanup
- // when mode is changed from tallMode to not tallMode
- if (configs.tallMode[oldMode] && !configs.tallMode[newMode]) {
- header.classList.remove(this.tallClass);
- this.async(function() {
- header.classList.remove('animate');
- }, animateDuration);
- } else {
- header.classList.toggle('animate', configs.tallMode[newMode]);
- }
- }
- this._keepScrollingState();
+ /**
+ * 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
},
- _keepScrollingState: function() {
- var main = this.scroller;
- var header = this.header;
-
- this._setAtTop(main.scrollTop === 0);
-
- if (header && this.tallClass && MODE_CONFIGS.tallMode[this.mode]) {
- this.toggleClass(this.tallClass, this.atTop ||
- header.classList.contains(this.tallClass) &&
- main.scrollHeight < this.offsetHeight, header);
- }
+ /**
+ * 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'
},
- _scroll: function() {
- this._keepScrollingState();
- this.fire('content-scroll', {target: this.scroller}, {bubbles: false});
+ /**
+ * The name of the variable to add to the binding scope with the index
+ * for the row. If `sort` is provided, the index will reflect the
+ * sorted order (rather than the original array order).
+ */
+ indexAs: {
+ type: String,
+ value: 'index'
},
- _getScrollerForMode: function(mode) {
- return MODE_CONFIGS.outerScroll[mode] ?
- this : this.$.mainContainer;
- }
-
- });
+ /**
+ * The name of the variable to add to the binding scope to indicate
+ * if the row is selected.
+ */
+ selectedAs: {
+ type: String,
+ value: 'selected'
+ },
- })();
-Polymer({
- is: 'paper-material',
+ /**
+ * 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
+ },
- properties: {
+ /**
+ * When `multiSelection` is false, this is the currently selected item, or `null`
+ * if no item is selected.
+ */
+ selectedItem: {
+ type: Object,
+ notify: true
+ },
/**
- * The z-depth of this element, from 0-5. Setting to 0 will remove the
- * shadow, and each increasing number greater than 0 will be "deeper"
- * than the last.
- *
- * @attribute elevation
- * @type number
- * @default 1
+ * When `multiSelection` is true, this is an array that contains the selected items.
*/
- elevation: {
- type: Number,
- reflectToAttribute: true,
- value: 1
+ selectedItems: {
+ type: Object,
+ notify: true
},
/**
- * Set this to true to animate the shadow when setting a new
- * `elevation` value.
- *
- * @attribute animated
- * @type boolean
- * @default false
+ * 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.
*/
- animated: {
+ multiSelection: {
type: Boolean,
- reflectToAttribute: true,
value: false
}
- }
- });
-(function() {
- 'use strict';
+ },
+
+ observers: [
+ '_itemsChanged(items.*)',
+ '_selectionEnabledChanged(selectionEnabled)',
+ '_multiSelectionChanged(multiSelection)'
+ ],
+
+ behaviors: [
+ Polymer.Templatizer,
+ Polymer.IronResizableBehavior
+ ],
+
+ listeners: {
+ 'iron-resize': '_resizeHandler'
+ },
/**
- * 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
+ * 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.
*/
- var KEY_IDENTIFIER = {
- 'U+0009': 'tab',
- 'U+001B': 'esc',
- 'U+0020': 'space',
- 'U+002A': '*',
- 'U+0030': '0',
- 'U+0031': '1',
- 'U+0032': '2',
- 'U+0033': '3',
- 'U+0034': '4',
- 'U+0035': '5',
- 'U+0036': '6',
- 'U+0037': '7',
- 'U+0038': '8',
- 'U+0039': '9',
- 'U+0041': 'a',
- 'U+0042': 'b',
- 'U+0043': 'c',
- 'U+0044': 'd',
- 'U+0045': 'e',
- 'U+0046': 'f',
- 'U+0047': 'g',
- 'U+0048': 'h',
- 'U+0049': 'i',
- 'U+004A': 'j',
- 'U+004B': 'k',
- 'U+004C': 'l',
- 'U+004D': 'm',
- 'U+004E': 'n',
- 'U+004F': 'o',
- 'U+0050': 'p',
- 'U+0051': 'q',
- 'U+0052': 'r',
- 'U+0053': 's',
- 'U+0054': 't',
- 'U+0055': 'u',
- 'U+0056': 'v',
- 'U+0057': 'w',
- 'U+0058': 'x',
- 'U+0059': 'y',
- 'U+005A': 'z',
- 'U+007F': 'del'
- };
+ _ratio: 0.5,
/**
- * 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
+ * The element that controls the scroll
+ * @type {?Element}
*/
- var KEY_CODE = {
- 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: '*'
- };
+ _scroller: null,
/**
- * 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.
+ * The padding-top value of the `scroller` element
*/
- var MODIFIER_KEYS = {
- 'shift': 'shiftKey',
- 'ctrl': 'ctrlKey',
- 'alt': 'altKey',
- 'meta': 'metaKey'
- };
+ _scrollerPaddingTop: 0,
/**
- * 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.
+ * This value is the same as `scrollTop`.
*/
- var KEY_CHAR = /[a-z0-9*]/;
+ _scrollPosition: 0,
/**
- * Matches a keyIdentifier string.
+ * The number of tiles in the DOM.
*/
- var IDENT_CHAR = /U\+/;
+ _physicalCount: 0,
/**
- * Matches arrow keys in Gecko 27.0+
+ * The k-th tile that is at the top of the scrolling list.
*/
- var ARROW_KEY = /^arrow/;
+ _physicalStart: 0,
/**
- * Matches space keys everywhere (notably including IE10's exceptional name
- * `spacebar`).
+ * The k-th tile that is at the bottom of the scrolling list.
*/
- var SPACE_KEY = /^space(bar)?/;
+ _physicalEnd: 0,
- function transformKey(key) {
- var validKey = '';
- if (key) {
- var lKey = key.toLowerCase();
- if (lKey.length == 1) {
- if (KEY_CHAR.test(lKey)) {
- validKey = lKey;
- }
- } else if (ARROW_KEY.test(lKey)) {
- validKey = lKey.replace('arrow', '');
- } else if (SPACE_KEY.test(lKey)) {
- validKey = 'space';
- } else if (lKey == 'multiply') {
- // numpad '*' can map to Multiply on IE/Windows
- validKey = '*';
- } else {
- validKey = lKey;
- }
- }
- return validKey;
- }
+ /**
+ * The sum of the heights of all the tiles in the DOM.
+ */
+ _physicalSize: 0,
- function transformKeyIdentifier(keyIdent) {
- var validKey = '';
- if (keyIdent) {
- if (IDENT_CHAR.test(keyIdent)) {
- validKey = KEY_IDENTIFIER[keyIdent];
- } else {
- validKey = keyIdent.toLowerCase();
- }
- }
- return validKey;
- }
+ /**
+ * The average `offsetHeight` of the tiles observed till now.
+ */
+ _physicalAverage: 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;
- }
+ /**
+ * The number of tiles which `offsetHeight` > 0 observed until now.
+ */
+ _physicalAverageCount: 0,
- function normalizedKeyForEvent(keyEvent) {
- // fall back from .key, to .keyIdentifier, to .keyCode, and then to
- // .detail.key to support artificial keyboard events
- return transformKey(keyEvent.key) ||
- transformKeyIdentifier(keyEvent.keyIdentifier) ||
- transformKeyCode(keyEvent.keyCode) ||
- transformKey(keyEvent.detail.key) || '';
- }
+ /**
+ * The Y position of the item rendered in the `_physicalStart`
+ * tile relative to the scrolling list.
+ */
+ _physicalTop: 0,
- function keyComboMatchesEvent(keyCombo, keyEvent) {
- return normalizedKeyForEvent(keyEvent) === keyCombo.key &&
- !!keyEvent.shiftKey === !!keyCombo.shiftKey &&
- !!keyEvent.ctrlKey === !!keyCombo.ctrlKey &&
- !!keyEvent.altKey === !!keyCombo.altKey &&
- !!keyEvent.metaKey === !!keyCombo.metaKey;
- }
+ /**
+ * The number of items in the list.
+ */
+ _virtualCount: 0,
- function parseKeyComboString(keyComboString) {
- return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
- var eventParts = keyComboPart.split(':');
- var keyName = eventParts[0];
- var event = eventParts[1];
+ /**
+ * The n-th item rendered in the `_physicalStart` tile.
+ */
+ _virtualStartVal: 0,
- if (keyName in MODIFIER_KEYS) {
- parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
- } else {
- parsedKeyCombo.key = keyName;
- parsedKeyCombo.event = event || 'keydown';
- }
+ /**
+ * A map between an item key and its physical item index
+ */
+ _physicalIndexForKey: null,
- return parsedKeyCombo;
- }, {
- combo: keyComboString.split(':').shift()
- });
- }
+ /**
+ * The estimated scroll height based on `_physicalAverage`
+ */
+ _estScrollHeight: 0,
- function parseEventString(eventString) {
- return eventString.split(' ').map(function(keyComboString) {
- return parseKeyComboString(keyComboString);
- });
- }
+ /**
+ * The scroll height of the dom node
+ */
+ _scrollHeight: 0,
+ /**
+ * The size of the viewport
+ */
+ _viewportSize: 0,
/**
- * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
- * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
- * The element takes care of browser differences with respect to Keyboard events
- * and uses an expressive syntax to filter key presses.
- *
- * Use the `keyBindings` prototype property to express what combination of keys
- * will trigger the event to fire.
- *
- * Use the `key-event-target` attribute to set up event handlers on a specific
- * node.
- * The `keys-pressed` event will fire when one of the key combinations set with the
- * `keys` property is pressed.
- *
- * @demo demo/index.html
- * @polymerBehavior
+ * An array of DOM nodes that are currently in the tree
+ * @type {?Array<!TemplatizerNode>}
*/
- Polymer.IronA11yKeysBehavior = {
- properties: {
- /**
- * The HTMLElement that will be firing relevant KeyboardEvents.
- */
- keyEventTarget: {
- type: Object,
- value: function() {
- return this;
- }
- },
+ _physicalItems: null,
- _boundKeyHandlers: {
- type: Array,
- value: function() {
- return [];
- }
- },
+ /**
+ * An array of heights for each item in `_physicalItems`
+ * @type {?Array<number>}
+ */
+ _physicalSizes: 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 {};
- }
- }
- },
+ /**
+ * A cached value for the visible index.
+ * See `firstVisibleIndex`
+ * @type {?number}
+ */
+ _firstVisibleIndexVal: null,
- observers: [
- '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
- ],
+ /**
+ * A Polymer collection for the items.
+ * @type {?Polymer.Collection}
+ */
+ _collection: null,
- keyBindings: {},
+ /**
+ * True if the current item list was rendered for the first time
+ * after attached.
+ */
+ _itemsRendered: false,
- registered: function() {
- this._prepKeyBindings();
- },
+ /**
+ * The bottom of the physical content.
+ */
+ get _physicalBottom() {
+ return this._physicalTop + this._physicalSize;
+ },
- attached: function() {
- this._listenKeyEventListeners();
- },
+ /**
+ * The n-th item rendered in the last physical item.
+ */
+ get _virtualEnd() {
+ return this._virtualStartVal + this._physicalCount - 1;
+ },
- detached: function() {
- this._unlistenKeyEventListeners();
- },
+ /**
+ * The lowest n-th value for an item such that it can be rendered in `_physicalStart`.
+ */
+ _minVirtualStart: 0,
- /**
- * 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 largest n-th value for an item such that it can be rendered in `_physicalStart`.
+ */
+ get _maxVirtualStart() {
+ return this._virtualCount < this._physicalCount ?
+ this._virtualCount : this._virtualCount - this._physicalCount;
+ },
- /**
- * When called, will remove all imperatively-added key bindings.
- */
- removeOwnKeyBindings: function() {
- this._imperativeKeyBindings = {};
- this._prepKeyBindings();
- this._resetKeyEventListeners();
- },
+ /**
+ * The height of the physical content that isn't on the screen.
+ */
+ get _hiddenContentSize() {
+ return this._physicalSize - this._viewportSize;
+ },
- keyboardEventMatchesKeys: function(event, eventString) {
- var keyCombos = parseEventString(eventString);
- var index;
+ /**
+ * The maximum scroll top value.
+ */
+ get _maxScrollTop() {
+ return this._estScrollHeight - this._viewportSize;
+ },
- for (index = 0; index < keyCombos.length; ++index) {
- if (keyComboMatchesEvent(keyCombos[index], event)) {
- return true;
- }
- }
+ /**
+ * 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;
+ },
- return false;
- },
+ /**
+ * Gets the n-th item rendered in `_physicalStart`
+ */
+ get _virtualStart() {
+ return this._virtualStartVal;
+ },
- _collectKeyBindings: function() {
- var keyBindings = this.behaviors.map(function(behavior) {
- return behavior.keyBindings;
- });
+ /**
+ * An optimal physical size such that we will have enough physical items
+ * to fill up the viewport and recycle when the user scrolls.
+ *
+ * This default value assumes that we will at least have the equivalent
+ * to a viewport of physical items above and below the user's viewport.
+ */
+ get _optPhysicalSize() {
+ return this._viewportSize * 3;
+ },
- if (keyBindings.indexOf(this.keyBindings) === -1) {
- keyBindings.push(this.keyBindings);
- }
+ /**
+ * True if the current list is visible.
+ */
+ get _isVisible() {
+ return this._scroller && Boolean(this._scroller.offsetWidth || this._scroller.offsetHeight);
+ },
- return keyBindings;
- },
+ /**
+ * Gets the first visible item in the viewport.
+ *
+ * @type {number}
+ */
+ get firstVisibleIndex() {
+ var physicalOffset;
- _prepKeyBindings: function() {
- this._keyBindings = {};
+ if (this._firstVisibleIndexVal === null) {
+ physicalOffset = this._physicalTop;
- this._collectKeyBindings().forEach(function(keyBindings) {
- for (var eventString in keyBindings) {
- this._addKeyBinding(eventString, keyBindings[eventString]);
- }
- }, this);
+ this._firstVisibleIndexVal = this._iterateItems(
+ function(pidx, vidx) {
+ physicalOffset += this._physicalSizes[pidx];
- for (var eventString in this._imperativeKeyBindings) {
- this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
- }
- },
+ if (physicalOffset > this._scrollPosition) {
+ return vidx;
+ }
+ }) || 0;
+ }
- _addKeyBinding: function(eventString, handlerName) {
- parseEventString(eventString).forEach(function(keyCombo) {
- this._keyBindings[keyCombo.event] =
- this._keyBindings[keyCombo.event] || [];
+ return this._firstVisibleIndexVal;
+ },
- this._keyBindings[keyCombo.event].push([
- keyCombo,
- handlerName
- ]);
- }, this);
- },
+ ready: function() {
+ if (IOS_TOUCH_SCROLLING) {
+ this._scrollListener = function() {
+ requestAnimationFrame(this._scrollHandler.bind(this));
+ }.bind(this);
+ } else {
+ this._scrollListener = this._scrollHandler.bind(this);
+ }
+ },
- _resetKeyEventListeners: function() {
- this._unlistenKeyEventListeners();
+ /**
+ * When the element has been attached to the DOM tree.
+ */
+ attached: function() {
+ // delegate to the parent's scroller
+ // e.g. paper-scroll-header-panel
+ var el = Polymer.dom(this);
- if (this.isAttached) {
- this._listenKeyEventListeners();
- }
- },
+ var parentNode = /** @type {?{scroller: ?Element}} */ (el.parentNode);
+ if (parentNode && parentNode.scroller) {
+ this._scroller = parentNode.scroller;
+ } else {
+ this._scroller = this;
+ this.classList.add('has-scroller');
+ }
- _listenKeyEventListeners: function() {
- Object.keys(this._keyBindings).forEach(function(eventName) {
- var keyBindings = this._keyBindings[eventName];
- var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
+ if (IOS_TOUCH_SCROLLING) {
+ this._scroller.style.webkitOverflowScrolling = 'touch';
+ }
- this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
+ this._scroller.addEventListener('scroll', this._scrollListener);
- this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
- }, this);
- },
+ this.updateViewportBoundaries();
+ this._render();
+ },
- _unlistenKeyEventListeners: function() {
- var keyHandlerTuple;
- var keyEventTarget;
- var eventName;
- var boundKeyHandler;
+ /**
+ * When the element has been removed from the DOM tree.
+ */
+ detached: function() {
+ this._itemsRendered = false;
+ if (this._scroller) {
+ this._scroller.removeEventListener('scroll', this._scrollListener);
+ }
+ },
- 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];
+ /**
+ * Invoke this method if you dynamically update the viewport's
+ * size or CSS padding.
+ *
+ * @method updateViewportBoundaries
+ */
+ updateViewportBoundaries: function() {
+ var scrollerStyle = window.getComputedStyle(this._scroller);
+ this._scrollerPaddingTop = parseInt(scrollerStyle['padding-top'], 10);
+ this._viewportSize = this._scroller.offsetHeight;
+ },
- keyEventTarget.removeEventListener(eventName, boundKeyHandler);
+ /**
+ * Update the models, the position of the
+ * items in the viewport and recycle tiles as needed.
+ */
+ _refresh: function() {
+ var SCROLL_DIRECTION_UP = -1;
+ var SCROLL_DIRECTION_DOWN = 1;
+ var SCROLL_DIRECTION_NONE = 0;
+
+ // 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, kth, recycledTileSet;
+ var ratio = this._ratio;
+ var delta = scrollTop - this._scrollPosition;
+ var direction = SCROLL_DIRECTION_NONE;
+ var recycledTiles = 0;
+ var hiddenContentSize = this._hiddenContentSize;
+ var currentRatio = ratio;
+ var movingUp = [];
+
+ // track the last `scrollTop`
+ this._scrollPosition = scrollTop;
+
+ // clear cached visible index
+ this._firstVisibleIndexVal = null;
+
+ // random access
+ if (Math.abs(delta) > this._physicalSize) {
+ this._physicalTop += delta;
+ direction = SCROLL_DIRECTION_NONE;
+ recycledTiles = Math.round(delta / this._physicalAverage);
+ }
+ // scroll up
+ else if (delta < 0) {
+ var topSpace = scrollTop - this._physicalTop;
+ var virtualStart = this._virtualStart;
+
+ direction = SCROLL_DIRECTION_UP;
+ 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
+ ) {
+
+ tileHeight = this._physicalSizes[kth] || this._physicalAverage;
+ currentRatio += tileHeight / hiddenContentSize;
+
+ recycledTileSet.push(kth);
+ recycledTiles++;
+ kth = (kth === 0) ? this._physicalCount - 1 : kth - 1;
}
- },
-
- _onKeyBindingEvent: function(keyBindings, event) {
- keyBindings.forEach(function(keyBinding) {
- var keyCombo = keyBinding[0];
- var handlerName = keyBinding[1];
-
- if (!event.defaultPrevented && keyComboMatchesEvent(keyCombo, event)) {
- this._triggerKeyHandler(keyCombo, handlerName, event);
- }
- }, this);
- },
- _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
- var detail = Object.create(keyCombo);
- detail.keyboardEvent = keyboardEvent;
+ movingUp = recycledTileSet;
+ recycledTiles = -recycledTiles;
- this[handlerName].call(this, new CustomEvent(keyCombo.event, {
- detail: detail
- }));
}
- };
- })();
-(function() {
- var Utility = {
- distance: function(x1, y1, x2, y2) {
- var xDelta = (x1 - x2);
- var yDelta = (y1 - y2);
-
- return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
- },
+ // scroll down
+ else if (delta > 0) {
+ var bottomSpace = this._physicalBottom - (scrollTop + this._viewportSize);
+ var virtualEnd = this._virtualEnd;
+ var lastVirtualItemIndex = this._virtualCount-1;
+
+ direction = SCROLL_DIRECTION_DOWN;
+ recycledTileSet = [];
+
+ kth = this._physicalStart;
+ currentRatio = bottomSpace / hiddenContentSize;
+
+ // move tiles from top to bottom
+ while (
+ // approximate `currentRatio` to `ratio`
+ currentRatio < ratio &&
+ // recycle less physical items than the total
+ recycledTiles < this._physicalCount &&
+ // ensure that these recycled tiles are needed
+ virtualEnd + recycledTiles < lastVirtualItemIndex
+ ) {
+
+ tileHeight = this._physicalSizes[kth] || this._physicalAverage;
+ currentRatio += tileHeight / hiddenContentSize;
+
+ this._physicalTop += tileHeight;
+ recycledTileSet.push(kth);
+ recycledTiles++;
+ kth = (kth + 1) % this._physicalCount;
+ }
+ }
- now: window.performance && window.performance.now ?
- window.performance.now.bind(window.performance) : Date.now
- };
+ if (recycledTiles !== 0) {
+ this._virtualStart = this._virtualStart + recycledTiles;
+ this._update(recycledTileSet, movingUp);
+ }
+ },
/**
- * @param {HTMLElement} element
- * @constructor
+ * Update the list of items, starting from the `_virtualStartVal` item.
+ * @param {!Array<number>=} itemSet
+ * @param {!Array<number>=} movingUp
*/
- function ElementMetrics(element) {
- this.element = element;
- this.width = this.boundingRect.width;
- this.height = this.boundingRect.height;
+ _update: function(itemSet, movingUp) {
+ // update models
+ this._assignModels(itemSet);
- this.size = Math.max(this.width, this.height);
- }
+ // measure heights
+ this._updateMetrics(itemSet);
- ElementMetrics.prototype = {
- get boundingRect () {
- return this.element.getBoundingClientRect();
- },
+ // adjust offset after measuring
+ if (movingUp) {
+ while (movingUp.length) {
+ this._physicalTop -= this._physicalSizes[movingUp.pop()];
+ }
+ }
+ // update the position of the items
+ this._positionItems();
- furthestCornerDistanceFrom: function(x, y) {
- var topLeft = Utility.distance(x, y, 0, 0);
- var topRight = Utility.distance(x, y, this.width, 0);
- var bottomLeft = Utility.distance(x, y, 0, this.height);
- var bottomRight = Utility.distance(x, y, this.width, this.height);
+ // set the scroller size
+ this._updateScrollerSize();
- return Math.max(topLeft, topRight, bottomLeft, bottomRight);
+ // increase the pool of physical items if needed
+ if (this._increasePoolIfNeeded()) {
+ // set models to the new items
+ this.async(this._update);
}
- };
+ },
/**
- * @param {HTMLElement} element
- * @constructor
+ * Creates a pool of DOM elements and attaches them to the local dom.
*/
- function Ripple(element) {
- this.element = element;
- this.color = window.getComputedStyle(element).color;
-
- this.wave = document.createElement('div');
- this.waveContainer = document.createElement('div');
- this.wave.style.backgroundColor = this.color;
- this.wave.classList.add('wave');
- this.waveContainer.classList.add('wave-container');
- Polymer.dom(this.waveContainer).appendChild(this.wave);
+ _createPool: function(size) {
+ var physicalItems = new Array(size);
- this.resetInteractionState();
- }
-
- Ripple.MAX_RADIUS = 300;
+ this._ensureTemplatized();
- Ripple.prototype = {
- get recenters() {
- return this.element.recenters;
- },
+ for (var i = 0; i < size; i++) {
+ var inst = this.stamp(null);
- get center() {
- return this.element.center;
- },
+ // 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);
+ }
- get mouseDownElapsed() {
- var elapsed;
+ return physicalItems;
+ },
- if (!this.mouseDownStart) {
- return 0;
- }
+ /**
+ * Increases the pool size. That is, the physical items in the DOM.
+ * This function will allocate additional physical items
+ * (limited by `MAX_PHYSICAL_COUNT`) if the content size is shorter than
+ * `_optPhysicalSize`
+ *
+ * @return boolean
+ */
+ _increasePoolIfNeeded: function() {
+ if (this._physicalSize >= this._optPhysicalSize || this._physicalAverage === 0) {
+ return false;
+ }
- elapsed = Utility.now() - this.mouseDownStart;
+ // the estimated number of physical items that we will need to reach
+ // the cap established by `_optPhysicalSize`.
+ var missingItems = Math.round(
+ (this._optPhysicalSize - this._physicalSize) * 1.2 / this._physicalAverage
+ );
- if (this.mouseUpStart) {
- elapsed -= this.mouseUpElapsed;
- }
+ // limit the size
+ var nextPhysicalCount = Math.min(
+ this._physicalCount + missingItems,
+ this._virtualCount,
+ MAX_PHYSICAL_COUNT
+ );
- return elapsed;
- },
+ var prevPhysicalCount = this._physicalCount;
+ var delta = nextPhysicalCount - prevPhysicalCount;
- get mouseUpElapsed() {
- return this.mouseUpStart ?
- Utility.now () - this.mouseUpStart : 0;
- },
+ if (delta <= 0) {
+ return false;
+ }
- get mouseDownElapsedSeconds() {
- return this.mouseDownElapsed / 1000;
- },
+ var newPhysicalItems = this._createPool(delta);
+ var emptyArray = new Array(delta);
- get mouseUpElapsedSeconds() {
- return this.mouseUpElapsed / 1000;
- },
+ [].push.apply(this._physicalItems, newPhysicalItems);
+ [].push.apply(this._physicalSizes, emptyArray);
- get mouseInteractionSeconds() {
- return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
- },
+ this._physicalCount = prevPhysicalCount + delta;
+
+ return true;
+ },
- get initialOpacity() {
- return this.element.initialOpacity;
- },
+ /**
+ * 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.
+ */
+ _render: function() {
+ var requiresUpdate = this._virtualCount > 0 || this._physicalCount > 0;
- get opacityDecayVelocity() {
- return this.element.opacityDecayVelocity;
- },
+ if (this.isAttached && !this._itemsRendered && this._isVisible && requiresUpdate) {
+ this._update();
+ this._itemsRendered = true;
+ }
+ },
- get radius() {
- var width2 = this.containerMetrics.width * this.containerMetrics.width;
- var height2 = this.containerMetrics.height * this.containerMetrics.height;
- var waveRadius = Math.min(
- Math.sqrt(width2 + height2),
- Ripple.MAX_RADIUS
- ) * 1.1 + 5;
+ /**
+ * Templetizes the user template.
+ */
+ _ensureTemplatized: function() {
+ if (!this.ctor) {
+ // Template instance props that should be excluded from forwarding
+ var props = {};
- var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
- var timeNow = this.mouseInteractionSeconds / duration;
- var size = waveRadius * (1 - Math.pow(80, -timeNow));
+ props.__key__ = true;
+ props[this.as] = true;
+ props[this.indexAs] = true;
+ props[this.selectedAs] = true;
- return Math.abs(size);
- },
+ this._instanceProps = props;
+ this._userTemplate = Polymer.dom(this).querySelector('template');
- get opacity() {
- if (!this.mouseUpStart) {
- return this.initialOpacity;
+ if (this._userTemplate) {
+ this.templatize(this._userTemplate);
+ } else {
+ console.warn('iron-list requires a template to be provided in light-dom');
}
+ }
+ },
- return Math.max(
- 0,
- this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVelocity
- );
- },
-
- get outerOpacity() {
- // Linear increase in background opacity, capped at the opacity
- // of the wavefront (waveOpacity).
- var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
- var waveOpacity = this.opacity;
-
- return Math.max(
- 0,
- Math.min(outerOpacity, waveOpacity)
- );
- },
+ /**
+ * Implements extension point from Templatizer mixin.
+ */
+ _getStampedChildren: function() {
+ return this._physicalItems;
+ },
- get isOpacityFullyDecayed() {
- return this.opacity < 0.01 &&
- this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
- },
+ /**
+ * 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.
+ */
+ _forwardInstancePath: function(inst, path, value) {
+ if (path.indexOf(this.as + '.') === 0) {
+ this.notifyPath('items.' + inst.__key__ + '.' +
+ path.slice(this.as.length + 1), value);
+ }
+ },
- get isRestingAtMaxRadius() {
- return this.opacity >= this.initialOpacity &&
- this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
- },
+ /**
+ * Implements extension point from Templatizer mixin
+ * Called as side-effect of a host property change, responsible for
+ * notifying parent path change on each row.
+ */
+ _forwardParentProp: function(prop, value) {
+ if (this._physicalItems) {
+ this._physicalItems.forEach(function(item) {
+ item._templateInstance[prop] = value;
+ }, this);
+ }
+ },
- get isAnimationComplete() {
- return this.mouseUpStart ?
- this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
- },
+ /**
+ * 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);
+ }
+ },
- get translationFraction() {
- return Math.min(
- 1,
- this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
- );
- },
+ /**
+ * Called as a side effect of a host items.<key>.<path> path change,
+ * responsible for notifying item.<path> changes to row for key.
+ */
+ _forwardItemPath: function(path, value) {
+ if (this._physicalIndexForKey) {
+ var dot = path.indexOf('.');
+ var key = path.substring(0, dot < 0 ? path.length : dot);
+ var idx = this._physicalIndexForKey[key];
+ var row = this._physicalItems[idx];
+ if (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;
+ }
+ }
+ }
+ },
- get xNow() {
- if (this.xEnd) {
- return this.xStart + this.translationFraction * (this.xEnd - this.xStart);
+ /**
+ * 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') {
+ // render the new set
+ this._itemsRendered = false;
+
+ // 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);
+
+ // 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);
}
- return this.xStart;
- },
+ this.debounce('refresh', this._render);
- get yNow() {
- if (this.yEnd) {
- return this.yStart + this.translationFraction * (this.yEnd - this.yStart);
- }
+ } else if (change.path === 'items.splices') {
+ // render the new set
+ this._itemsRendered = false;
- return this.yStart;
- },
+ this._adjustVirtualIndex(change.value.indexSplices);
+ this._virtualCount = this.items ? this.items.length : 0;
- get isMouseDown() {
- return this.mouseDownStart && !this.mouseUpStart;
- },
+ this.debounce('refresh', this._render);
- resetInteractionState: function() {
- this.maxRadius = 0;
- this.mouseDownStart = 0;
- this.mouseUpStart = 0;
+ } else {
+ // update a single item
+ this._forwardItemPath(change.path.split('.').slice(1).join('.'), change.value);
+ }
+ },
- this.xStart = 0;
- this.yStart = 0;
- this.xEnd = 0;
- this.yEnd = 0;
- this.slideDistance = 0;
+ /**
+ * @param {!Array<!PolymerSplice>} splices
+ */
+ _adjustVirtualIndex: function(splices) {
+ var i, splice, idx;
- this.containerMetrics = new ElementMetrics(this.element);
- },
+ for (i = 0; i < splices.length; i++) {
+ splice = splices[i];
- draw: function() {
- var scale;
- var translateString;
- var dx;
- var dy;
+ // deselect removed items
+ splice.removed.forEach(this.$.selector.deselect, this.$.selector);
- this.wave.style.opacity = this.opacity;
+ idx = splice.index;
+ // We only need to care about changes happening above the current position
+ if (idx >= this._virtualStartVal) {
+ break;
+ }
- scale = this.radius / (this.containerMetrics.size / 2);
- dx = this.xNow - (this.containerMetrics.width / 2);
- dy = this.yNow - (this.containerMetrics.height / 2);
+ this._virtualStart = this._virtualStart +
+ Math.max(splice.addedCount - splice.removed.length, idx - this._virtualStartVal);
+ }
+ },
+ _scrollHandler: function() {
+ this._refresh();
+ },
- // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
- // https://bugs.webkit.org/show_bug.cgi?id=98538
- this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
- this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
- this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
- this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
- },
+ /**
+ * 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;
+
+ 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;
+ }
+ }
+ } else {
+ pidx = this._physicalStart;
+ vidx = this._virtualStartVal;
- /** @param {Event=} event */
- downAction: function(event) {
- var xCenter = this.containerMetrics.width / 2;
- var yCenter = this.containerMetrics.height / 2;
+ for (; pidx < this._physicalCount; pidx++, vidx++) {
+ if ((rtn = fn.call(this, pidx, vidx)) != null) {
+ return rtn;
+ }
+ }
- this.resetInteractionState();
- this.mouseDownStart = Utility.now();
+ pidx = 0;
- if (this.center) {
- this.xStart = xCenter;
- this.yStart = yCenter;
- this.slideDistance = Utility.distance(
- this.xStart, this.yStart, this.xEnd, this.yEnd
- );
- } else {
- this.xStart = event ?
- event.detail.x - this.containerMetrics.boundingRect.left :
- this.containerMetrics.width / 2;
- this.yStart = event ?
- event.detail.y - this.containerMetrics.boundingRect.top :
- this.containerMetrics.height / 2;
+ for (; pidx < this._physicalStart; pidx++, vidx++) {
+ if ((rtn = fn.call(this, pidx, vidx)) != null) {
+ return rtn;
+ }
}
+ }
+ },
- if (this.recenters) {
- this.xEnd = xCenter;
- this.yEnd = yCenter;
- this.slideDistance = Utility.distance(
- this.xStart, this.yStart, this.xEnd, this.yEnd
- );
+ /**
+ * 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];
+
+ 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', '');
}
- this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
- this.xStart,
- this.yStart
- );
+ }, itemSet);
+ },
- this.waveContainer.style.top =
- (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px';
- this.waveContainer.style.left =
- (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
+ /**
+ * Updates the height for a given set of items.
+ *
+ * @param {!Array<number>=} itemSet
+ */
+ _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();
+
+ this._iterateItems(function(pidx, vidx) {
+ oldPhysicalSize += this._physicalSizes[pidx] || 0;
+ this._physicalSizes[pidx] = this._physicalItems[pidx].offsetHeight;
+ newPhysicalSize += this._physicalSizes[pidx];
+ this._physicalAverageCount += this._physicalSizes[pidx] ? 1 : 0;
+ }, itemSet);
+
+ this._physicalSize = this._physicalSize + newPhysicalSize - oldPhysicalSize;
+ this._viewportSize = this._scroller.offsetHeight;
+
+ // update the average if we measured something
+ if (this._physicalAverageCount !== prevAvgCount) {
+ this._physicalAverage = Math.round(
+ ((prevPhysicalAvg * prevAvgCount) + newPhysicalSize) /
+ this._physicalAverageCount);
+ }
+ },
- this.waveContainer.style.width = this.containerMetrics.size + 'px';
- this.waveContainer.style.height = this.containerMetrics.size + 'px';
- },
+ /**
+ * Updates the position of the physical items.
+ */
+ _positionItems: function() {
+ this._adjustScrollPosition();
- /** @param {Event=} event */
- upAction: function(event) {
- if (!this.isMouseDown) {
- return;
+ var y = this._physicalTop;
+
+ this._iterateItems(function(pidx) {
+
+ this.transform('translate3d(0, ' + y + 'px, 0)', this._physicalItems[pidx]);
+ y += this._physicalSizes[pidx];
+
+ });
+ },
+
+ /**
+ * Adjusts the scroll position when it was overestimated.
+ */
+ _adjustScrollPosition: function() {
+ var deltaHeight = this._virtualStartVal === 0 ? this._physicalTop :
+ Math.min(this._scrollPosition + this._physicalTop, 0);
+
+ 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._scroller.scrollTop - deltaHeight);
}
+ }
+ },
- this.mouseUpStart = Utility.now();
- },
+ /**
+ * Sets the position of the scroll.
+ */
+ _resetScrollPosition: function(pos) {
+ if (this._scroller) {
+ this._scroller.scrollTop = pos;
+ this._scrollPosition = this._scroller.scrollTop;
+ }
+ },
- remove: function() {
- Polymer.dom(this.waveContainer.parentNode).removeChild(
- this.waveContainer
- );
+ /**
+ * 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._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;
}
- };
+ },
- Polymer({
- is: 'paper-ripple',
+ /**
+ * 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;
+ }
- behaviors: [
- Polymer.IronA11yKeysBehavior
- ],
+ var firstVisible = this.firstVisibleIndex;
- properties: {
- /**
- * The initial opacity set on the wave.
- *
- * @attribute initialOpacity
- * @type number
- * @default 0.25
- */
- initialOpacity: {
- type: Number,
- value: 0.25
- },
+ idx = Math.min(Math.max(idx, 0), this._virtualCount-1);
- /**
- * How fast (opacity per second) the wave fades out.
- *
- * @attribute opacityDecayVelocity
- * @type number
- * @default 0.8
- */
- opacityDecayVelocity: {
- type: Number,
- value: 0.8
- },
+ // start at the previous virtual item
+ // so we have a item above the first visible item
+ this._virtualStart = idx - 1;
- /**
- * If true, ripples will exhibit a gravitational pull towards
- * the center of their container as they fade away.
- *
- * @attribute recenters
- * @type boolean
- * @default false
- */
- recenters: {
- type: Boolean,
- value: false
- },
+ // assign new models
+ this._assignModels();
- /**
- * If true, ripples will center inside its container
- *
- * @attribute recenters
- * @type boolean
- * @default false
- */
- center: {
- type: Boolean,
- value: false
- },
+ // measure the new sizes
+ this._updateMetrics();
- /**
- * A list of the visual ripples.
- *
- * @attribute ripples
- * @type Array
- * @default []
- */
- ripples: {
- type: Array,
- value: function() {
- return [];
- }
- },
-
- /**
- * True when there are visible ripples animating within the
- * element.
- */
- animating: {
- type: Boolean,
- readOnly: true,
- reflectToAttribute: true,
- value: false
- },
-
- /**
- * If true, the ripple will remain in the "down" state until `holdDown`
- * is set to false again.
- */
- holdDown: {
- type: Boolean,
- value: false,
- observer: '_holdDownChanged'
- },
-
- _animating: {
- type: Boolean
- },
-
- _boundAnimate: {
- type: Function,
- value: function() {
- return this.animate.bind(this);
- }
- }
- },
-
- get target () {
- var ownerRoot = Polymer.dom(this).getOwnerRoot();
- var target;
-
- if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
- target = ownerRoot.host;
- } else {
- target = this.parentNode;
- }
+ // estimate new physical offset
+ this._physicalTop = this._virtualStart * this._physicalAverage;
- return target;
- },
+ var currentTopItem = this._physicalStart;
+ var currentVirtualItem = this._virtualStart;
+ var targetOffsetTop = 0;
+ var hiddenContentSize = this._hiddenContentSize;
- keyBindings: {
- 'enter:keydown': '_onEnterKeydown',
- 'space:keydown': '_onSpaceKeydown',
- 'space:keyup': '_onSpaceKeyup'
- },
+ // 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++;
+ }
- attached: function() {
- this.listen(this.target, 'up', 'upAction');
- this.listen(this.target, 'down', 'downAction');
+ // update the scroller size
+ this._updateScrollerSize(true);
- if (!this.target.hasAttribute('noink')) {
- this.keyEventTarget = this.target;
- }
- },
+ // update the position of the items
+ this._positionItems();
- get shouldKeepAnimating () {
- for (var index = 0; index < this.ripples.length; ++index) {
- if (!this.ripples[index].isAnimationComplete) {
- return true;
- }
- }
+ // set the new scroll position
+ this._resetScrollPosition(this._physicalTop + targetOffsetTop + 1);
- return false;
- },
+ // increase the pool of physical items if needed
+ if (this._increasePoolIfNeeded()) {
+ // set models to the new items
+ this.async(this._update);
+ }
- simulatedRipple: function() {
- this.downAction(null);
+ // clear cached visible index
+ this._firstVisibleIndexVal = null;
+ },
- // Please see polymer/polymer#1305
- this.async(function() {
- this.upAction();
- }, 1);
- },
+ /**
+ * Reset the physical average and the average count.
+ */
+ _resetAverage: function() {
+ this._physicalAverage = 0;
+ this._physicalAverageCount = 0;
+ },
- /** @param {Event=} event */
- downAction: function(event) {
- if (this.holdDown && this.ripples.length > 0) {
- return;
+ /**
+ * A handler for the `iron-resize` event triggered by `IronResizableBehavior`
+ * when the element is resized.
+ */
+ _resizeHandler: function() {
+ this.debounce('resize', function() {
+ this._render();
+ if (this._itemsRendered && this._physicalItems && this._isVisible) {
+ this._resetAverage();
+ this.updateViewportBoundaries();
+ this.scrollToIndex(this.firstVisibleIndex);
}
+ });
+ },
- var ripple = this.addRipple();
-
- ripple.downAction(event);
+ _getModelFromItem: function(item) {
+ var key = this._collection.getKey(item);
+ var pidx = this._physicalIndexForKey[key];
- if (!this._animating) {
- this.animate();
- }
- },
+ if (pidx !== undefined) {
+ return this._physicalItems[pidx]._templateInstance;
+ }
+ return null;
+ },
- /** @param {Event=} event */
- upAction: function(event) {
- if (this.holdDown) {
- return;
+ /**
+ * 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 (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');
+ }
+ return item;
+ },
- this.ripples.forEach(function(ripple) {
- ripple.upAction(event);
- });
-
- this.animate();
- },
-
- onAnimationComplete: function() {
- this._animating = false;
- this.$.background.style.backgroundColor = null;
- this.fire('transitionend');
- },
+ /**
+ * 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);
- addRipple: function() {
- var ripple = new Ripple(this);
+ if (!this.multiSelection && this.selectedItem) {
+ this.deselectItem(this.selectedItem);
+ }
+ if (model) {
+ model[this.selectedAs] = true;
+ }
+ this.$.selector.select(item);
+ },
- Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
- this.$.background.style.backgroundColor = ripple.color;
- this.ripples.push(ripple);
+ /**
+ * Deselects the given item list if it is already selected.
+ *
- this._setAnimating(true);
+ * @method deselect
+ * @param {(Object|number)} item The item object or its index
+ */
+ deselectItem: function(item) {
+ item = this._getNormalizedItem(item);
+ var model = this._getModelFromItem(item);
- return ripple;
- },
+ if (model) {
+ model[this.selectedAs] = false;
+ }
+ this.$.selector.deselect(item);
+ },
- removeRipple: function(ripple) {
- var rippleIndex = this.ripples.indexOf(ripple);
+ /**
+ * Select or deselect a given item depending on whether the item
+ * has already been selected.
+ *
+ * @method toggleSelectionForItem
+ * @param {(Object|number)} item The item object or its index
+ */
+ toggleSelectionForItem: function(item) {
+ item = this._getNormalizedItem(item);
+ if (/** @type {!ArraySelectorElement} */ (this.$.selector).isSelected(item)) {
+ this.deselectItem(item);
+ } else {
+ this.selectItem(item);
+ }
+ },
- if (rippleIndex < 0) {
- return;
+ /**
+ * Clears the current selection state of the list.
+ *
+ * @method clearSelection
+ */
+ clearSelection: function() {
+ function unselect(item) {
+ var model = this._getModelFromItem(item);
+ if (model) {
+ model[this.selectedAs] = false;
}
+ }
- this.ripples.splice(rippleIndex, 1);
+ if (Array.isArray(this.selectedItems)) {
+ this.selectedItems.forEach(unselect, this);
+ } else if (this.selectedItem) {
+ unselect.call(this, this.selectedItem);
+ }
- ripple.remove();
+ /** @type {!ArraySelectorElement} */ (this.$.selector).clearSelection();
+ },
- if (!this.ripples.length) {
- this._setAnimating(false);
- }
- },
+ /**
+ * Add an event listener to `tap` if `selectionEnabled` is true,
+ * it will remove the listener otherwise.
+ */
+ _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');
+ }
+ },
- animate: function() {
- var index;
- var ripple;
+ /**
+ * Select an item from an event object.
+ */
+ _selectionHandler: function(e) {
+ if (e.type !== 'keypress' || e.keyCode === 13) {
+ var model = this.modelForElement(e.target);
+ if (model) {
+ this.toggleSelectionForItem(model[this.as]);
+ }
+ }
+ },
- this._animating = true;
+ _multiSelectionChanged: function(multiSelection) {
+ this.clearSelection();
+ this.$.selector.multi = multiSelection;
+ },
- for (index = 0; index < this.ripples.length; ++index) {
- ripple = this.ripples[index];
+ /**
+ * Updates the size of an item.
+ *
+ * @method updateSizeForItem
+ * @param {(Object|number)} item The item object or its index
+ */
+ updateSizeForItem: function(item) {
+ item = this._getNormalizedItem(item);
+ var key = this._collection.getKey(item);
+ var pidx = this._physicalIndexForKey[key];
+
+ if (pidx !== undefined) {
+ this._updateMetrics([pidx]);
+ this._positionItems();
+ }
+ }
+ });
- ripple.draw();
+})();
+(function() {
- this.$.background.style.opacity = ripple.outerOpacity;
+ 'use strict';
- if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
- this.removeRipple(ripple);
- }
- }
+ var SHADOW_WHEN_SCROLLING = 1;
+ var SHADOW_ALWAYS = 2;
- if (!this.shouldKeepAnimating && this.ripples.length === 0) {
- this.onAnimationComplete();
- } else {
- window.requestAnimationFrame(this._boundAnimate);
- }
- },
- _onEnterKeydown: function() {
- this.downAction();
- this.async(this.upAction, 1);
- },
+ var MODE_CONFIGS = {
- _onSpaceKeydown: function() {
- this.downAction();
+ outerScroll: {
+ 'scroll': true
},
- _onSpaceKeyup: function() {
- this.upAction();
+ shadowMode: {
+ 'standard': SHADOW_ALWAYS,
+ 'waterfall': SHADOW_WHEN_SCROLLING,
+ 'waterfall-tall': SHADOW_WHEN_SCROLLING
},
- _holdDownChanged: function(holdDown) {
- if (holdDown) {
- this.downAction();
- } else {
- this.upAction();
- }
+ tallMode: {
+ 'waterfall-tall': true
}
- });
- })();
-/**
- * @demo demo/index.html
- * @polymerBehavior
- */
- Polymer.IronControlState = {
+ };
- properties: {
+ Polymer({
- /**
- * If true, the element currently has focus.
- */
- focused: {
- type: Boolean,
- value: false,
- notify: true,
- readOnly: true,
- reflectToAttribute: true
- },
+ is: 'paper-header-panel',
/**
- * If true, the user cannot interact with this element.
- */
- disabled: {
- type: Boolean,
- value: false,
- notify: true,
- observer: '_disabledChanged',
- reflectToAttribute: true
- },
-
- _oldTabIndex: {
- type: Number
- },
-
- _boundFocusBlurHandler: {
- type: Function,
- value: function() {
- return this._focusBlurHandler.bind(this);
- }
- }
-
- },
-
- observers: [
- '_changedControlState(focused, disabled)'
- ],
+ * Fired when the content has been scrolled. `event.detail.target` returns
+ * the scrollable element which you can use to access scroll info such as
+ * `scrollTop`.
+ *
+ * <paper-header-panel on-content-scroll="scrollHandler">
+ * ...
+ * </paper-header-panel>
+ *
+ *
+ * scrollHandler: function(event) {
+ * var scroller = event.detail.target;
+ * console.log(scroller.scrollTop);
+ * }
+ *
+ * @event content-scroll
+ */
- ready: function() {
- this.addEventListener('focus', this._boundFocusBlurHandler, true);
- this.addEventListener('blur', this._boundFocusBlurHandler, true);
- },
+ properties: {
- _focusBlurHandler: function(event) {
- // NOTE(cdata): if we are in ShadowDOM land, `event.target` will
- // eventually become `this` due to retargeting; if we are not in
- // ShadowDOM land, `event.target` will eventually become `this` due
- // to the second conditional which fires a synthetic event (that is also
- // handled). In either case, we can disregard `event.path`.
+ /**
+ * Controls header and scrolling behavior. Options are
+ * `standard`, `seamed`, `waterfall`, `waterfall-tall`, `scroll` and
+ * `cover`. Default is `standard`.
+ *
+ * `standard`: The header is a step above the panel. The header will consume the
+ * panel at the point of entry, preventing it from passing through to the
+ * opposite side.
+ *
+ * `seamed`: The header is presented as seamed with the panel.
+ *
+ * `waterfall`: Similar to standard mode, but header is initially presented as
+ * seamed with panel, but then separates to form the step.
+ *
+ * `waterfall-tall`: The header is initially taller (`tall` class is added to
+ * the header). As the user scrolls, the header separates (forming an edge)
+ * while condensing (`tall` class is removed from the header).
+ *
+ * `scroll`: The header keeps its seam with the panel, and is pushed off screen.
+ *
+ * `cover`: The panel covers the whole `paper-header-panel` including the
+ * header. This allows user to style the panel in such a way that the panel is
+ * partially covering the header.
+ *
+ * <paper-header-panel mode="cover">
+ * <paper-toolbar class="tall">
+ * <core-icon-button icon="menu"></core-icon-button>
+ * </paper-toolbar>
+ * <div class="content"></div>
+ * </paper-header-panel>
+ */
+ mode: {
+ type: String,
+ value: 'standard',
+ observer: '_modeChanged',
+ reflectToAttribute: true
+ },
- if (event.target === this) {
- var focused = event.type === 'focus';
- this._setFocused(focused);
- } else if (!this.shadowRoot) {
- this.fire(event.type, {sourceEvent: event}, {
- node: this,
- bubbles: event.bubbles,
- cancelable: event.cancelable
- });
- }
- },
+ /**
+ * If true, the drop-shadow is always shown no matter what mode is set to.
+ */
+ shadow: {
+ type: Boolean,
+ value: false
+ },
- _disabledChanged: function(disabled, old) {
- this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
- this.style.pointerEvents = disabled ? 'none' : '';
- if (disabled) {
- this._oldTabIndex = this.tabIndex;
- this.focused = false;
- this.tabIndex = -1;
- } else if (this._oldTabIndex !== undefined) {
- this.tabIndex = this._oldTabIndex;
- }
- },
+ /**
+ * The class used in waterfall-tall mode. Change this if the header
+ * accepts a different class for toggling height, e.g. "medium-tall"
+ */
+ tallClass: {
+ type: String,
+ value: 'tall'
+ },
- _changedControlState: function() {
- // _controlStateChanged is abstract, follow-on behaviors may implement it
- if (this._controlStateChanged) {
- this._controlStateChanged();
- }
- }
+ /**
+ * If true, the scroller is at the top
+ */
+ atTop: {
+ type: Boolean,
+ value: true,
+ readOnly: true
+ }
+ },
- };
-/**
- * @demo demo/index.html
- * @polymerBehavior Polymer.IronButtonState
- */
- Polymer.IronButtonStateImpl = {
+ observers: [
+ '_computeDropShadowHidden(atTop, mode, shadow)'
+ ],
- properties: {
+ ready: function() {
+ this.scrollHandler = this._scroll.bind(this);
+ this._addListener();
- /**
- * If true, the user is currently holding down the button.
- */
- pressed: {
- type: Boolean,
- readOnly: true,
- value: false,
- reflectToAttribute: true,
- observer: '_pressedChanged'
+ // Run `scroll` logic once to initialze class names, etc.
+ this._keepScrollingState();
},
- /**
- * If true, the button toggles the active state with each tap or press
- * of the spacebar.
- */
- toggles: {
- type: Boolean,
- value: false,
- reflectToAttribute: true
+ detached: function() {
+ this._removeListener();
},
/**
- * If true, the button is a toggle and is currently in the active state.
+ * Returns the header element
+ *
+ * @property header
+ * @type Object
*/
- active: {
- type: Boolean,
- value: false,
- notify: true,
- reflectToAttribute: true
+ get header() {
+ return Polymer.dom(this.$.headerContent).getDistributedNodes()[0];
},
/**
- * True if the element is currently being pressed by a "pointer," which
- * is loosely defined as mouse or touch input (but specifically excluding
- * keyboard input).
+ * Returns the scrollable element.
+ *
+ * @property scroller
+ * @type Object
*/
- pointerDown: {
- type: Boolean,
- readOnly: true,
- value: false
+ get scroller() {
+ return this._getScrollerForMode(this.mode);
},
/**
- * True if the input device that caused the element to receive focus
- * was a keyboard.
+ * Returns true if the scroller has a visible shadow.
+ *
+ * @property visibleShadow
+ * @type Boolean
*/
- receivedFocusFromKeyboard: {
- type: Boolean,
- readOnly: true
+ get visibleShadow() {
+ return this.$.dropShadow.classList.contains('has-shadow');
},
- /**
- * The aria attribute to be set if the button is a toggle and in the
- * active state.
- */
- ariaActiveAttribute: {
- type: String,
- value: 'aria-pressed',
- observer: '_ariaActiveAttributeChanged'
- }
- },
+ _computeDropShadowHidden: function(atTop, mode, shadow) {
- listeners: {
- down: '_downHandler',
- up: '_upHandler',
- tap: '_tapHandler'
- },
+ var shadowMode = MODE_CONFIGS.shadowMode[mode];
- observers: [
- '_detectKeyboardFocus(focused)',
- '_activeChanged(active, ariaActiveAttribute)'
- ],
+ if (this.shadow) {
+ this.toggleClass('has-shadow', true, this.$.dropShadow);
- keyBindings: {
- 'enter:keydown': '_asyncClick',
- 'space:keydown': '_spaceKeyDownHandler',
- 'space:keyup': '_spaceKeyUpHandler',
- },
+ } else if (shadowMode === SHADOW_ALWAYS) {
+ this.toggleClass('has-shadow', true, this.$.dropShadow);
- _mouseEventRe: /^mouse/,
+ } else if (shadowMode === SHADOW_WHEN_SCROLLING && !atTop) {
+ this.toggleClass('has-shadow', true, this.$.dropShadow);
- _tapHandler: function() {
- if (this.toggles) {
- // a tap is needed to toggle the active state
- this._userActivate(!this.active);
- } else {
- this.active = false;
- }
- },
+ } else {
+ this.toggleClass('has-shadow', false, this.$.dropShadow);
- _detectKeyboardFocus: function(focused) {
- this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
- },
+ }
+ },
- // to emulate native checkbox, (de-)activations from a user interaction fire
- // 'change' events
- _userActivate: function(active) {
- if (this.active !== active) {
- this.active = active;
- this.fire('change');
- }
- },
-
- _eventSourceIsPrimaryInput: function(event) {
- event = event.detail.sourceEvent || event;
-
- // Always true for non-mouse events....
- if (!this._mouseEventRe.test(event.type)) {
- return true;
- }
-
- // http://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
- if ('buttons' in event) {
- return event.buttons === 1;
- }
-
- // http://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/which
- if (typeof event.which === 'number') {
- return event.which < 2;
- }
+ _computeMainContainerClass: function(mode) {
+ // TODO: It will be useful to have a utility for classes
+ // e.g. Polymer.Utils.classes({ foo: true });
- // http://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/button
- return event.button < 1;
- },
+ var classes = {};
- _downHandler: function(event) {
- if (!this._eventSourceIsPrimaryInput(event)) {
- return;
- }
+ classes['flex'] = mode !== 'cover';
- this._setPointerDown(true);
- this._setPressed(true);
- this._setReceivedFocusFromKeyboard(false);
- },
+ return Object.keys(classes).filter(
+ function(className) {
+ return classes[className];
+ }).join(' ');
+ },
- _upHandler: function() {
- this._setPointerDown(false);
- this._setPressed(false);
- },
+ _addListener: function() {
+ this.scroller.addEventListener('scroll', this.scrollHandler, false);
+ },
- _spaceKeyDownHandler: function(event) {
- var keyboardEvent = event.detail.keyboardEvent;
- keyboardEvent.preventDefault();
- keyboardEvent.stopImmediatePropagation();
- this._setPressed(true);
- },
+ _removeListener: function() {
+ this.scroller.removeEventListener('scroll', this.scrollHandler);
+ },
- _spaceKeyUpHandler: function() {
- if (this.pressed) {
- this._asyncClick();
- }
- this._setPressed(false);
- },
+ _modeChanged: function(newMode, oldMode) {
+ var configs = MODE_CONFIGS;
+ var header = this.header;
+ var animateDuration = 200;
- // trigger click asynchronously, the asynchrony is useful to allow one
- // event handler to unwind before triggering another event
- _asyncClick: function() {
- this.async(function() {
- this.click();
- }, 1);
- },
+ if (header) {
+ // in tallMode it may add tallClass to the header; so do the cleanup
+ // when mode is changed from tallMode to not tallMode
+ if (configs.tallMode[oldMode] && !configs.tallMode[newMode]) {
+ header.classList.remove(this.tallClass);
+ this.async(function() {
+ header.classList.remove('animate');
+ }, animateDuration);
+ } else {
+ header.classList.toggle('animate', configs.tallMode[newMode]);
+ }
+ }
+ this._keepScrollingState();
+ },
- // any of these changes are considered a change to button state
+ _keepScrollingState: function() {
+ var main = this.scroller;
+ var header = this.header;
- _pressedChanged: function(pressed) {
- this._changedButtonState();
- },
+ this._setAtTop(main.scrollTop === 0);
- _ariaActiveAttributeChanged: function(value, oldValue) {
- if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
- this.removeAttribute(oldValue);
- }
- },
+ if (header && this.tallClass && MODE_CONFIGS.tallMode[this.mode]) {
+ this.toggleClass(this.tallClass, this.atTop ||
+ header.classList.contains(this.tallClass) &&
+ main.scrollHeight < this.offsetHeight, header);
+ }
+ },
- _activeChanged: function(active, ariaActiveAttribute) {
- if (this.toggles) {
- this.setAttribute(this.ariaActiveAttribute,
- active ? 'true' : 'false');
- } else {
- this.removeAttribute(this.ariaActiveAttribute);
- }
- this._changedButtonState();
- },
+ _scroll: function() {
+ this._keepScrollingState();
+ this.fire('content-scroll', {target: this.scroller}, {bubbles: false});
+ },
- _controlStateChanged: function() {
- if (this.disabled) {
- this._setPressed(false);
- } else {
- this._changedButtonState();
+ _getScrollerForMode: function(mode) {
+ return MODE_CONFIGS.outerScroll[mode] ?
+ this : this.$.mainContainer;
}
- },
- // provide hook for follow-on behaviors to react to button-state
+ });
- _changedButtonState: function() {
- if (this._buttonStateChanged) {
- this._buttonStateChanged(); // abstract
- }
- }
+ })();
+(function() {
- };
+ // monostate data
+ var metaDatas = {};
+ var metaArrays = {};
- /** @polymerBehavior */
- Polymer.IronButtonState = [
- Polymer.IronA11yKeysBehavior,
- Polymer.IronButtonStateImpl
- ];
-/** @polymerBehavior */
- Polymer.PaperButtonBehaviorImpl = {
+ Polymer.IronMeta = Polymer({
- properties: {
+ is: 'iron-meta',
- _elevation: {
- type: Number
- }
+ properties: {
- },
+ /**
+ * The type of meta-data. All meta-data of the same type is stored
+ * together.
+ */
+ type: {
+ type: String,
+ value: 'default',
+ observer: '_typeChanged'
+ },
- observers: [
- '_calculateElevation(focused, disabled, active, pressed, receivedFocusFromKeyboard)'
- ],
+ /**
+ * The key used to store `value` under the `type` namespace.
+ */
+ key: {
+ type: String,
+ observer: '_keyChanged'
+ },
- hostAttributes: {
- role: 'button',
- tabindex: '0'
- },
+ /**
+ * The meta-data to store or retrieve.
+ */
+ value: {
+ type: Object,
+ notify: true,
+ observer: '_valueChanged'
+ },
- _calculateElevation: function() {
- var e = 1;
- if (this.disabled) {
- e = 0;
- } else if (this.active || this.pressed) {
- e = 4;
- } else if (this.receivedFocusFromKeyboard) {
- e = 3;
- }
- this._elevation = e;
- }
- };
+ /**
+ * If true, `value` is set to the iron-meta instance itself.
+ */
+ self: {
+ type: Boolean,
+ observer: '_selfChanged'
+ },
- /** @polymerBehavior */
- Polymer.PaperButtonBehavior = [
- Polymer.IronButtonState,
- Polymer.IronControlState,
- Polymer.PaperButtonBehaviorImpl
- ];
-Polymer({
- is: 'paper-button',
+ /**
+ * Array of all meta-data values for the given type.
+ */
+ list: {
+ type: Array,
+ notify: true
+ }
- behaviors: [
- Polymer.PaperButtonBehavior
- ],
+ },
- properties: {
/**
- * If true, the button should be styled with a shadow.
+ * Only runs if someone invokes the factory/constructor directly
+ * e.g. `new Polymer.IronMeta()`
*/
- raised: {
- type: Boolean,
- reflectToAttribute: true,
- value: false,
- observer: '_calculateElevation'
- }
- },
+ factoryImpl: function(config) {
+ if (config) {
+ for (var n in config) {
+ switch(n) {
+ case 'type':
+ case 'key':
+ case 'value':
+ this[n] = config[n];
+ break;
+ }
+ }
+ }
+ },
- _calculateElevation: function() {
- if (!this.raised) {
- this._elevation = 0;
- } else {
- Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
- }
- },
+ created: function() {
+ // TODO(sjmiles): good for debugging?
+ this._metaDatas = metaDatas;
+ this._metaArrays = metaArrays;
+ },
- _computeContentClass: function(receivedFocusFromKeyboard) {
- var className = 'content ';
- if (receivedFocusFromKeyboard) {
- className += ' keyboard-focus';
- }
- return className;
- }
- });
-/**
- * `iron-range-behavior` provides the behavior for something with a minimum to maximum range.
- *
- * @demo demo/index.html
- * @polymerBehavior
- */
- Polymer.IronRangeBehavior = {
+ _keyChanged: function(key, old) {
+ this._resetRegistration(old);
+ },
- properties: {
+ _valueChanged: function(value) {
+ this._resetRegistration(this.key);
+ },
- /**
- * The number that represents the current value.
- */
- value: {
- type: Number,
- value: 0,
- notify: true,
- reflectToAttribute: true
- },
+ _selfChanged: function(self) {
+ if (self) {
+ this.value = this;
+ }
+ },
- /**
- * The number that indicates the minimum value of the range.
- */
- min: {
- type: Number,
- value: 0,
- notify: true
- },
+ _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);
+ },
- /**
- * The number that indicates the maximum value of the range.
- */
- max: {
- type: Number,
- value: 100,
- notify: true
- },
+ /**
+ * 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];
+ },
- /**
- * Specifies the value granularity of the range's value.
- */
- step: {
- type: Number,
- value: 1,
- notify: true
- },
+ _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);
+ },
+
+ _register: function(key, value, data, list) {
+ if (key && data && value !== undefined) {
+ data[key] = value;
+ list.push(value);
+ }
+ },
+
+ _unregister: function(key, data, list) {
+ if (key && data) {
+ if (key in data) {
+ var value = data[key];
+ delete data[key];
+ this.arrayDelete(list, value);
+ }
+ }
+ }
+
+ });
/**
- * Returns the ratio of the value.
- */
- ratio: {
- type: Number,
- value: 0,
- readOnly: true,
- notify: true
- },
- },
+ `iron-meta-query` can be used to access infomation stored in `iron-meta`.
- observers: [
- '_update(value, min, max, step)'
- ],
+ Examples:
- _calcRatio: function(value) {
- return (this._clampValue(value) - this.min) / (this.max - this.min);
- },
+ If I create an instance like this:
- _clampValue: function(value) {
- return Math.min(this.max, Math.max(this.min, this._calcStep(value)));
- },
+ <iron-meta key="info" value="foo/bar"></iron-meta>
- _calcStep: function(value) {
- /**
- * if we calculate the step using
- * `Math.round(value / step) * step` we may hit a precision point issue
- * eg. 0.1 * 0.2 = 0.020000000000000004
- * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
- *
- * as a work around we can divide by the reciprocal of `step`
+ Note that value="foo/bar" is the metadata I've defined. I could define more
+ attributes or use child nodes to define additional metadata.
+
+ Now I can access that element (and it's metadata) from any `iron-meta-query` instance:
+
+ var value = new Polymer.IronMetaQuery({key: 'info'}).value;
+
+ @group Polymer Iron Elements
+ @element iron-meta-query
*/
- // polymer/issues/2493
- value = parseFloat(value);
- return this.step ? (Math.round((value + this.min) / this.step) / (1 / this.step)) - this.min : value;
- },
+ Polymer.IronMetaQuery = Polymer({
- _validateValue: function() {
- var v = this._clampValue(this.value);
- this.value = this.oldValue = isNaN(v) ? this.oldValue : v;
- return this.value !== v;
- },
+ is: 'iron-meta-query',
- _update: function() {
- this._validateValue();
- this._setRatio(this._calcRatio(this.value) * 100);
- }
+ properties: {
-};
-Polymer({
+ /**
+ * The type of meta-data. All meta-data of the same type is stored
+ * together.
+ */
+ type: {
+ type: String,
+ value: 'default',
+ observer: '_typeChanged'
+ },
- is: 'paper-progress',
+ /**
+ * Specifies a key to use for retrieving `value` from the `type`
+ * namespace.
+ */
+ key: {
+ type: String,
+ observer: '_keyChanged'
+ },
- behaviors: [
- Polymer.IronRangeBehavior
- ],
+ /**
+ * The meta-data to store or retrieve.
+ */
+ value: {
+ type: Object,
+ notify: true,
+ readOnly: true
+ },
- properties: {
+ /**
+ * Array of all meta-data values for the given type.
+ */
+ list: {
+ type: Array,
+ notify: true
+ }
- /**
- * The number that represents the current secondary progress.
- */
- secondaryProgress: {
- type: Number,
- value: 0
},
/**
- * The secondary ratio
+ * Actually a factory method, not a true constructor. Only runs if
+ * someone invokes it directly (via `new Polymer.IronMeta()`);
*/
- secondaryRatio: {
- type: Number,
- value: 0,
- readOnly: true
+ factoryImpl: function(config) {
+ if (config) {
+ for (var n in config) {
+ switch(n) {
+ case 'type':
+ case 'key':
+ this[n] = config[n];
+ break;
+ }
+ }
+ }
},
- /**
- * Use an indeterminate progress indicator.
- */
- indeterminate: {
- type: Boolean,
- value: false,
- observer: '_toggleIndeterminate'
+ 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);
+ }
},
/**
- * True if the progress is disabled.
+ * Retrieves meta data value by key.
+ * @param {string} key The key of the meta-data to be returned.
+ * @return {*}
*/
- disabled: {
- type: Boolean,
- value: false,
- reflectToAttribute: true,
- observer: '_disabledChanged'
+ byKey: function(key) {
+ return this._metaData && this._metaData[key];
}
- },
- observers: [
- '_progressChanged(secondaryProgress, value, min, max)'
- ],
-
- hostAttributes: {
- role: 'progressbar'
- },
+ });
- _toggleIndeterminate: function(indeterminate) {
- // If we use attribute/class binding, the animation sometimes doesn't translate properly
- // on Safari 7.1. So instead, we toggle the class here in the update method.
- this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress);
- },
-
- _transformProgress: function(progress, ratio) {
- var transform = 'scaleX(' + (ratio / 100) + ')';
- progress.style.transform = progress.style.webkitTransform = transform;
- },
-
- _mainRatioChanged: function(ratio) {
- this._transformProgress(this.$.primaryProgress, ratio);
- },
-
- _progressChanged: function(secondaryProgress, value, min, max) {
- secondaryProgress = this._clampValue(secondaryProgress);
- value = this._clampValue(value);
-
- var secondaryRatio = this._calcRatio(secondaryProgress) * 100;
- var mainRatio = this._calcRatio(value) * 100;
-
- this._setSecondaryRatio(secondaryRatio);
- this._transformProgress(this.$.secondaryProgress, secondaryRatio);
- this._transformProgress(this.$.primaryProgress, mainRatio);
-
- this.secondaryProgress = secondaryProgress;
+ })();
+Polymer({
- this.setAttribute('aria-valuenow', value);
- this.setAttribute('aria-valuemin', min);
- this.setAttribute('aria-valuemax', max);
- },
+ is: 'iron-icon',
- _disabledChanged: function(disabled) {
- this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
- },
+ properties: {
- _hideSecondaryProgress: function(secondaryRatio) {
- return secondaryRatio === 0;
- }
+ /**
+ * The name of the icon to use. The name should be of the form:
+ * `iconset_name:icon_name`.
+ */
+ icon: {
+ type: String,
+ observer: '_iconChanged'
+ },
- });
-// Copyright 2015 The Chromium Authors. All rights reserved.
-// Use of this source code is governed by a BSD-style license that can be
-// found in the LICENSE file.
+ /**
+ * The name of the theme to used, if one is specified by the
+ * iconset.
+ */
+ theme: {
+ type: String,
+ observer: '_updateIcon'
+ },
-cr.define('downloads', function() {
- var Item = Polymer({
- is: 'downloads-item',
+ /**
+ * 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'
+ },
- /**
- * @param {!downloads.ThrottledIconLoader} iconLoader
- */
- factoryImpl: function(iconLoader) {
- /** @private {!downloads.ThrottledIconLoader} */
- this.iconLoader_ = iconLoader;
- },
+ /**
+ * @type {!Polymer.IronMeta}
+ */
+ _meta: {
+ value: Polymer.Base.create('iron-meta', {type: 'iconset'})
+ }
- properties: {
- data: {
- type: Object,
},
- hideDate: {
- type: Boolean,
- value: true,
- },
+ _DEFAULT_ICONSET: 'icons',
- readyPromise: {
- type: Object,
- value: function() {
- return new Promise(function(resolve, reject) {
- this.resolveReadyPromise_ = resolve;
- }.bind(this));
- },
+ _iconChanged: function(icon) {
+ var parts = (icon || '').split(':');
+ this._iconName = parts.pop();
+ this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
+ this._updateIcon();
},
- completelyOnDisk_: {
- computed: 'computeCompletelyOnDisk_(' +
- 'data.state, data.file_externally_removed)',
- type: Boolean,
- value: true,
+ _srcChanged: function(src) {
+ this._updateIcon();
},
- controlledBy_: {
- computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)',
- type: String,
- value: '',
+ _usesIconset: function() {
+ return this.icon || !this.src;
},
- i18n_: {
- readOnly: true,
- type: Object,
- value: function() {
- return {
- cancel: loadTimeData.getString('controlCancel'),
- discard: loadTimeData.getString('dangerDiscard'),
- pause: loadTimeData.getString('controlPause'),
- remove: loadTimeData.getString('controlRemoveFromList'),
- resume: loadTimeData.getString('controlResume'),
- restore: loadTimeData.getString('dangerRestore'),
- retry: loadTimeData.getString('controlRetry'),
- save: loadTimeData.getString('dangerSave'),
- };
- },
- },
+ /** @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);
+ }
+ }
- isActive_: {
- computed: 'computeIsActive_(' +
- 'data.state, data.file_externally_removed)',
- type: Boolean,
- value: true,
- },
+ });
+/**
+ * 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="50" y="50" width="50" height="50" />
+ * <circle cx="50" cy="50" r="50" />
+ * </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
+ */
+ Polymer({
- isDangerous_: {
- computed: 'computeIsDangerous_(data.state)',
- type: Boolean,
- value: false,
- },
+ is: 'iron-iconset-svg',
- isInProgress_: {
- computed: 'computeIsInProgress_(data.state)',
- type: Boolean,
- value: false,
- },
+ properties: {
- showCancel_: {
- computed: 'computeShowCancel_(data.state)',
- type: Boolean,
- value: false,
+ /**
+ * The name of the iconset.
+ *
+ * @attribute name
+ * @type string
+ */
+ name: {
+ type: String,
+ observer: '_nameChanged'
},
- showProgress_: {
- computed: 'computeShowProgress_(showCancel_, data.percent)',
- type: Boolean,
- value: false,
- },
+ /**
+ * The size of an individual icon. Note that icons must be square.
+ *
+ * @attribute iconSize
+ * @type number
+ * @default 24
+ */
+ size: {
+ type: Number,
+ value: 24
+ }
- isMalware_: {
- computed: 'computeIsMalware_(isDangerous_, data.danger_type)',
- type: Boolean,
- value: false,
- },
},
- observers: [
- // TODO(dbeam): this gets called way more when I observe data.by_ext_id
- // and data.by_ext_name directly. Why?
- 'observeControlledBy_(controlledBy_)',
- ],
+ /**
+ * 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;
+ }
+
+ });
+Polymer({
+ is: 'paper-material',
+
+ properties: {
+
+ /**
+ * The z-depth of this element, from 0-5. Setting to 0 will remove the
+ * shadow, and each increasing number greater than 0 will be "deeper"
+ * than the last.
+ *
+ * @attribute elevation
+ * @type number
+ * @default 1
+ */
+ elevation: {
+ type: Number,
+ reflectToAttribute: true,
+ value: 1
+ },
+
+ /**
+ * Set this to true to animate the shadow when setting a new
+ * `elevation` value.
+ *
+ * @attribute animated
+ * @type boolean
+ * @default false
+ */
+ animated: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: false
+ }
+ }
+ });
+(function() {
+ 'use strict';
+
+ /**
+ * 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+0009': 'tab',
+ 'U+001B': 'esc',
+ 'U+0020': 'space',
+ 'U+002A': '*',
+ 'U+0030': '0',
+ 'U+0031': '1',
+ 'U+0032': '2',
+ 'U+0033': '3',
+ 'U+0034': '4',
+ 'U+0035': '5',
+ 'U+0036': '6',
+ 'U+0037': '7',
+ 'U+0038': '8',
+ 'U+0039': '9',
+ 'U+0041': 'a',
+ 'U+0042': 'b',
+ 'U+0043': 'c',
+ 'U+0044': 'd',
+ 'U+0045': 'e',
+ 'U+0046': 'f',
+ 'U+0047': 'g',
+ 'U+0048': 'h',
+ 'U+0049': 'i',
+ 'U+004A': 'j',
+ 'U+004B': 'k',
+ 'U+004C': 'l',
+ 'U+004D': 'm',
+ 'U+004E': 'n',
+ 'U+004F': 'o',
+ 'U+0050': 'p',
+ 'U+0051': 'q',
+ 'U+0052': 'r',
+ 'U+0053': 's',
+ 'U+0054': 't',
+ 'U+0055': 'u',
+ 'U+0056': 'v',
+ 'U+0057': 'w',
+ 'U+0058': 'x',
+ 'U+0059': 'y',
+ 'U+005A': 'z',
+ 'U+007F': 'del'
+ };
+
+ /**
+ * 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 = {
+ 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: '*'
+ };
+
+ /**
+ * 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'
+ };
+
+ /**
+ * 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*]/;
+
+ /**
+ * Matches a keyIdentifier string.
+ */
+ var IDENT_CHAR = /U\+/;
+
+ /**
+ * Matches arrow keys in Gecko 27.0+
+ */
+ var ARROW_KEY = /^arrow/;
+
+ /**
+ * Matches space keys everywhere (notably including IE10's exceptional name
+ * `spacebar`).
+ */
+ var SPACE_KEY = /^space(bar)?/;
+
+ function transformKey(key) {
+ var validKey = '';
+ if (key) {
+ var lKey = key.toLowerCase();
+ if (lKey.length == 1) {
+ if (KEY_CHAR.test(lKey)) {
+ validKey = lKey;
+ }
+ } else if (ARROW_KEY.test(lKey)) {
+ validKey = lKey.replace('arrow', '');
+ } else if (SPACE_KEY.test(lKey)) {
+ validKey = 'space';
+ } else if (lKey == 'multiply') {
+ // numpad '*' can map to Multiply on IE/Windows
+ validKey = '*';
+ } else {
+ validKey = lKey;
+ }
+ }
+ return validKey;
+ }
+
+ function transformKeyIdentifier(keyIdent) {
+ var validKey = '';
+ if (keyIdent) {
+ if (IDENT_CHAR.test(keyIdent)) {
+ validKey = KEY_IDENTIFIER[keyIdent];
+ } else {
+ validKey = keyIdent.toLowerCase();
+ }
+ }
+ return validKey;
+ }
+
+ function transformKeyCode(keyCode) {
+ var validKey = '';
+ if (Number(keyCode)) {
+ if (keyCode >= 65 && keyCode <= 90) {
+ // ascii a-z
+ // lowercase is 32 offset from uppercase
+ validKey = String.fromCharCode(32 + keyCode);
+ } else if (keyCode >= 112 && keyCode <= 123) {
+ // function keys f1-f12
+ validKey = 'f' + (keyCode - 112);
+ } else if (keyCode >= 48 && keyCode <= 57) {
+ // top 0-9 keys
+ validKey = String(48 - keyCode);
+ } else if (keyCode >= 96 && keyCode <= 105) {
+ // num pad 0-9
+ validKey = String(96 - keyCode);
+ } else {
+ validKey = KEY_CODE[keyCode];
+ }
+ }
+ return validKey;
+ }
+
+ function normalizedKeyForEvent(keyEvent) {
+ // fall back from .key, to .keyIdentifier, to .keyCode, and then to
+ // .detail.key to support artificial keyboard events
+ return transformKey(keyEvent.key) ||
+ transformKeyIdentifier(keyEvent.keyIdentifier) ||
+ transformKeyCode(keyEvent.keyCode) ||
+ transformKey(keyEvent.detail.key) || '';
+ }
+
+ function keyComboMatchesEvent(keyCombo, keyEvent) {
+ return normalizedKeyForEvent(keyEvent) === keyCombo.key &&
+ !!keyEvent.shiftKey === !!keyCombo.shiftKey &&
+ !!keyEvent.ctrlKey === !!keyCombo.ctrlKey &&
+ !!keyEvent.altKey === !!keyCombo.altKey &&
+ !!keyEvent.metaKey === !!keyCombo.metaKey;
+ }
+
+ function parseKeyComboString(keyComboString) {
+ return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
+ var eventParts = keyComboPart.split(':');
+ var keyName = eventParts[0];
+ var event = eventParts[1];
+
+ if (keyName in MODIFIER_KEYS) {
+ parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
+ } else {
+ parsedKeyCombo.key = keyName;
+ parsedKeyCombo.event = event || 'keydown';
+ }
+
+ return parsedKeyCombo;
+ }, {
+ combo: keyComboString.split(':').shift()
+ });
+ }
+
+ function parseEventString(eventString) {
+ return eventString.split(' ').map(function(keyComboString) {
+ return parseKeyComboString(keyComboString);
+ });
+ }
+
+
+ /**
+ * `Polymer.IronA11yKeysBehavior` provides a normalized interface for processing
+ * keyboard commands that pertain to [WAI-ARIA best practices](http://www.w3.org/TR/wai-aria-practices/#kbd_general_binding).
+ * The element takes care of browser differences with respect to Keyboard events
+ * and uses an expressive syntax to filter key presses.
+ *
+ * Use the `keyBindings` prototype property to express what combination of keys
+ * will trigger the event to fire.
+ *
+ * Use the `key-event-target` attribute to set up event handlers on a specific
+ * node.
+ * The `keys-pressed` event will fire when one of the key combinations set with the
+ * `keys` property is pressed.
+ *
+ * @demo demo/index.html
+ * @polymerBehavior
+ */
+ Polymer.IronA11yKeysBehavior = {
+ properties: {
+ /**
+ * The HTMLElement that will be firing relevant KeyboardEvents.
+ */
+ keyEventTarget: {
+ type: Object,
+ value: function() {
+ return this;
+ }
+ },
+
+ _boundKeyHandlers: {
+ 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 {};
+ }
+ }
+ },
+
+ observers: [
+ '_resetKeyEventListeners(keyEventTarget, _boundKeyHandlers)'
+ ],
+
+ keyBindings: {},
+
+ registered: function() {
+ this._prepKeyBindings();
+ },
+
+ attached: function() {
+ this._listenKeyEventListeners();
+ },
+
+ detached: function() {
+ this._unlistenKeyEventListeners();
+ },
+
+ /**
+ * 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();
+ },
+
+ /**
+ * When called, will remove all imperatively-added key bindings.
+ */
+ removeOwnKeyBindings: function() {
+ this._imperativeKeyBindings = {};
+ this._prepKeyBindings();
+ this._resetKeyEventListeners();
+ },
+
+ keyboardEventMatchesKeys: function(event, eventString) {
+ var keyCombos = parseEventString(eventString);
+ var index;
+
+ for (index = 0; index < keyCombos.length; ++index) {
+ if (keyComboMatchesEvent(keyCombos[index], event)) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ _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]);
+ }
+ },
+
+ _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) {
+ keyBindings.forEach(function(keyBinding) {
+ var keyCombo = keyBinding[0];
+ var handlerName = keyBinding[1];
+
+ if (!event.defaultPrevented && keyComboMatchesEvent(keyCombo, event)) {
+ this._triggerKeyHandler(keyCombo, handlerName, event);
+ }
+ }, this);
+ },
+
+ _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
+ var detail = Object.create(keyCombo);
+ detail.keyboardEvent = keyboardEvent;
+
+ this[handlerName].call(this, new CustomEvent(keyCombo.event, {
+ detail: detail
+ }));
+ }
+ };
+ })();
+(function() {
+ var Utility = {
+ distance: function(x1, y1, x2, y2) {
+ var xDelta = (x1 - x2);
+ var yDelta = (y1 - y2);
+
+ return Math.sqrt(xDelta * xDelta + yDelta * yDelta);
+ },
+
+ now: window.performance && window.performance.now ?
+ window.performance.now.bind(window.performance) : Date.now
+ };
+
+ /**
+ * @param {HTMLElement} element
+ * @constructor
+ */
+ function ElementMetrics(element) {
+ this.element = element;
+ this.width = this.boundingRect.width;
+ this.height = this.boundingRect.height;
+
+ this.size = Math.max(this.width, this.height);
+ }
+
+ ElementMetrics.prototype = {
+ get boundingRect () {
+ return this.element.getBoundingClientRect();
+ },
+
+ furthestCornerDistanceFrom: function(x, y) {
+ var topLeft = Utility.distance(x, y, 0, 0);
+ var topRight = Utility.distance(x, y, this.width, 0);
+ var bottomLeft = Utility.distance(x, y, 0, this.height);
+ var bottomRight = Utility.distance(x, y, this.width, this.height);
+
+ return Math.max(topLeft, topRight, bottomLeft, bottomRight);
+ }
+ };
+
+ /**
+ * @param {HTMLElement} element
+ * @constructor
+ */
+ function Ripple(element) {
+ this.element = element;
+ this.color = window.getComputedStyle(element).color;
+
+ this.wave = document.createElement('div');
+ this.waveContainer = document.createElement('div');
+ this.wave.style.backgroundColor = this.color;
+ this.wave.classList.add('wave');
+ this.waveContainer.classList.add('wave-container');
+ Polymer.dom(this.waveContainer).appendChild(this.wave);
+
+ this.resetInteractionState();
+ }
+
+ Ripple.MAX_RADIUS = 300;
+
+ Ripple.prototype = {
+ get recenters() {
+ return this.element.recenters;
+ },
+
+ get center() {
+ return this.element.center;
+ },
+
+ get mouseDownElapsed() {
+ var elapsed;
+
+ if (!this.mouseDownStart) {
+ return 0;
+ }
+
+ elapsed = Utility.now() - this.mouseDownStart;
+
+ if (this.mouseUpStart) {
+ elapsed -= this.mouseUpElapsed;
+ }
+
+ return elapsed;
+ },
+
+ get mouseUpElapsed() {
+ return this.mouseUpStart ?
+ Utility.now () - this.mouseUpStart : 0;
+ },
+
+ get mouseDownElapsedSeconds() {
+ return this.mouseDownElapsed / 1000;
+ },
+
+ get mouseUpElapsedSeconds() {
+ return this.mouseUpElapsed / 1000;
+ },
+
+ get mouseInteractionSeconds() {
+ return this.mouseDownElapsedSeconds + this.mouseUpElapsedSeconds;
+ },
+
+ get initialOpacity() {
+ return this.element.initialOpacity;
+ },
+
+ get opacityDecayVelocity() {
+ return this.element.opacityDecayVelocity;
+ },
+
+ get radius() {
+ var width2 = this.containerMetrics.width * this.containerMetrics.width;
+ var height2 = this.containerMetrics.height * this.containerMetrics.height;
+ var waveRadius = Math.min(
+ Math.sqrt(width2 + height2),
+ Ripple.MAX_RADIUS
+ ) * 1.1 + 5;
+
+ var duration = 1.1 - 0.2 * (waveRadius / Ripple.MAX_RADIUS);
+ var timeNow = this.mouseInteractionSeconds / duration;
+ var size = waveRadius * (1 - Math.pow(80, -timeNow));
+
+ return Math.abs(size);
+ },
+
+ get opacity() {
+ if (!this.mouseUpStart) {
+ return this.initialOpacity;
+ }
+
+ return Math.max(
+ 0,
+ this.initialOpacity - this.mouseUpElapsedSeconds * this.opacityDecayVelocity
+ );
+ },
+
+ get outerOpacity() {
+ // Linear increase in background opacity, capped at the opacity
+ // of the wavefront (waveOpacity).
+ var outerOpacity = this.mouseUpElapsedSeconds * 0.3;
+ var waveOpacity = this.opacity;
+
+ return Math.max(
+ 0,
+ Math.min(outerOpacity, waveOpacity)
+ );
+ },
+
+ get isOpacityFullyDecayed() {
+ return this.opacity < 0.01 &&
+ this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
+ },
+
+ get isRestingAtMaxRadius() {
+ return this.opacity >= this.initialOpacity &&
+ this.radius >= Math.min(this.maxRadius, Ripple.MAX_RADIUS);
+ },
+
+ get isAnimationComplete() {
+ return this.mouseUpStart ?
+ this.isOpacityFullyDecayed : this.isRestingAtMaxRadius;
+ },
+
+ get translationFraction() {
+ return Math.min(
+ 1,
+ this.radius / this.containerMetrics.size * 2 / Math.sqrt(2)
+ );
+ },
+
+ get xNow() {
+ if (this.xEnd) {
+ return this.xStart + this.translationFraction * (this.xEnd - this.xStart);
+ }
+
+ return this.xStart;
+ },
+
+ get yNow() {
+ if (this.yEnd) {
+ return this.yStart + this.translationFraction * (this.yEnd - this.yStart);
+ }
+
+ return this.yStart;
+ },
+
+ get isMouseDown() {
+ return this.mouseDownStart && !this.mouseUpStart;
+ },
+
+ resetInteractionState: function() {
+ this.maxRadius = 0;
+ this.mouseDownStart = 0;
+ this.mouseUpStart = 0;
+
+ this.xStart = 0;
+ this.yStart = 0;
+ this.xEnd = 0;
+ this.yEnd = 0;
+ this.slideDistance = 0;
+
+ this.containerMetrics = new ElementMetrics(this.element);
+ },
+
+ draw: function() {
+ var scale;
+ var translateString;
+ var dx;
+ var dy;
+
+ this.wave.style.opacity = this.opacity;
+
+ scale = this.radius / (this.containerMetrics.size / 2);
+ dx = this.xNow - (this.containerMetrics.width / 2);
+ dy = this.yNow - (this.containerMetrics.height / 2);
+
+
+ // 2d transform for safari because of border-radius and overflow:hidden clipping bug.
+ // https://bugs.webkit.org/show_bug.cgi?id=98538
+ this.waveContainer.style.webkitTransform = 'translate(' + dx + 'px, ' + dy + 'px)';
+ this.waveContainer.style.transform = 'translate3d(' + dx + 'px, ' + dy + 'px, 0)';
+ this.wave.style.webkitTransform = 'scale(' + scale + ',' + scale + ')';
+ this.wave.style.transform = 'scale3d(' + scale + ',' + scale + ',1)';
+ },
+
+ /** @param {Event=} event */
+ downAction: function(event) {
+ var xCenter = this.containerMetrics.width / 2;
+ var yCenter = this.containerMetrics.height / 2;
+
+ this.resetInteractionState();
+ this.mouseDownStart = Utility.now();
+
+ if (this.center) {
+ this.xStart = xCenter;
+ this.yStart = yCenter;
+ this.slideDistance = Utility.distance(
+ this.xStart, this.yStart, this.xEnd, this.yEnd
+ );
+ } else {
+ this.xStart = event ?
+ event.detail.x - this.containerMetrics.boundingRect.left :
+ this.containerMetrics.width / 2;
+ this.yStart = event ?
+ event.detail.y - this.containerMetrics.boundingRect.top :
+ this.containerMetrics.height / 2;
+ }
+
+ if (this.recenters) {
+ this.xEnd = xCenter;
+ this.yEnd = yCenter;
+ this.slideDistance = Utility.distance(
+ this.xStart, this.yStart, this.xEnd, this.yEnd
+ );
+ }
+
+ this.maxRadius = this.containerMetrics.furthestCornerDistanceFrom(
+ this.xStart,
+ this.yStart
+ );
+
+ this.waveContainer.style.top =
+ (this.containerMetrics.height - this.containerMetrics.size) / 2 + 'px';
+ this.waveContainer.style.left =
+ (this.containerMetrics.width - this.containerMetrics.size) / 2 + 'px';
+
+ this.waveContainer.style.width = this.containerMetrics.size + 'px';
+ this.waveContainer.style.height = this.containerMetrics.size + 'px';
+ },
+
+ /** @param {Event=} event */
+ upAction: function(event) {
+ if (!this.isMouseDown) {
+ return;
+ }
+
+ this.mouseUpStart = Utility.now();
+ },
+
+ remove: function() {
+ Polymer.dom(this.waveContainer.parentNode).removeChild(
+ this.waveContainer
+ );
+ }
+ };
+
+ Polymer({
+ is: 'paper-ripple',
+
+ behaviors: [
+ Polymer.IronA11yKeysBehavior
+ ],
+
+ properties: {
+ /**
+ * The initial opacity set on the wave.
+ *
+ * @attribute initialOpacity
+ * @type number
+ * @default 0.25
+ */
+ initialOpacity: {
+ type: Number,
+ value: 0.25
+ },
+
+ /**
+ * How fast (opacity per second) the wave fades out.
+ *
+ * @attribute opacityDecayVelocity
+ * @type number
+ * @default 0.8
+ */
+ opacityDecayVelocity: {
+ type: Number,
+ value: 0.8
+ },
+
+ /**
+ * If true, ripples will exhibit a gravitational pull towards
+ * the center of their container as they fade away.
+ *
+ * @attribute recenters
+ * @type boolean
+ * @default false
+ */
+ recenters: {
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * If true, ripples will center inside its container
+ *
+ * @attribute recenters
+ * @type boolean
+ * @default false
+ */
+ center: {
+ type: Boolean,
+ value: false
+ },
+
+ /**
+ * A list of the visual ripples.
+ *
+ * @attribute ripples
+ * @type Array
+ * @default []
+ */
+ ripples: {
+ type: Array,
+ value: function() {
+ return [];
+ }
+ },
+
+ /**
+ * True when there are visible ripples animating within the
+ * element.
+ */
+ animating: {
+ type: Boolean,
+ readOnly: true,
+ reflectToAttribute: true,
+ value: false
+ },
+
+ /**
+ * If true, the ripple will remain in the "down" state until `holdDown`
+ * is set to false again.
+ */
+ holdDown: {
+ type: Boolean,
+ value: false,
+ observer: '_holdDownChanged'
+ },
+
+ /**
+ * If true, the ripple will not generate a ripple effect
+ * via pointer interaction.
+ * Calling ripple's imperative api like `simulatedRipple` will
+ * still generate the ripple effect.
+ */
+ noink: {
+ type: Boolean,
+ value: false
+ },
+
+ _animating: {
+ type: Boolean
+ },
+
+ _boundAnimate: {
+ type: Function,
+ value: function() {
+ return this.animate.bind(this);
+ }
+ }
+ },
+
+ observers: [
+ '_noinkChanged(noink, isAttached)'
+ ],
+
+ get target () {
+ var ownerRoot = Polymer.dom(this).getOwnerRoot();
+ var target;
+
+ if (this.parentNode.nodeType == 11) { // DOCUMENT_FRAGMENT_NODE
+ target = ownerRoot.host;
+ } else {
+ target = this.parentNode;
+ }
+
+ return target;
+ },
+
+ keyBindings: {
+ 'enter:keydown': '_onEnterKeydown',
+ 'space:keydown': '_onSpaceKeydown',
+ 'space:keyup': '_onSpaceKeyup'
+ },
+
+ attached: function() {
+ this.listen(this.target, 'up', 'uiUpAction');
+ this.listen(this.target, 'down', 'uiDownAction');
+ },
+
+ detached: function() {
+ this.unlisten(this.target, 'up', 'uiUpAction');
+ this.unlisten(this.target, 'down', 'uiDownAction');
+ },
+
+ get shouldKeepAnimating () {
+ for (var index = 0; index < this.ripples.length; ++index) {
+ if (!this.ripples[index].isAnimationComplete) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+
+ simulatedRipple: function() {
+ this.downAction(null);
+
+ // Please see polymer/polymer#1305
+ this.async(function() {
+ this.upAction();
+ }, 1);
+ },
+
+ /**
+ * Provokes a ripple down effect via a UI event,
+ * respecting the `noink` property.
+ * @param {Event=} event
+ */
+ uiDownAction: function(event) {
+ if (!this.noink) {
+ this.downAction(event);
+ }
+ },
+
+ /**
+ * Provokes a ripple down effect via a UI event,
+ * *not* respecting the `noink` property.
+ * @param {Event=} event
+ */
+ downAction: function(event) {
+ if (this.holdDown && this.ripples.length > 0) {
+ return;
+ }
+
+ var ripple = this.addRipple();
+
+ ripple.downAction(event);
+
+ if (!this._animating) {
+ this.animate();
+ }
+ },
+
+ /**
+ * Provokes a ripple up effect via a UI event,
+ * respecting the `noink` property.
+ * @param {Event=} event
+ */
+ uiUpAction: function(event) {
+ if (!this.noink) {
+ this.upAction(event);
+ }
+ },
+
+ /**
+ * Provokes a ripple up effect via a UI event,
+ * *not* respecting the `noink` property.
+ * @param {Event=} event
+ */
+ upAction: function(event) {
+ if (this.holdDown) {
+ return;
+ }
+
+ this.ripples.forEach(function(ripple) {
+ ripple.upAction(event);
+ });
+
+ this.animate();
+ },
+
+ onAnimationComplete: function() {
+ this._animating = false;
+ this.$.background.style.backgroundColor = null;
+ this.fire('transitionend');
+ },
+
+ addRipple: function() {
+ var ripple = new Ripple(this);
+
+ Polymer.dom(this.$.waves).appendChild(ripple.waveContainer);
+ this.$.background.style.backgroundColor = ripple.color;
+ this.ripples.push(ripple);
+
+ this._setAnimating(true);
+
+ return ripple;
+ },
+
+ removeRipple: function(ripple) {
+ var rippleIndex = this.ripples.indexOf(ripple);
+
+ if (rippleIndex < 0) {
+ return;
+ }
+
+ this.ripples.splice(rippleIndex, 1);
+
+ ripple.remove();
+
+ if (!this.ripples.length) {
+ this._setAnimating(false);
+ }
+ },
+
+ animate: function() {
+ var index;
+ var ripple;
+
+ this._animating = true;
+
+ for (index = 0; index < this.ripples.length; ++index) {
+ ripple = this.ripples[index];
+
+ ripple.draw();
+
+ this.$.background.style.opacity = ripple.outerOpacity;
+
+ if (ripple.isOpacityFullyDecayed && !ripple.isRestingAtMaxRadius) {
+ this.removeRipple(ripple);
+ }
+ }
+
+ if (!this.shouldKeepAnimating && this.ripples.length === 0) {
+ this.onAnimationComplete();
+ } else {
+ window.requestAnimationFrame(this._boundAnimate);
+ }
+ },
+
+ _onEnterKeydown: function() {
+ this.uiDownAction();
+ this.async(this.uiUpAction, 1);
+ },
+
+ _onSpaceKeydown: function() {
+ this.uiDownAction();
+ },
+
+ _onSpaceKeyup: function() {
+ this.uiUpAction();
+ },
+
+ // note: holdDown does not respect noink since it can be a focus based
+ // effect.
+ _holdDownChanged: function(newVal, oldVal) {
+ if (oldVal === undefined) {
+ return;
+ }
+ if (newVal) {
+ this.downAction();
+ } else {
+ this.upAction();
+ }
+ },
+
+ _noinkChanged: function(noink, attached) {
+ if (attached) {
+ this.keyEventTarget = noink ? this : this.target;
+ }
+ }
+ });
+ })();
+/**
+ * @demo demo/index.html
+ * @polymerBehavior
+ */
+ Polymer.IronControlState = {
+
+ properties: {
+
+ /**
+ * If true, the element currently has focus.
+ */
+ focused: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ readOnly: true,
+ reflectToAttribute: true
+ },
+
+ /**
+ * If true, the user cannot interact with this element.
+ */
+ disabled: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ observer: '_disabledChanged',
+ reflectToAttribute: true
+ },
+
+ _oldTabIndex: {
+ type: Number
+ },
+
+ _boundFocusBlurHandler: {
+ type: Function,
+ value: function() {
+ return this._focusBlurHandler.bind(this);
+ }
+ }
+
+ },
+
+ observers: [
+ '_changedControlState(focused, disabled)'
+ ],
ready: function() {
- this.content = this.$.content;
- this.resolveReadyPromise_();
+ this.addEventListener('focus', this._boundFocusBlurHandler, true);
+ this.addEventListener('blur', this._boundFocusBlurHandler, true);
+ },
+
+ _focusBlurHandler: function(event) {
+ // NOTE(cdata): if we are in ShadowDOM land, `event.target` will
+ // eventually become `this` due to retargeting; if we are not in
+ // ShadowDOM land, `event.target` will eventually become `this` due
+ // to the second conditional which fires a synthetic event (that is also
+ // handled). In either case, we can disregard `event.path`.
+
+ if (event.target === this) {
+ var focused = event.type === 'focus';
+ this._setFocused(focused);
+ } else if (!this.shadowRoot) {
+ this.fire(event.type, {sourceEvent: event}, {
+ node: this,
+ bubbles: event.bubbles,
+ cancelable: event.cancelable
+ });
+ }
+ },
+
+ _disabledChanged: function(disabled, old) {
+ this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
+ this.style.pointerEvents = disabled ? 'none' : '';
+ if (disabled) {
+ this._oldTabIndex = this.tabIndex;
+ this.focused = false;
+ this.tabIndex = -1;
+ } else if (this._oldTabIndex !== undefined) {
+ this.tabIndex = this._oldTabIndex;
+ }
+ },
+
+ _changedControlState: function() {
+ // _controlStateChanged is abstract, follow-on behaviors may implement it
+ if (this._controlStateChanged) {
+ this._controlStateChanged();
+ }
+ }
+
+ };
+/**
+ * @demo demo/index.html
+ * @polymerBehavior Polymer.IronButtonState
+ */
+ Polymer.IronButtonStateImpl = {
+
+ properties: {
+
+ /**
+ * If true, the user is currently holding down the button.
+ */
+ pressed: {
+ type: Boolean,
+ readOnly: true,
+ value: false,
+ reflectToAttribute: true,
+ observer: '_pressedChanged'
+ },
+
+ /**
+ * If true, the button toggles the active state with each tap or press
+ * of the spacebar.
+ */
+ toggles: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true
+ },
+
+ /**
+ * If true, the button is a toggle and is currently in the active state.
+ */
+ active: {
+ type: Boolean,
+ value: false,
+ notify: true,
+ reflectToAttribute: true
+ },
+
+ /**
+ * True if the element is currently being pressed by a "pointer," which
+ * is loosely defined as mouse or touch input (but specifically excluding
+ * keyboard input).
+ */
+ pointerDown: {
+ type: Boolean,
+ readOnly: true,
+ value: false
+ },
+
+ /**
+ * True if the input device that caused the element to receive focus
+ * was a keyboard.
+ */
+ receivedFocusFromKeyboard: {
+ type: Boolean,
+ readOnly: true
+ },
+
+ /**
+ * The aria attribute to be set if the button is a toggle and in the
+ * active state.
+ */
+ ariaActiveAttribute: {
+ type: String,
+ value: 'aria-pressed',
+ observer: '_ariaActiveAttributeChanged'
+ }
+ },
+
+ listeners: {
+ down: '_downHandler',
+ up: '_upHandler',
+ tap: '_tapHandler'
+ },
+
+ observers: [
+ '_detectKeyboardFocus(focused)',
+ '_activeChanged(active, ariaActiveAttribute)'
+ ],
+
+ keyBindings: {
+ 'enter:keydown': '_asyncClick',
+ 'space:keydown': '_spaceKeyDownHandler',
+ 'space:keyup': '_spaceKeyUpHandler',
+ },
+
+ _mouseEventRe: /^mouse/,
+
+ _tapHandler: function() {
+ if (this.toggles) {
+ // a tap is needed to toggle the active state
+ this._userActivate(!this.active);
+ } else {
+ this.active = false;
+ }
+ },
+
+ _detectKeyboardFocus: function(focused) {
+ this._setReceivedFocusFromKeyboard(!this.pointerDown && focused);
+ },
+
+ // to emulate native checkbox, (de-)activations from a user interaction fire
+ // 'change' events
+ _userActivate: function(active) {
+ if (this.active !== active) {
+ this.active = active;
+ this.fire('change');
+ }
+ },
+
+ _downHandler: function(event) {
+ this._setPointerDown(true);
+ this._setPressed(true);
+ this._setReceivedFocusFromKeyboard(false);
+ },
+
+ _upHandler: function() {
+ this._setPointerDown(false);
+ this._setPressed(false);
+ },
+
+ _spaceKeyDownHandler: function(event) {
+ var keyboardEvent = event.detail.keyboardEvent;
+ keyboardEvent.preventDefault();
+ keyboardEvent.stopImmediatePropagation();
+ this._setPressed(true);
+ },
+
+ _spaceKeyUpHandler: function() {
+ if (this.pressed) {
+ this._asyncClick();
+ }
+ this._setPressed(false);
+ },
+
+ // trigger click asynchronously, the asynchrony is useful to allow one
+ // event handler to unwind before triggering another event
+ _asyncClick: function() {
+ this.async(function() {
+ this.click();
+ }, 1);
+ },
+
+ // any of these changes are considered a change to button state
+
+ _pressedChanged: function(pressed) {
+ this._changedButtonState();
},
- /** @param {!downloads.Data} data */
- update: function(data) {
- this.data = data;
+ _ariaActiveAttributeChanged: function(value, oldValue) {
+ if (oldValue && oldValue != value && this.hasAttribute(oldValue)) {
+ this.removeAttribute(oldValue);
+ }
+ },
- if (!this.isDangerous_) {
- var icon = 'chrome://fileicon/' + encodeURIComponent(data.file_path);
- this.iconLoader_.loadScaledIcon(this.$['file-icon'], icon);
+ _activeChanged: function(active, ariaActiveAttribute) {
+ if (this.toggles) {
+ this.setAttribute(this.ariaActiveAttribute,
+ active ? 'true' : 'false');
+ } else {
+ this.removeAttribute(this.ariaActiveAttribute);
}
+ this._changedButtonState();
},
- /** @private */
- computeClass_: function() {
- var classes = [];
+ _controlStateChanged: function() {
+ if (this.disabled) {
+ this._setPressed(false);
+ } else {
+ this._changedButtonState();
+ }
+ },
- if (this.isActive_)
- classes.push('is-active');
+ // provide hook for follow-on behaviors to react to button-state
- if (this.isDangerous_)
- classes.push('dangerous');
+ _changedButtonState: function() {
+ if (this._buttonStateChanged) {
+ this._buttonStateChanged(); // abstract
+ }
+ }
- if (this.showProgress_)
- classes.push('show-progress');
+ };
- return classes.join(' ');
+ /** @polymerBehavior */
+ Polymer.IronButtonState = [
+ Polymer.IronA11yKeysBehavior,
+ Polymer.IronButtonStateImpl
+ ];
+/**
+ * `Polymer.PaperRippleBehavior` dynamically implements a ripple
+ * when the element has focus via pointer or keyboard.
+ *
+ * NOTE: This behavior is intended to be used in conjunction with and after
+ * `Polymer.IronButtonState` and `Polymer.IronControlState`.
+ *
+ * @polymerBehavior Polymer.PaperRippleBehavior
+ */
+ Polymer.PaperRippleBehavior = {
+
+ properties: {
+ /**
+ * If true, the element will not produce a ripple effect when interacted
+ * with via the pointer.
+ */
+ noink: {
+ type: Boolean,
+ observer: '_noinkChanged'
+ }
},
- /** @private */
- computeCompletelyOnDisk_: function() {
- return this.data.state == downloads.States.COMPLETE &&
- !this.data.file_externally_removed;
+ /**
+ * Ensures a `<paper-ripple>` element is available when the element is
+ * focused.
+ */
+ _buttonStateChanged: function() {
+ if (this.focused) {
+ this.ensureRipple();
+ }
},
- /** @private */
- computeControlledBy_: function() {
- if (!this.data.by_ext_id || !this.data.by_ext_name)
- return '';
+ /**
+ * In addition to the functionality provided in `IronButtonState`, ensures
+ * a ripple effect is created when the element is in a `pressed` state.
+ */
+ _downHandler: function(event) {
+ Polymer.IronButtonStateImpl._downHandler.call(this, event);
+ if (this.pressed) {
+ this.ensureRipple(event);
+ }
+ },
- var url = 'chrome://extensions#' + this.data.by_ext_id;
- var name = this.data.by_ext_name;
- return loadTimeData.getStringF('controlledByUrl', url, name);
+ /**
+ * Ensures this element contains a ripple effect. For startup efficiency
+ * the ripple effect is dynamically on demand when needed.
+ * @param {!Event=} opt_triggeringEvent (optional) event that triggered the
+ * ripple.
+ */
+ ensureRipple: function(opt_triggeringEvent) {
+ if (!this.hasRipple()) {
+ this._ripple = this._createRipple();
+ this._ripple.noink = this.noink;
+ var rippleContainer = this._rippleContainer || this.root;
+ if (rippleContainer) {
+ Polymer.dom(rippleContainer).appendChild(this._ripple);
+ }
+ var domContainer = rippleContainer === this.shadyRoot ? this :
+ rippleContainer;
+ if (opt_triggeringEvent &&
+ domContainer.contains(opt_triggeringEvent.target)) {
+ this._ripple.uiDownAction(opt_triggeringEvent);
+ }
+ }
},
- /** @private */
- computeDate_: function() {
- if (this.hideDate)
- return '';
- return assert(this.data.since_string || this.data.date_string);
+ /**
+ * Returns the `<paper-ripple>` element used by this element to create
+ * ripple effects. The element's ripple is created on demand, when
+ * necessary, and calling this method will force the
+ * ripple to be created.
+ */
+ getRipple: function() {
+ this.ensureRipple();
+ return this._ripple;
},
- /** @private */
- computeDescription_: function() {
- var data = this.data;
+ /**
+ * Returns true if this element currently contains a ripple effect.
+ * @return {boolean}
+ */
+ hasRipple: function() {
+ return Boolean(this._ripple);
+ },
- switch (data.state) {
- case downloads.States.DANGEROUS:
- var fileName = data.file_name;
- switch (data.danger_type) {
- case downloads.DangerType.DANGEROUS_FILE:
- return loadTimeData.getStringF('dangerFileDesc', fileName);
- case downloads.DangerType.DANGEROUS_URL:
- return loadTimeData.getString('dangerUrlDesc');
- case downloads.DangerType.DANGEROUS_CONTENT: // Fall through.
- case downloads.DangerType.DANGEROUS_HOST:
- return loadTimeData.getStringF('dangerContentDesc', fileName);
- case downloads.DangerType.UNCOMMON_CONTENT:
- return loadTimeData.getStringF('dangerUncommonDesc', fileName);
- case downloads.DangerType.POTENTIALLY_UNWANTED:
- return loadTimeData.getStringF('dangerSettingsDesc', fileName);
- }
- break;
+ /**
+ * Create the element's ripple effect via creating a `<paper-ripple>`.
+ * Override this method to customize the ripple element.
+ * @return {element} Returns a `<paper-ripple>` element.
+ */
+ _createRipple: function() {
+ return document.createElement('paper-ripple');
+ },
- case downloads.States.IN_PROGRESS:
- case downloads.States.PAUSED: // Fallthrough.
- return data.progress_status_text;
+ _noinkChanged: function(noink) {
+ if (this.hasRipple()) {
+ this._ripple.noink = noink;
}
+ }
- return '';
- },
+ };
+/** @polymerBehavior Polymer.PaperButtonBehavior */
+ Polymer.PaperButtonBehaviorImpl = {
- /** @private */
- computeIsActive_: function() {
- return this.data.state != downloads.States.CANCELLED &&
- this.data.state != downloads.States.INTERRUPTED &&
- !this.data.file_externally_removed;
- },
+ properties: {
- /** @private */
- computeIsDangerous_: function() {
- return this.data.state == downloads.States.DANGEROUS;
- },
+ /**
+ * The z-depth of this element, from 0-5. Setting to 0 will remove the
+ * shadow, and each increasing number greater than 0 will be "deeper"
+ * than the last.
+ *
+ * @attribute elevation
+ * @type number
+ * @default 1
+ */
+ elevation: {
+ type: Number,
+ reflectToAttribute: true,
+ readOnly: true
+ }
- /** @private */
- computeIsInProgress_: function() {
- return this.data.state == downloads.States.IN_PROGRESS;
},
- /** @private */
- computeIsMalware_: function() {
- return this.isDangerous_ &&
- (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT ||
- this.data.danger_type == downloads.DangerType.DANGEROUS_HOST ||
- this.data.danger_type == downloads.DangerType.DANGEROUS_URL ||
- this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED);
+ observers: [
+ '_calculateElevation(focused, disabled, active, pressed, receivedFocusFromKeyboard)',
+ '_computeKeyboardClass(receivedFocusFromKeyboard)'
+ ],
+
+ hostAttributes: {
+ role: 'button',
+ tabindex: '0',
+ animated: true
},
- /** @private */
- computeRemoveStyle_: function() {
- var canDelete = loadTimeData.getBoolean('allowDeletingHistory');
- var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete;
- return hideRemove ? 'visibility: hidden' : '';
+ _calculateElevation: function() {
+ var e = 1;
+ if (this.disabled) {
+ e = 0;
+ } else if (this.active || this.pressed) {
+ e = 4;
+ } else if (this.receivedFocusFromKeyboard) {
+ e = 3;
+ }
+ this._setElevation(e);
},
- /** @private */
- computeShowCancel_: function() {
- return this.data.state == downloads.States.IN_PROGRESS ||
- this.data.state == downloads.States.PAUSED;
+ _computeKeyboardClass: function(receivedFocusFromKeyboard) {
+ this.classList.toggle('keyboard-focus', receivedFocusFromKeyboard);
},
- /** @private */
- computeShowProgress_: function() {
- return this.showCancel_ && this.data.percent >= -1;
+ /**
+ * In addition to `IronButtonState` behavior, when space key goes down,
+ * create a ripple down effect.
+ */
+ _spaceKeyDownHandler: function(event) {
+ Polymer.IronButtonStateImpl._spaceKeyDownHandler.call(this, event);
+ if (this.hasRipple()) {
+ this._ripple.uiDownAction();
+ }
},
- /** @private */
- computeTag_: function() {
- switch (this.data.state) {
- case downloads.States.CANCELLED:
- return loadTimeData.getString('statusCancelled');
+ /**
+ * In addition to `IronButtonState` behavior, when space key goes up,
+ * create a ripple up effect.
+ */
+ _spaceKeyUpHandler: function(event) {
+ Polymer.IronButtonStateImpl._spaceKeyUpHandler.call(this, event);
+ if (this.hasRipple()) {
+ this._ripple.uiUpAction();
+ }
+ }
- case downloads.States.INTERRUPTED:
- return this.data.last_reason_text;
+ };
- case downloads.States.COMPLETE:
- return this.data.file_externally_removed ?
- loadTimeData.getString('statusRemoved') : '';
- }
+ /** @polymerBehavior */
+ Polymer.PaperButtonBehavior = [
+ Polymer.IronButtonState,
+ Polymer.IronControlState,
+ Polymer.PaperRippleBehavior,
+ Polymer.PaperButtonBehaviorImpl
+ ];
+Polymer({
+ is: 'paper-button',
- return '';
- },
+ behaviors: [
+ Polymer.PaperButtonBehavior
+ ],
- /** @private */
- isIndeterminate_: function() {
- return this.data.percent == -1;
+ properties: {
+ /**
+ * If true, the button should be styled with a shadow.
+ */
+ raised: {
+ type: Boolean,
+ reflectToAttribute: true,
+ value: false,
+ observer: '_calculateElevation'
+ }
},
- /** @private */
- observeControlledBy_: function() {
- this.$['controlled-by'].innerHTML = this.controlledBy_;
- },
+ _calculateElevation: function() {
+ if (!this.raised) {
+ this.elevation = 0;
+ } else {
+ Polymer.PaperButtonBehaviorImpl._calculateElevation.apply(this);
+ }
+ }
+ });
+/**
+ * `iron-range-behavior` provides the behavior for something with a minimum to maximum range.
+ *
+ * @demo demo/index.html
+ * @polymerBehavior
+ */
+ Polymer.IronRangeBehavior = {
- /** @private */
- onCancelTap_: function() {
- downloads.ActionService.getInstance().cancel(this.data.id);
- },
+ properties: {
- /** @private */
- onDiscardDangerousTap_: function() {
- downloads.ActionService.getInstance().discardDangerous(this.data.id);
+ /**
+ * The number that represents the current value.
+ */
+ value: {
+ type: Number,
+ value: 0,
+ notify: true,
+ reflectToAttribute: true
},
/**
- * @private
- * @param {Event} e
+ * The number that indicates the minimum value of the range.
*/
- onDragStart_: function(e) {
- e.preventDefault();
- downloads.ActionService.getInstance().drag(this.data.id);
+ min: {
+ type: Number,
+ value: 0,
+ notify: true
},
/**
- * @param {Event} e
- * @private
+ * The number that indicates the maximum value of the range.
*/
- onFileLinkTap_: function(e) {
- e.preventDefault();
- downloads.ActionService.getInstance().openFile(this.data.id);
- },
-
- /** @private */
- onPauseTap_: function() {
- downloads.ActionService.getInstance().pause(this.data.id);
- },
-
- /** @private */
- onRemoveTap_: function() {
- downloads.ActionService.getInstance().remove(this.data.id);
- },
-
- /** @private */
- onResumeTap_: function() {
- downloads.ActionService.getInstance().resume(this.data.id);
- },
-
- /** @private */
- onRetryTap_: function() {
- downloads.ActionService.getInstance().download(this.data.url);
+ max: {
+ type: Number,
+ value: 100,
+ notify: true
},
- /** @private */
- onSaveDangerousTap_: function() {
- downloads.ActionService.getInstance().saveDangerous(this.data.id);
+ /**
+ * Specifies the value granularity of the range's value.
+ */
+ step: {
+ type: Number,
+ value: 1,
+ notify: true
},
- /** @private */
- onShowTap_: function() {
- downloads.ActionService.getInstance().show(this.data.id);
+ /**
+ * Returns the ratio of the value.
+ */
+ ratio: {
+ type: Number,
+ value: 0,
+ readOnly: true,
+ notify: true
},
- });
-
- return {Item: Item};
-});
-(function() {
+ },
- // monostate data
- var metaDatas = {};
- var metaArrays = {};
+ observers: [
+ '_update(value, min, max, step)'
+ ],
- Polymer.IronMeta = Polymer({
+ _calcRatio: function(value) {
+ return (this._clampValue(value) - this.min) / (this.max - this.min);
+ },
- is: 'iron-meta',
+ _clampValue: function(value) {
+ return Math.min(this.max, Math.max(this.min, this._calcStep(value)));
+ },
- properties: {
+ _calcStep: function(value) {
+ /**
+ * if we calculate the step using
+ * `Math.round(value / step) * step` we may hit a precision point issue
+ * eg. 0.1 * 0.2 = 0.020000000000000004
+ * http://docs.oracle.com/cd/E19957-01/806-3568/ncg_goldberg.html
+ *
+ * as a work around we can divide by the reciprocal of `step`
+ */
+ // polymer/issues/2493
+ value = parseFloat(value);
+ return this.step ? (Math.round((value + this.min) / this.step) / (1 / this.step)) - this.min : value;
+ },
- /**
- * The type of meta-data. All meta-data of the same type is stored
- * together.
- */
- type: {
- type: String,
- value: 'default',
- observer: '_typeChanged'
- },
+ _validateValue: function() {
+ var v = this._clampValue(this.value);
+ this.value = this.oldValue = isNaN(v) ? this.oldValue : v;
+ return this.value !== v;
+ },
- /**
- * The key used to store `value` under the `type` namespace.
- */
- key: {
- type: String,
- observer: '_keyChanged'
- },
+ _update: function() {
+ this._validateValue();
+ this._setRatio(this._calcRatio(this.value) * 100);
+ }
- /**
- * The meta-data to store or retrieve.
- */
- value: {
- type: Object,
- notify: true,
- observer: '_valueChanged'
- },
+};
+Polymer({
- /**
- * If true, `value` is set to the iron-meta instance itself.
- */
- self: {
- type: Boolean,
- observer: '_selfChanged'
- },
+ is: 'paper-progress',
- /**
- * Array of all meta-data values for the given type.
- */
- list: {
- type: Array,
- notify: true
- }
+ behaviors: [
+ Polymer.IronRangeBehavior
+ ],
- },
+ properties: {
/**
- * Only runs if someone invokes the factory/constructor directly
- * e.g. `new Polymer.IronMeta()`
+ * The number that represents the current secondary progress.
*/
- factoryImpl: function(config) {
- if (config) {
- for (var n in config) {
- switch(n) {
- case 'type':
- case 'key':
- case 'value':
- this[n] = config[n];
- break;
- }
- }
- }
- },
-
- created: function() {
- // TODO(sjmiles): good for debugging?
- this._metaDatas = metaDatas;
- this._metaArrays = metaArrays;
- },
-
- _keyChanged: function(key, old) {
- this._resetRegistration(old);
- },
-
- _valueChanged: function(value) {
- this._resetRegistration(this.key);
+ secondaryProgress: {
+ type: Number,
+ value: 0
},
- _selfChanged: function(self) {
- if (self) {
- this.value = this;
- }
+ /**
+ * The secondary ratio
+ */
+ secondaryRatio: {
+ type: Number,
+ value: 0,
+ readOnly: true
},
- _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);
+ /**
+ * Use an indeterminate progress indicator.
+ */
+ indeterminate: {
+ type: Boolean,
+ value: false,
+ observer: '_toggleIndeterminate'
},
/**
- * Retrieves meta data value by key.
- *
- * @method byKey
- * @param {string} key The key of the meta-data to be returned.
- * @return {*}
+ * True if the progress is disabled.
*/
- byKey: function(key) {
- return this._metaData && this._metaData[key];
- },
+ disabled: {
+ type: Boolean,
+ value: false,
+ reflectToAttribute: true,
+ observer: '_disabledChanged'
+ }
+ },
- _resetRegistration: function(oldKey) {
- this._unregisterKey(oldKey);
- this._registerKeyValue(this.key, this.value);
- },
+ observers: [
+ '_progressChanged(secondaryProgress, value, min, max)'
+ ],
- _unregisterKey: function(key) {
- this._unregister(key, this._metaData, this.list);
- },
+ hostAttributes: {
+ role: 'progressbar'
+ },
- _registerKeyValue: function(key, value) {
- this._register(key, value, this._metaData, this.list);
- },
+ _toggleIndeterminate: function(indeterminate) {
+ // If we use attribute/class binding, the animation sometimes doesn't translate properly
+ // on Safari 7.1. So instead, we toggle the class here in the update method.
+ this.toggleClass('indeterminate', indeterminate, this.$.primaryProgress);
+ },
- _register: function(key, value, data, list) {
- if (key && data && value !== undefined) {
- data[key] = value;
- list.push(value);
- }
- },
+ _transformProgress: function(progress, ratio) {
+ var transform = 'scaleX(' + (ratio / 100) + ')';
+ progress.style.transform = progress.style.webkitTransform = transform;
+ },
- _unregister: function(key, data, list) {
- if (key && data) {
- if (key in data) {
- var value = data[key];
- delete data[key];
- this.arrayDelete(list, value);
- }
- }
- }
+ _mainRatioChanged: function(ratio) {
+ this._transformProgress(this.$.primaryProgress, ratio);
+ },
- });
+ _progressChanged: function(secondaryProgress, value, min, max) {
+ secondaryProgress = this._clampValue(secondaryProgress);
+ value = this._clampValue(value);
- /**
- `iron-meta-query` can be used to access infomation stored in `iron-meta`.
+ var secondaryRatio = this._calcRatio(secondaryProgress) * 100;
+ var mainRatio = this._calcRatio(value) * 100;
- Examples:
+ this._setSecondaryRatio(secondaryRatio);
+ this._transformProgress(this.$.secondaryProgress, secondaryRatio);
+ this._transformProgress(this.$.primaryProgress, mainRatio);
- If I create an instance like this:
+ this.secondaryProgress = secondaryProgress;
- <iron-meta key="info" value="foo/bar"></iron-meta>
+ this.setAttribute('aria-valuenow', value);
+ this.setAttribute('aria-valuemin', min);
+ this.setAttribute('aria-valuemax', max);
+ },
- Note that value="foo/bar" is the metadata I've defined. I could define more
- attributes or use child nodes to define additional metadata.
+ _disabledChanged: function(disabled) {
+ this.setAttribute('aria-disabled', disabled ? 'true' : 'false');
+ },
- Now I can access that element (and it's metadata) from any `iron-meta-query` instance:
+ _hideSecondaryProgress: function(secondaryRatio) {
+ return secondaryRatio === 0;
+ }
- var value = new Polymer.IronMetaQuery({key: 'info'}).value;
+ });
+// Copyright 2015 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
- @group Polymer Iron Elements
- @element iron-meta-query
- */
- Polymer.IronMetaQuery = Polymer({
+cr.define('downloads', function() {
+ var Item = Polymer({
+ is: 'downloads-item',
- is: 'iron-meta-query',
+ properties: {
+ data: {
+ type: Object,
+ },
- properties: {
+ hideDate: {
+ type: Boolean,
+ value: true,
+ },
- /**
- * The type of meta-data. All meta-data of the same type is stored
- * together.
- */
- type: {
- type: String,
- value: 'default',
- observer: '_typeChanged'
- },
+ completelyOnDisk_: {
+ computed: 'computeCompletelyOnDisk_(' +
+ 'data.state, data.file_externally_removed)',
+ type: Boolean,
+ value: true,
+ },
- /**
- * Specifies a key to use for retrieving `value` from the `type`
- * namespace.
- */
- key: {
- type: String,
- observer: '_keyChanged'
- },
+ controlledBy_: {
+ computed: 'computeControlledBy_(data.by_ext_id, data.by_ext_name)',
+ type: String,
+ value: '',
+ },
- /**
- * The meta-data to store or retrieve.
- */
- value: {
- type: Object,
- notify: true,
- readOnly: true
+ i18n_: {
+ readOnly: true,
+ type: Object,
+ value: function() {
+ return {
+ cancel: loadTimeData.getString('controlCancel'),
+ discard: loadTimeData.getString('dangerDiscard'),
+ pause: loadTimeData.getString('controlPause'),
+ remove: loadTimeData.getString('controlRemoveFromList'),
+ resume: loadTimeData.getString('controlResume'),
+ restore: loadTimeData.getString('dangerRestore'),
+ retry: loadTimeData.getString('controlRetry'),
+ save: loadTimeData.getString('dangerSave'),
+ };
},
+ },
- /**
- * Array of all meta-data values for the given type.
- */
- list: {
- type: Array,
- notify: true
- }
+ isActive_: {
+ computed: 'computeIsActive_(' +
+ 'data.state, data.file_externally_removed)',
+ type: Boolean,
+ value: true,
+ },
+ isDangerous_: {
+ computed: 'computeIsDangerous_(data.state)',
+ type: Boolean,
+ value: false,
},
- /**
- * Actually a factory method, not a true constructor. Only runs if
- * someone invokes it directly (via `new Polymer.IronMeta()`);
- */
- factoryImpl: function(config) {
- if (config) {
- for (var n in config) {
- switch(n) {
- case 'type':
- case 'key':
- this[n] = config[n];
- break;
- }
- }
- }
+ isInProgress_: {
+ computed: 'computeIsInProgress_(data.state)',
+ type: Boolean,
+ value: false,
},
- created: function() {
- // TODO(sjmiles): good for debugging?
- this._metaDatas = metaDatas;
- this._metaArrays = metaArrays;
+ showCancel_: {
+ computed: 'computeShowCancel_(data.state)',
+ type: Boolean,
+ value: false,
},
- _keyChanged: function(key) {
- this._setValue(this._metaData && this._metaData[key]);
+ showProgress_: {
+ computed: 'computeShowProgress_(showCancel_, data.percent)',
+ type: Boolean,
+ value: false,
},
- _typeChanged: function(type) {
- this._metaData = metaDatas[type];
- this.list = metaArrays[type];
- if (this.key) {
- this._keyChanged(this.key);
- }
+ isMalware_: {
+ computed: 'computeIsMalware_(isDangerous_, data.danger_type)',
+ type: Boolean,
+ value: false,
},
+ },
- /**
- * 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];
- }
+ observers: [
+ // TODO(dbeam): this gets called way more when I observe data.by_ext_id
+ // and data.by_ext_name directly. Why?
+ 'observeControlledBy_(controlledBy_)',
+ 'observeIsDangerous_(isDangerous_, data.file_path)',
+ ],
- });
+ ready: function() {
+ this.content = this.$.content;
+ },
- })();
-Polymer({
+ /** @private */
+ computeClass_: function() {
+ var classes = [];
- is: 'iron-icon',
+ if (this.isActive_)
+ classes.push('is-active');
- properties: {
+ if (this.isDangerous_)
+ classes.push('dangerous');
- /**
- * The name of the icon to use. The name should be of the form:
- * `iconset_name:icon_name`.
- */
- icon: {
- type: String,
- observer: '_iconChanged'
- },
+ if (this.showProgress_)
+ classes.push('show-progress');
- /**
- * The name of the theme to used, if one is specified by the
- * iconset.
- */
- theme: {
- type: String,
- observer: '_updateIcon'
- },
+ return classes.join(' ');
+ },
- /**
- * 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'
- },
+ /** @private */
+ computeCompletelyOnDisk_: function() {
+ return this.data.state == downloads.States.COMPLETE &&
+ !this.data.file_externally_removed;
+ },
- /**
- * @type {!Polymer.IronMeta}
- */
- _meta: {
- value: Polymer.Base.create('iron-meta', {type: 'iconset'})
- }
+ /** @private */
+ computeControlledBy_: function() {
+ if (!this.data.by_ext_id || !this.data.by_ext_name)
+ return '';
- },
+ var url = 'chrome://extensions#' + this.data.by_ext_id;
+ var name = this.data.by_ext_name;
+ return loadTimeData.getStringF('controlledByUrl', url, name);
+ },
- _DEFAULT_ICONSET: 'icons',
+ /** @private */
+ computeDangerIcon_: function() {
+ if (!this.isDangerous_)
+ return '';
- _iconChanged: function(icon) {
- var parts = (icon || '').split(':');
- this._iconName = parts.pop();
- this._iconsetName = parts.pop() || this._DEFAULT_ICONSET;
- this._updateIcon();
- },
+ switch (this.data.danger_type) {
+ case downloads.DangerType.DANGEROUS_CONTENT:
+ case downloads.DangerType.DANGEROUS_HOST:
+ case downloads.DangerType.DANGEROUS_URL:
+ case downloads.DangerType.POTENTIALLY_UNWANTED:
+ case downloads.DangerType.UNCOMMON_CONTENT:
+ return 'remove-circle';
+ default:
+ return 'warning';
+ }
+ },
- _srcChanged: function(src) {
- this._updateIcon();
- },
+ /** @private */
+ computeDate_: function() {
+ if (this.hideDate)
+ return '';
+ return assert(this.data.since_string || this.data.date_string);
+ },
- _usesIconset: function() {
- return this.icon || !this.src;
- },
+ /** @private */
+ computeDescription_: function() {
+ var data = this.data;
- /** @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;
+ switch (data.state) {
+ case downloads.States.DANGEROUS:
+ var fileName = data.file_name;
+ switch (data.danger_type) {
+ case downloads.DangerType.DANGEROUS_FILE:
+ return loadTimeData.getStringF('dangerFileDesc', fileName);
+ case downloads.DangerType.DANGEROUS_URL:
+ return loadTimeData.getString('dangerUrlDesc');
+ case downloads.DangerType.DANGEROUS_CONTENT: // Fall through.
+ case downloads.DangerType.DANGEROUS_HOST:
+ return loadTimeData.getStringF('dangerContentDesc', fileName);
+ case downloads.DangerType.UNCOMMON_CONTENT:
+ return loadTimeData.getStringF('dangerUncommonDesc', fileName);
+ case downloads.DangerType.POTENTIALLY_UNWANTED:
+ return loadTimeData.getStringF('dangerSettingsDesc', fileName);
}
- this._img.src = this.src;
- Polymer.dom(this.root).appendChild(this._img);
- }
+ break;
+
+ case downloads.States.IN_PROGRESS:
+ case downloads.States.PAUSED: // Fallthrough.
+ return data.progress_status_text;
}
- });
-/**
- * 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="50" y="50" width="50" height="50" />
- * <circle cx="50" cy="50" r="50" />
- * </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
- */
- Polymer({
+ return '';
+ },
- is: 'iron-iconset-svg',
+ /** @private */
+ computeIsActive_: function() {
+ return this.data.state != downloads.States.CANCELLED &&
+ this.data.state != downloads.States.INTERRUPTED &&
+ !this.data.file_externally_removed;
+ },
- properties: {
+ /** @private */
+ computeIsDangerous_: function() {
+ return this.data.state == downloads.States.DANGEROUS;
+ },
- /**
- * The name of the iconset.
- *
- * @attribute name
- * @type string
- */
- name: {
- type: String,
- observer: '_nameChanged'
- },
+ /** @private */
+ computeIsInProgress_: function() {
+ return this.data.state == downloads.States.IN_PROGRESS;
+ },
- /**
- * The size of an individual icon. Note that icons must be square.
- *
- * @attribute iconSize
- * @type number
- * @default 24
- */
- size: {
- type: Number,
- value: 24
- }
+ /** @private */
+ computeIsMalware_: function() {
+ return this.isDangerous_ &&
+ (this.data.danger_type == downloads.DangerType.DANGEROUS_CONTENT ||
+ this.data.danger_type == downloads.DangerType.DANGEROUS_HOST ||
+ this.data.danger_type == downloads.DangerType.DANGEROUS_URL ||
+ this.data.danger_type == downloads.DangerType.POTENTIALLY_UNWANTED);
+ },
+ /** @private */
+ computeRemoveStyle_: function() {
+ var canDelete = loadTimeData.getBoolean('allowDeletingHistory');
+ var hideRemove = this.isDangerous_ || this.showCancel_ || !canDelete;
+ return hideRemove ? 'visibility: hidden' : '';
},
- /**
- * 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);
+ /** @private */
+ computeShowCancel_: function() {
+ return this.data.state == downloads.States.IN_PROGRESS ||
+ this.data.state == downloads.States.PAUSED;
},
- /**
- * 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;
+ /** @private */
+ computeShowProgress_: function() {
+ return this.showCancel_ && this.data.percent >= -1;
},
- /**
- * 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;
+ /** @private */
+ computeTag_: function() {
+ switch (this.data.state) {
+ case downloads.States.CANCELLED:
+ return loadTimeData.getString('statusCancelled');
+
+ case downloads.States.INTERRUPTED:
+ return this.data.last_reason_text;
+
+ case downloads.States.COMPLETE:
+ return this.data.file_externally_removed ?
+ loadTimeData.getString('statusRemoved') : '';
}
+
+ return '';
},
- /**
- *
- * 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});
- });
+ /** @private */
+ isIndeterminate_: function() {
+ return this.data.percent == -1;
},
- /**
- * 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;
+ /** @private */
+ observeControlledBy_: function() {
+ this.$['controlled-by'].innerHTML = this.controlledBy_;
+ },
+
+ /** @private */
+ observeIsDangerous_: function() {
+ if (this.data && !this.isDangerous_) {
+ var filePath = encodeURIComponent(this.data.file_path);
+ this.$['file-icon'].src = 'chrome://fileicon/' + filePath;
+ }
+ },
+
+ /** @private */
+ onCancelTap_: function() {
+ downloads.ActionService.getInstance().cancel(this.data.id);
+ },
+
+ /** @private */
+ onDiscardDangerousTap_: function() {
+ downloads.ActionService.getInstance().discardDangerous(this.data.id);
},
/**
- * 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`.
+ * @private
+ * @param {Event} e
*/
- _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);
+ onDragStart_: function(e) {
+ e.preventDefault();
+ downloads.ActionService.getInstance().drag(this.data.id);
},
/**
- * @param {Element} sourceSvg
- * @param {number} size
- * @return {Element}
+ * @param {Event} e
+ * @private
*/
- _prepareSvgClone: function(sourceSvg, size) {
- if (sourceSvg) {
- var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
- svg.setAttribute('viewBox', ['0', '0', size, size].join(' '));
- 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(sourceSvg.cloneNode(true)).removeAttribute('id');
- return svg;
- }
- return null;
- }
+ onFileLinkTap_: function(e) {
+ e.preventDefault();
+ downloads.ActionService.getInstance().openFile(this.data.id);
+ },
+
+ /** @private */
+ onPauseTap_: function() {
+ downloads.ActionService.getInstance().pause(this.data.id);
+ },
+
+ /** @private */
+ onRemoveTap_: function() {
+ downloads.ActionService.getInstance().remove(this.data.id);
+ },
+
+ /** @private */
+ onResumeTap_: function() {
+ downloads.ActionService.getInstance().resume(this.data.id);
+ },
+
+ /** @private */
+ onRetryTap_: function() {
+ downloads.ActionService.getInstance().download(this.data.url);
+ },
+
+ /** @private */
+ onSaveDangerousTap_: function() {
+ downloads.ActionService.getInstance().saveDangerous(this.data.id);
+ },
+ /** @private */
+ onShowTap_: function() {
+ downloads.ActionService.getInstance().show(this.data.id);
+ },
});
+
+ return {Item: Item};
+});
Polymer({
is: 'paper-item',
@@ -11642,7 +13113,7 @@ Polymer({
* @type {object}
* @default {template: 1}
*/
- excludedLocalNames: {
+ _excludedLocalNames: {
type: Object,
value: function() {
return {
@@ -11659,6 +13130,9 @@ Polymer({
created: function() {
this._bindFilterItem = this._filterItem.bind(this);
this._selection = new Polymer.IronSelection(this._applySelection.bind(this));
+ // TODO(cdata): When polymer/polymer#2535 lands, we do not need to do this
+ // book keeping anymore:
+ this.__listeningForActivate = false;
},
attached: function() {
@@ -11667,6 +13141,7 @@ Polymer({
if (!this.selectedItem && this.selected) {
this._updateSelected(this.attrForSelected,this.selected)
}
+ this._addListener(this.activateEvent);
},
detached: function() {
@@ -11733,11 +13208,17 @@ Polymer({
},
_addListener: function(eventName) {
+ if (!this.isAttached || this.__listeningForActivate) {
+ return;
+ }
+
+ this.__listeningForActivate = true;
this.listen(this, eventName, '_activateHandler');
},
_removeListener: function(eventName) {
this.unlisten(this, eventName, '_activateHandler');
+ this.__listeningForActivate = false;
},
_activateEventChanged: function(eventName, old) {
@@ -11754,7 +13235,7 @@ Polymer({
},
_filterItem: function(node) {
- return !this.excludedLocalNames[node.localName];
+ return !this._excludedLocalNames[node.localName];
},
_valueToItem: function(value) {
@@ -12143,305 +13624,127 @@ Polymer({
var index;
for (index = 0; index < mutations.length; ++index) {
- mutation = mutations[index];
-
- if (mutation.addedNodes.length) {
- this._resetTabindices();
- break;
- }
- }
- },
-
- /**
- * Handler that is called when a shift+tab keypress is detected by the menu.
- *
- * @param {CustomEvent} event A key combination event.
- */
- _onShiftTabDown: function(event) {
- var oldTabIndex;
-
- Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
-
- oldTabIndex = this.getAttribute('tabindex');
-
- this.setAttribute('tabindex', '-1');
-
- this.async(function() {
- this.setAttribute('tabindex', oldTabIndex);
- Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
- // NOTE(cdata): polymer/polymer#1305
- }, 1);
- },
-
- /**
- * Handler that is called when the menu receives focus.
- *
- * @param {FocusEvent} event A focus event.
- */
- _onFocus: function(event) {
- if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
- return;
- }
- // do not focus the menu itself
- this.blur();
- // clear the cached focus item
- this._setFocusedItem(null);
- this._defaultFocusAsync = this.async(function() {
- // focus the selected item when the menu receives focus, or the first item
- // if no item is selected
- var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[0]) : this.selectedItem;
- if (selectedItem) {
- this._setFocusedItem(selectedItem);
- } else {
- this._setFocusedItem(this.items[0]);
- }
- // async 100ms to wait for `select` to get called from `_itemActivate`
- }, 100);
- },
-
- /**
- * Handler that is called when the up key is pressed.
- *
- * @param {CustomEvent} event A key combination event.
- */
- _onUpKey: function(event) {
- // up and down arrows moves the focus
- this._focusPrevious();
- },
-
- /**
- * Handler that is called when the down key is pressed.
- *
- * @param {CustomEvent} event A key combination event.
- */
- _onDownKey: function(event) {
- this._focusNext();
- },
-
- /**
- * Handler that is called when the esc key is pressed.
- *
- * @param {CustomEvent} event A key combination event.
- */
- _onEscKey: function(event) {
- // esc blurs the control
- this.focusedItem.blur();
- },
-
- /**
- * Handler that is called when a keydown event is detected.
- *
- * @param {KeyboardEvent} event A keyboard event.
- */
- _onKeydown: function(event) {
- if (this.keyboardEventMatchesKeys(event, 'up down esc')) {
- return;
- }
-
- // all other keys focus the menu item starting with that character
- this._focusWithKeyboardEvent(event);
- }
- };
-
- Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
-
- /** @polymerBehavior Polymer.IronMenuBehavior */
- Polymer.IronMenuBehavior = [
- Polymer.IronMultiSelectableBehavior,
- Polymer.IronA11yKeysBehavior,
- Polymer.IronMenuBehaviorImpl
- ];
-(function() {
-
- Polymer({
-
- is: 'paper-menu',
-
- behaviors: [
- Polymer.IronMenuBehavior
- ]
-
- });
-
-})();
-/**
- * `IronResizableBehavior` is a behavior that can be used in Polymer elements to
- * coordinate the flow of resize events between "resizers" (elements that control the
- * size or hidden state of their children) and "resizables" (elements that need to be
- * notified when they are resized or un-hidden by their parents in order to take
- * action on their new measurements).
- * Elements that perform measurement should add the `IronResizableBehavior` behavior to
- * their element definition and listen for the `iron-resize` event on themselves.
- * This event will be fired when they become showing after having been hidden,
- * when they are resized explicitly by another resizable, or when the window has been
- * resized.
- * Note, the `iron-resize` event is non-bubbling.
- *
- * @polymerBehavior Polymer.IronResizableBehavior
- * @demo demo/index.html
- **/
- Polymer.IronResizableBehavior = {
- properties: {
- /**
- * The closest ancestor element that implements `IronResizableBehavior`.
- */
- _parentResizable: {
- type: Object,
- observer: '_parentResizableChanged'
- },
-
- /**
- * True if this element is currently notifying its descedant elements of
- * resize.
- */
- _notifyingDescendant: {
- type: Boolean,
- value: false
- }
- },
-
- listeners: {
- 'iron-request-resize-notifications': '_onIronRequestResizeNotifications'
- },
-
- created: function() {
- // We don't really need property effects on these, and also we want them
- // to be created before the `_parentResizable` observer fires:
- this._interestedResizables = [];
- this._boundNotifyResize = this.notifyResize.bind(this);
- },
-
- attached: function() {
- this.fire('iron-request-resize-notifications', null, {
- node: this,
- bubbles: true,
- cancelable: true
- });
-
- if (!this._parentResizable) {
- window.addEventListener('resize', this._boundNotifyResize);
- this.notifyResize();
+ mutation = mutations[index];
+
+ if (mutation.addedNodes.length) {
+ this._resetTabindices();
+ break;
+ }
}
},
- detached: function() {
- if (this._parentResizable) {
- this._parentResizable.stopResizeNotificationsFor(this);
- } else {
- window.removeEventListener('resize', this._boundNotifyResize);
- }
+ /**
+ * Handler that is called when a shift+tab keypress is detected by the menu.
+ *
+ * @param {CustomEvent} event A key combination event.
+ */
+ _onShiftTabDown: function(event) {
+ var oldTabIndex;
- this._parentResizable = null;
+ Polymer.IronMenuBehaviorImpl._shiftTabPressed = true;
+
+ oldTabIndex = this.getAttribute('tabindex');
+
+ this.setAttribute('tabindex', '-1');
+
+ this.async(function() {
+ this.setAttribute('tabindex', oldTabIndex);
+ Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
+ // NOTE(cdata): polymer/polymer#1305
+ }, 1);
},
/**
- * Can be called to manually notify a resizable and its descendant
- * resizables of a resize change.
+ * Handler that is called when the menu receives focus.
+ *
+ * @param {FocusEvent} event A focus event.
*/
- notifyResize: function() {
- if (!this.isAttached) {
+ _onFocus: function(event) {
+ if (Polymer.IronMenuBehaviorImpl._shiftTabPressed) {
return;
}
-
- this._interestedResizables.forEach(function(resizable) {
- if (this.resizerShouldNotify(resizable)) {
- this._notifyDescendant(resizable);
+ // do not focus the menu itself
+ this.blur();
+ // clear the cached focus item
+ this._setFocusedItem(null);
+ this._defaultFocusAsync = this.async(function() {
+ // focus the selected item when the menu receives focus, or the first item
+ // if no item is selected
+ var selectedItem = this.multi ? (this.selectedItems && this.selectedItems[0]) : this.selectedItem;
+ if (selectedItem) {
+ this._setFocusedItem(selectedItem);
+ } else {
+ this._setFocusedItem(this.items[0]);
}
- }, this);
-
- this._fireResize();
+ // async 100ms to wait for `select` to get called from `_itemActivate`
+ }, 100);
},
/**
- * Used to assign the closest resizable ancestor to this resizable
- * if the ancestor detects a request for notifications.
+ * Handler that is called when the up key is pressed.
+ *
+ * @param {CustomEvent} event A key combination event.
*/
- assignParentResizable: function(parentResizable) {
- this._parentResizable = parentResizable;
+ _onUpKey: function(event) {
+ // up and down arrows moves the focus
+ this._focusPrevious();
},
/**
- * Used to remove a resizable descendant from the list of descendants
- * that should be notified of a resize change.
+ * Handler that is called when the down key is pressed.
+ *
+ * @param {CustomEvent} event A key combination event.
*/
- stopResizeNotificationsFor: function(target) {
- var index = this._interestedResizables.indexOf(target);
-
- if (index > -1) {
- this._interestedResizables.splice(index, 1);
- this.unlisten(target, 'iron-resize', '_onDescendantIronResize');
- }
+ _onDownKey: function(event) {
+ this._focusNext();
},
/**
- * This method can be overridden to filter nested elements that should or
- * should not be notified by the current element. Return true if an element
- * should be notified, or false if it should not be notified.
+ * Handler that is called when the esc key is pressed.
*
- * @param {HTMLElement} element A candidate descendant element that
- * implements `IronResizableBehavior`.
- * @return {boolean} True if the `element` should be notified of resize.
+ * @param {CustomEvent} event A key combination event.
*/
- resizerShouldNotify: function(element) { return true; },
+ _onEscKey: function(event) {
+ // esc blurs the control
+ this.focusedItem.blur();
+ },
- _onDescendantIronResize: function(event) {
- if (this._notifyingDescendant) {
- event.stopPropagation();
+ /**
+ * Handler that is called when a keydown event is detected.
+ *
+ * @param {KeyboardEvent} event A keyboard event.
+ */
+ _onKeydown: function(event) {
+ if (this.keyboardEventMatchesKeys(event, 'up down esc')) {
return;
}
- // NOTE(cdata): In ShadowDOM, event retargetting makes echoing of the
- // otherwise non-bubbling event "just work." We do it manually here for
- // the case where Polymer is not using shadow roots for whatever reason:
- if (!Polymer.Settings.useShadow) {
- this._fireResize();
- }
- },
-
- _fireResize: function() {
- this.fire('iron-resize', null, {
- node: this,
- bubbles: false
- });
- },
-
- _onIronRequestResizeNotifications: function(event) {
- var target = event.path ? event.path[0] : event.target;
+ // all other keys focus the menu item starting with that character
+ this._focusWithKeyboardEvent(event);
+ }
+ };
- if (target === this) {
- return;
- }
+ Polymer.IronMenuBehaviorImpl._shiftTabPressed = false;
- if (this._interestedResizables.indexOf(target) === -1) {
- this._interestedResizables.push(target);
- this.listen(target, 'iron-resize', '_onDescendantIronResize');
- }
+ /** @polymerBehavior Polymer.IronMenuBehavior */
+ Polymer.IronMenuBehavior = [
+ Polymer.IronMultiSelectableBehavior,
+ Polymer.IronA11yKeysBehavior,
+ Polymer.IronMenuBehaviorImpl
+ ];
+(function() {
- target.assignParentResizable(this);
- this._notifyDescendant(target);
+ Polymer({
- event.stopPropagation();
- },
+ is: 'paper-menu',
- _parentResizableChanged: function(parentResizable) {
- if (parentResizable) {
- window.removeEventListener('resize', this._boundNotifyResize);
- }
- },
+ behaviors: [
+ Polymer.IronMenuBehavior
+ ]
- _notifyDescendant: function(descendant) {
- // NOTE(cdata): In IE10, attached is fired on children first, so it's
- // important not to notify them if the parent is not attached yet (or
- // else they will get redundantly notified when the parent attaches).
- if (!this.isAttached) {
- return;
- }
+ });
- this._notifyingDescendant = true;
- descendant.notifyResize();
- this._notifyingDescendant = false;
- }
- };
+})();
/**
Polymer.IronFitBehavior fits an element in another element using `max-height` and `max-width`, and
optionally centers it in the window or another element.
@@ -12857,7 +14160,8 @@ intent. Closing generally implies that the user acknowledged the content on the
it will cancel whenever the user taps outside it or presses the escape key. This behavior is
configurable with the `no-cancel-on-esc-key` and the `no-cancel-on-outside-click` properties.
`close()` should be called explicitly by the implementer when the user interacts with a control
-in the overlay element.
+in the overlay element. When the dialog is canceled, the overlay fires an 'iron-overlay-canceled'
+event. Call `preventDefault` on this event to prevent the overlay from closing.
### Positioning
@@ -13028,6 +14332,11 @@ context. You should place this element as a child of `<body>` whenever possible.
* Cancels the overlay.
*/
cancel: function() {
+ var cancelEvent = this.fire('iron-overlay-canceled', undefined, {cancelable: true});
+ if (cancelEvent.defaultPrevented) {
+ return;
+ }
+
this.opened = false;
this._setCanceled(true);
},
@@ -14576,7 +15885,7 @@ Polymer({
/**
* `Polymer.PaperInkyFocusBehavior` implements a ripple when the element has keyboard focus.
*
- * @polymerBehavior Polymer.PaperInkyFocusBehavior
+ * @polymerBehavior Polymer.PaperInkyFocusBehaviorImpl
*/
Polymer.PaperInkyFocusBehaviorImpl = {
@@ -14585,11 +15894,20 @@ Polymer({
],
_focusedChanged: function(receivedFocusFromKeyboard) {
- if (!this.$.ink) {
- return;
+ if (receivedFocusFromKeyboard) {
+ this.ensureRipple();
+ }
+ if (this.hasRipple()) {
+ this._ripple.holdDown = receivedFocusFromKeyboard;
}
+ },
- this.$.ink.holdDown = receivedFocusFromKeyboard;
+ _createRipple: function() {
+ var ripple = Polymer.PaperRippleBehavior._createRipple();
+ ripple.id = 'ink';
+ ripple.setAttribute('center', '');
+ ripple.classList.add('circle');
+ return ripple;
}
};
@@ -14598,56 +15916,57 @@ Polymer({
Polymer.PaperInkyFocusBehavior = [
Polymer.IronButtonState,
Polymer.IronControlState,
+ Polymer.PaperRippleBehavior,
Polymer.PaperInkyFocusBehaviorImpl
];
Polymer({
- is: 'paper-icon-button',
+ is: 'paper-icon-button',
- hostAttributes: {
- role: 'button',
- tabindex: '0'
- },
+ hostAttributes: {
+ role: 'button',
+ tabindex: '0'
+ },
- behaviors: [
- Polymer.PaperInkyFocusBehavior
- ],
+ behaviors: [
+ Polymer.PaperInkyFocusBehavior
+ ],
- properties: {
- /**
- * The URL of an image for the icon. If the src property is specified,
- * the icon property should not be.
- */
- src: {
- type: String
- },
+ properties: {
+ /**
+ * The URL of an image for the icon. If the src property is specified,
+ * the icon property should not be.
+ */
+ src: {
+ type: String
+ },
- /**
- * Specifies the icon name or index in the set of icons available in
- * the icon's icon set. If the icon property is specified,
- * the src property should not be.
- */
- icon: {
- type: String
- },
+ /**
+ * Specifies the icon name or index in the set of icons available in
+ * the icon's icon set. If the icon property is specified,
+ * the src property should not be.
+ */
+ icon: {
+ type: String
+ },
- /**
- * Specifies the alternate text for the button, for accessibility.
- */
- alt: {
- type: String,
- observer: "_altChanged"
- }
- },
+ /**
+ * Specifies the alternate text for the button, for accessibility.
+ */
+ alt: {
+ type: String,
+ observer: "_altChanged"
+ }
+ },
- _altChanged: function(newValue, oldValue) {
- var label = this.getAttribute('aria-label');
+ _altChanged: function(newValue, oldValue) {
+ var label = this.getAttribute('aria-label');
- // Don't stomp over a user-set aria-label.
- if (!label || oldValue == label) {
- this.setAttribute('aria-label', newValue);
+ // Don't stomp over a user-set aria-label.
+ if (!label || oldValue == label) {
+ this.setAttribute('aria-label', newValue);
+ }
}
- }
- });
+ });
/**
* Use `Polymer.IronValidatableBehavior` to implement an element that validates user input.
*
@@ -15445,15 +16764,10 @@ cr.define('downloads', function() {
type: Boolean,
value: false,
},
- },
- /**
- * @return {number} A guess at how many items could be visible at once.
- * @private
- */
- guesstimateNumberOfVisibleItems_: function() {
- var toolbarHeight = this.$.toolbar.offsetHeight;
- return Math.floor((window.innerHeight - toolbarHeight) / 46) + 1;
+ items_: {
+ type: Array,
+ },
},
/**
@@ -15493,33 +16807,6 @@ cr.define('downloads', function() {
downloads.ActionService.getInstance().search('');
},
- /** @private */
- rebuildFocusGrid_: function() {
- var activeElement = this.shadowRoot.activeElement;
-
- var activeItem;
- if (activeElement && activeElement.tagName == 'downloads-item')
- activeItem = activeElement;
-
- var activeControl = activeItem && activeItem.shadowRoot.activeElement;
-
- /** @private {!cr.ui.FocusGrid} */
- this.focusGrid_ = this.focusGrid_ || new cr.ui.FocusGrid;
- this.focusGrid_.destroy();
-
- var boundary = this.$['downloads-list'];
-
- this.items_.forEach(function(item) {
- var focusRow = new downloads.FocusRow(item.content, boundary);
- this.focusGrid_.addRow(focusRow);
-
- if (item == activeItem && !cr.ui.FocusRow.isFocusable(activeControl))
- focusRow.getEquivalentElement(activeControl).focus();
- }, this);
-
- this.focusGrid_.ensureRowActive();
- },
-
/**
* @return {number} The number of downloads shown on the page.
* @private
@@ -15534,62 +16821,31 @@ cr.define('downloads', function() {
* @private
*/
updateAll_: function(list) {
- var oldIdMap = this.idMap_ || {};
-
- /** @private {!Object<!downloads.Item>} */
- this.idMap_ = {};
-
- /** @private {!Array<!downloads.Item>} */
- this.items_ = [];
+ /** @private {!Object<number>} */
+ this.idToIndex_ = {};
- if (!this.iconLoader_) {
- var guesstimate = Math.max(this.guesstimateNumberOfVisibleItems_(), 1);
- /** @private {downloads.ThrottledIconLoader} */
- this.iconLoader_ = new downloads.ThrottledIconLoader(guesstimate);
- }
+ var items = [];
for (var i = 0; i < list.length; ++i) {
var data = list[i];
var id = data.id;
- // Re-use old items when possible (saves work, preserves focus).
- var item = oldIdMap[id] || new downloads.Item(this.iconLoader_);
-
- this.idMap_[id] = item; // Associated by ID for fast lookup.
- this.items_.push(item); // Add to sorted list for order.
+ this.idToIndex_[id] = i;
- // Render |item| but don't actually add to the DOM yet. |this.items_|
- // must be fully created to be able to find the right spot to insert.
- item.update(data);
-
- // Collapse redundant dates.
var prev = list[i - 1];
- item.hideDate = !!prev && prev.date_string == data.date_string;
-
- delete oldIdMap[id];
- }
- // Remove stale, previously rendered items from the DOM.
- for (var id in oldIdMap) {
- if (oldIdMap[id].parentNode)
- oldIdMap[id].parentNode.removeChild(oldIdMap[id]);
- delete oldIdMap[id];
+ items.push({
+ index: i,
+ item: data,
+ hideDate: !!prev && prev.date_string == data.date_string,
+ });
}
- for (var i = 0; i < this.items_.length; ++i) {
- var item = this.items_[i];
- if (item.parentNode) // Already in the DOM; skip.
- continue;
-
- var before = null;
- // Find the next rendered item after this one, and insert before it.
- for (var j = i + 1; !before && j < this.items_.length; ++j) {
- if (this.items_[j].parentNode)
- before = this.items_[j];
- }
- // If |before| is null, |item| will just get added at the end.
- this.$['downloads-list'].insertBefore(item, before);
- }
+ // TODO(dbeam): this is a huge hack. Let's figure out a better way.
+ var resetScrollPosition = this.$['downloads-list']._resetScrollPosition;
+ this.$['downloads-list']._resetScrollPosition = function() {};
+ this.items_ = items;
+ this.$['downloads-list']._resetScrollPosition = resetScrollPosition;
var hasDownloads = this.size_() > 0;
if (!hasDownloads) {
@@ -15604,9 +16860,6 @@ cr.define('downloads', function() {
this.$.toolbar.downloadsShowing = this.hasDownloads_;
this.$.panel.classList.remove('loading');
-
- var allReady = this.items_.map(function(i) { return i.readyPromise; });
- Promise.all(allReady).then(this.rebuildFocusGrid_.bind(this));
},
/**
@@ -15614,19 +16867,9 @@ cr.define('downloads', function() {
* @private
*/
updateItem_: function(data) {
- var item = this.idMap_[data.id];
-
- var activeControl = this.shadowRoot.activeElement == item ?
- item.shadowRoot.activeElement : null;
-
- item.update(data);
-
- this.async(function() {
- if (activeControl && !cr.ui.FocusRow.isFocusable(activeControl)) {
- var focusRow = this.focusGrid_.getRowForRoot(item.content);
- focusRow.getEquivalentElement(activeControl).focus();
- }
- }.bind(this));
+ var index = this.idToIndex_[data.id];
+ this.set('items_.' + index + '.item', data);
+ this.$['downloads-list'].updateSizeForItem(index);
},
});
« no previous file with comments | « no previous file | chrome/browser/resources/md_downloads/item.css » ('j') | chrome/browser/resources/md_downloads/manager.js » ('J')

Powered by Google App Engine
This is Rietveld 408576698