| 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);
|
| },
|
| });
|
|
|
|
|