| Index: appengine/swarming/elements/build/elements.html
|
| diff --git a/appengine/swarming/elements/build/elements.html b/appengine/swarming/elements/build/elements.html
|
| index f540c748c3a2093b69088909af11bd910742effc..553de914fc68e5c3da1c13716d8efdeca67b3554 100644
|
| --- a/appengine/swarming/elements/build/elements.html
|
| +++ b/appengine/swarming/elements/build/elements.html
|
| @@ -15644,7 +15644,2691 @@ You can bind to `isAuthorized` property to monitor authorization state.
|
| },
|
| });
|
| </script>
|
| -</dom-module><script>
|
| +</dom-module>
|
| +
|
| +<dom-module id="iron-a11y-announcer" assetpath="/res/imp/bower_components/iron-a11y-announcer/">
|
| + <template>
|
| + <style>
|
| + :host {
|
| + display: inline-block;
|
| + position: fixed;
|
| + clip: rect(0px,0px,0px,0px);
|
| + }
|
| + </style>
|
| + <div aria-live$="[[mode]]">[[_text]]</div>
|
| + </template>
|
| +
|
| + <script>
|
| +
|
| + (function() {
|
| + 'use strict';
|
| +
|
| + Polymer.IronA11yAnnouncer = Polymer({
|
| + is: 'iron-a11y-announcer',
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * The value of mode is used to set the `aria-live` attribute
|
| + * for the element that will be announced. Valid values are: `off`,
|
| + * `polite` and `assertive`.
|
| + */
|
| + mode: {
|
| + type: String,
|
| + value: 'polite'
|
| + },
|
| +
|
| + _text: {
|
| + type: String,
|
| + value: ''
|
| + }
|
| + },
|
| +
|
| + created: function() {
|
| + if (!Polymer.IronA11yAnnouncer.instance) {
|
| + Polymer.IronA11yAnnouncer.instance = this;
|
| + }
|
| +
|
| + document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(this));
|
| + },
|
| +
|
| + /**
|
| + * Cause a text string to be announced by screen readers.
|
| + *
|
| + * @param {string} text The text that should be announced.
|
| + */
|
| + announce: function(text) {
|
| + this._text = '';
|
| + this.async(function() {
|
| + this._text = text;
|
| + }, 100);
|
| + },
|
| +
|
| + _onIronAnnounce: function(event) {
|
| + if (event.detail && event.detail.text) {
|
| + this.announce(event.detail.text);
|
| + }
|
| + }
|
| + });
|
| +
|
| + Polymer.IronA11yAnnouncer.instance = null;
|
| +
|
| + Polymer.IronA11yAnnouncer.requestAvailability = function() {
|
| + if (!Polymer.IronA11yAnnouncer.instance) {
|
| + Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-announcer');
|
| + }
|
| +
|
| + document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
|
| + };
|
| + })();
|
| +
|
| + </script>
|
| +</dom-module>
|
| +<script>
|
| +/**
|
| +`Polymer.IronFitBehavior` fits an element in another element using `max-height` and `max-width`, and
|
| +optionally centers it in the window or another element.
|
| +
|
| +The element will only be sized and/or positioned if it has not already been sized and/or positioned
|
| +by CSS.
|
| +
|
| +CSS properties | Action
|
| +-----------------------------|-------------------------------------------
|
| +`position` set | Element is not centered horizontally or vertically
|
| +`top` or `bottom` set | Element is not vertically centered
|
| +`left` or `right` set | Element is not horizontally centered
|
| +`max-height` set | Element respects `max-height`
|
| +`max-width` set | Element respects `max-width`
|
| +
|
| +`Polymer.IronFitBehavior` can position an element into another element using
|
| +`verticalAlign` and `horizontalAlign`. This will override the element's css position.
|
| +
|
| + <div class="container">
|
| + <iron-fit-impl vertical-align="top" horizontal-align="auto">
|
| + Positioned into the container
|
| + </iron-fit-impl>
|
| + </div>
|
| +
|
| +Use `noOverlap` to position the element around another element without overlapping it.
|
| +
|
| + <div class="container">
|
| + <iron-fit-impl no-overlap vertical-align="auto" horizontal-align="auto">
|
| + Positioned around the container
|
| + </iron-fit-impl>
|
| + </div>
|
| +
|
| +@demo demo/index.html
|
| +@polymerBehavior
|
| +*/
|
| +
|
| + Polymer.IronFitBehavior = {
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * The element that will receive a `max-height`/`width`. By default it is the same as `this`,
|
| + * but it can be set to a child element. This is useful, for example, for implementing a
|
| + * scrolling region inside the element.
|
| + * @type {!Element}
|
| + */
|
| + sizingTarget: {
|
| + type: Object,
|
| + value: function() {
|
| + return this;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * The element to fit `this` into.
|
| + */
|
| + fitInto: {
|
| + type: Object,
|
| + value: window
|
| + },
|
| +
|
| + /**
|
| + * Will position the element around the positionTarget without overlapping it.
|
| + */
|
| + noOverlap: {
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * The element that should be used to position the element. If not set, it will
|
| + * default to the parent node.
|
| + * @type {!Element}
|
| + */
|
| + positionTarget: {
|
| + type: Element
|
| + },
|
| +
|
| + /**
|
| + * The orientation against which to align the element horizontally
|
| + * relative to the `positionTarget`. Possible values are "left", "right", "auto".
|
| + */
|
| + horizontalAlign: {
|
| + type: String
|
| + },
|
| +
|
| + /**
|
| + * The orientation against which to align the element vertically
|
| + * relative to the `positionTarget`. Possible values are "top", "bottom", "auto".
|
| + */
|
| + verticalAlign: {
|
| + type: String
|
| + },
|
| +
|
| + /**
|
| + * If true, it will use `horizontalAlign` and `verticalAlign` values as preferred alignment
|
| + * and if there's not enough space, it will pick the values which minimize the cropping.
|
| + */
|
| + dynamicAlign: {
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * The same as setting margin-left and margin-right css properties.
|
| + * @deprecated
|
| + */
|
| + horizontalOffset: {
|
| + type: Number,
|
| + value: 0,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * The same as setting margin-top and margin-bottom css properties.
|
| + * @deprecated
|
| + */
|
| + verticalOffset: {
|
| + type: Number,
|
| + value: 0,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * Set to true to auto-fit on attach.
|
| + */
|
| + autoFitOnAttach: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /** @type {?Object} */
|
| + _fitInfo: {
|
| + type: Object
|
| + }
|
| + },
|
| +
|
| + get _fitWidth() {
|
| + var fitWidth;
|
| + if (this.fitInto === window) {
|
| + fitWidth = this.fitInto.innerWidth;
|
| + } else {
|
| + fitWidth = this.fitInto.getBoundingClientRect().width;
|
| + }
|
| + return fitWidth;
|
| + },
|
| +
|
| + get _fitHeight() {
|
| + var fitHeight;
|
| + if (this.fitInto === window) {
|
| + fitHeight = this.fitInto.innerHeight;
|
| + } else {
|
| + fitHeight = this.fitInto.getBoundingClientRect().height;
|
| + }
|
| + return fitHeight;
|
| + },
|
| +
|
| + get _fitLeft() {
|
| + var fitLeft;
|
| + if (this.fitInto === window) {
|
| + fitLeft = 0;
|
| + } else {
|
| + fitLeft = this.fitInto.getBoundingClientRect().left;
|
| + }
|
| + return fitLeft;
|
| + },
|
| +
|
| + get _fitTop() {
|
| + var fitTop;
|
| + if (this.fitInto === window) {
|
| + fitTop = 0;
|
| + } else {
|
| + fitTop = this.fitInto.getBoundingClientRect().top;
|
| + }
|
| + return fitTop;
|
| + },
|
| +
|
| + /**
|
| + * The element that should be used to position the element,
|
| + * if no position target is configured.
|
| + */
|
| + get _defaultPositionTarget() {
|
| + var parent = Polymer.dom(this).parentNode;
|
| +
|
| + if (parent && parent.nodeType === Node.DOCUMENT_FRAGMENT_NODE) {
|
| + parent = parent.host;
|
| + }
|
| +
|
| + return parent;
|
| + },
|
| +
|
| + /**
|
| + * The horizontal align value, accounting for the RTL/LTR text direction.
|
| + */
|
| + get _localeHorizontalAlign() {
|
| + if (this._isRTL) {
|
| + // In RTL, "left" becomes "right".
|
| + if (this.horizontalAlign === 'right') {
|
| + return 'left';
|
| + }
|
| + if (this.horizontalAlign === 'left') {
|
| + return 'right';
|
| + }
|
| + }
|
| + return this.horizontalAlign;
|
| + },
|
| +
|
| + attached: function() {
|
| + // Memoize this to avoid expensive calculations & relayouts.
|
| + this._isRTL = window.getComputedStyle(this).direction == 'rtl';
|
| + this.positionTarget = this.positionTarget || this._defaultPositionTarget;
|
| + if (this.autoFitOnAttach) {
|
| + if (window.getComputedStyle(this).display === 'none') {
|
| + setTimeout(function() {
|
| + this.fit();
|
| + }.bind(this));
|
| + } else {
|
| + this.fit();
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Positions and fits the element into the `fitInto` element.
|
| + */
|
| + fit: function() {
|
| + this.position();
|
| + this.constrain();
|
| + this.center();
|
| + },
|
| +
|
| + /**
|
| + * Memoize information needed to position and size the target element.
|
| + * @suppress {deprecated}
|
| + */
|
| + _discoverInfo: function() {
|
| + if (this._fitInfo) {
|
| + return;
|
| + }
|
| + var target = window.getComputedStyle(this);
|
| + var sizer = window.getComputedStyle(this.sizingTarget);
|
| +
|
| + this._fitInfo = {
|
| + inlineStyle: {
|
| + top: this.style.top || '',
|
| + left: this.style.left || '',
|
| + position: this.style.position || ''
|
| + },
|
| + sizerInlineStyle: {
|
| + maxWidth: this.sizingTarget.style.maxWidth || '',
|
| + maxHeight: this.sizingTarget.style.maxHeight || '',
|
| + boxSizing: this.sizingTarget.style.boxSizing || ''
|
| + },
|
| + positionedBy: {
|
| + vertically: target.top !== 'auto' ? 'top' : (target.bottom !== 'auto' ?
|
| + 'bottom' : null),
|
| + horizontally: target.left !== 'auto' ? 'left' : (target.right !== 'auto' ?
|
| + 'right' : null)
|
| + },
|
| + sizedBy: {
|
| + height: sizer.maxHeight !== 'none',
|
| + width: sizer.maxWidth !== 'none',
|
| + minWidth: parseInt(sizer.minWidth, 10) || 0,
|
| + minHeight: parseInt(sizer.minHeight, 10) || 0
|
| + },
|
| + margin: {
|
| + top: parseInt(target.marginTop, 10) || 0,
|
| + right: parseInt(target.marginRight, 10) || 0,
|
| + bottom: parseInt(target.marginBottom, 10) || 0,
|
| + left: parseInt(target.marginLeft, 10) || 0
|
| + }
|
| + };
|
| +
|
| + // Support these properties until they are removed.
|
| + if (this.verticalOffset) {
|
| + this._fitInfo.margin.top = this._fitInfo.margin.bottom = this.verticalOffset;
|
| + this._fitInfo.inlineStyle.marginTop = this.style.marginTop || '';
|
| + this._fitInfo.inlineStyle.marginBottom = this.style.marginBottom || '';
|
| + this.style.marginTop = this.style.marginBottom = this.verticalOffset + 'px';
|
| + }
|
| + if (this.horizontalOffset) {
|
| + this._fitInfo.margin.left = this._fitInfo.margin.right = this.horizontalOffset;
|
| + this._fitInfo.inlineStyle.marginLeft = this.style.marginLeft || '';
|
| + this._fitInfo.inlineStyle.marginRight = this.style.marginRight || '';
|
| + this.style.marginLeft = this.style.marginRight = this.horizontalOffset + 'px';
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Resets the target element's position and size constraints, and clear
|
| + * the memoized data.
|
| + */
|
| + resetFit: function() {
|
| + var info = this._fitInfo || {};
|
| + for (var property in info.sizerInlineStyle) {
|
| + this.sizingTarget.style[property] = info.sizerInlineStyle[property];
|
| + }
|
| + for (var property in info.inlineStyle) {
|
| + this.style[property] = info.inlineStyle[property];
|
| + }
|
| +
|
| + this._fitInfo = null;
|
| + },
|
| +
|
| + /**
|
| + * Equivalent to calling `resetFit()` and `fit()`. Useful to call this after
|
| + * the element or the `fitInto` element has been resized, or if any of the
|
| + * positioning properties (e.g. `horizontalAlign, verticalAlign`) is updated.
|
| + * It preserves the scroll position of the sizingTarget.
|
| + */
|
| + refit: function() {
|
| + var scrollLeft = this.sizingTarget.scrollLeft;
|
| + var scrollTop = this.sizingTarget.scrollTop;
|
| + this.resetFit();
|
| + this.fit();
|
| + this.sizingTarget.scrollLeft = scrollLeft;
|
| + this.sizingTarget.scrollTop = scrollTop;
|
| + },
|
| +
|
| + /**
|
| + * Positions the element according to `horizontalAlign, verticalAlign`.
|
| + */
|
| + position: function() {
|
| + if (!this.horizontalAlign && !this.verticalAlign) {
|
| + // needs to be centered, and it is done after constrain.
|
| + return;
|
| + }
|
| + this._discoverInfo();
|
| +
|
| + this.style.position = 'fixed';
|
| + // Need border-box for margin/padding.
|
| + this.sizingTarget.style.boxSizing = 'border-box';
|
| + // Set to 0, 0 in order to discover any offset caused by parent stacking contexts.
|
| + this.style.left = '0px';
|
| + this.style.top = '0px';
|
| +
|
| + var rect = this.getBoundingClientRect();
|
| + var positionRect = this.__getNormalizedRect(this.positionTarget);
|
| + var fitRect = this.__getNormalizedRect(this.fitInto);
|
| +
|
| + var margin = this._fitInfo.margin;
|
| +
|
| + // Consider the margin as part of the size for position calculations.
|
| + var size = {
|
| + width: rect.width + margin.left + margin.right,
|
| + height: rect.height + margin.top + margin.bottom
|
| + };
|
| +
|
| + var position = this.__getPosition(this._localeHorizontalAlign, this.verticalAlign, size, positionRect, fitRect);
|
| +
|
| + var left = position.left + margin.left;
|
| + var top = position.top + margin.top;
|
| +
|
| + // Use original size (without margin).
|
| + var right = Math.min(fitRect.right - margin.right, left + rect.width);
|
| + var bottom = Math.min(fitRect.bottom - margin.bottom, top + rect.height);
|
| +
|
| + var minWidth = this._fitInfo.sizedBy.minWidth;
|
| + var minHeight = this._fitInfo.sizedBy.minHeight;
|
| + if (left < margin.left) {
|
| + left = margin.left;
|
| + if (right - left < minWidth) {
|
| + left = right - minWidth;
|
| + }
|
| + }
|
| + if (top < margin.top) {
|
| + top = margin.top;
|
| + if (bottom - top < minHeight) {
|
| + top = bottom - minHeight;
|
| + }
|
| + }
|
| +
|
| + this.sizingTarget.style.maxWidth = (right - left) + 'px';
|
| + this.sizingTarget.style.maxHeight = (bottom - top) + 'px';
|
| +
|
| + // Remove the offset caused by any stacking context.
|
| + this.style.left = (left - rect.left) + 'px';
|
| + this.style.top = (top - rect.top) + 'px';
|
| + },
|
| +
|
| + /**
|
| + * Constrains the size of the element to `fitInto` by setting `max-height`
|
| + * and/or `max-width`.
|
| + */
|
| + constrain: function() {
|
| + if (this.horizontalAlign || this.verticalAlign) {
|
| + return;
|
| + }
|
| + this._discoverInfo();
|
| +
|
| + var info = this._fitInfo;
|
| + // position at (0px, 0px) if not already positioned, so we can measure the natural size.
|
| + if (!info.positionedBy.vertically) {
|
| + this.style.position = 'fixed';
|
| + this.style.top = '0px';
|
| + }
|
| + if (!info.positionedBy.horizontally) {
|
| + this.style.position = 'fixed';
|
| + this.style.left = '0px';
|
| + }
|
| +
|
| + // need border-box for margin/padding
|
| + this.sizingTarget.style.boxSizing = 'border-box';
|
| + // constrain the width and height if not already set
|
| + var rect = this.getBoundingClientRect();
|
| + if (!info.sizedBy.height) {
|
| + this.__sizeDimension(rect, info.positionedBy.vertically, 'top', 'bottom', 'Height');
|
| + }
|
| + if (!info.sizedBy.width) {
|
| + this.__sizeDimension(rect, info.positionedBy.horizontally, 'left', 'right', 'Width');
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * @protected
|
| + * @deprecated
|
| + */
|
| + _sizeDimension: function(rect, positionedBy, start, end, extent) {
|
| + this.__sizeDimension(rect, positionedBy, start, end, extent);
|
| + },
|
| +
|
| + /**
|
| + * @private
|
| + */
|
| + __sizeDimension: function(rect, positionedBy, start, end, extent) {
|
| + var info = this._fitInfo;
|
| + var fitRect = this.__getNormalizedRect(this.fitInto);
|
| + var max = extent === 'Width' ? fitRect.width : fitRect.height;
|
| + var flip = (positionedBy === end);
|
| + var offset = flip ? max - rect[end] : rect[start];
|
| + var margin = info.margin[flip ? start : end];
|
| + var offsetExtent = 'offset' + extent;
|
| + var sizingOffset = this[offsetExtent] - this.sizingTarget[offsetExtent];
|
| + this.sizingTarget.style['max' + extent] = (max - margin - offset - sizingOffset) + 'px';
|
| + },
|
| +
|
| + /**
|
| + * Centers horizontally and vertically if not already positioned. This also sets
|
| + * `position:fixed`.
|
| + */
|
| + center: function() {
|
| + if (this.horizontalAlign || this.verticalAlign) {
|
| + return;
|
| + }
|
| + this._discoverInfo();
|
| +
|
| + var positionedBy = this._fitInfo.positionedBy;
|
| + if (positionedBy.vertically && positionedBy.horizontally) {
|
| + // Already positioned.
|
| + return;
|
| + }
|
| + // Need position:fixed to center
|
| + this.style.position = 'fixed';
|
| + // Take into account the offset caused by parents that create stacking
|
| + // contexts (e.g. with transform: translate3d). Translate to 0,0 and
|
| + // measure the bounding rect.
|
| + if (!positionedBy.vertically) {
|
| + this.style.top = '0px';
|
| + }
|
| + if (!positionedBy.horizontally) {
|
| + this.style.left = '0px';
|
| + }
|
| + // It will take in consideration margins and transforms
|
| + var rect = this.getBoundingClientRect();
|
| + var fitRect = this.__getNormalizedRect(this.fitInto);
|
| + if (!positionedBy.vertically) {
|
| + var top = fitRect.top - rect.top + (fitRect.height - rect.height) / 2;
|
| + this.style.top = top + 'px';
|
| + }
|
| + if (!positionedBy.horizontally) {
|
| + var left = fitRect.left - rect.left + (fitRect.width - rect.width) / 2;
|
| + this.style.left = left + 'px';
|
| + }
|
| + },
|
| +
|
| + __getNormalizedRect: function(target) {
|
| + if (target === document.documentElement || target === window) {
|
| + return {
|
| + top: 0,
|
| + left: 0,
|
| + width: window.innerWidth,
|
| + height: window.innerHeight,
|
| + right: window.innerWidth,
|
| + bottom: window.innerHeight
|
| + };
|
| + }
|
| + return target.getBoundingClientRect();
|
| + },
|
| +
|
| + __getCroppedArea: function(position, size, fitRect) {
|
| + var verticalCrop = Math.min(0, position.top) + Math.min(0, fitRect.bottom - (position.top + size.height));
|
| + var horizontalCrop = Math.min(0, position.left) + Math.min(0, fitRect.right - (position.left + size.width));
|
| + return Math.abs(verticalCrop) * size.width + Math.abs(horizontalCrop) * size.height;
|
| + },
|
| +
|
| +
|
| + __getPosition: function(hAlign, vAlign, size, positionRect, fitRect) {
|
| + // All the possible configurations.
|
| + // Ordered as top-left, top-right, bottom-left, bottom-right.
|
| + var positions = [{
|
| + verticalAlign: 'top',
|
| + horizontalAlign: 'left',
|
| + top: positionRect.top,
|
| + left: positionRect.left
|
| + }, {
|
| + verticalAlign: 'top',
|
| + horizontalAlign: 'right',
|
| + top: positionRect.top,
|
| + left: positionRect.right - size.width
|
| + }, {
|
| + verticalAlign: 'bottom',
|
| + horizontalAlign: 'left',
|
| + top: positionRect.bottom - size.height,
|
| + left: positionRect.left
|
| + }, {
|
| + verticalAlign: 'bottom',
|
| + horizontalAlign: 'right',
|
| + top: positionRect.bottom - size.height,
|
| + left: positionRect.right - size.width
|
| + }];
|
| +
|
| + if (this.noOverlap) {
|
| + // Duplicate.
|
| + for (var i = 0, l = positions.length; i < l; i++) {
|
| + var copy = {};
|
| + for (var key in positions[i]) {
|
| + copy[key] = positions[i][key];
|
| + }
|
| + positions.push(copy);
|
| + }
|
| + // Horizontal overlap only.
|
| + positions[0].top = positions[1].top += positionRect.height;
|
| + positions[2].top = positions[3].top -= positionRect.height;
|
| + // Vertical overlap only.
|
| + positions[4].left = positions[6].left += positionRect.width;
|
| + positions[5].left = positions[7].left -= positionRect.width;
|
| + }
|
| +
|
| + // Consider auto as null for coding convenience.
|
| + vAlign = vAlign === 'auto' ? null : vAlign;
|
| + hAlign = hAlign === 'auto' ? null : hAlign;
|
| +
|
| + var position;
|
| + for (var i = 0; i < positions.length; i++) {
|
| + var pos = positions[i];
|
| +
|
| + // If both vAlign and hAlign are defined, return exact match.
|
| + // For dynamicAlign and noOverlap we'll have more than one candidate, so
|
| + // we'll have to check the croppedArea to make the best choice.
|
| + if (!this.dynamicAlign && !this.noOverlap &&
|
| + pos.verticalAlign === vAlign && pos.horizontalAlign === hAlign) {
|
| + position = pos;
|
| + break;
|
| + }
|
| +
|
| + // Align is ok if alignment preferences are respected. If no preferences,
|
| + // it is considered ok.
|
| + var alignOk = (!vAlign || pos.verticalAlign === vAlign) &&
|
| + (!hAlign || pos.horizontalAlign === hAlign);
|
| +
|
| + // Filter out elements that don't match the alignment (if defined).
|
| + // With dynamicAlign, we need to consider all the positions to find the
|
| + // one that minimizes the cropped area.
|
| + if (!this.dynamicAlign && !alignOk) {
|
| + continue;
|
| + }
|
| +
|
| + position = position || pos;
|
| + pos.croppedArea = this.__getCroppedArea(pos, size, fitRect);
|
| + var diff = pos.croppedArea - position.croppedArea;
|
| + // Check which crops less. If it crops equally, check if align is ok.
|
| + if (diff < 0 || (diff === 0 && alignOk)) {
|
| + position = pos;
|
| + }
|
| + // If not cropped and respects the align requirements, keep it.
|
| + // This allows to prefer positions overlapping horizontally over the
|
| + // ones overlapping vertically.
|
| + if (position.croppedArea === 0 && alignOk) {
|
| + break;
|
| + }
|
| + }
|
| +
|
| + return position;
|
| + }
|
| +
|
| + };
|
| +</script>
|
| +<script>
|
| + (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+0008': 'backspace',
|
| + 'U+0009': 'tab',
|
| + 'U+001B': 'esc',
|
| + 'U+0020': 'space',
|
| + '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 = {
|
| + 8: 'backspace',
|
| + 9: 'tab',
|
| + 13: 'enter',
|
| + 27: 'esc',
|
| + 33: 'pageup',
|
| + 34: 'pagedown',
|
| + 35: 'end',
|
| + 36: 'home',
|
| + 32: 'space',
|
| + 37: 'left',
|
| + 38: 'up',
|
| + 39: 'right',
|
| + 40: 'down',
|
| + 46: 'del',
|
| + 106: '*'
|
| + };
|
| +
|
| + /**
|
| + * 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)?/;
|
| +
|
| + /**
|
| + * Matches ESC key.
|
| + *
|
| + * Value from: http://w3c.github.io/uievents-key/#key-Escape
|
| + */
|
| + var ESC_KEY = /^escape$/;
|
| +
|
| + /**
|
| + * Transforms the key.
|
| + * @param {string} key The KeyBoardEvent.key
|
| + * @param {Boolean} [noSpecialChars] Limits the transformation to
|
| + * alpha-numeric characters.
|
| + */
|
| + function transformKey(key, noSpecialChars) {
|
| + var validKey = '';
|
| + if (key) {
|
| + var lKey = key.toLowerCase();
|
| + if (lKey === ' ' || SPACE_KEY.test(lKey)) {
|
| + validKey = 'space';
|
| + } else if (ESC_KEY.test(lKey)) {
|
| + validKey = 'esc';
|
| + } else if (lKey.length == 1) {
|
| + if (!noSpecialChars || KEY_CHAR.test(lKey)) {
|
| + validKey = lKey;
|
| + }
|
| + } else if (ARROW_KEY.test(lKey)) {
|
| + validKey = lKey.replace('arrow', '');
|
| + } else if (lKey == 'multiply') {
|
| + // numpad '*' can map to Multiply on IE/Windows
|
| + validKey = '*';
|
| + } else {
|
| + validKey = lKey;
|
| + }
|
| + }
|
| + return validKey;
|
| + }
|
| +
|
| + function transformKeyIdentifier(keyIdent) {
|
| + var validKey = '';
|
| + if (keyIdent) {
|
| + if (keyIdent in KEY_IDENTIFIER) {
|
| + validKey = KEY_IDENTIFIER[keyIdent];
|
| + } else if (IDENT_CHAR.test(keyIdent)) {
|
| + keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
|
| + validKey = String.fromCharCode(keyIdent).toLowerCase();
|
| + } else {
|
| + validKey = keyIdent.toLowerCase();
|
| + }
|
| + }
|
| + return validKey;
|
| + }
|
| +
|
| + function transformKeyCode(keyCode) {
|
| + var validKey = '';
|
| + if (Number(keyCode)) {
|
| + if (keyCode >= 65 && keyCode <= 90) {
|
| + // ascii a-z
|
| + // lowercase is 32 offset from uppercase
|
| + validKey = String.fromCharCode(32 + keyCode);
|
| + } else if (keyCode >= 112 && keyCode <= 123) {
|
| + // function keys f1-f12
|
| + validKey = 'f' + (keyCode - 112);
|
| + } else if (keyCode >= 48 && keyCode <= 57) {
|
| + // top 0-9 keys
|
| + validKey = String(keyCode - 48);
|
| + } else if (keyCode >= 96 && keyCode <= 105) {
|
| + // num pad 0-9
|
| + validKey = String(keyCode - 96);
|
| + } else {
|
| + validKey = KEY_CODE[keyCode];
|
| + }
|
| + }
|
| + return validKey;
|
| + }
|
| +
|
| + /**
|
| + * Calculates the normalized key for a KeyboardEvent.
|
| + * @param {KeyboardEvent} keyEvent
|
| + * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
|
| + * transformation to alpha-numeric chars. This is useful with key
|
| + * combinations like shift + 2, which on FF for MacOS produces
|
| + * keyEvent.key = @
|
| + * To get 2 returned, set noSpecialChars = true
|
| + * To get @ returned, set noSpecialChars = false
|
| + */
|
| + function normalizedKeyForEvent(keyEvent, noSpecialChars) {
|
| + // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
|
| + // .detail.key to support artificial keyboard events.
|
| + return transformKey(keyEvent.key, noSpecialChars) ||
|
| + transformKeyIdentifier(keyEvent.keyIdentifier) ||
|
| + transformKeyCode(keyEvent.keyCode) ||
|
| + transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || '';
|
| + }
|
| +
|
| + function keyComboMatchesEvent(keyCombo, event) {
|
| + // For combos with modifiers we support only alpha-numeric keys
|
| + var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
|
| + return keyEvent === keyCombo.key &&
|
| + (!keyCombo.hasModifiers || (
|
| + !!event.shiftKey === !!keyCombo.shiftKey &&
|
| + !!event.ctrlKey === !!keyCombo.ctrlKey &&
|
| + !!event.altKey === !!keyCombo.altKey &&
|
| + !!event.metaKey === !!keyCombo.metaKey)
|
| + );
|
| + }
|
| +
|
| + function parseKeyComboString(keyComboString) {
|
| + if (keyComboString.length === 1) {
|
| + return {
|
| + combo: keyComboString,
|
| + key: keyComboString,
|
| + event: 'keydown'
|
| + };
|
| + }
|
| + return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
|
| + var eventParts = keyComboPart.split(':');
|
| + var keyName = eventParts[0];
|
| + var event = eventParts[1];
|
| +
|
| + if (keyName in MODIFIER_KEYS) {
|
| + parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
|
| + parsedKeyCombo.hasModifiers = true;
|
| + } else {
|
| + parsedKeyCombo.key = keyName;
|
| + parsedKeyCombo.event = event || 'keydown';
|
| + }
|
| +
|
| + return parsedKeyCombo;
|
| + }, {
|
| + combo: keyComboString.split(':').shift()
|
| + });
|
| + }
|
| +
|
| + function parseEventString(eventString) {
|
| + return eventString.trim().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 callback. A key binding has the format
|
| + * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
|
| + * `"KEY:EVENT": "callback"` are valid as well). Some examples:
|
| + *
|
| + * keyBindings: {
|
| + * 'space': '_onKeydown', // same as 'space:keydown'
|
| + * 'shift+tab': '_onKeydown',
|
| + * 'enter:keypress': '_onKeypress',
|
| + * 'esc:keyup': '_onKeyup'
|
| + * }
|
| + *
|
| + * The callback will receive with an event containing the following information in `event.detail`:
|
| + *
|
| + * _onKeydown: function(event) {
|
| + * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
|
| + * console.log(event.detail.key); // KEY only, e.g. "tab"
|
| + * console.log(event.detail.event); // EVENT, e.g. "keydown"
|
| + * console.log(event.detail.keyboardEvent); // the original KeyboardEvent
|
| + * }
|
| + *
|
| + * Use the `keyEventTarget` attribute to set up event handlers on a specific
|
| + * node.
|
| + *
|
| + * See the [demo source code](https://github.com/PolymerElements/iron-a11y-keys-behavior/blob/master/demo/x-key-aware.html)
|
| + * for an example.
|
| + *
|
| + * @demo demo/index.html
|
| + * @polymerBehavior
|
| + */
|
| + Polymer.IronA11yKeysBehavior = {
|
| + properties: {
|
| + /**
|
| + * The EventTarget that will be firing relevant KeyboardEvents. Set it to
|
| + * `null` to disable the listeners.
|
| + * @type {?EventTarget}
|
| + */
|
| + keyEventTarget: {
|
| + type: Object,
|
| + value: function() {
|
| + return this;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * If true, this property will cause the implementing element to
|
| + * automatically stop propagation on any handled KeyboardEvents.
|
| + */
|
| + stopKeyboardEventPropagation: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + _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)'
|
| + ],
|
| +
|
| +
|
| + /**
|
| + * To be used to express what combination of keys will trigger the relative
|
| + * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
|
| + * @type {Object}
|
| + */
|
| + 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();
|
| + },
|
| +
|
| + /**
|
| + * Returns true if a keyboard event matches `eventString`.
|
| + *
|
| + * @param {KeyboardEvent} event
|
| + * @param {string} eventString
|
| + * @return {boolean}
|
| + */
|
| + keyboardEventMatchesKeys: function(event, eventString) {
|
| + var keyCombos = parseEventString(eventString);
|
| + for (var i = 0; i < keyCombos.length; ++i) {
|
| + if (keyComboMatchesEvent(keyCombos[i], event)) {
|
| + return true;
|
| + }
|
| + }
|
| + return false;
|
| + },
|
| +
|
| + _collectKeyBindings: function() {
|
| + var keyBindings = this.behaviors.map(function(behavior) {
|
| + return behavior.keyBindings;
|
| + });
|
| +
|
| + if (keyBindings.indexOf(this.keyBindings) === -1) {
|
| + keyBindings.push(this.keyBindings);
|
| + }
|
| +
|
| + return keyBindings;
|
| + },
|
| +
|
| + _prepKeyBindings: function() {
|
| + this._keyBindings = {};
|
| +
|
| + this._collectKeyBindings().forEach(function(keyBindings) {
|
| + for (var eventString in keyBindings) {
|
| + this._addKeyBinding(eventString, keyBindings[eventString]);
|
| + }
|
| + }, this);
|
| +
|
| + for (var eventString in this._imperativeKeyBindings) {
|
| + this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
|
| + }
|
| +
|
| + // Give precedence to combos with modifiers to be checked first.
|
| + for (var eventName in this._keyBindings) {
|
| + this._keyBindings[eventName].sort(function (kb1, kb2) {
|
| + var b1 = kb1[0].hasModifiers;
|
| + var b2 = kb2[0].hasModifiers;
|
| + return (b1 === b2) ? 0 : b1 ? -1 : 1;
|
| + })
|
| + }
|
| + },
|
| +
|
| + _addKeyBinding: function(eventString, handlerName) {
|
| + parseEventString(eventString).forEach(function(keyCombo) {
|
| + this._keyBindings[keyCombo.event] =
|
| + this._keyBindings[keyCombo.event] || [];
|
| +
|
| + this._keyBindings[keyCombo.event].push([
|
| + keyCombo,
|
| + handlerName
|
| + ]);
|
| + }, this);
|
| + },
|
| +
|
| + _resetKeyEventListeners: function() {
|
| + this._unlistenKeyEventListeners();
|
| +
|
| + if (this.isAttached) {
|
| + this._listenKeyEventListeners();
|
| + }
|
| + },
|
| +
|
| + _listenKeyEventListeners: function() {
|
| + if (!this.keyEventTarget) {
|
| + return;
|
| + }
|
| + Object.keys(this._keyBindings).forEach(function(eventName) {
|
| + var keyBindings = this._keyBindings[eventName];
|
| + var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
|
| +
|
| + this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
|
| +
|
| + this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
|
| + }, this);
|
| + },
|
| +
|
| + _unlistenKeyEventListeners: function() {
|
| + var keyHandlerTuple;
|
| + var keyEventTarget;
|
| + var eventName;
|
| + var boundKeyHandler;
|
| +
|
| + while (this._boundKeyHandlers.length) {
|
| + // My kingdom for block-scope binding and destructuring assignment..
|
| + keyHandlerTuple = this._boundKeyHandlers.pop();
|
| + keyEventTarget = keyHandlerTuple[0];
|
| + eventName = keyHandlerTuple[1];
|
| + boundKeyHandler = keyHandlerTuple[2];
|
| +
|
| + keyEventTarget.removeEventListener(eventName, boundKeyHandler);
|
| + }
|
| + },
|
| +
|
| + _onKeyBindingEvent: function(keyBindings, event) {
|
| + if (this.stopKeyboardEventPropagation) {
|
| + event.stopPropagation();
|
| + }
|
| +
|
| + // if event has been already prevented, don't do anything
|
| + if (event.defaultPrevented) {
|
| + return;
|
| + }
|
| +
|
| + for (var i = 0; i < keyBindings.length; i++) {
|
| + var keyCombo = keyBindings[i][0];
|
| + var handlerName = keyBindings[i][1];
|
| + if (keyComboMatchesEvent(keyCombo, event)) {
|
| + this._triggerKeyHandler(keyCombo, handlerName, event);
|
| + // exit the loop if eventDefault was prevented
|
| + if (event.defaultPrevented) {
|
| + return;
|
| + }
|
| + }
|
| + }
|
| + },
|
| +
|
| + _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
|
| + var detail = Object.create(keyCombo);
|
| + detail.keyboardEvent = keyboardEvent;
|
| + var event = new CustomEvent(keyCombo.event, {
|
| + detail: detail,
|
| + cancelable: true
|
| + });
|
| + this[handlerName].call(this, event);
|
| + if (event.defaultPrevented) {
|
| + keyboardEvent.preventDefault();
|
| + }
|
| + }
|
| + };
|
| + })();
|
| +</script>
|
| +
|
| +
|
| +<dom-module id="iron-overlay-backdrop" assetpath="/res/imp/bower_components/iron-overlay-behavior/">
|
| +
|
| + <template>
|
| + <style>
|
| + :host {
|
| + position: fixed;
|
| + top: 0;
|
| + left: 0;
|
| + width: 100%;
|
| + height: 100%;
|
| + background-color: var(--iron-overlay-backdrop-background-color, #000);
|
| + opacity: 0;
|
| + transition: opacity 0.2s;
|
| + pointer-events: none;
|
| + @apply(--iron-overlay-backdrop);
|
| + }
|
| +
|
| + :host(.opened) {
|
| + opacity: var(--iron-overlay-backdrop-opacity, 0.6);
|
| + pointer-events: auto;
|
| + @apply(--iron-overlay-backdrop-opened);
|
| + }
|
| + </style>
|
| +
|
| + <content></content>
|
| + </template>
|
| +
|
| +</dom-module>
|
| +
|
| +<script>
|
| +(function() {
|
| +'use strict';
|
| +
|
| + Polymer({
|
| +
|
| + is: 'iron-overlay-backdrop',
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * Returns true if the backdrop is opened.
|
| + */
|
| + opened: {
|
| + reflectToAttribute: true,
|
| + type: Boolean,
|
| + value: false,
|
| + observer: '_openedChanged'
|
| + }
|
| +
|
| + },
|
| +
|
| + listeners: {
|
| + 'transitionend': '_onTransitionend'
|
| + },
|
| +
|
| + created: function() {
|
| + // Used to cancel previous requestAnimationFrame calls when opened changes.
|
| + this.__openedRaf = null;
|
| + },
|
| +
|
| + attached: function() {
|
| + this.opened && this._openedChanged(this.opened);
|
| + },
|
| +
|
| + /**
|
| + * Appends the backdrop to document body if needed.
|
| + */
|
| + prepare: function() {
|
| + if (this.opened && !this.parentNode) {
|
| + Polymer.dom(document.body).appendChild(this);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Shows the backdrop.
|
| + */
|
| + open: function() {
|
| + this.opened = true;
|
| + },
|
| +
|
| + /**
|
| + * Hides the backdrop.
|
| + */
|
| + close: function() {
|
| + this.opened = false;
|
| + },
|
| +
|
| + /**
|
| + * Removes the backdrop from document body if needed.
|
| + */
|
| + complete: function() {
|
| + if (!this.opened && this.parentNode === document.body) {
|
| + Polymer.dom(this.parentNode).removeChild(this);
|
| + }
|
| + },
|
| +
|
| + _onTransitionend: function(event) {
|
| + if (event && event.target === this) {
|
| + this.complete();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * @param {boolean} opened
|
| + * @private
|
| + */
|
| + _openedChanged: function(opened) {
|
| + if (opened) {
|
| + // Auto-attach.
|
| + this.prepare();
|
| + } else {
|
| + // Animation might be disabled via the mixin or opacity custom property.
|
| + // If it is disabled in other ways, it's up to the user to call complete.
|
| + var cs = window.getComputedStyle(this);
|
| + if (cs.transitionDuration === '0s' || cs.opacity == 0) {
|
| + this.complete();
|
| + }
|
| + }
|
| +
|
| + if (!this.isAttached) {
|
| + return;
|
| + }
|
| +
|
| + // Always cancel previous requestAnimationFrame.
|
| + if (this.__openedRaf) {
|
| + window.cancelAnimationFrame(this.__openedRaf);
|
| + this.__openedRaf = null;
|
| + }
|
| + // Force relayout to ensure proper transitions.
|
| + this.scrollTop = this.scrollTop;
|
| + this.__openedRaf = window.requestAnimationFrame(function() {
|
| + this.__openedRaf = null;
|
| + this.toggleClass('opened', this.opened);
|
| + }.bind(this));
|
| + }
|
| + });
|
| +
|
| +})();
|
| +
|
| +</script>
|
| +<script>
|
| +
|
| + /**
|
| + * @struct
|
| + * @constructor
|
| + * @private
|
| + */
|
| + Polymer.IronOverlayManagerClass = function() {
|
| + /**
|
| + * Used to keep track of the opened overlays.
|
| + * @private {Array<Element>}
|
| + */
|
| + this._overlays = [];
|
| +
|
| + /**
|
| + * iframes have a default z-index of 100,
|
| + * so this default should be at least that.
|
| + * @private {number}
|
| + */
|
| + this._minimumZ = 101;
|
| +
|
| + /**
|
| + * Memoized backdrop element.
|
| + * @private {Element|null}
|
| + */
|
| + this._backdropElement = null;
|
| +
|
| + // Enable document-wide tap recognizer.
|
| + Polymer.Gestures.add(document, 'tap', null);
|
| + // Need to have useCapture=true, Polymer.Gestures doesn't offer that.
|
| + document.addEventListener('tap', this._onCaptureClick.bind(this), true);
|
| + document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
|
| + document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
|
| + };
|
| +
|
| + Polymer.IronOverlayManagerClass.prototype = {
|
| +
|
| + constructor: Polymer.IronOverlayManagerClass,
|
| +
|
| + /**
|
| + * The shared backdrop element.
|
| + * @type {!Element} backdropElement
|
| + */
|
| + get backdropElement() {
|
| + if (!this._backdropElement) {
|
| + this._backdropElement = document.createElement('iron-overlay-backdrop');
|
| + }
|
| + return this._backdropElement;
|
| + },
|
| +
|
| + /**
|
| + * The deepest active element.
|
| + * @type {!Element} activeElement the active element
|
| + */
|
| + get deepActiveElement() {
|
| + // document.activeElement can be null
|
| + // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
|
| + // In case of null, default it to document.body.
|
| + var active = document.activeElement || document.body;
|
| + while (active.root && Polymer.dom(active.root).activeElement) {
|
| + active = Polymer.dom(active.root).activeElement;
|
| + }
|
| + return active;
|
| + },
|
| +
|
| + /**
|
| + * Brings the overlay at the specified index to the front.
|
| + * @param {number} i
|
| + * @private
|
| + */
|
| + _bringOverlayAtIndexToFront: function(i) {
|
| + var overlay = this._overlays[i];
|
| + if (!overlay) {
|
| + return;
|
| + }
|
| + var lastI = this._overlays.length - 1;
|
| + var currentOverlay = this._overlays[lastI];
|
| + // Ensure always-on-top overlay stays on top.
|
| + if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
|
| + lastI--;
|
| + }
|
| + // If already the top element, return.
|
| + if (i >= lastI) {
|
| + return;
|
| + }
|
| + // Update z-index to be on top.
|
| + var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
|
| + if (this._getZ(overlay) <= minimumZ) {
|
| + this._applyOverlayZ(overlay, minimumZ);
|
| + }
|
| +
|
| + // Shift other overlays behind the new on top.
|
| + while (i < lastI) {
|
| + this._overlays[i] = this._overlays[i + 1];
|
| + i++;
|
| + }
|
| + this._overlays[lastI] = overlay;
|
| + },
|
| +
|
| + /**
|
| + * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
|
| + * Also updates the backdrop z-index.
|
| + * @param {!Element} overlay
|
| + */
|
| + addOrRemoveOverlay: function(overlay) {
|
| + if (overlay.opened) {
|
| + this.addOverlay(overlay);
|
| + } else {
|
| + this.removeOverlay(overlay);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Tracks overlays for z-index and focus management.
|
| + * Ensures the last added overlay with always-on-top remains on top.
|
| + * @param {!Element} overlay
|
| + */
|
| + addOverlay: function(overlay) {
|
| + var i = this._overlays.indexOf(overlay);
|
| + if (i >= 0) {
|
| + this._bringOverlayAtIndexToFront(i);
|
| + this.trackBackdrop();
|
| + return;
|
| + }
|
| + var insertionIndex = this._overlays.length;
|
| + var currentOverlay = this._overlays[insertionIndex - 1];
|
| + var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
|
| + var newZ = this._getZ(overlay);
|
| +
|
| + // Ensure always-on-top overlay stays on top.
|
| + if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
|
| + // This bumps the z-index of +2.
|
| + this._applyOverlayZ(currentOverlay, minimumZ);
|
| + insertionIndex--;
|
| + // Update minimumZ to match previous overlay's z-index.
|
| + var previousOverlay = this._overlays[insertionIndex - 1];
|
| + minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
|
| + }
|
| +
|
| + // Update z-index and insert overlay.
|
| + if (newZ <= minimumZ) {
|
| + this._applyOverlayZ(overlay, minimumZ);
|
| + }
|
| + this._overlays.splice(insertionIndex, 0, overlay);
|
| +
|
| + // Get focused node.
|
| + var element = this.deepActiveElement;
|
| + overlay.restoreFocusNode = this._overlayParent(element) ? null : element;
|
| + this.trackBackdrop();
|
| + },
|
| +
|
| + /**
|
| + * @param {!Element} overlay
|
| + */
|
| + removeOverlay: function(overlay) {
|
| + var i = this._overlays.indexOf(overlay);
|
| + if (i === -1) {
|
| + return;
|
| + }
|
| + this._overlays.splice(i, 1);
|
| +
|
| + var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null;
|
| + overlay.restoreFocusNode = null;
|
| + // Focus back only if still contained in document.body
|
| + if (node && Polymer.dom(document.body).deepContains(node)) {
|
| + node.focus();
|
| + }
|
| + this.trackBackdrop();
|
| + },
|
| +
|
| + /**
|
| + * Returns the current overlay.
|
| + * @return {Element|undefined}
|
| + */
|
| + currentOverlay: function() {
|
| + var i = this._overlays.length - 1;
|
| + return this._overlays[i];
|
| + },
|
| +
|
| + /**
|
| + * Returns the current overlay z-index.
|
| + * @return {number}
|
| + */
|
| + currentOverlayZ: function() {
|
| + return this._getZ(this.currentOverlay());
|
| + },
|
| +
|
| + /**
|
| + * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
|
| + * This does not effect the z-index of any existing overlays.
|
| + * @param {number} minimumZ
|
| + */
|
| + ensureMinimumZ: function(minimumZ) {
|
| + this._minimumZ = Math.max(this._minimumZ, minimumZ);
|
| + },
|
| +
|
| + focusOverlay: function() {
|
| + var current = /** @type {?} */ (this.currentOverlay());
|
| + // We have to be careful to focus the next overlay _after_ any current
|
| + // transitions are complete (due to the state being toggled prior to the
|
| + // transition). Otherwise, we risk infinite recursion when a transitioning
|
| + // (closed) overlay becomes the current overlay.
|
| + //
|
| + // NOTE: We make the assumption that any overlay that completes a transition
|
| + // will call into focusOverlay to kick the process back off. Currently:
|
| + // transitionend -> _applyFocus -> focusOverlay.
|
| + if (current && !current.transitioning) {
|
| + current._applyFocus();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Updates the backdrop z-index.
|
| + */
|
| + trackBackdrop: function() {
|
| + var overlay = this._overlayWithBackdrop();
|
| + // Avoid creating the backdrop if there is no overlay with backdrop.
|
| + if (!overlay && !this._backdropElement) {
|
| + return;
|
| + }
|
| + this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
|
| + this.backdropElement.opened = !!overlay;
|
| + },
|
| +
|
| + /**
|
| + * @return {Array<Element>}
|
| + */
|
| + getBackdrops: function() {
|
| + var backdrops = [];
|
| + for (var i = 0; i < this._overlays.length; i++) {
|
| + if (this._overlays[i].withBackdrop) {
|
| + backdrops.push(this._overlays[i]);
|
| + }
|
| + }
|
| + return backdrops;
|
| + },
|
| +
|
| + /**
|
| + * Returns the z-index for the backdrop.
|
| + * @return {number}
|
| + */
|
| + backdropZ: function() {
|
| + return this._getZ(this._overlayWithBackdrop()) - 1;
|
| + },
|
| +
|
| + /**
|
| + * Returns the first opened overlay that has a backdrop.
|
| + * @return {Element|undefined}
|
| + * @private
|
| + */
|
| + _overlayWithBackdrop: function() {
|
| + for (var i = 0; i < this._overlays.length; i++) {
|
| + if (this._overlays[i].withBackdrop) {
|
| + return this._overlays[i];
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Calculates the minimum z-index for the overlay.
|
| + * @param {Element=} overlay
|
| + * @private
|
| + */
|
| + _getZ: function(overlay) {
|
| + var z = this._minimumZ;
|
| + if (overlay) {
|
| + var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
|
| + // Check if is a number
|
| + // Number.isNaN not supported in IE 10+
|
| + if (z1 === z1) {
|
| + z = z1;
|
| + }
|
| + }
|
| + return z;
|
| + },
|
| +
|
| + /**
|
| + * @param {!Element} element
|
| + * @param {number|string} z
|
| + * @private
|
| + */
|
| + _setZ: function(element, z) {
|
| + element.style.zIndex = z;
|
| + },
|
| +
|
| + /**
|
| + * @param {!Element} overlay
|
| + * @param {number} aboveZ
|
| + * @private
|
| + */
|
| + _applyOverlayZ: function(overlay, aboveZ) {
|
| + this._setZ(overlay, aboveZ + 2);
|
| + },
|
| +
|
| + /**
|
| + * Returns the overlay containing the provided node. If the node is an overlay,
|
| + * it returns the node.
|
| + * @param {Element=} node
|
| + * @return {Element|undefined}
|
| + * @private
|
| + */
|
| + _overlayParent: function(node) {
|
| + while (node && node !== document.body) {
|
| + // Check if it is an overlay.
|
| + if (node._manager === this) {
|
| + return node;
|
| + }
|
| + // Use logical parentNode, or native ShadowRoot host.
|
| + node = Polymer.dom(node).parentNode || node.host;
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Returns the deepest overlay in the path.
|
| + * @param {Array<Element>=} path
|
| + * @return {Element|undefined}
|
| + * @suppress {missingProperties}
|
| + * @private
|
| + */
|
| + _overlayInPath: function(path) {
|
| + path = path || [];
|
| + for (var i = 0; i < path.length; i++) {
|
| + if (path[i]._manager === this) {
|
| + return path[i];
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures the click event is delegated to the right overlay.
|
| + * @param {!Event} event
|
| + * @private
|
| + */
|
| + _onCaptureClick: function(event) {
|
| + var overlay = /** @type {?} */ (this.currentOverlay());
|
| + // Check if clicked outside of top overlay.
|
| + if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
|
| + overlay._onCaptureClick(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures the focus event is delegated to the right overlay.
|
| + * @param {!Event} event
|
| + * @private
|
| + */
|
| + _onCaptureFocus: function(event) {
|
| + var overlay = /** @type {?} */ (this.currentOverlay());
|
| + if (overlay) {
|
| + overlay._onCaptureFocus(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Ensures TAB and ESC keyboard events are delegated to the right overlay.
|
| + * @param {!Event} event
|
| + * @private
|
| + */
|
| + _onCaptureKeyDown: function(event) {
|
| + var overlay = /** @type {?} */ (this.currentOverlay());
|
| + if (overlay) {
|
| + if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
|
| + overlay._onCaptureEsc(event);
|
| + } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
|
| + overlay._onCaptureTab(event);
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Returns if the overlay1 should be behind overlay2.
|
| + * @param {!Element} overlay1
|
| + * @param {!Element} overlay2
|
| + * @return {boolean}
|
| + * @suppress {missingProperties}
|
| + * @private
|
| + */
|
| + _shouldBeBehindOverlay: function(overlay1, overlay2) {
|
| + return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
|
| + }
|
| + };
|
| +
|
| + Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
|
| +</script>
|
| +<script>
|
| +(function() {
|
| +'use strict';
|
| +
|
| +/**
|
| +Use `Polymer.IronOverlayBehavior` to implement an element that can be hidden or shown, and displays
|
| +on top of other content. It includes an optional backdrop, and can be used to implement a variety
|
| +of UI controls including dialogs and drop downs. Multiple overlays may be displayed at once.
|
| +
|
| +See the [demo source code](https://github.com/PolymerElements/iron-overlay-behavior/blob/master/demo/simple-overlay.html)
|
| +for an example.
|
| +
|
| +### Closing and canceling
|
| +
|
| +An overlay may be hidden by closing or canceling. The difference between close and cancel is user
|
| +intent. Closing generally implies that the user acknowledged the content on the overlay. By default,
|
| +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. 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
|
| +
|
| +By default the element is sized and positioned to fit and centered inside the window. You can
|
| +position and size it manually using CSS. See `Polymer.IronFitBehavior`.
|
| +
|
| +### Backdrop
|
| +
|
| +Set the `with-backdrop` attribute to display a backdrop behind the overlay. The backdrop is
|
| +appended to `<body>` and is of type `<iron-overlay-backdrop>`. See its doc page for styling
|
| +options.
|
| +
|
| +In addition, `with-backdrop` will wrap the focus within the content in the light DOM.
|
| +Override the [`_focusableNodes` getter](#Polymer.IronOverlayBehavior:property-_focusableNodes)
|
| +to achieve a different behavior.
|
| +
|
| +### Limitations
|
| +
|
| +The element is styled to appear on top of other content by setting its `z-index` property. You
|
| +must ensure no element has a stacking context with a higher `z-index` than its parent stacking
|
| +context. You should place this element as a child of `<body>` whenever possible.
|
| +
|
| +@demo demo/index.html
|
| +@polymerBehavior Polymer.IronOverlayBehavior
|
| +*/
|
| +
|
| + Polymer.IronOverlayBehaviorImpl = {
|
| +
|
| + properties: {
|
| +
|
| + /**
|
| + * True if the overlay is currently displayed.
|
| + */
|
| + opened: {
|
| + observer: '_openedChanged',
|
| + type: Boolean,
|
| + value: false,
|
| + notify: true
|
| + },
|
| +
|
| + /**
|
| + * True if the overlay was canceled when it was last closed.
|
| + */
|
| + canceled: {
|
| + observer: '_canceledChanged',
|
| + readOnly: true,
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to display a backdrop behind the overlay. It traps the focus
|
| + * within the light DOM of the overlay.
|
| + */
|
| + withBackdrop: {
|
| + observer: '_withBackdropChanged',
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * Set to true to disable auto-focusing the overlay or child nodes with
|
| + * the `autofocus` attribute` when the overlay is opened.
|
| + */
|
| + noAutoFocus: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to disable canceling the overlay with the ESC key.
|
| + */
|
| + noCancelOnEscKey: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to disable canceling the overlay by clicking outside it.
|
| + */
|
| + noCancelOnOutsideClick: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Contains the reason(s) this overlay was last closed (see `iron-overlay-closed`).
|
| + * `IronOverlayBehavior` provides the `canceled` reason; implementers of the
|
| + * behavior can provide other reasons in addition to `canceled`.
|
| + */
|
| + closingReason: {
|
| + // was a getter before, but needs to be a property so other
|
| + // behaviors can override this.
|
| + type: Object
|
| + },
|
| +
|
| + /**
|
| + * Set to true to enable restoring of focus when overlay is closed.
|
| + */
|
| + restoreFocusOnClose: {
|
| + type: Boolean,
|
| + value: false
|
| + },
|
| +
|
| + /**
|
| + * Set to true to keep overlay always on top.
|
| + */
|
| + alwaysOnTop: {
|
| + type: Boolean
|
| + },
|
| +
|
| + /**
|
| + * Shortcut to access to the overlay manager.
|
| + * @private
|
| + * @type {Polymer.IronOverlayManagerClass}
|
| + */
|
| + _manager: {
|
| + type: Object,
|
| + value: Polymer.IronOverlayManager
|
| + },
|
| +
|
| + /**
|
| + * The node being focused.
|
| + * @type {?Node}
|
| + */
|
| + _focusedChild: {
|
| + type: Object
|
| + }
|
| +
|
| + },
|
| +
|
| + listeners: {
|
| + 'iron-resize': '_onIronResize'
|
| + },
|
| +
|
| + /**
|
| + * The backdrop element.
|
| + * @type {Element}
|
| + */
|
| + get backdropElement() {
|
| + return this._manager.backdropElement;
|
| + },
|
| +
|
| + /**
|
| + * Returns the node to give focus to.
|
| + * @type {Node}
|
| + */
|
| + get _focusNode() {
|
| + return this._focusedChild || Polymer.dom(this).querySelector('[autofocus]') || this;
|
| + },
|
| +
|
| + /**
|
| + * Array of nodes that can receive focus (overlay included), ordered by `tabindex`.
|
| + * This is used to retrieve which is the first and last focusable nodes in order
|
| + * to wrap the focus for overlays `with-backdrop`.
|
| + *
|
| + * If you know what is your content (specifically the first and last focusable children),
|
| + * you can override this method to return only `[firstFocusable, lastFocusable];`
|
| + * @type {Array<Node>}
|
| + * @protected
|
| + */
|
| + get _focusableNodes() {
|
| + // Elements that can be focused even if they have [disabled] attribute.
|
| + var FOCUSABLE_WITH_DISABLED = [
|
| + 'a[href]',
|
| + 'area[href]',
|
| + 'iframe',
|
| + '[tabindex]',
|
| + '[contentEditable=true]'
|
| + ];
|
| +
|
| + // Elements that cannot be focused if they have [disabled] attribute.
|
| + var FOCUSABLE_WITHOUT_DISABLED = [
|
| + 'input',
|
| + 'select',
|
| + 'textarea',
|
| + 'button'
|
| + ];
|
| +
|
| + // Discard elements with tabindex=-1 (makes them not focusable).
|
| + var selector = FOCUSABLE_WITH_DISABLED.join(':not([tabindex="-1"]),') +
|
| + ':not([tabindex="-1"]),' +
|
| + FOCUSABLE_WITHOUT_DISABLED.join(':not([disabled]):not([tabindex="-1"]),') +
|
| + ':not([disabled]):not([tabindex="-1"])';
|
| +
|
| + var focusables = Polymer.dom(this).querySelectorAll(selector);
|
| + if (this.tabIndex >= 0) {
|
| + // Insert at the beginning because we might have all elements with tabIndex = 0,
|
| + // and the overlay should be the first of the list.
|
| + focusables.splice(0, 0, this);
|
| + }
|
| + // Sort by tabindex.
|
| + return focusables.sort(function (a, b) {
|
| + if (a.tabIndex === b.tabIndex) {
|
| + return 0;
|
| + }
|
| + if (a.tabIndex === 0 || a.tabIndex > b.tabIndex) {
|
| + return 1;
|
| + }
|
| + return -1;
|
| + });
|
| + },
|
| +
|
| + ready: function() {
|
| + // Used to skip calls to notifyResize and refit while the overlay is animating.
|
| + this.__isAnimating = false;
|
| + // with-backdrop needs tabindex to be set in order to trap the focus.
|
| + // If it is not set, IronOverlayBehavior will set it, and remove it if with-backdrop = false.
|
| + this.__shouldRemoveTabIndex = false;
|
| + // Used for wrapping the focus on TAB / Shift+TAB.
|
| + this.__firstFocusableNode = this.__lastFocusableNode = null;
|
| + // Used for requestAnimationFrame when opened changes.
|
| + this.__openChangedAsync = null;
|
| + // Used for requestAnimationFrame when iron-resize is fired.
|
| + this.__onIronResizeAsync = null;
|
| + this._ensureSetup();
|
| + },
|
| +
|
| + attached: function() {
|
| + // Call _openedChanged here so that position can be computed correctly.
|
| + if (this.opened) {
|
| + this._openedChanged();
|
| + }
|
| + this._observer = Polymer.dom(this).observeNodes(this._onNodesChange);
|
| + },
|
| +
|
| + detached: function() {
|
| + Polymer.dom(this).unobserveNodes(this._observer);
|
| + this._observer = null;
|
| + this.opened = false;
|
| + },
|
| +
|
| + /**
|
| + * Toggle the opened state of the overlay.
|
| + */
|
| + toggle: function() {
|
| + this._setCanceled(false);
|
| + this.opened = !this.opened;
|
| + },
|
| +
|
| + /**
|
| + * Open the overlay.
|
| + */
|
| + open: function() {
|
| + this._setCanceled(false);
|
| + this.opened = true;
|
| + },
|
| +
|
| + /**
|
| + * Close the overlay.
|
| + */
|
| + close: function() {
|
| + this._setCanceled(false);
|
| + this.opened = false;
|
| + },
|
| +
|
| + /**
|
| + * Cancels the overlay.
|
| + * @param {Event=} event The original event
|
| + */
|
| + cancel: function(event) {
|
| + var cancelEvent = this.fire('iron-overlay-canceled', event, {cancelable: true});
|
| + if (cancelEvent.defaultPrevented) {
|
| + return;
|
| + }
|
| +
|
| + this._setCanceled(true);
|
| + this.opened = false;
|
| + },
|
| +
|
| + _ensureSetup: function() {
|
| + if (this._overlaySetup) {
|
| + return;
|
| + }
|
| + this._overlaySetup = true;
|
| + this.style.outline = 'none';
|
| + this.style.display = 'none';
|
| + },
|
| +
|
| + _openedChanged: function() {
|
| + if (this.opened) {
|
| + this.removeAttribute('aria-hidden');
|
| + } else {
|
| + this.setAttribute('aria-hidden', 'true');
|
| + }
|
| +
|
| + // wait to call after ready only if we're initially open
|
| + if (!this._overlaySetup) {
|
| + return;
|
| + }
|
| +
|
| + if (this.__openChangedAsync) {
|
| + window.cancelAnimationFrame(this.__openChangedAsync);
|
| + }
|
| +
|
| + // Synchronously remove the overlay.
|
| + // The adding is done asynchronously to go out of the scope of the event
|
| + // which might have generated the opening.
|
| + if (!this.opened) {
|
| + this._manager.removeOverlay(this);
|
| + }
|
| +
|
| + // Defer any animation-related code on attached
|
| + // (_openedChanged gets called again on attached).
|
| + if (!this.isAttached) {
|
| + return;
|
| + }
|
| +
|
| + this.__isAnimating = true;
|
| +
|
| + // requestAnimationFrame for non-blocking rendering
|
| + this.__openChangedAsync = window.requestAnimationFrame(function() {
|
| + this.__openChangedAsync = null;
|
| + if (this.opened) {
|
| + this._manager.addOverlay(this);
|
| + this._prepareRenderOpened();
|
| + this._renderOpened();
|
| + } else {
|
| + // Move the focus before actually closing.
|
| + this._applyFocus();
|
| + this._renderClosed();
|
| + }
|
| + }.bind(this));
|
| + },
|
| +
|
| + _canceledChanged: function() {
|
| + this.closingReason = this.closingReason || {};
|
| + this.closingReason.canceled = this.canceled;
|
| + },
|
| +
|
| + _withBackdropChanged: function() {
|
| + // If tabindex is already set, no need to override it.
|
| + if (this.withBackdrop && !this.hasAttribute('tabindex')) {
|
| + this.setAttribute('tabindex', '-1');
|
| + this.__shouldRemoveTabIndex = true;
|
| + } else if (this.__shouldRemoveTabIndex) {
|
| + this.removeAttribute('tabindex');
|
| + this.__shouldRemoveTabIndex = false;
|
| + }
|
| + if (this.opened && this.isAttached) {
|
| + this._manager.trackBackdrop();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * tasks which must occur before opening; e.g. making the element visible.
|
| + * @protected
|
| + */
|
| + _prepareRenderOpened: function() {
|
| +
|
| + // Needed to calculate the size of the overlay so that transitions on its size
|
| + // will have the correct starting points.
|
| + this._preparePositioning();
|
| + this.refit();
|
| + this._finishPositioning();
|
| +
|
| + // Move the focus to the child node with [autofocus].
|
| + this._applyFocus();
|
| +
|
| + // Safari will apply the focus to the autofocus element when displayed for the first time,
|
| + // so we blur it. Later, _applyFocus will set the focus if necessary.
|
| + if (this.noAutoFocus && document.activeElement === this._focusNode) {
|
| + this._focusNode.blur();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Tasks which cause the overlay to actually open; typically play an animation.
|
| + * @protected
|
| + */
|
| + _renderOpened: function() {
|
| + this._finishRenderOpened();
|
| + },
|
| +
|
| + /**
|
| + * Tasks which cause the overlay to actually close; typically play an animation.
|
| + * @protected
|
| + */
|
| + _renderClosed: function() {
|
| + this._finishRenderClosed();
|
| + },
|
| +
|
| + /**
|
| + * Tasks to be performed at the end of open action. Will fire `iron-overlay-opened`.
|
| + * @protected
|
| + */
|
| + _finishRenderOpened: function() {
|
| +
|
| + this.notifyResize();
|
| + this.__isAnimating = false;
|
| +
|
| + // Store it so we don't query too much.
|
| + var focusableNodes = this._focusableNodes;
|
| + this.__firstFocusableNode = focusableNodes[0];
|
| + this.__lastFocusableNode = focusableNodes[focusableNodes.length - 1];
|
| +
|
| + this.fire('iron-overlay-opened');
|
| + },
|
| +
|
| + /**
|
| + * Tasks to be performed at the end of close action. Will fire `iron-overlay-closed`.
|
| + * @protected
|
| + */
|
| + _finishRenderClosed: function() {
|
| + // Hide the overlay and remove the backdrop.
|
| + this.style.display = 'none';
|
| + // Reset z-index only at the end of the animation.
|
| + this.style.zIndex = '';
|
| +
|
| + this.notifyResize();
|
| + this.__isAnimating = false;
|
| + this.fire('iron-overlay-closed', this.closingReason);
|
| + },
|
| +
|
| + _preparePositioning: function() {
|
| + this.style.transition = this.style.webkitTransition = 'none';
|
| + this.style.transform = this.style.webkitTransform = 'none';
|
| + this.style.display = '';
|
| + },
|
| +
|
| + _finishPositioning: function() {
|
| + // First, make it invisible & reactivate animations.
|
| + this.style.display = 'none';
|
| + // Force reflow before re-enabling animations so that they don't start.
|
| + // Set scrollTop to itself so that Closure Compiler doesn't remove this.
|
| + this.scrollTop = this.scrollTop;
|
| + this.style.transition = this.style.webkitTransition = '';
|
| + this.style.transform = this.style.webkitTransform = '';
|
| + // Now that animations are enabled, make it visible again
|
| + this.style.display = '';
|
| + // Force reflow, so that following animations are properly started.
|
| + // Set scrollTop to itself so that Closure Compiler doesn't remove this.
|
| + this.scrollTop = this.scrollTop;
|
| + },
|
| +
|
| + /**
|
| + * Applies focus according to the opened state.
|
| + * @protected
|
| + */
|
| + _applyFocus: function() {
|
| + if (this.opened) {
|
| + if (!this.noAutoFocus) {
|
| + this._focusNode.focus();
|
| + }
|
| + } else {
|
| + this._focusNode.blur();
|
| + this._focusedChild = null;
|
| + this._manager.focusOverlay();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Cancels (closes) the overlay. Call when click happens outside the overlay.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| + _onCaptureClick: function(event) {
|
| + if (!this.noCancelOnOutsideClick) {
|
| + this.cancel(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Keeps track of the focused child. If withBackdrop, traps focus within overlay.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| + _onCaptureFocus: function (event) {
|
| + if (!this.withBackdrop) {
|
| + return;
|
| + }
|
| + var path = Polymer.dom(event).path;
|
| + if (path.indexOf(this) === -1) {
|
| + event.stopPropagation();
|
| + this._applyFocus();
|
| + } else {
|
| + this._focusedChild = path[0];
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Handles the ESC key event and cancels (closes) the overlay.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| + _onCaptureEsc: function(event) {
|
| + if (!this.noCancelOnEscKey) {
|
| + this.cancel(event);
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Handles TAB key events to track focus changes.
|
| + * Will wrap focus for overlays withBackdrop.
|
| + * @param {!Event} event
|
| + * @protected
|
| + */
|
| + _onCaptureTab: function(event) {
|
| + if (!this.withBackdrop) {
|
| + return;
|
| + }
|
| + // TAB wraps from last to first focusable.
|
| + // Shift + TAB wraps from first to last focusable.
|
| + var shift = event.shiftKey;
|
| + var nodeToCheck = shift ? this.__firstFocusableNode : this.__lastFocusableNode;
|
| + var nodeToSet = shift ? this.__lastFocusableNode : this.__firstFocusableNode;
|
| + var shouldWrap = false;
|
| + if (nodeToCheck === nodeToSet) {
|
| + // If nodeToCheck is the same as nodeToSet, it means we have an overlay
|
| + // with 0 or 1 focusables; in either case we still need to trap the
|
| + // focus within the overlay.
|
| + shouldWrap = true;
|
| + } else {
|
| + // In dom=shadow, the manager will receive focus changes on the main
|
| + // root but not the ones within other shadow roots, so we can't rely on
|
| + // _focusedChild, but we should check the deepest active element.
|
| + var focusedNode = this._manager.deepActiveElement;
|
| + // If the active element is not the nodeToCheck but the overlay itself,
|
| + // it means the focus is about to go outside the overlay, hence we
|
| + // should prevent that (e.g. user opens the overlay and hit Shift+TAB).
|
| + shouldWrap = (focusedNode === nodeToCheck || focusedNode === this);
|
| + }
|
| +
|
| + if (shouldWrap) {
|
| + // When the overlay contains the last focusable element of the document
|
| + // and it's already focused, pressing TAB would move the focus outside
|
| + // the document (e.g. to the browser search bar). Similarly, when the
|
| + // overlay contains the first focusable element of the document and it's
|
| + // already focused, pressing Shift+TAB would move the focus outside the
|
| + // document (e.g. to the browser search bar).
|
| + // In both cases, we would not receive a focus event, but only a blur.
|
| + // In order to achieve focus wrapping, we prevent this TAB event and
|
| + // force the focus. This will also prevent the focus to temporarily move
|
| + // outside the overlay, which might cause scrolling.
|
| + event.preventDefault();
|
| + this._focusedChild = nodeToSet;
|
| + this._applyFocus();
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Refits if the overlay is opened and not animating.
|
| + * @protected
|
| + */
|
| + _onIronResize: function() {
|
| + if (this.__onIronResizeAsync) {
|
| + window.cancelAnimationFrame(this.__onIronResizeAsync);
|
| + this.__onIronResizeAsync = null;
|
| + }
|
| + if (this.opened && !this.__isAnimating) {
|
| + this.__onIronResizeAsync = window.requestAnimationFrame(function() {
|
| + this.__onIronResizeAsync = null;
|
| + this.refit();
|
| + }.bind(this));
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Will call notifyResize if overlay is opened.
|
| + * Can be overridden in order to avoid multiple observers on the same node.
|
| + * @protected
|
| + */
|
| + _onNodesChange: function() {
|
| + if (this.opened && !this.__isAnimating) {
|
| + this.notifyResize();
|
| + }
|
| + }
|
| + };
|
| +
|
| + /** @polymerBehavior */
|
| + Polymer.IronOverlayBehavior = [Polymer.IronFitBehavior, Polymer.IronResizableBehavior, Polymer.IronOverlayBehaviorImpl];
|
| +
|
| + /**
|
| + * Fired after the overlay opens.
|
| + * @event iron-overlay-opened
|
| + */
|
| +
|
| + /**
|
| + * Fired when the overlay is canceled, but before it is closed.
|
| + * @event iron-overlay-canceled
|
| + * @param {Event} event The closing of the overlay can be prevented
|
| + * by calling `event.preventDefault()`. The `event.detail` is the original event that
|
| + * originated the canceling (e.g. ESC keyboard event or click event outside the overlay).
|
| + */
|
| +
|
| + /**
|
| + * Fired after the overlay closes.
|
| + * @event iron-overlay-closed
|
| + * @param {Event} event The `event.detail` is the `closingReason` property
|
| + * (contains `canceled`, whether the overlay was canceled).
|
| + */
|
| +
|
| +})();
|
| +</script>
|
| +
|
| +
|
| +<dom-module id="paper-toast" assetpath="/res/imp/bower_components/paper-toast/">
|
| + <template>
|
| + <style>
|
| + :host {
|
| + display: block;
|
| + position: fixed;
|
| + background-color: var(--paper-toast-background-color, #323232);
|
| + color: var(--paper-toast-color, #f1f1f1);
|
| + min-height: 48px;
|
| + min-width: 288px;
|
| + padding: 16px 24px;
|
| + box-sizing: border-box;
|
| + box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.26);
|
| + border-radius: 2px;
|
| + margin: 12px;
|
| + font-size: 14px;
|
| + cursor: default;
|
| + -webkit-transition: -webkit-transform 0.3s, opacity 0.3s;
|
| + transition: transform 0.3s, opacity 0.3s;
|
| + opacity: 0;
|
| + -webkit-transform: translateY(100px);
|
| + transform: translateY(100px);
|
| + @apply(--paper-font-common-base);
|
| + }
|
| +
|
| + :host(.capsule) {
|
| + border-radius: 24px;
|
| + }
|
| +
|
| + :host(.fit-bottom) {
|
| + width: 100%;
|
| + min-width: 0;
|
| + border-radius: 0;
|
| + margin: 0;
|
| + }
|
| +
|
| + :host(.paper-toast-open) {
|
| + opacity: 1;
|
| + -webkit-transform: translateY(0px);
|
| + transform: translateY(0px);
|
| + }
|
| + </style>
|
| +
|
| + <span id="label">{{text}}</span>
|
| + <content></content>
|
| + </template>
|
| +
|
| + <script>
|
| + (function() {
|
| + // Keeps track of the toast currently opened.
|
| + var currentToast = null;
|
| +
|
| + Polymer({
|
| + is: 'paper-toast',
|
| +
|
| + behaviors: [
|
| + Polymer.IronOverlayBehavior
|
| + ],
|
| +
|
| + properties: {
|
| + /**
|
| + * The element to fit `this` into.
|
| + * Overridden from `Polymer.IronFitBehavior`.
|
| + */
|
| + fitInto: {
|
| + type: Object,
|
| + value: window,
|
| + observer: '_onFitIntoChanged'
|
| + },
|
| +
|
| + /**
|
| + * The orientation against which to align the dropdown content
|
| + * horizontally relative to `positionTarget`.
|
| + * Overridden from `Polymer.IronFitBehavior`.
|
| + */
|
| + horizontalAlign: {
|
| + type: String,
|
| + value: 'left'
|
| + },
|
| +
|
| + /**
|
| + * The orientation against which to align the dropdown content
|
| + * vertically relative to `positionTarget`.
|
| + * Overridden from `Polymer.IronFitBehavior`.
|
| + */
|
| + verticalAlign: {
|
| + type: String,
|
| + value: 'bottom'
|
| + },
|
| +
|
| + /**
|
| + * The duration in milliseconds to show the toast.
|
| + * Set to `0`, a negative number, or `Infinity`, to disable the
|
| + * toast auto-closing.
|
| + */
|
| + duration: {
|
| + type: Number,
|
| + value: 3000
|
| + },
|
| +
|
| + /**
|
| + * The text to display in the toast.
|
| + */
|
| + text: {
|
| + type: String,
|
| + value: ''
|
| + },
|
| +
|
| + /**
|
| + * Overridden from `IronOverlayBehavior`.
|
| + * Set to false to enable closing of the toast by clicking outside it.
|
| + */
|
| + noCancelOnOutsideClick: {
|
| + type: Boolean,
|
| + value: true
|
| + },
|
| +
|
| + /**
|
| + * Overridden from `IronOverlayBehavior`.
|
| + * Set to true to disable auto-focusing the toast or child nodes with
|
| + * the `autofocus` attribute` when the overlay is opened.
|
| + */
|
| + noAutoFocus: {
|
| + type: Boolean,
|
| + value: true
|
| + }
|
| + },
|
| +
|
| + listeners: {
|
| + 'transitionend': '__onTransitionEnd'
|
| + },
|
| +
|
| + /**
|
| + * Read-only. Deprecated. Use `opened` from `IronOverlayBehavior`.
|
| + * @property visible
|
| + * @deprecated
|
| + */
|
| + get visible() {
|
| + Polymer.Base._warn('`visible` is deprecated, use `opened` instead');
|
| + return this.opened;
|
| + },
|
| +
|
| + /**
|
| + * Read-only. Can auto-close if duration is a positive finite number.
|
| + * @property _canAutoClose
|
| + */
|
| + get _canAutoClose() {
|
| + return this.duration > 0 && this.duration !== Infinity;
|
| + },
|
| +
|
| + created: function() {
|
| + this._autoClose = null;
|
| + Polymer.IronA11yAnnouncer.requestAvailability();
|
| + },
|
| +
|
| + /**
|
| + * Show the toast. Without arguments, this is the same as `open()` from `IronOverlayBehavior`.
|
| + * @param {(Object|string)=} properties Properties to be set before opening the toast.
|
| + * e.g. `toast.show('hello')` or `toast.show({text: 'hello', duration: 3000})`
|
| + */
|
| + show: function(properties) {
|
| + if (typeof properties == 'string') {
|
| + properties = { text: properties };
|
| + }
|
| + for (var property in properties) {
|
| + if (property.indexOf('_') === 0) {
|
| + Polymer.Base._warn('The property "' + property + '" is private and was not set.');
|
| + } else if (property in this) {
|
| + this[property] = properties[property];
|
| + } else {
|
| + Polymer.Base._warn('The property "' + property + '" is not valid.');
|
| + }
|
| + }
|
| + this.open();
|
| + },
|
| +
|
| + /**
|
| + * Hide the toast. Same as `close()` from `IronOverlayBehavior`.
|
| + */
|
| + hide: function() {
|
| + this.close();
|
| + },
|
| +
|
| + /**
|
| + * Called on transitions of the toast, indicating a finished animation
|
| + * @private
|
| + */
|
| + __onTransitionEnd: function(e) {
|
| + // there are different transitions that are happening when opening and
|
| + // closing the toast. The last one so far is for `opacity`.
|
| + // This marks the end of the transition, so we check for this to determine if this
|
| + // is the correct event.
|
| + if (e && e.target === this && e.propertyName === 'opacity') {
|
| + if (this.opened) {
|
| + this._finishRenderOpened();
|
| + } else {
|
| + this._finishRenderClosed();
|
| + }
|
| + }
|
| + },
|
| +
|
| + /**
|
| + * Overridden from `IronOverlayBehavior`.
|
| + * Called when the value of `opened` changes.
|
| + */
|
| + _openedChanged: function() {
|
| + if (this._autoClose !== null) {
|
| + this.cancelAsync(this._autoClose);
|
| + this._autoClose = null;
|
| + }
|
| + if (this.opened) {
|
| + if (currentToast && currentToast !== this) {
|
| + currentToast.close();
|
| + }
|
| + currentToast = this;
|
| + this.fire('iron-announce', {
|
| + text: this.text
|
| + });
|
| + if (this._canAutoClose) {
|
| + this._autoClose = this.async(this.close, this.duration);
|
| + }
|
| + } else if (currentToast === this) {
|
| + currentToast = null;
|
| + }
|
| + Polymer.IronOverlayBehaviorImpl._openedChanged.apply(this, arguments);
|
| + },
|
| +
|
| + /**
|
| + * Overridden from `IronOverlayBehavior`.
|
| + */
|
| + _renderOpened: function() {
|
| + this.classList.add('paper-toast-open');
|
| + },
|
| +
|
| + /**
|
| + * Overridden from `IronOverlayBehavior`.
|
| + */
|
| + _renderClosed: function() {
|
| + this.classList.remove('paper-toast-open');
|
| + },
|
| +
|
| + /**
|
| + * @private
|
| + */
|
| + _onFitIntoChanged: function(fitInto) {
|
| + this.positionTarget = fitInto;
|
| + }
|
| +
|
| + /**
|
| + * Fired when `paper-toast` is opened.
|
| + *
|
| + * @event 'iron-announce'
|
| + * @param {{text: string}} detail Contains text that will be announced.
|
| + */
|
| + });
|
| + })();
|
| + </script>
|
| +</dom-module>
|
| +<dom-module id="url-param" assetpath="/res/imp/common/">
|
| + <template>
|
| + <paper-toast id="toast"></paper-toast>
|
| + </template>
|
| + <script>
|
| + (function(){
|
| + Polymer({
|
| + is: 'url-param',
|
| + properties: {
|
| + default_value: {
|
| + type: String,
|
| + },
|
| + default_values: {
|
| + type: Array,
|
| + },
|
| + multi: {
|
| + type: Boolean,
|
| + value: false,
|
| + },
|
| + name: {
|
| + type: String,
|
| + },
|
| + valid: {
|
| + type: Array,
|
| + },
|
| + value: {
|
| + type: String,
|
| + value: '',
|
| + notify: true,
|
| + observer: '_valueChanged',
|
| + },
|
| +
|
| + _loaded: {
|
| + type: Boolean,
|
| + value: false,
|
| + }
|
| + },
|
| + // Listens to array changes for multi urls
|
| + observers: ["_valueChanged(value.splices)"],
|
| +
|
| + ready: function () {
|
| + this._loaded = true;
|
| +
|
| + // Read the URL parameters. If our variable is set, save its value.
|
| + // Otherwise, place our value in the URL.
|
| + var val = this._getURL();
|
| + if (val && this._isValid(val)) {
|
| + this.set('value', val);
|
| + } else if (this.default_value && this._isValid(this.default_value)) {
|
| + this.set('value', this.default_value);
|
| + }
|
| + else if (this.multi && this.default_values && this._isValid(this.default_values)) {
|
| + this.set('value', this.default_values);
|
| + }
|
| + else {
|
| + this._putURL();
|
| + }
|
| + },
|
| + // Retrieve the value for our variable from the URL.
|
| + _getURL: function () {
|
| + var vals = sk.query.toParamSet(window.location.search.substring(1))[this.name];
|
| + if (!vals) {
|
| + return null;
|
| + }
|
| + if (this.multi) {
|
| + return vals;
|
| + }
|
| + if (vals.length > 1) {
|
| + this._error('Multiple values provided for ' + this.name + ' but only one accepted: ' + vals);
|
| + return null;
|
| + }
|
| + return vals[0];
|
| + },
|
| + // Store the value for our variable in the URL.
|
| + _putURL: function () {
|
| + var params = sk.query.toParamSet(window.location.search.substring(1));
|
| + delete params[this.name];
|
| + if (!this.value || Array.isArray(this.value) && this.value.length == 0) {
|
| + } else
|
| + // Don't insert undefined/empty values.
|
| + {
|
| + if (this.multi) {
|
| + params[this.name] = this.value;
|
| + } else {
|
| + params[this.name] = [this.value];
|
| + }
|
| + }
|
| + var newUrl = window.location.href.split('?')[0] + '?' + sk.query.fromParamSet(params);
|
| + window.history.replaceState('', '', newUrl);
|
| + },
|
| + // Check to see whether the given value is valid.
|
| + _isValid: function (val) {
|
| + var checkValid = function (val) {
|
| + if (this.valid) {
|
| + for (var i = 0; i < this.valid.length; i++) {
|
| + if (val == this.valid[i]) {
|
| + return true;
|
| + }
|
| + }
|
| + this._error('Invalid value for ' + this.name + ': "' + val + '". Must be one of: ' + this.valid);
|
| + return false;
|
| + }
|
| + return true;
|
| + }.bind(this);
|
| + if (this.multi) {
|
| + // Verify that it's an array and that all elements are valid.
|
| + if (!Array.isArray(val)) {
|
| + this._error('url-param-sk: Value is not an array: ' + val);
|
| + return false;
|
| + }
|
| + for (var i = 0; i < val.length; i++) {
|
| + if (!checkValid(val[i])) {
|
| + return false;
|
| + }
|
| + }
|
| + } else {
|
| + if (Array.isArray(val)) {
|
| + this._error('Multiple values provided for ' + this.name + ' but only one accepted: ' + val);
|
| + }
|
| + return checkValid(val);
|
| + }
|
| + return true;
|
| + },
|
| + _valueChanged: function () {
|
| + if (this._loaded) {
|
| + // Save our value to the URL.
|
| + this._putURL();
|
| + }
|
| + },
|
| + _error: function (msg) {
|
| + console.log('[ERROR] '+msg);
|
| + this.set('$.toast.text', msg);
|
| + this.$.toast.show();
|
| + }
|
| + });
|
| + })()
|
| + </script>
|
| +</dom-module>
|
| +<script>
|
|
|
| /**
|
| * @param {!Function} selectCallback
|
| @@ -16715,486 +19399,6 @@ You can bind to `isAuthorized` property to monitor authorization state.
|
|
|
| </script>
|
| <script>
|
| - (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+0008': 'backspace',
|
| - 'U+0009': 'tab',
|
| - 'U+001B': 'esc',
|
| - 'U+0020': 'space',
|
| - '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 = {
|
| - 8: 'backspace',
|
| - 9: 'tab',
|
| - 13: 'enter',
|
| - 27: 'esc',
|
| - 33: 'pageup',
|
| - 34: 'pagedown',
|
| - 35: 'end',
|
| - 36: 'home',
|
| - 32: 'space',
|
| - 37: 'left',
|
| - 38: 'up',
|
| - 39: 'right',
|
| - 40: 'down',
|
| - 46: 'del',
|
| - 106: '*'
|
| - };
|
| -
|
| - /**
|
| - * 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)?/;
|
| -
|
| - /**
|
| - * Matches ESC key.
|
| - *
|
| - * Value from: http://w3c.github.io/uievents-key/#key-Escape
|
| - */
|
| - var ESC_KEY = /^escape$/;
|
| -
|
| - /**
|
| - * Transforms the key.
|
| - * @param {string} key The KeyBoardEvent.key
|
| - * @param {Boolean} [noSpecialChars] Limits the transformation to
|
| - * alpha-numeric characters.
|
| - */
|
| - function transformKey(key, noSpecialChars) {
|
| - var validKey = '';
|
| - if (key) {
|
| - var lKey = key.toLowerCase();
|
| - if (lKey === ' ' || SPACE_KEY.test(lKey)) {
|
| - validKey = 'space';
|
| - } else if (ESC_KEY.test(lKey)) {
|
| - validKey = 'esc';
|
| - } else if (lKey.length == 1) {
|
| - if (!noSpecialChars || KEY_CHAR.test(lKey)) {
|
| - validKey = lKey;
|
| - }
|
| - } else if (ARROW_KEY.test(lKey)) {
|
| - validKey = lKey.replace('arrow', '');
|
| - } else if (lKey == 'multiply') {
|
| - // numpad '*' can map to Multiply on IE/Windows
|
| - validKey = '*';
|
| - } else {
|
| - validKey = lKey;
|
| - }
|
| - }
|
| - return validKey;
|
| - }
|
| -
|
| - function transformKeyIdentifier(keyIdent) {
|
| - var validKey = '';
|
| - if (keyIdent) {
|
| - if (keyIdent in KEY_IDENTIFIER) {
|
| - validKey = KEY_IDENTIFIER[keyIdent];
|
| - } else if (IDENT_CHAR.test(keyIdent)) {
|
| - keyIdent = parseInt(keyIdent.replace('U+', '0x'), 16);
|
| - validKey = String.fromCharCode(keyIdent).toLowerCase();
|
| - } else {
|
| - validKey = keyIdent.toLowerCase();
|
| - }
|
| - }
|
| - return validKey;
|
| - }
|
| -
|
| - function transformKeyCode(keyCode) {
|
| - var validKey = '';
|
| - if (Number(keyCode)) {
|
| - if (keyCode >= 65 && keyCode <= 90) {
|
| - // ascii a-z
|
| - // lowercase is 32 offset from uppercase
|
| - validKey = String.fromCharCode(32 + keyCode);
|
| - } else if (keyCode >= 112 && keyCode <= 123) {
|
| - // function keys f1-f12
|
| - validKey = 'f' + (keyCode - 112);
|
| - } else if (keyCode >= 48 && keyCode <= 57) {
|
| - // top 0-9 keys
|
| - validKey = String(keyCode - 48);
|
| - } else if (keyCode >= 96 && keyCode <= 105) {
|
| - // num pad 0-9
|
| - validKey = String(keyCode - 96);
|
| - } else {
|
| - validKey = KEY_CODE[keyCode];
|
| - }
|
| - }
|
| - return validKey;
|
| - }
|
| -
|
| - /**
|
| - * Calculates the normalized key for a KeyboardEvent.
|
| - * @param {KeyboardEvent} keyEvent
|
| - * @param {Boolean} [noSpecialChars] Set to true to limit keyEvent.key
|
| - * transformation to alpha-numeric chars. This is useful with key
|
| - * combinations like shift + 2, which on FF for MacOS produces
|
| - * keyEvent.key = @
|
| - * To get 2 returned, set noSpecialChars = true
|
| - * To get @ returned, set noSpecialChars = false
|
| - */
|
| - function normalizedKeyForEvent(keyEvent, noSpecialChars) {
|
| - // Fall back from .key, to .keyIdentifier, to .keyCode, and then to
|
| - // .detail.key to support artificial keyboard events.
|
| - return transformKey(keyEvent.key, noSpecialChars) ||
|
| - transformKeyIdentifier(keyEvent.keyIdentifier) ||
|
| - transformKeyCode(keyEvent.keyCode) ||
|
| - transformKey(keyEvent.detail ? keyEvent.detail.key : keyEvent.detail, noSpecialChars) || '';
|
| - }
|
| -
|
| - function keyComboMatchesEvent(keyCombo, event) {
|
| - // For combos with modifiers we support only alpha-numeric keys
|
| - var keyEvent = normalizedKeyForEvent(event, keyCombo.hasModifiers);
|
| - return keyEvent === keyCombo.key &&
|
| - (!keyCombo.hasModifiers || (
|
| - !!event.shiftKey === !!keyCombo.shiftKey &&
|
| - !!event.ctrlKey === !!keyCombo.ctrlKey &&
|
| - !!event.altKey === !!keyCombo.altKey &&
|
| - !!event.metaKey === !!keyCombo.metaKey)
|
| - );
|
| - }
|
| -
|
| - function parseKeyComboString(keyComboString) {
|
| - if (keyComboString.length === 1) {
|
| - return {
|
| - combo: keyComboString,
|
| - key: keyComboString,
|
| - event: 'keydown'
|
| - };
|
| - }
|
| - return keyComboString.split('+').reduce(function(parsedKeyCombo, keyComboPart) {
|
| - var eventParts = keyComboPart.split(':');
|
| - var keyName = eventParts[0];
|
| - var event = eventParts[1];
|
| -
|
| - if (keyName in MODIFIER_KEYS) {
|
| - parsedKeyCombo[MODIFIER_KEYS[keyName]] = true;
|
| - parsedKeyCombo.hasModifiers = true;
|
| - } else {
|
| - parsedKeyCombo.key = keyName;
|
| - parsedKeyCombo.event = event || 'keydown';
|
| - }
|
| -
|
| - return parsedKeyCombo;
|
| - }, {
|
| - combo: keyComboString.split(':').shift()
|
| - });
|
| - }
|
| -
|
| - function parseEventString(eventString) {
|
| - return eventString.trim().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 callback. A key binding has the format
|
| - * `"KEY+MODIFIER:EVENT": "callback"` (`"KEY": "callback"` or
|
| - * `"KEY:EVENT": "callback"` are valid as well). Some examples:
|
| - *
|
| - * keyBindings: {
|
| - * 'space': '_onKeydown', // same as 'space:keydown'
|
| - * 'shift+tab': '_onKeydown',
|
| - * 'enter:keypress': '_onKeypress',
|
| - * 'esc:keyup': '_onKeyup'
|
| - * }
|
| - *
|
| - * The callback will receive with an event containing the following information in `event.detail`:
|
| - *
|
| - * _onKeydown: function(event) {
|
| - * console.log(event.detail.combo); // KEY+MODIFIER, e.g. "shift+tab"
|
| - * console.log(event.detail.key); // KEY only, e.g. "tab"
|
| - * console.log(event.detail.event); // EVENT, e.g. "keydown"
|
| - * console.log(event.detail.keyboardEvent); // the original KeyboardEvent
|
| - * }
|
| - *
|
| - * Use the `keyEventTarget` attribute to set up event handlers on a specific
|
| - * node.
|
| - *
|
| - * See the [demo source code](https://github.com/PolymerElements/iron-a11y-keys-behavior/blob/master/demo/x-key-aware.html)
|
| - * for an example.
|
| - *
|
| - * @demo demo/index.html
|
| - * @polymerBehavior
|
| - */
|
| - Polymer.IronA11yKeysBehavior = {
|
| - properties: {
|
| - /**
|
| - * The EventTarget that will be firing relevant KeyboardEvents. Set it to
|
| - * `null` to disable the listeners.
|
| - * @type {?EventTarget}
|
| - */
|
| - keyEventTarget: {
|
| - type: Object,
|
| - value: function() {
|
| - return this;
|
| - }
|
| - },
|
| -
|
| - /**
|
| - * If true, this property will cause the implementing element to
|
| - * automatically stop propagation on any handled KeyboardEvents.
|
| - */
|
| - stopKeyboardEventPropagation: {
|
| - type: Boolean,
|
| - value: false
|
| - },
|
| -
|
| - _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)'
|
| - ],
|
| -
|
| -
|
| - /**
|
| - * To be used to express what combination of keys will trigger the relative
|
| - * callback. e.g. `keyBindings: { 'esc': '_onEscPressed'}`
|
| - * @type {Object}
|
| - */
|
| - 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();
|
| - },
|
| -
|
| - /**
|
| - * Returns true if a keyboard event matches `eventString`.
|
| - *
|
| - * @param {KeyboardEvent} event
|
| - * @param {string} eventString
|
| - * @return {boolean}
|
| - */
|
| - keyboardEventMatchesKeys: function(event, eventString) {
|
| - var keyCombos = parseEventString(eventString);
|
| - for (var i = 0; i < keyCombos.length; ++i) {
|
| - if (keyComboMatchesEvent(keyCombos[i], event)) {
|
| - return true;
|
| - }
|
| - }
|
| - return false;
|
| - },
|
| -
|
| - _collectKeyBindings: function() {
|
| - var keyBindings = this.behaviors.map(function(behavior) {
|
| - return behavior.keyBindings;
|
| - });
|
| -
|
| - if (keyBindings.indexOf(this.keyBindings) === -1) {
|
| - keyBindings.push(this.keyBindings);
|
| - }
|
| -
|
| - return keyBindings;
|
| - },
|
| -
|
| - _prepKeyBindings: function() {
|
| - this._keyBindings = {};
|
| -
|
| - this._collectKeyBindings().forEach(function(keyBindings) {
|
| - for (var eventString in keyBindings) {
|
| - this._addKeyBinding(eventString, keyBindings[eventString]);
|
| - }
|
| - }, this);
|
| -
|
| - for (var eventString in this._imperativeKeyBindings) {
|
| - this._addKeyBinding(eventString, this._imperativeKeyBindings[eventString]);
|
| - }
|
| -
|
| - // Give precedence to combos with modifiers to be checked first.
|
| - for (var eventName in this._keyBindings) {
|
| - this._keyBindings[eventName].sort(function (kb1, kb2) {
|
| - var b1 = kb1[0].hasModifiers;
|
| - var b2 = kb2[0].hasModifiers;
|
| - return (b1 === b2) ? 0 : b1 ? -1 : 1;
|
| - })
|
| - }
|
| - },
|
| -
|
| - _addKeyBinding: function(eventString, handlerName) {
|
| - parseEventString(eventString).forEach(function(keyCombo) {
|
| - this._keyBindings[keyCombo.event] =
|
| - this._keyBindings[keyCombo.event] || [];
|
| -
|
| - this._keyBindings[keyCombo.event].push([
|
| - keyCombo,
|
| - handlerName
|
| - ]);
|
| - }, this);
|
| - },
|
| -
|
| - _resetKeyEventListeners: function() {
|
| - this._unlistenKeyEventListeners();
|
| -
|
| - if (this.isAttached) {
|
| - this._listenKeyEventListeners();
|
| - }
|
| - },
|
| -
|
| - _listenKeyEventListeners: function() {
|
| - if (!this.keyEventTarget) {
|
| - return;
|
| - }
|
| - Object.keys(this._keyBindings).forEach(function(eventName) {
|
| - var keyBindings = this._keyBindings[eventName];
|
| - var boundKeyHandler = this._onKeyBindingEvent.bind(this, keyBindings);
|
| -
|
| - this._boundKeyHandlers.push([this.keyEventTarget, eventName, boundKeyHandler]);
|
| -
|
| - this.keyEventTarget.addEventListener(eventName, boundKeyHandler);
|
| - }, this);
|
| - },
|
| -
|
| - _unlistenKeyEventListeners: function() {
|
| - var keyHandlerTuple;
|
| - var keyEventTarget;
|
| - var eventName;
|
| - var boundKeyHandler;
|
| -
|
| - while (this._boundKeyHandlers.length) {
|
| - // My kingdom for block-scope binding and destructuring assignment..
|
| - keyHandlerTuple = this._boundKeyHandlers.pop();
|
| - keyEventTarget = keyHandlerTuple[0];
|
| - eventName = keyHandlerTuple[1];
|
| - boundKeyHandler = keyHandlerTuple[2];
|
| -
|
| - keyEventTarget.removeEventListener(eventName, boundKeyHandler);
|
| - }
|
| - },
|
| -
|
| - _onKeyBindingEvent: function(keyBindings, event) {
|
| - if (this.stopKeyboardEventPropagation) {
|
| - event.stopPropagation();
|
| - }
|
| -
|
| - // if event has been already prevented, don't do anything
|
| - if (event.defaultPrevented) {
|
| - return;
|
| - }
|
| -
|
| - for (var i = 0; i < keyBindings.length; i++) {
|
| - var keyCombo = keyBindings[i][0];
|
| - var handlerName = keyBindings[i][1];
|
| - if (keyComboMatchesEvent(keyCombo, event)) {
|
| - this._triggerKeyHandler(keyCombo, handlerName, event);
|
| - // exit the loop if eventDefault was prevented
|
| - if (event.defaultPrevented) {
|
| - return;
|
| - }
|
| - }
|
| - }
|
| - },
|
| -
|
| - _triggerKeyHandler: function(keyCombo, handlerName, keyboardEvent) {
|
| - var detail = Object.create(keyCombo);
|
| - detail.keyboardEvent = keyboardEvent;
|
| - var event = new CustomEvent(keyCombo.event, {
|
| - detail: detail,
|
| - cancelable: true
|
| - });
|
| - this[handlerName].call(this, event);
|
| - if (event.defaultPrevented) {
|
| - keyboardEvent.preventDefault();
|
| - }
|
| - }
|
| - };
|
| - })();
|
| -</script>
|
| -<script>
|
|
|
| /**
|
| * @demo demo/index.html
|
| @@ -18726,86 +20930,6 @@ You can bind to `isAuthorized` property to monitor authorization state.
|
| });
|
| </script>
|
| </dom-module>
|
| -
|
| -
|
| -<dom-module id="iron-a11y-announcer" assetpath="/res/imp/bower_components/iron-a11y-announcer/">
|
| - <template>
|
| - <style>
|
| - :host {
|
| - display: inline-block;
|
| - position: fixed;
|
| - clip: rect(0px,0px,0px,0px);
|
| - }
|
| - </style>
|
| - <div aria-live$="[[mode]]">[[_text]]</div>
|
| - </template>
|
| -
|
| - <script>
|
| -
|
| - (function() {
|
| - 'use strict';
|
| -
|
| - Polymer.IronA11yAnnouncer = Polymer({
|
| - is: 'iron-a11y-announcer',
|
| -
|
| - properties: {
|
| -
|
| - /**
|
| - * The value of mode is used to set the `aria-live` attribute
|
| - * for the element that will be announced. Valid values are: `off`,
|
| - * `polite` and `assertive`.
|
| - */
|
| - mode: {
|
| - type: String,
|
| - value: 'polite'
|
| - },
|
| -
|
| - _text: {
|
| - type: String,
|
| - value: ''
|
| - }
|
| - },
|
| -
|
| - created: function() {
|
| - if (!Polymer.IronA11yAnnouncer.instance) {
|
| - Polymer.IronA11yAnnouncer.instance = this;
|
| - }
|
| -
|
| - document.body.addEventListener('iron-announce', this._onIronAnnounce.bind(this));
|
| - },
|
| -
|
| - /**
|
| - * Cause a text string to be announced by screen readers.
|
| - *
|
| - * @param {string} text The text that should be announced.
|
| - */
|
| - announce: function(text) {
|
| - this._text = '';
|
| - this.async(function() {
|
| - this._text = text;
|
| - }, 100);
|
| - },
|
| -
|
| - _onIronAnnounce: function(event) {
|
| - if (event.detail && event.detail.text) {
|
| - this.announce(event.detail.text);
|
| - }
|
| - }
|
| - });
|
| -
|
| - Polymer.IronA11yAnnouncer.instance = null;
|
| -
|
| - Polymer.IronA11yAnnouncer.requestAvailability = function() {
|
| - if (!Polymer.IronA11yAnnouncer.instance) {
|
| - Polymer.IronA11yAnnouncer.instance = document.createElement('iron-a11y-announcer');
|
| - }
|
| -
|
| - document.body.appendChild(Polymer.IronA11yAnnouncer.instance);
|
| - };
|
| - })();
|
| -
|
| - </script>
|
| -</dom-module>
|
| <script>
|
|
|
| /*
|
| @@ -20566,11 +22690,17 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| (function(){
|
| var ANDROID_ALIASES = {
|
| "bullhead": "Nexus 5X",
|
| - "flo": "Nexus 7",
|
| + "flo": "Nexus 7 (2013)",
|
| "flounder": "Nexus 9",
|
| + "foster": "NVIDIA Shield",
|
| + "fugu": "Nexus Player",
|
| + "grouper": "Nexus 7 (2012)",
|
| "hammerhead": "Nexus 5",
|
| + "m0": "Galaxy S3",
|
| "mako": "Nexus 4",
|
| + "manta": "Nexus 10",
|
| "shamu": "Nexus 6",
|
| + "sprout": "Android One",
|
| };
|
| // Taken from http://developer.android.com/reference/android/os/BatteryManager.html
|
| var BATTERY_HEALTH_UNKNOWN = 1;
|
| @@ -20585,12 +22715,14 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| "1002": "AMD",
|
| "1002:6779": "AMD Radeon HD 6450/7450/8450",
|
| "1002:6821": "AMD Radeon HD 8870M",
|
| + "1002:683d": "AMD Radeon HD 7770/8760",
|
| "1002:9830": "AMD Radeon HD 8400",
|
| "102b": "Matrox",
|
| "102b:0522": "Matrox MGA G200e",
|
| "102b:0532": "Matrox MGA G200eW",
|
| "102b:0534": "Matrox G200eR2",
|
| "10de": "NVIDIA",
|
| + "10de:08a4": "NVIDIA GeForce 320M",
|
| "10de:08aa": "NVIDIA GeForce 320M",
|
| "10de:0fe9": "NVIDIA GeForce GT 750M Mac Edition",
|
| "10de:104a": "NVIDIA GeForce GT 610",
|
| @@ -20598,9 +22730,11 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| "10de:1244": "NVIDIA GeForce GTX 550 Ti",
|
| "10de:1401": "NVIDIA GeForce GTX 960",
|
| "8086": "Intel",
|
| + "8086:0412": "Intel Haswell Integrated",
|
| "8086:041a": "Intel Xeon Integrated",
|
| "8086:0a2e": "Intel Haswell Integrated",
|
| "8086:0d26": "Intel Crystal Well Integrated",
|
| + "8086:22b1": "Intel Braswell Integrated",
|
| }
|
|
|
| // For consistency, all aliases are displayed like:
|
| @@ -20612,15 +22746,6 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| SwarmingBehaviors.BotListBehavior = {
|
|
|
| properties: {
|
| - // TODO(kjlubick): Add more of these things from state, as they
|
| - // needed/useful/requested.
|
| - DIMENSIONS: {
|
| - type: Array,
|
| - value: function(){
|
| - return ["android_devices", "cores", "cpu", "device_type",
|
| - "device_os", "gpu", "id", "os", "pool"];
|
| - },
|
| - },
|
| DIMENSIONS_WITH_ALIASES: {
|
| type: Array,
|
| value: function(){
|
| @@ -20630,6 +22755,8 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| BOT_PROPERTIES: {
|
| type: Array,
|
| value: function() {
|
| + // TODO(kjlubick): Add more of these things from state, as they
|
| + // needed/useful/requested.
|
| return ["disk_space", "task", "status"];
|
| }
|
| },
|
| @@ -20660,6 +22787,9 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| var o = d[key];
|
| o.serial = key;
|
| o.okay = (o.state === AVAILABLE);
|
| + // It is easier to assume all devices on a bot are of the same type
|
| + // than to pick through the (incomplete) device state and find it.
|
| + o.device_type = this._attribute(bot, "device_type")[0];
|
| devices.push(o);
|
| }
|
| return devices;
|
| @@ -20667,12 +22797,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|
|
| // _deviceType returns the codename of a given Android device.
|
| _deviceType: function(device) {
|
| - if (!device || !device.build) {
|
| - return UNKNOWN;
|
| - }
|
| - var t = device.build["build.product"] || device.build["product.board"] ||
|
| - device.build["product.device"] || UNKNOWN;
|
| - return t.toLowerCase();
|
| + return device.device_type.toLowerCase();
|
| },
|
|
|
| // _dimension returns the given dimension of a bot. If it is defined, it
|
| @@ -20824,15 +22949,27 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| }
|
| </style>
|
|
|
| + <url-param name="filters" value="{{_filters}}" default_values="[]" multi="">
|
| + </url-param>
|
| + <url-param name="columns" value="{{columns}}" default_values="["id","os","task","status"]" multi="">
|
| + </url-param>
|
| + <url-param name="query" value="{{_query}}">
|
| + </url-param>
|
| + <url-param name="verbose" value="{{verbose}}">
|
| + </url-param>
|
| + <url-param name="limit" default_value="200" value="{{limit}}">
|
| + </url-param>
|
| +
|
| <div class="container horizontal layout">
|
|
|
|
|
| <div class="narrow-down-selector">
|
| <div>
|
| - <paper-input id="filter" label="Search columns and filters" placeholder="gpu nvidia" value="{{_query}}"></paper-input>
|
| + <paper-input id="filter" label="Search columns and filters" placeholder="gpu nvidia" value="{{_query}}">
|
| + </paper-input>
|
| </div>
|
|
|
| - <div class="selector side-by-side">
|
| + <div class="selector side-by-side" title="This shows all bot dimension names and other interesting bot properties. Mark the check box to add as a column. Select the row to see filter options.">
|
| <iron-selector attr-for-selected="label" selected="{{_primarySelected}}">
|
| <template is="dom-repeat" items="[[_primaryItems]]" as="item">
|
| <div class="selectable item horizontal layout" label="[[item]]">
|
| @@ -20846,7 +22983,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| </iron-selector>
|
| </div>
|
|
|
| - <div class="selector side-by-side">
|
| + <div class="selector side-by-side" title="These are all options (if any) that the bot list can be filtered on.">
|
| <template is="dom-repeat" id="secondaryList" items="[[_secondaryItems]]" as="item">
|
| <div class="item horizontal layout" label="[[item]]">
|
|
|
| @@ -20858,7 +22995,8 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| </template>
|
| </div>
|
|
|
| - <div class="selector side-by-side">
|
| + <div class="selector side-by-side" title="These filters are AND'd together and applied to all bots in
|
| +the fleet.">
|
| <template is="dom-repeat" items="[[_filters]]" as="fil">
|
| <div class="item horizontal layout" label="[[fil]]">
|
| <span>[[fil]]</span>
|
| @@ -20869,7 +23007,11 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| </template>
|
| </div>
|
|
|
| + <div class="side-by-side">
|
| <paper-checkbox checked="{{verbose}}">Verbose Entries</paper-checkbox>
|
| + <paper-input id="limit" label="Limit Results" auto-validate="" min="0" max="1000" pattern="[0-9]+" value="{{limit}}">
|
| + </paper-input>
|
| + </div>
|
| </div>
|
|
|
| </div>
|
| @@ -20881,20 +23023,13 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| // filterMap is a map of primary -> function. The function returns a
|
| // boolean "does the bot (first arg) match the second argument". These
|
| // functions will have "this" be the botlist, and will have access to all
|
| - // functions defined in bot-list and bot-list-shared.
|
| + // functions defined in bot-list and bot-list-shared. If there is not
|
| + // one provided, a default will be used, see _makeFilter().
|
| var filterMap = {
|
| android_devices: function(bot, num) {
|
| var o = this._attribute(bot, "android_devices", "0");
|
| return o.indexOf(num) !== -1;
|
| },
|
| - cores: function(bot, cores) {
|
| - var o = this._attribute(bot, "cores");
|
| - return o.indexOf(cores) !== -1;
|
| - },
|
| - cpu: function(bot, cpu) {
|
| - var o = this._attribute(bot, "cpu");
|
| - return o.indexOf(cpu) !== -1;
|
| - },
|
| device_os: function(bot, os) {
|
| var o = this._attribute(bot, "device_os", "none");
|
| return o.indexOf(os) !== -1;
|
| @@ -20913,21 +23048,13 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| id: function(bot, id) {
|
| return true;
|
| },
|
| - os: function(bot, os) {
|
| - var o = this._attribute(bot, "os");
|
| - return o.indexOf(os) !== -1;
|
| - },
|
| - pool: function(bot, pool) {
|
| - var o = this._attribute(bot, "pool");
|
| - return o.indexOf(pool) !== -1;
|
| - },
|
| status: function(bot, status) {
|
| if (status === "quarantined") {
|
| return bot.quarantined;
|
| } else if (status === "dead") {
|
| return bot.is_dead;
|
| } else {
|
| - // Status must be "available".
|
| + // Status must be "alive".
|
| return !bot.quarantined && !bot.is_dead;
|
| }
|
| },
|
| @@ -20985,39 +23112,36 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| primary_arr: {
|
| type: Array,
|
| },
|
| + dimensions: {
|
| + type: Array,
|
| + },
|
|
|
| // output
|
| columns: {
|
| type: Array,
|
| - value: function() {
|
| - // TODO(kjlubick) back these up to URL params and load them from
|
| - // there on reload.
|
| - return ["id","os","task","status"];
|
| - },
|
| - notify: true,
|
| - },
|
| - dimensions: {
|
| - type: Array,
|
| - computed: "_extractDimensions(DIMENSIONS.*,_filters.*)",
|
| notify: true,
|
| },
|
| filter: {
|
| - type: Object,
|
| + type: Function,
|
| computed: "_makeFilter(_filters.*)",
|
| notify: true,
|
| },
|
| + query_params: {
|
| + type: Object,
|
| + computed: "_extractQueryParams(dimensions.*,_filters.*, limit)",
|
| + notify: true,
|
| + },
|
| verbose: {
|
| type: Boolean,
|
| - value: false,
|
| notify: true,
|
| },
|
|
|
| // private
|
| _filters: {
|
| type:Array,
|
| - value: function() {
|
| - return [];
|
| - }
|
| + },
|
| + _limit: {
|
| + type: Number,
|
| },
|
| _primaryItems: {
|
| type: Array,
|
| @@ -21030,7 +23154,6 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| // query is treated as a space separated list.
|
| _query: {
|
| type:String,
|
| - value: "",
|
| },
|
| _secondaryItems: {
|
| type: Array,
|
| @@ -21106,8 +23229,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| },
|
|
|
| _makeFilter: function() {
|
| - // The filters belonging to the same primary key will be or'd together.
|
| - // Those groups will be and'd together.
|
| + // All filters will be AND'd together.
|
| // filterGroups will be a map of primary (i.e. column) -> array of
|
| // options that should be filtered to.
|
| // e.g. "os" -> ["Windows", "Linux"]
|
| @@ -21129,13 +23251,17 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| for (primary in filterGroups){
|
| var params = filterGroups[primary];
|
| var filter = filterMap[primary];
|
| - var groupResult = false;
|
| + if (!filter) {
|
| + filter = function(bot, c) {
|
| + var o = this._attribute(bot, primary);
|
| + return o.indexOf(c) !== -1;
|
| + }.bind(this);
|
| + }
|
| if (filter) {
|
| params.forEach(function(param){
|
| - groupResult = groupResult || filter.bind(this)(bot,param);
|
| + retVal = retVal && filter.bind(this)(bot,param);
|
| }.bind(this));
|
| }
|
| - retVal = retVal && groupResult;
|
| }
|
| return retVal;
|
| }
|
| @@ -21226,17 +23352,40 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| return item.substring(match.idx + match.part.length);
|
| },
|
|
|
| - _extractDimensions: function() {
|
| - var dims = []
|
| + _extractQueryParams: function() {
|
| + var params = {};
|
| + var dims = [];
|
| this._filters.forEach(function(f) {
|
| var split = f.split(FILTER_SEP, 1)
|
| var col = split[0];
|
| - if (this.DIMENSIONS.indexOf(col) !== -1) {
|
| + if (this.dimensions.indexOf(col) !== -1) {
|
| var rest = f.substring(col.length + FILTER_SEP.length);
|
| dims.push(col + FILTER_SEP + this._unalias(rest))
|
| - };
|
| + } else if (col === "status") {
|
| + var rest = f.substring(col.length + FILTER_SEP.length);
|
| + if (rest === "alive") {
|
| + params["is_dead"] = "FALSE";
|
| + params["quarantined"] = "FALSE";
|
| + } else if (rest === "quarantined") {
|
| + params["quarantined"] = "TRUE";
|
| + } else if (rest === "dead") {
|
| + params["is_dead"] = "TRUE";
|
| + }
|
| + }
|
| }.bind(this));
|
| - return dims;
|
| + params["dimensions"] = dims;
|
| + var lim = Math.floor(this.limit)
|
| + if (Number.isInteger(lim)) {
|
| + // Clamp the limit
|
| + lim = Math.max(lim, 1);
|
| + lim = Math.min(1000, lim);
|
| + params["limit"] = lim;
|
| + // not !-- because limit could be "900"
|
| + if (this.limit != lim) {
|
| + this.set("limit", lim);
|
| + }
|
| + }
|
| + return params;
|
| }
|
|
|
| });
|
| @@ -21244,7 +23393,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| </script>
|
| </dom-module><dom-module id="bot-list-data" assetpath="/res/imp/botlist/">
|
| <template>
|
| - <iron-ajax id="botlist" url="/_ah/api/swarming/v1/bots/list" headers="[[auth_headers]]" params="[[_botlistParams(dimensions.*)]]" handle-as="json" last-response="{{_list}}" loading="{{_busy1}}">
|
| + <iron-ajax id="botlist" url="/_ah/api/swarming/v1/bots/list" headers="[[auth_headers]]" params="[[query_params]]" handle-as="json" last-response="{{_list}}" loading="{{_busy1}}">
|
| </iron-ajax>
|
|
|
| <iron-ajax id="dimensions" url="/_ah/api/swarming/v1/bots/dimensions" headers="[[auth_headers]]" handle-as="json" last-response="{{_dimensions}}" loading="{{_busy2}}">
|
| @@ -21255,6 +23404,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| </template>
|
| <script>
|
| (function(){
|
| + var BLACKLIST_DIMENSIONS = ["quarantined", "error"];
|
|
|
| Polymer({
|
| is: 'bot-list-data',
|
| @@ -21267,8 +23417,8 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| type: Object,
|
| observer: "signIn",
|
| },
|
| - dimensions: {
|
| - type: Array,
|
| + query_params: {
|
| + type: Object,
|
| },
|
|
|
| //outputs
|
| @@ -21282,6 +23432,11 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| computed: "_or(_busy1,_busy2,_busy3)",
|
| notify: true,
|
| },
|
| + dimensions: {
|
| + type: Array,
|
| + computed: "_makeArray(_dimensions)",
|
| + notify: true,
|
| + },
|
| fleet: {
|
| type: Object,
|
| computed: "_fleet(_count)",
|
| @@ -21294,8 +23449,8 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| },
|
| primary_arr: {
|
| type: Array,
|
| - // DIMENSIONS and BOT_PROPERTIES are inherited from BotListBehavior
|
| - computed: "_primaryArr(DIMENSIONS, BOT_PROPERTIES)",
|
| + //BOT_PROPERTIES is inherited from BotListBehavior
|
| + computed: "_primaryArr(dimensions, BOT_PROPERTIES)",
|
| notify: true,
|
| },
|
|
|
| @@ -21312,20 +23467,15 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| },
|
|
|
| signIn: function(){
|
| + // Auto on iron-ajax means to automatically re-make the request if
|
| + // the url or the query params change. Auto does not trigger if the
|
| + // [auth] headers change, so we wait until the user is signed in
|
| + // before making any requests.
|
| this.$.botlist.auto = true;
|
| this.$.dimensions.auto = true;
|
| this.$.fleet.auto = true;
|
| },
|
|
|
| - _botlistParams: function() {
|
| - if (!this.dimensions) {
|
| - return {};
|
| - }
|
| - return {
|
| - dimensions: this.dimensions,
|
| - };
|
| - },
|
| -
|
| _bots: function(){
|
| if (!this._list || !this._list.items) {
|
| return [];
|
| @@ -21360,23 +23510,37 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| return {};
|
| }
|
| return {
|
| - alive: this._count.count || -1,
|
| + all: this._count.count || -1,
|
| + alive: (this._count.count - this._count.dead) || -1,
|
| busy: this._count.busy || -1,
|
| - idle: this._count.count && this._count.busy &&
|
| - this._count.count - this._count.busy,
|
| + idle: (this._count.count - this._count.busy) || -1,
|
| dead: this._count.dead || -1,
|
| quarantined: this._count.quarantined || -1,
|
| }
|
| },
|
|
|
| + _makeArray: function(dimObj) {
|
| + if (!dimObj || !dimObj.bots_dimensions) {
|
| + return [];
|
| + }
|
| + var dims = [];
|
| + dimObj.bots_dimensions.forEach(function(d){
|
| + if (BLACKLIST_DIMENSIONS.indexOf(d.key) === -1) {
|
| + dims.push(d.key);
|
| + }
|
| + });
|
| + dims.sort();
|
| + return dims;
|
| + },
|
| +
|
| _primaryArr: function(dimensions, properties) {
|
| return dimensions.concat(properties);
|
| },
|
|
|
| _primaryMap: function(dimensions){
|
| - // map will keep track of dimensions that we have seen at least once.
|
| - // This will then basically get turned into an array to be used for
|
| - // filtering.
|
| + // pMap will have a list of columns to available values (primary key
|
| + // to secondary values). This includes bot dimensions, but also
|
| + // includes state like disk_space, quarantined, busy, etc.
|
| dimensions = dimensions.bots_dimensions;
|
|
|
| var pMap = {};
|
| @@ -21421,7 +23585,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| // Create custom filter options
|
| pMap["disk_space"] = [];
|
| pMap["task"] = ["busy", "idle"];
|
| - pMap["status"] = ["available", "dead", "quarantined"];
|
| + pMap["status"] = ["alive", "dead", "quarantined"];
|
|
|
| // No need to sort any of this, bot-filters sorts secondary items
|
| // automatically, especially when the user types a query.
|
| @@ -21453,26 +23617,41 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| </style>
|
|
|
| <div class="header">Fleet</div>
|
| -
|
| <table>
|
| <tbody><tr>
|
| - <td class="right"><a href="/newui/botlist?alive">Alive</a>:</td>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('','',columns.*,filtered_bots.*,sort,verbose)]]">All</a>:
|
| + </td>
|
| + <td class="left">[[fleet.all]]</td>
|
| + </tr>
|
| + <tr>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('alive','',columns.*,filtered_bots.*,sort,verbose)]]">Alive</a>:
|
| + </td>
|
| <td class="left">[[fleet.alive]]</td>
|
| </tr>
|
| <tr>
|
| - <td class="right"><a href="/newui/botlist?busy">Busy</a>:</td>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('busy','',columns.*,filtered_bots.*,sort,verbose)]]">Busy</a>:
|
| + </td>
|
| <td class="left">[[fleet.busy]]</td>
|
| </tr>
|
| <tr>
|
| - <td class="right"><a href="/newui/botlist?idle">Idle</a>:</td>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('idle','',columns.*,filtered_bots.*,sort,verbose)]]">Idle</a>:
|
| + </td>
|
| <td class="left">[[fleet.idle]]</td>
|
| </tr>
|
| <tr>
|
| - <td class="right"><a href="/newui/botlist?dead">Dead</a>:</td>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('dead','',columns.*,filtered_bots.*,sort,verbose)]]">Dead</a>:
|
| + </td>
|
| <td class="left">[[fleet.dead]]</td>
|
| </tr>
|
| <tr>
|
| - <td class="right"><a href="/newui/botlist?quaren">Quarantined</a>:</td>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('quarantined','',columns.*,filtered_bots.*,sort,verbose)]]">Quarantined</a>:
|
| + </td>
|
| <td class="left">[[fleet.quarantined]]</td>
|
| </tr>
|
| </tbody></table>
|
| @@ -21480,23 +23659,39 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| <div class="header">Displayed</div>
|
| <table>
|
| <tbody><tr>
|
| - <td class="right"><a href="/newui/botlist?alive2">Alive</a>:</td>
|
| + <td class="right">
|
| + All:
|
| + </td>
|
| + <td class="left">[[_currently_showing.all]]</td>
|
| + </tr>
|
| + <tr>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('alive','true',columns.*,filtered_bots.*,sort,verbose)]]">Alive</a>:
|
| + </td>
|
| <td class="left">[[_currently_showing.alive]]</td>
|
| </tr>
|
| <tr>
|
| - <td class="right"><a href="/newui/botlist?busy2">Busy</a>:</td>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('busy','true',columns.*,filtered_bots.*,sort,verbose)]]">Busy</a>:
|
| + </td>
|
| <td class="left">[[_currently_showing.busy]]</td>
|
| </tr>
|
| <tr>
|
| - <td class="right"><a href="/newui/botlist?idle2">Idle</a>:</td>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('idle','true',columns.*,filtered_bots.*,sort,verbose)]]">Idle</a>:
|
| + </td>
|
| <td class="left">[[_currently_showing.idle]]</td>
|
| </tr>
|
| <tr>
|
| - <td class="right"><a href="/newui/botlist?dead2">Dead</a>:</td>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('dead','true',columns.*,filtered_bots.*,sort,verbose)]]">Dead</a>:
|
| + </td>
|
| <td class="left">[[_currently_showing.dead]]</td>
|
| </tr>
|
| <tr>
|
| - <td class="right"><a href="/newui/botlist?quaren2">Quarantined</a>:</td>
|
| + <td class="right">
|
| + <a href$="[[_makeURL('quarantined','true',columns.*,filtered_bots.*,sort,verbose)]]">Quarantined</a>:
|
| + </td>
|
| <td class="left">[[_currently_showing.quarantined]]</td>
|
| </tr>
|
| </tbody></table>
|
| @@ -21509,17 +23704,27 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| behaviors: [SwarmingBehaviors.BotListBehavior],
|
|
|
| properties: {
|
| + columns: {
|
| + type: Array,
|
| + },
|
| filtered_bots: {
|
| type: Array,
|
| },
|
| fleet: {
|
| type: Object,
|
| },
|
| + sort: {
|
| + type: String,
|
| + },
|
| + verbose: {
|
| + type: Boolean,
|
| + },
|
|
|
| _currently_showing: {
|
| type: Object,
|
| value: function() {
|
| return {
|
| + all: -1,
|
| alive: -1,
|
| busy: -1,
|
| idle: -1,
|
| @@ -21534,8 +23739,42 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| // property observers
|
| observers: ["_recount(filtered_bots.*)"],
|
|
|
| + _getFilterStr: function(filter) {
|
| + if (!filter) {
|
| + return "";
|
| + }
|
| + if (filter === "alive" || filter === "dead" ||
|
| + filter === "quarantined") {
|
| + return "status:" + filter;
|
| + } else {
|
| + return "task:" + filter;
|
| + }
|
| + },
|
| +
|
| + _makeURL: function(filter, preserveOthers) {
|
| + if (preserveOthers) {
|
| + var fstr = encodeURIComponent(this._getFilterStr(filter));
|
| + if (window.location.href.indexOf(fstr) === -1) {
|
| + return window.location.href + "&filters=" + fstr;
|
| + }
|
| + // The filter is already on the list.
|
| + return undefined;
|
| + }
|
| + var params = {
|
| + sort: [this.sort],
|
| + columns: this.columns,
|
| + verbose: [this.verbose],
|
| + }
|
| + if (filter) {
|
| + params["filters"] = [this._getFilterStr(filter)];
|
| + }
|
| +
|
| + return window.location.href.split('?')[0] + '?' + sk.query.fromParamSet(params);
|
| + },
|
| +
|
| _recount: function() {
|
| var curr = {
|
| + all: 0,
|
| alive: 0,
|
| busy: 0,
|
| idle: 0,
|
| @@ -21559,6 +23798,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| } else {
|
| curr.alive++;
|
| }
|
| + curr.all++;
|
| }.bind(this));
|
| this.set("_currently_showing", curr);
|
| }
|
| @@ -21607,6 +23847,9 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| }
|
| </style>
|
|
|
| + <url-param name="sort" value="{{_sortstr}}" default_value="id:asc">
|
| + </url-param>
|
| +
|
| <swarming-app client_id="[[client_id]]" auth_headers="{{_auth_headers}}" signed_in="{{_signed_in}}" busy="[[_busy]]" name="Swarming Bot List">
|
|
|
| <h2 hidden$="[[_signed_in]]">You must sign in to see anything useful.</h2>
|
| @@ -21615,15 +23858,15 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|
|
| <div class="horizontal layout">
|
|
|
| - <bot-filters primary_map="[[_primary_map]]" primary_arr="[[_primary_arr]]" columns="{{_columns}}" dimensions="{{_dimensions}}" filter="{{_filter}}" verbose="{{_verbose}}">
|
| + <bot-filters dimensions="[[_dimensions]]" primary_map="[[_primary_map]]" primary_arr="[[_primary_arr]]" columns="{{_columns}}" query_params="{{_query_params}}" filter="{{_filter}}" verbose="{{_verbose}}">
|
| </bot-filters>
|
|
|
| - <bot-list-summary fleet="[[_fleet]]" filtered_bots="[[_filteredSortedBots]]">
|
| + <bot-list-summary columns="[[_columns]]" fleet="[[_fleet]]" filtered_bots="[[_filteredSortedBots]]" sort="[[_sortstr]]" verbose="[[_verbose]]">
|
| </bot-list-summary>
|
|
|
| </div>
|
|
|
| - <bot-list-data auth_headers="[[_auth_headers]]" dimensions="[[_dimensions]]" bots="{{_bots}}" busy="{{_busy}}" fleet="{{_fleet}}" primary_map="{{_primary_map}}" primary_arr="{{_primary_arr}}">
|
| + <bot-list-data auth_headers="[[_auth_headers]]" query_params="[[_query_params]]" bots="{{_bots}}" busy="{{_busy}}" dimensions="{{_dimensions}}" fleet="{{_fleet}}" primary_map="{{_primary_map}}" primary_arr="{{_primary_arr}}">
|
| </bot-list-data>
|
|
|
| <table class="bot-list">
|
| @@ -21700,6 +23943,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| "android_devices": "Android Devices",
|
| "cores": "Cores",
|
| "cpu": "CPU",
|
| + "device": "Non-android Device",
|
| "device_os": "Device OS",
|
| "device_type": "Device Type",
|
| "disk_space": "Free Space (MB)",
|
| @@ -21707,11 +23951,13 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| "os": "OS",
|
| "pool": "Pool",
|
| "status": "Status",
|
| + "xcode_version": "XCode Version",
|
| };
|
|
|
| // This maps column name to a function that will return the content for a
|
| // given bot. These functions are bound to this element, and have access
|
| - // to all functions defined here and in bot-list-shared.
|
| + // to all functions defined here and in bot-list-shared. If a column
|
| + // is not listed here, a sane default will be used (see _column()).
|
| var columnMap = {
|
| android_devices: function(bot) {
|
| var devs = this._attribute(bot, "android_devices", "0");
|
| @@ -21721,38 +23967,19 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| // max() works on strings as long as they can be coerced to Number.
|
| return Math.max(...devs) + " devices available";
|
| },
|
| - cores: function(bot){
|
| - var cores = this._attribute(bot, "cores");
|
| - if (this._verbose){
|
| - return cores.join(" | ");
|
| - }
|
| - return cores[0];
|
| - },
|
| - cpu: function(bot){
|
| - var cpus = this._attribute(bot, "cpu");
|
| - if (this._verbose){
|
| - return cpus.join(" | ");
|
| - }
|
| - return cpus[0];
|
| - },
|
| - device_os: function(bot){
|
| - var os = this._attribute(bot, "device_os", "none");
|
| - if (this._verbose) {
|
| - return os.join(" | ");
|
| - }
|
| - return os[0];
|
| - },
|
| - device_type: function(bot){
|
| + device_type: function(bot) {
|
| var dt = this._attribute(bot, "device_type", "none");
|
| - if (this._verbose) {
|
| - return dt.join(" | ");
|
| + dt = dt[0];
|
| + var alias = this._androidAlias(dt);
|
| + if (alias === "unknown") {
|
| + return dt;
|
| }
|
| - return dt[0];
|
| + return this._applyAlias(dt, alias);
|
| },
|
| disk_space: function(bot) {
|
| var aliased = [];
|
| bot.disks.forEach(function(disk){
|
| - var alias = swarming.humanBytes(disk.mb, swarming.MB);
|
| + var alias = sk.human.bytes(disk.mb, swarming.MB);
|
| aliased.push(this._applyAlias(disk.mb, disk.id + " "+ alias));
|
| }.bind(this));
|
| if (this._verbose) {
|
| @@ -21788,13 +24015,6 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| id: function(bot) {
|
| return bot.bot_id;
|
| },
|
| - os: function(bot) {
|
| - var os = this._attribute(bot, "os");
|
| - if (this._verbose){
|
| - return os.join(" | ");
|
| - }
|
| - return os[0];
|
| - },
|
| pool: function(bot) {
|
| var pool = this._attribute(bot, "pool");
|
| return pool.join(" | ");
|
| @@ -21803,7 +24023,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| // If a bot is both dead and quarantined, show the deadness over the
|
| // quarentinedness.
|
| if (bot.is_dead) {
|
| - return "Dead. Last seen " + swarming.diffDate(bot.last_seen_ts) +
|
| + return "Dead. Last seen " + sk.human.diffDate(bot.last_seen_ts) +
|
| " ago";
|
| }
|
| if (bot.quarantined) {
|
| @@ -21840,7 +24060,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| // specialSort defines any custom sorting rules. By default, a
|
| // naturalCompare of the column content is done.
|
| var specialSort = {
|
| - device_type: function(dir, botA, botB) {
|
| + android_devices: function(dir, botA, botB) {
|
| // We sort on the number of attached devices. Note that this
|
| // may not be the same as android_devices, because _devices().length
|
| // counts all devices plugged into the bot, whereas android_devices
|
| @@ -21895,12 +24115,7 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| // _sort is an Object {name:String, direction:String}.
|
| _sort: {
|
| type: Object,
|
| - value: function() {
|
| - return {
|
| - name: "id",
|
| - direction: "asc",
|
| - };
|
| - }
|
| + computed: "_makeObject(_sortstr)",
|
| },
|
|
|
| _verbose: {
|
| @@ -21925,7 +24140,17 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
|
|
|
|
| _column: function(col, bot) {
|
| - return columnMap[col].bind(this)(bot);
|
| + var f = columnMap[col];
|
| + if (!f) {
|
| + f = function(bot) {
|
| + var c = this._attribute(bot, col, "none");
|
| + if (this._verbose) {
|
| + return c.join(" | ");
|
| + }
|
| + return c[0];
|
| + }
|
| + }
|
| + return f.bind(this)(bot);
|
| },
|
|
|
| _androidAliasDevice: function(device) {
|
| @@ -21964,13 +24189,28 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| },
|
|
|
| _header: function(col){
|
| - return headerMap[col];
|
| + return headerMap[col] || col;
|
| },
|
|
|
| _hide: function(col) {
|
| return this._columns.indexOf(col) === -1;
|
| },
|
|
|
| + _makeObject: function(sortstr){
|
| + if (!sortstr) {
|
| + return undefined;
|
| + }
|
| + var pieces = sortstr.split(":");
|
| + if (pieces.length != 2) {
|
| + // fail safe
|
| + return {name: "id", direction:"desc"};
|
| + }
|
| + return {
|
| + name: pieces[0],
|
| + direction: pieces[1],
|
| + }
|
| + },
|
| +
|
| _reRender: function(filter, sort) {
|
| this.$.bot_table.render();
|
| },
|
| @@ -22000,8 +24240,8 @@ is separate from validation, and `allowed-pattern` does not affect how the input
|
| if (!(e && e.detail && e.detail.name)) {
|
| return;
|
| }
|
| - // should trigger __filterAndSort
|
| - this.set("_sort", e.detail);
|
| + // should trigger the computation of _sort and __filterAndSort
|
| + this.set("_sortstr", e.detail.name +":"+e.detail.direction);
|
| },
|
|
|
| // _stripSpecial removes the special columns and sorts the remaining
|
|
|