This commit is contained in:
Eric Bidelman 2015-07-29 14:58:53 -07:00
Родитель 518b2d0031
Коммит e3de37b102
15 изменённых файлов: 1292 добавлений и 74 удалений

Просмотреть файл

@ -10,9 +10,10 @@ module.exports = function(grunt) {
// "polymer.html$"
// ]
// },
strip: true,
csp: true,
inline: true
stripComments: true,
inlineScripts: true,
inlineCss: true
// csp: true,
},
build: {
files: {

Просмотреть файл

@ -4,7 +4,7 @@
"devDependencies": {
"grunt": "*",
"grunt-appengine": "^0.1.5",
"grunt-vulcanize": "0.6.4",
"grunt-vulcanize": "^1.0.0",
"load-grunt-tasks": "*"
}
}

Просмотреть файл

@ -29,4 +29,4 @@ MEMCACHE_KEY_PREFIX = APP_VERSION # For memcache busting on new version
RSS_FEED_LIMIT = 15
VULCANIZE = True #PROD
VULCANIZE = False #PROD

Просмотреть файл

@ -19,15 +19,13 @@
"test",
"tests"
],
"dependencies": {
"polymer": "Polymer/polymer#^0.5.6",
"core-ajax": "Polymer/core-ajax#^0.5.6",
"google-apis": "GoogleWebComponents/google-apis#^0.4.4",
"paper-dialog": "Polymer/paper-dialog#^0.4.2",
"webcomponentsjs": "webcomponents/webcomponentsjs#^0.6.1"
"devDependencies": {
"iron-component-page": "PolymerElements/iron-component-page#^1.0.5"
},
"resolutions": {
"polymer": "0.5.6",
"core-component-page": "0.5.6"
"dependencies": {
"polymer": "Polymer/polymer#^1.0.8",
"iron-ajax": "PolymerElements/iron-ajax#^1.0.3",
"google-apis": "GoogleWebComponents/google-apis#^1.0.2",
"paper-dialog": "PolymerElements/paper-dialog#^1.0.1"
}
}

Просмотреть файл

@ -1,14 +1,53 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="chromedash-color-status" attributes="value max"><!-- width height corner">-->
<dom-module id="chromedash-color-status">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-color-status.css">
<!--<style>
span {
width: {{width}}px;
height: {{height}}px;
<span id="status"></span>
</template>
</dom-module>
<script>
Polymer({
is: 'chromedash-color-status',
properties: {
height: {
type: Number,
value: 10
},
max: {
type: Number,
value: 7,
notify: true,
observer: 'maxChanged'
},
value: {
notify: true,
observer: 'valueChanged'
},
width: {
type: Number,
value: 10
}
</style>-->
},
updateColor: function () {
var h = Math.round(120 - this.value * 120 / this.max);
this.$.status.style.backgroundColor = 'hsl(' + h + ', 100%, 50%)';
},
valueChanged: function () {
this.updateColor();
},
maxChanged: function () {
this.updateColor();
}
});
</script>
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="chromedash-color-status" attributes="value max">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-color-status.css">
<span id="status"></span>
</template>
<script>
@ -28,4 +67,4 @@
}
});
</script>
</polymer-element>
</polymer-element> -->

Просмотреть файл

@ -4,6 +4,312 @@
<!-- https://github.com/ljosa/urlize.js -->
<script src="urlize.js"></script>
<dom-module id="html-echo">
</dom-module>
<script>
Polymer({
is: 'html-echo',
properties: {
html: {
notify: true,
observer: 'htmlChanged'
}
},
htmlChanged: function () {
this.innerHTML = this.html;
}
});
</script>
<dom-module id="multi-links">
<template>
<link rel="stylesheet" href="../css/shared.css">
<template is="dom-repeat" items="{{links}}" as="link" index-as="i">
<a href="{{link}}" target="_blank">{{computeExpression1(link, prettyURLFilter)}}</a><template is="dom-if" if="{{computeIf(i, links)}}">,</template>
</template>
</template>
</dom-module>
<script>
Polymer({
is: 'multi-links',
properties: { links: { notify: true } },
prettyURLFilter: function (val) {
return val.replace(/https?:\/\//, '');
},
computeIf: function (i, links) {
return links.length > 1 && i < links.length - 1;
},
computeExpression1: function (link, prettyURLFilter) {
return link | prettyURLFilter;
}
});
</script>
<dom-module id="chromedash-feature">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-feature.css">
<div on-tap="{{toggle}}">
<div class="topcorner pull-right">
<span hidden$="{{computeWarningHidden(deprecated, removed)}}" class="tooltip" title="{{computeTitle(removed)}}">
<i class="icon-warning-sign" data-tooltip></i>
</span>
<span hidden$="{{!needsflag}}" class="tooltip" title="Experimental feature behind a flag.">
<i class="icon-beaker" data-tooltip></i>
</span>
<a href="{{computeStandaloneHref(feature)}}" target="_blank" class="tooltip" title="View on standalone page.">
<i class="icon-external-link-sign" data-tooltip></i>
</a>
<content select=".edit"></content>
</div>
<hgroup>
<chromedash-color-status class="tooltip corner" title="Compatibility risk: perceived interest from browser vendors and web developers" value="{{compatRisk}}" max="{{MAX_RISK}}"></chromedash-color-status>
<h2>{{feature.name}}</h2>
<label class="category" on-tap="{{categoryFilter}}">{{feature.category}}</label>
</hgroup>
<section class="desc"><summary class="{{computeSummaryClass(open)}}">{{feature.summary}}</summary></section>
<template is="dom-if" if="{{open}}">
<section>
<h3>Implementation Status</h3>
<div class="impl_status">
<template is="dom-if" if="{{feature.shipped_milestone}}">
<span class="chrome_desktop tooltip" title="Chrome desktop"><label>{{feature.impl_status_chrome}}</label>{{feature.shipped_milestone}}</span>
</template>
<template is="dom-if" if="{{!feature.shipped_milestone}}">
<span class="chrome_desktop"><label>{{feature.impl_status_chrome}}</label></span>
</template>
<template is="dom-if" if="{{feature.shipped_android_milestone}}">
<span class="chrome_android tooltip" title="Chrome for Android"><label><i class="icon-android"></i></label>{{feature.shipped_android_milestone}}</span>
</template>
<template is="dom-if" if="{{feature.shipped_webview_milestone}}">
<span class="chrome_webview tooltip" title="Chrome Webview"><label>Webview</label>{{feature.shipped_webview_milestone}}</span>
</template>
<span hidden$="{{!feature.prefixed}}"><label>Prefixed</label>Yes</span>
<span hidden$="{{!feature.bug_url}}"><a href="{{feature.bug_url}}" target="_blank">Launch bug</a></span>
<span class="owner" hidden$="{{!feature.owner.length}}">
<label>Owner(s)</label><template is="dom-repeat" items="{{feature.owner}}" as="owner" index-as="i">
<span on-tap="{{ownerFilter}}">{{owner}}</span><template is="dom-if" if="{{computeShowComma(feature, i)}}">,</template>
</template>
</span>
</div>
</section>
<section>
<h3>Consensus &amp; Standardization</h3>
<div class="views">
<template is="dom-if" if="{{feature.shipped_opera_milestone}}">
<span title="{{feature.impl_status_chrome}}">
Opera <span>{{feature.shipped_opera_milestone}}</span>
</span>
</template>
<template is="dom-if" if="{{feature.shipped_opera_android_milestone}}">
<span title="{{feature.impl_status_chrome}}">
Opera for Android <span>{{feature.shipped_opera_android_milestone}}</span>
</span>
</template>
<span title="{{feature.ff_views.text}}">
<chromedash-color-status value="{{feature.ff_views.value}}" max="{{MAX_VENDOR_VIEW}}"></chromedash-color-status>
<template is="dom-if" if="{{feature.ff_views_link}}">
<a href="{{feature.ff_views_link}}" target="_blank">Firefox</a>
</template>
<label hidden$="{{feature.ff_views_link}}">Firefox</label>
</span>
<span title="{{feature.ie_views.text}}">
<chromedash-color-status value="{{feature.ie_views.value}}" max="{{MAX_VENDOR_VIEW}}"></chromedash-color-status>
<template is="dom-if" if="{{feature.ie_views_link}}">
<a href="{{feature.ie_views_link}}" target="_blank">IE</a>
</template>
<label hidden$="{{feature.ie_views_link}}">IE</label>
</span>
<span title="{{feature.safari_views.text}}">
<chromedash-color-status value="{{feature.safari_views.value}}" max="{{MAX_VENDOR_VIEW}}"></chromedash-color-status>
<template is="dom-if" if="{{feature.safari_views_link}}">
<a href="{{feature.safari_views_link}}" target="_blank">Safari</a>
</template>
<label hidden$="{{feature.safari_views_link}}">Safari</label>
</span>
<span title="{{feature.web_dev_views.text}}">
<chromedash-color-status value="{{feature.web_dev_views.value}}" max="{{MAX_WEBDEV_VIEW}}"></chromedash-color-status>
<label>Web developers</label>
</span>
</div>
<div class="standardization">
<span>
<chromedash-color-status value="{{feature.standardization.value}}" max="{{MAX_STANDARDS_VAL}}"></chromedash-color-status>
<template is="dom-if" if="{{feature.spec_link}}">
<a href="{{feature.spec_link}}" target="_blank">{{feature.standardization.text}}</a>
</template>
<span hidden$="{{feature.spec_link}}">{{feature.standardization.text}}</span>
</span>
</div>
</section>
<template is="dom-if" if="{{computeShowResources(feature)}}">
<section>
<h3>Developer resources</h3>
<div>
<template is="dom-if" if="{{computeShowDocLinks(feature)}}">
<span class="doc_links" hidden$="{{!feature.doc_links.length}}">
<label>Documentation</label>
<multi-links links="[[feature.doc_links]]"></multi-links>
</span>
</template>
</div>
<div>
<template is="dom-if" if="{{computeShowDocLinks(feature)}}">
<span class="sample_links" hidden$="{{!feature.sample_links.length}}">
<label>Samples</label>
<multi-links links="[[feature.sample_links]]"></multi-links>
</span>
</template>
</div>
</section>
</template>
<template is="dom-if" if="{{feature.comments}}">
<section>
<h3>Comments</h3>
<summary class="comments"><html-echo html="{{computeHtml(feature)}}"></html-echo></summary>
</section>
</template>
</template>
</div>
</template>
</dom-module>
<script>
Polymer({
is: 'chromedash-feature',
extends: 'li',
hostAttributes: {
tabindex: '0'
},
properties: {
MAX_RISK: {
type: Number,
computed: 'computeMaxRisk(MAX_VENDOR_VIEW, MAX_WEBDEV_VIEW, MAX_STANDARDS_VAL)'
},
MAX_STANDARDS_VAL: {
type: Number,
value: 6,
readOnly: true
},
MAX_VENDOR_VIEW: {
type: Number,
value: 7,
readOnly: true
},
MAX_WEBDEV_VIEW: {
type: Number,
value: 6,
readOnly: true
},
compatRisk: {
type: Number,
value: 0
},
deprecated: {
type: Boolean,
value: false
},
feature: {
value: null,
notify: true,
observer: 'featureChanged'
},
needsflag: {
type: Boolean,
value: false,
notify: true
},
open: {
type: Boolean,
value: false,
observer: 'openChanged'
}
},
computeMaxRisk: function(vendorViewVal, webdevViewVal, standardsVal) {
return vendorViewVal + webdevViewVal + standardsVal;
},
openChanged: function() {
this.open ? this.classList.add('open') : this.classList.remove('open');
this.fire('feature-toggled', {
feature: this.feature,
open: this.open
});
},
toggle: function(e, details, sender) {
// Don't toggle panel if tooltip or link is being clicked.
if (e.target.classList.contains('tooltip') || 'tooltip' in e.target.dataset || e.target.tagName == 'A' || e.target.tagName == 'MULTI-LINKS') {
return;
}
this.open = !this.open;
},
calculateCompatRisk: function() {
var vendors = (this.feature.ff_views.value + this.feature.ie_views.value + this.feature.safari_views.value) / 3;
var webdevs = this.feature.web_dev_views.value;
var standards = this.feature.standardization.value;
this.compatRisk = vendors + webdevs + standards;
},
featureChanged: function() {
this.calculateCompatRisk(); // TODO: these values are brittle if the strings change.
// TODO: these values are brittle if the strings change.
this.removed = this.feature.impl_status_chrome == 'Removed';
this.deprecated = this.feature.impl_status_chrome == 'Deprecated' || this.feature.impl_status_chrome == 'No longer pursuing';
},
categoryFilter: function(e, details, sender) {
e.stopPropagation();
this.fire('filter-category', {val: sender.textContent});
},
ownerFilter: function(e, details, sender) {
e.stopPropagation();
this.fire('filter-owner', {val: sender.textContent});
},
_urlizeFilter: function(val) {
return urlize(val, {target: '_blank', trim: 'www', autoescape: true});
},
computeDeprecatedHidden: function(deprecated, removed) {
return !removed && !deprecated;
},
computeTitle: function(removed) {
return removed ? 'Removed feature' : 'Deprecated feature';
},
computeStandaloneHref: function(feature) {
return '/feature/' + feature.id;
},
computeSummaryClass: function(open) {
return open ? 'open' : '';
},
computeShowResources: function(feature) {
return feature.doc_links.length || feature.sample_links.length;
},
computeShowComma: function(feature, i) {
return feature.owner.length > 1 && i < feature.owner.length - 1;
},
computeShowDocLinks: function(feature) {
return feature.doc_links && feature.doc_links.length > 0;
},
computeSampleLinks: function(feature) {
return feature.sample_links && feature.sample_links.length > 0;
},
computeHtml: function(feature) {
return this._urlizeFilter(feature.comments);
}
});
</script>
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="chromedash-color-status.html">
<script src="urlize.js"></script>
<polymer-element name="html-echo" attributes="html">
<script>
@ -217,3 +523,4 @@
</script>
</polymer-element>
-->

Просмотреть файл

@ -1,6 +1,280 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="chromedash-feature.html">
<dom-module id="chromedash-featurelist">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-featurelist.css">
<ul>
<template is="dom-repeat" items="{{filtered}}" as="feature">
<div hidden$="{{computeMilestoneHidden(feature, features, filtered)}}" class="milestone-marker">{{feature.meta.milestone_str}}</div>
<li id="{{feature.id}}" is="chromedash-feature" feature="{{feature}}" needsflag="{{feature.meta.needsflag}}">
<a href="{{computeHref(feature)}}" class="edit" hidden$="{{computeEditLinkHidden(whitelisted)}}">edit</a>
</li>
</template>
</ul>
</template>
</dom-module>
<script>
(function () {
function onMetadataChanged_(e) {
this.scrollToMilestone(e.detail.version);
}
function matchesMilestone_(milestone, operator, version) {
switch (operator) {
case '<':
return milestone < version;
break;
case '<=':
return milestone <= version;
break;
case '>':
return milestone > version;
break;
case '>=':
return milestone >= version;
break;
case '=': // Support both '=' and '=='.
// Support both '=' and '=='.
case '==':
return milestone == version;
break;
default:
return false;
}
}
var featureLiList_ = [];
Polymer({
is: 'chromedash-featurelist',
properties: {
features: {
notify: true,
observer: 'featuresChanged'
},
filtered: { observer: 'filteredChanged' },
metadata: {
value: null,
observer: 'metadataChanged'
},
search: { value: null },
whitelisted: {
type: Boolean,
value: false,
notify: true
}
},
created: function () {
this.features = this.features || [];
this.filtered = this.filtered || [];
},
onKeyUp: function (e, detail, sender) {
var focusedEl = this.shadowRoot.querySelector(':focus');
if (focusedEl) {
switch (e.keyCode) {
case 13:
// enter
//focusedEl.toggle();
focusedEl.open = !focusedEl.open;
break;
default: // noop
}
}
},
// noop
onScrollList: function (e, detail, sender) {
var feature = this.featureInView(sender.scrollTop);
this.metadata.selectMilestone(feature);
},
onFeatureToggled: function (e, detail, sender) {
var feature = detail.feature;
var open = detail.open;
if (history && history.replaceState) {
if (open) {
history.pushState({ id: feature.id }, feature.name, '/features/' + feature.id);
} else {
var hash = this.search.value ? '#' + this.search.value : '';
history.replaceState({ id: null }, feature.name, '/features' + hash);
}
}
},
metadataChanged: function (_, oldVal) {
// TODO: probably need to remove the listener if metadata element changes.
//this.metadata.removeEventListener('milestoneselect', onMetadataChanged_.bind(this));
this.metadata.addEventListener('milestoneselect', onMetadataChanged_.bind(this));
},
featuresChanged: function () {
if (!this.features || !this.features.length) {
return;
}
this.filter(location.hash.substr(1));
document.body.classList.add('ready'); // unmask app.
// unmask app.
this.async(function () {
// If there's an id in the URL, highlight and scroll to the feature.
// Otherwise, go to the first "in development" feature.
// TODO: really want this in ready(), but featureLiList and metadata may
// not be set yet due to timing issues.
var lastSlash = location.pathname.lastIndexOf('/');
if (lastSlash > 0) {
var id = parseInt(location.pathname.substring(lastSlash + 1));
this.scrollToFeature(id);
} else {
this.scrollToMilestone(this.metadata.implStatuses[this.metadata.STATUS.IN_DEVELOPMENT - 1].val);
}
});
},
filterOnCategory: function (val) {
var regex = new RegExp(val, 'i');
this.filtered = this.features.filter(function (feature, idx, array) {
return regex.test(feature.category);
});
return this.filtered.length;
},
filterOnOwner: function (val) {
var regex = new RegExp(val, 'i');
this.filtered = this.features.filter(function (feature, idx, array) {
return regex.test(feature.owner.toString());
});
return this.filtered.length;
},
filteredChanged: function () {
// Wait one rAF do model has updated the DOM.
this.async(function () {
featureLiList_ = this.shadowRoot.querySelectorAll('li');
});
},
filter: function (val) {
// Clear filter if there's no search or if called directly.
if (!val) {
if (history && history.replaceState) {
history.replaceState('', document.title, location.pathname + location.search);
} else {
location.hash = '';
}
this.filtered = this.features;
} else {
val = val.trim();
if (history && history.replaceState) {
history.replaceState({ id: null }, document.title, '/features#' + val);
} // owner: user@chromium.org
// owner: user@chromium.org
var match = val.match(/^owner:\s*(.*)/);
if (match) {
return this.filterOnOwner(match[1]);
} // category: Multimedia
// category: Multimedia
var match = val.match(/^category:\s*(.*)/);
if (match) {
return this.filterOnCategory(match[1]);
} // Returns operator and version query e.g. ["<=25", "<=", "25"].
// Returns operator and version query e.g. ["<=25", "<=", "25"].
var result = /^([<>=]=?)\s*?([0-9]+)/.exec(val);
if (result) {
var operator = result[1];
var version = parseInt(result[2]);
this.filtered = this.features.filter(function (feature, idx, array) {
var platformMilestones = [
parseInt(feature.shipped_milestone),
parseInt(feature.shipped_android_milestone),
parseInt(feature.shipped_ios_milestone),
parseInt(feature.shipped_webview_milestone)
];
for (var i = 0, milestone; milestone = platformMilestones[i]; ++i) {
if (matchesMilestone_(milestone, operator, version)) {
return true; // Only one of the platforms needs to match.
}
}
});
} else {
// Only one of the platforms needs to match.
// Do a general search over misc fields.
// Remove "=" prefix so "in development"/"proposed" version queries
// return results.
if (val.indexOf('=') == 0) {
val = val.substring(1);
}
var regex = new RegExp(val, 'i');
this.filtered = this.features.filter(function (feature, idx, array) {
return regex.test(feature.name) || regex.test(feature.category) || regex.test(feature.summary) || regex.test(feature.search_tags) || /*regex.test(feature.shipped_milestone) ||*/
regex.test(feature.impl_status_chrome);
});
}
}
this.filteredChanged();
return this.filtered.length;
},
// Returns the closest feature <li> to the scroll top position passed in.
featureInView: function (containerScrollTop) {
var closest = null;
for (var i = 0, li; li = featureLiList_[i]; ++i) {
var dist = li.offsetTop - containerScrollTop;
if (dist < 0) {
dist = -dist * 1.5;
}
if (closest == null || dist < closest.dist) {
//closest = this.features[i];
closest = this.filtered[i];
closest.dist = dist;
}
}
return closest;
},
// Returns the index of the first feature of a given milestone string.
firstOfMilestone: function (milestone, opt_startFrom) {
var start = opt_startFrom != undefined ? opt_startFrom : 0;
for (var i = start, feature; feature = this.filtered[i]; ++i) {
if (feature.first_of_milestone && [
String(feature.shipped_milestone),
feature.impl_status_chrome
].indexOf(String(milestone)) != -1) {
return i;
}
}
return -1;
},
scrollToMilestone: function (milestone) {
var idx = this.firstOfMilestone(milestone);
if (idx != -1) {
var el = featureLiList_[idx].previousElementSibling || featureLiList_[idx];
// TODO(ericbidelman): Implement smooth scrolling when
// https://twitter.com/ebidel/status/364825118171602944 lands.
el.scrollIntoView(true); // el.scrollIntoView(true, {behavior: 'smooth'});
}
},
// el.scrollIntoView(true, {behavior: 'smooth'});
scrollToFeature: function (id) {
if (!id) {
return;
}
for (var i = 0, f; f = this.filtered[i]; ++i) {
if (f.id == id) {
featureLiList_[i].scrollIntoView(true);
featureLiList_[i].open = true;
break;
}
}
},
listeners: {
'scroll': 'onScrollList',
'keyup': 'onKeyUp',
'feature-toggled': 'onFeatureToggled'
},
computeMilestoneHidden: function (feature, features, filtered) {
return filtered.length != features.length || !feature.first_of_milestone;
},
computeEditLinkHidden: function (feature) {
return '/admin/features/edit/' + feature.id;
},
computeHidden: function (whitelisted) {
return !whitelisted;
}
});
}());
</script>
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="chromedash-feature.html">
<polymer-element name="chromedash-featurelist" attributes="whitelisted features" on-scroll="{{onScrollList}}" on-keyup="{{onKeyUp}}" on-feature-toggled="{{onFeatureToggled}}">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-featurelist.css">
@ -261,3 +535,4 @@
})();
</script>
</polymer-element>
-->

Просмотреть файл

@ -1,5 +1,84 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="../bower_components/neon-animation/animations/scale-up-animation.html">
<link rel="import" href="../bower_components/neon-animation/animations/fade-out-animation.html">
<dom-module id="chromedash-legend">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-legend.css">
<paper-dialog id="overlay" opened="{{opened}}" entry-animation="scale-up-animation" exit-animation="fade-out-animation" with-backdrop>
<div class="dialog">
<h3>About the data</h3>
<section>
<content class="description"></content>
</section>
<h3>Color legend</h3>
<p>Colors indicate the "compatibility risk" for a given feature. The risk
increases as <chromedash-color-status value="1" max="{{views.vendors.length}}"></chromedash-color-status><chromedash-color-status value="{{views.vendors.length}}" max="{{views.vendors.length}}"></chromedash-color-status>, and the color meaning differs for browser
vendors, web developers, and the standards process.</p>
<section class="views">
<div>
<label>Browser vendors</label>
<ul>
<template is="dom-repeat" items="{{views.vendors}}" as="o">
<li><chromedash-color-status value="{{o.key}}" max="{{views.vendors.length}}"></chromedash-color-status>{{o.val}}</li>
</template>
</ul>
</div>
<div>
<label>Web developer</label>
<ul>
<template is="dom-repeat" items="{{views.webdevs}}" as="o">
<li><chromedash-color-status value="{{o.key}}" max="{{views.webdevs.length}}"></chromedash-color-status>{{o.val}}</li>
</template>
</ul>
</div>
<div>
<label>Standards values</label>
<ul>
<template is="dom-repeat" items="{{views.standards}}" as="o">
<li><chromedash-color-status value="{{o.key}}" max="{{views.standards.length}}"></chromedash-color-status>{{o.val}}</li>
</template>
</ul>
</div>
</section>
<h3>Search</h3>
<section>
<label>Example search queries</label>
<ul class="queries">
<li><span>"&lt;30"</span>features that landed before 30</li>
<li><span>"&lt;=30"</span>features in 30</li>
<li><span>"=30"</span>features that landed in 30</li>
<li><span>"&gt;28"</span>features since 28
</li><li><span>"behind a flag"</span>all experimental features
<!-- <li><span>"category: CSS"</span>features in the category CSS</li> -->
</li><li><span>"owner: user@example.org"</span>features owned by user@example.org</li>
</ul>
</section>
<content></content>
</div>
</paper-dialog>
</template>
</dom-module>
<script>
Polymer({
is: 'chromedash-legend',
properties: {
opened: {
type: Boolean,
value: false,
notify: true
}
},
toggle: function () {
this.$.overlay.toggle();
}
});
</script>
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/paper-dialog/paper-dialog.html">
<link rel="import" href="../bower_components/paper-dialog/paper-dialog-transition.html">
<polymer-element name="chromedash-legend" attributes="opened">
@ -50,7 +129,6 @@
<li><span>"=30"</span>features that landed in 30</li>
<li><span>">28"</span>features since 28
<li><span>"behind a flag"</span>all experimental features
<!-- <li><span>"category: CSS"</span>features in the category CSS</li> -->
<li><span>"owner: user@example.org"</span>features owned by user@example.org</li>
</ul>
</section>
@ -67,3 +145,4 @@
});
</script>
</polymer-element>
-->

Просмотреть файл

@ -1,4 +1,103 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
<dom-module id="chromedash-metadata">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-metadata.css">
<iron-ajax auto="" url="https://omahaproxy.appspot.com/all.json" handle-as="json" on-core-response="{{onResponse}}"></iron-ajax>
<ul class="{{computeClass(betaIsDev, canaryIsDev, tokenList)}}">
<template is="dom-repeat" items="{{versions}}" as="v">
<li data-version="{{v}}" on-tap="{{selectMilestone}}" selected$="{{computeSelected(selected, v)}}">{{v}}</li>
</template>
</ul>
<div hidden$="{{computeHidden(fetchError)}}" class="error">Error fetching Chrome version information.</div>
</template>
</dom-module>
<script>
Polymer({
is: 'chromedash-metadata',
properties: {
betaIsDev: {
type: Boolean,
value: false
},
canaryIsDev: {
type: Boolean,
value: false
},
fetchError: {
type: Boolean,
value: false
},
selected: { value: null }
},
created: function () {
this.implStatuses = this.implStatuses || {};
this.STATUS = this.STATUS || {};
this.channels = this.channels || {};
this.versions = this.versions || [];
},
onResponse: function (e, detail, sender) {
if (detail.response.type == 'error') {
this.fetchError = true;
return;
}
// TODO(ericbidelman): Share this data across instances.
var windowsVersions = detail.response[0];
for (var i = 0, el; el = windowsVersions.versions[i]; ++i) {
// Include previous version if current is foobar'd.
this.channels[el.channel] = parseInt(el.version) || parseInt(el.prev_version);
}
// Dev channel explicitly left out. Treat same as canary.
this.versions = [
this.implStatuses[this.STATUS.NO_ACTIVE_DEV - 1].val,
this.implStatuses[this.STATUS.PROPOSED - 1].val,
this.implStatuses[this.STATUS.IN_DEVELOPMENT - 1].val,
this.channels.canary,
this.channels.dev,
this.channels.beta,
this.channels.stable
];
// Consolidate channels if they're the same.
if (this.channels.canary == this.channels.dev) {
this.versions.splice(4, 1);
this.canaryIsDev = true;
} else if (this.channels.dev == this.channels.beta) {
this.versions.splice(5, 1);
this.betaIsDev = true;
}
for (var i = this.channels.stable - 1; i >= 1; i--) {
this.versions.push(i);
}
this.versions.push(this.implStatuses[this.implStatuses.length - 1].val);
},
selectMilestone: function (e, details, sender) {
if (details) {
// Came from an internal click.
this.selected = sender.dataset.version;
this.fire('milestoneselect', { version: this.selected });
} else {
// Called directly (from outside). e is a feature.
this.selected = e.meta.milestone_str;
}
},
computeClass: function (betaIsDev, canaryIsDev, tokenList) {
return {
canaryisdev: canaryIsDev,
betaisdev: betaIsDev
} | tokenList;
},
computeHidden: function (fetchError) {
return !fetchError;
},
computeSelected: function (selected, v) {
return v == selected;
}
});
</script>
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-ajax/core-ajax.html">
<polymer-element name="chromedash-metadata">
@ -76,3 +175,4 @@
});
</script>
</polymer-element>
-->

Просмотреть файл

@ -1,6 +1,133 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
<link rel="import" href="x-meter.html">
<dom-module id="chromedash-metrics">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-metrics.css">
<iron-ajax auto="" url="{{endpoint}}" last-response="{{props}}" handle-as="json"></iron-ajax>
<b>Showing <span>{{props.length}}</span> properties</b>
<ol id="stack-rank-list">
<li class="header">
<label on-tap="{{sort}}" data-order="property_name">Name <i class="{{computePropertyNameClass(sortOrders, tokenList)}}"></i></label>
<label on-tap="{{sort}}" data-order="percentage" class="flex2">Percentage <i class="{{computePercentClass(sortOrders, tokenList)}}"></i></label>
</li>
<template is="dom-if" if="{{computeIf(props)}}">
<li class="loading">Loading...</li>
</template>
<template is="dom-repeat" items="{{viewList}}" as="prop">
<li id="{{prop.property_name}}" hidden$="{{computeHidden(prop)}}" tabindex="0">
<label><a href="{{computeHref(prop)}}" title="Deep link to this property" alt="Deep link to this property">{{prop.property_name}}</a></label>
<x-meter value="{{prop.percentage}}" on-tap="{{showTimeline}}" title="Click to see a timeline view of this property"></x-meter>
</li>
</template>
</ol>
</template>
</dom-module>
<script>
(function () {
function sortBy_(prop, arr, opt_compareAsNumbers) {
var compareAsNumbers = opt_compareAsNumbers || false;
arr.sort(function (a, b) {
var propA = compareAsNumbers ? Number(a[prop]) : a[prop];
var propB = compareAsNumbers ? Number(b[prop]) : b[prop];
if (propA > propB) {
return 1;
}
if (propA < propB) {
return -1;
}
return 0;
});
}
Polymer({
is: 'chromedash-metrics',
properties: {
props: { observer: 'propsChanged' },
sortOrders: {
type: Object,
value: function () {
return {
property_name: {
reverse: false,
activated: false
},
percentage: {
reverse: true,
activated: true
}
};
}
},
type: {
type: String,
value: '',
notify: true
},
view: { notify: true }
},
created: function () {
this.props = [];
this.viewList = [];
},
propsChanged: function () {
if (!this.props || !this.props.length) {
return;
}
for (var i = 0, prop; prop = this.props[i]; ++i) {
prop.percentage = (prop.day_percentage * 100).toFixed(4);
}
this.viewList = this.props;
if (location.hash) {
this.async(function () {
this.scrollToProperty(location.hash.split('#')[1]);
});
}
},
get endpoint() {
return '/data/' + this.type + this.view;
},
sort: function (e, detail, sender) {
e.preventDefault();
var order = sender.dataset.order;
sortBy_(order, this.viewList, order == 'percentage');
this.sortOrders[order].activated = true;
this.sortOrders[order].reverse = !this.sortOrders[order].reverse;
if (this.sortOrders[order].reverse) {
this.viewList.reverse();
}
},
showTimeline: function (e, detail, sender) {
window.location.href = '/metrics/' + this.type + '/timeline/' + this.view + '/' + e.target.templateInstance.model.prop.bucket_id;
},
scrollToProperty: function (prop) {
if (prop) {
var el = this.$['stack-rank-list'].querySelector('#' + prop);
el.scrollIntoView(true); //el.scrollIntoView(true, {behavior: 'smooth'});
}
},
computePropertyNameClass: function (sortOrders, tokenList) {
return 'icon-long-arrow-' + (sortOrders.property_name.reverse ? 'up' : 'down') + ' ' + ({ activated: sortOrders.property_name.activated } | tokenList);
},
computePercentClass: function (sortOrders, tokenList) {
return 'icon-long-arrow-' + (sortOrders.percentage.reverse ? 'down' : 'up') + ' ' + ({ activated: sortOrders.percentage.activated } | tokenList);
},
computeIf: function (props) {
return !props.length;
},
computeHidden: function (prop) {
return prop.property_name == 'ERROR';
},
computeHref: function (prop) {
return '#' + prop.property_name;
}
});
}());
</script>
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-ajax/core-ajax.html">
<!-- <link rel="import" href="chromedash-color-status.html"> -->
<link rel="import" href="x-meter.html">
<polymer-element name="chromedash-metrics" attributes="type view">
@ -103,3 +230,4 @@
})();
</script>
</polymer-element>
-->

Просмотреть файл

@ -1,4 +1,132 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-ajax/iron-ajax.html">
<link rel="import" href="../bower_components/google-apis/google-js-api.html">
<dom-module id="chromedash-feature-timeline">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-timeline.css">
<google-js-api on-api-load="{{chartAPILoaded}}"></google-js-api>
<select value="{{selectedBucketId::input}}">
<option disabled value="1">Select a property</option>
<option template repeat="{{computeRepeat(bucket, props)}}" value="{{bucket[0]}}">{{bucket[1]}}</option>
</select>
<div id="chart"></div>
</template>
</dom-module>
<script>
Polymer({
is: 'chromedash-feature-timeline',
properties: {
selectedBucketId: {
type: Number,
value: 1,
observer: 'selectedBucketIdChanged'
},
timeline: {
notify: true
},
title: {
type: String,
value: ''
},
type: {
type: String,
value: '',
notify: true
},
view: {
type: String,
value: '',
notify: true
},
props: {
type: Array,
value: function() { return []; }
}
},
selectedBucketIdChanged: function () {
var ajax = document.createElement('iron-ajax');
ajax.url = '/data/timeline/' + this.type + this.view;
ajax.params = {bucket_id: this.selectedBucketId};
ajax.handleAs = 'json';
ajax.addEventListener('response', function(e) {
this.drawVisualization(e.detail.response, this.selectedBucketId);
}.bind(this));
ajax.generateRequest();
if (history.pushState) {
history.pushState({ id: this.selectedBucketId }, '', '/metrics/' + this.type + '/timeline/' + this.view + '/' + this.selectedBucketId);
}
},
chartAPILoaded: function (e, detail, sender) {
google.load('visualization', '1.0', {
packages: ['corechart'],
callback: function () {
// If there's an id in the URL, load the property it.
var lastSlash = location.pathname.lastIndexOf('/');
if (lastSlash > 0) {
var id = parseInt(location.pathname.substring(lastSlash + 1));
if (String(id) != 'NaN') {
this.selectedBucketId = id;
}
}
}.bind(this)
});
},
drawVisualization: function (data, bucketId) {
var table = new google.visualization.DataTable();
table.addColumn('date', 'Date');
table.addColumn('number', 'Percentage');
var rowArray = [];
for (var i = 0, item; item = data[i]; ++i) {
var dateStr = item.date.split('-');
var date = new Date();
date.setFullYear(parseInt(dateStr[0]), parseInt(dateStr[1]) - 1, parseInt(dateStr[2]));
rowArray.push([
date,
parseFloat((item.day_percentage * 100).toFixed(4))
]);
}
table.addRows(rowArray);
var options = {
title: this.title,
legend: { position: 'none' },
vAxis: {
title: 'Percentage',
// maxValue: 100,
minValue: 0
},
hAxis: {
title: 'Date',
format: 'yyyy-MM-dd'
},
width: '100%',
height: '100%',
// chartArea: {width: '75%'},
pointSize: 5
}; // var dataView = new google.visualization.DataView(table);
// dataView.setColumns([{calc: function(data, row) {
// return data.getFormattedValue(row, 0);
// }, type:'string'}, 1]);
// var dataView = new google.visualization.DataView(table);
// dataView.setColumns([{calc: function(data, row) {
// return data.getFormattedValue(row, 0);
// }, type:'string'}, 1]);
var chart = new google.visualization.LineChart(this.$.chart);
chart.draw(table, options);
},
computeRepeat: function (bucket, props) {
return bucket in props;
}
});
</script>
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-ajax/core-ajax.html">
<link rel="import" href="../bower_components/google-apis/google-jsapi.html">
@ -10,9 +138,7 @@
<option disabled value="1">Select a property</option>
<option template repeat="{{bucket in props}}" value="{{bucket[0]}}">{{bucket[1]}}</option>
</select>
<!-- <div id="chart-container" style="width:900px;margin: 0 auto;"> -->
<div id="chart"></div>
<!-- </div> -->
<div id="chart"></div>
</template>
<script>
Polymer({
@ -104,4 +230,4 @@
}
});
</script>
</polymer-element>
</polymer-element> -->

Просмотреть файл

@ -1,5 +1,103 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-ajax/core-xhr.html">
<link rel="import" href="../bower_components/iron-ajax/iron-request.html">
<dom-module id="chromedash-userlist">
<template>
<link rel="stylesheet" href="../css/elements/chromedash-userlist.css">
<form id="form" name="user_form" method="post" action="{{action}}" onsubmit="return false;">
<table>
<tr>
<td><input type="email" placeholder="Email address" name="email" id="id_email" required></td>
<td><input type="submit" on-tap="{{ajaxSubmit}}"></td>
</tr>
</table>
</form>
<hr>
<ul id="user-list">
<template is="dom-repeat" items="{{users}}" as="user">
<li>
<a href="{{computeDeleteHref(action, user)}}" on-tap="{{ajaxDelete}}">delete</a><span>{{user.email}}</span>
</li>
</template>
</ul>
</template>
</dom-module>
<script>
Polymer({
is: 'chromedash-userlist',
properties: {
action: {
type: String
},
users: {
type: Array,
value: function () { return []; },
notify: true
}
},
computeDeleteHref: function(action, user) {
return action + '/' + user.id;
},
addUser: function (user) {
this.users.splice(0, 0, user);
},
removeUser: function (idx) {
return this.users.splice(idx, 1);
},
ajaxSubmit: function (e, details, sender) {
e.preventDefault();
if (this.$.form.checkValidity()) {
// TODO(ericbidelman): move back to this.$.form.email.value when SD
// polyfill merges the commit that wraps element.form.
var email = this.$.form.querySelector('input[name="email"]').value;
var formData = new FormData();
formData.append('email', email);
var xhr = document.createElement('iron-request');
xhr.send({
url: this.action,
method: 'POST',
body: formData
}).then(function (request) {
if (request.status == 201) {
this.addUser(JSON.parse(response));
this.$.form.reset();
} else if (request.status == 200) {
alert('Thanks. But that user already exists');
}
}.bind(this));
}
},
ajaxDelete: function (e, details, sender) {
e.preventDefault();
if (!confirm('Remove user?')) {
return;
} // Get index of user model instance that was clicked from template.
// Get index of user model instance that was clicked from template.
var user = e.target.templateInstance.model.user;
var idx = this.users.indexOf(user);
var xhr = document.createElement('iron-request');
xhr.request({
url: sender.href,
method: 'POST'
}).then(function (request) {
e.target.parentElement.classList.add('faded');
if ('ontransitionend' in window) {
var li = sender.parentElement;
li.addEventListener('transitionend', function (e) {
this.removeUser(idx);
}.bind(this));
li.classList.add('faded');
} else {
this.removeUser(idx);
}
}.bind(this));
}
});
</script>
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-ajax/iron-request.html">
<polymer-element name="chromedash-userlist" attributes="users action">
<template>
@ -7,25 +105,32 @@
<form id="form" name="user_form" method="post" action="{{action}}" onsubmit="return false;">
<table>
<tr>
<td><input type="email" placeholder="Email address" name="email" id="id_email" required /></td>
<td><input type="email" placeholder="Email address" name="email" id="id_email" required></td>
<td><input type="submit" on-tap="{{ajaxSubmit}}"></td>
</tr>
</tr>
</table>
</form>
<hr>
<ul id="user-list">
<template repeat="{{user in users}}">
<li><a href="{{action}}/{{user.id}}" on-tap="{{ajaxDelete}}">delete</a>{{user.email}}</li>
<li>
<a href="{{action}}/{{user.id}}" on-tap="{{ajaxDelete}}">delete</a><span>{{user.email}}</span>
</li>
</template>
</ul>
</template>
<script>
Polymer({
created: function() {
this.users = this.users || [];
},
test: function(e, details, sender) {
e.preventDefault();
is: 'chromedash-userlist',
properties: {
action: {
type: String
},
users: {
type: Array,
value: function() { return []; },
notify: true
}
},
addUser: function(user) {
this.users.splice(0, 0, user);
@ -44,17 +149,15 @@ Polymer({
var formData = new FormData();
formData.append('email', email);
var xhr = document.createElement('core-xhr');
xhr.request({url: this.action, method: 'POST', body: formData, callback:
function(response, xhr) {
if (xhr.status == 201) {
this.addUser(JSON.parse(response));
this.$.form.reset();
} else if (xhr.status == 200) {
alert('Thanks. But that user already exists');
}
}.bind(this)
});
var xhr = document.createElement('iron-request');
xhr.send({url: this.action, method: 'POST', body: formData}).then(function(request) {
if (request.status == 201) {
this.addUser(JSON.parse(response));
this.$.form.reset();
} else if (request.status == 200) {
alert('Thanks. But that user already exists');
}
}.bind(this));
}
},
ajaxDelete: function(e, details, sender) {
@ -68,9 +171,9 @@ Polymer({
var user = e.target.templateInstance.model.user;
var idx = this.users.indexOf(user);
var xhr = document.createElement('core-xhr');
xhr.request({url: sender.href, method: 'POST', callback: function(response, xhr) {
sender.parentElement.classList.add('faded');
var xhr = document.createElement('iron-request');
xhr.send({url: sender.href, method: 'POST'}).then(function(request) {
e.target.parentElement.classList.add('faded');
if ('ontransitionend' in window) {
var li = sender.parentElement;
@ -81,27 +184,8 @@ Polymer({
} else {
this.removeUser(idx);
}
}.bind(this)});
// var xhr = new XMLHttpRequest();
// xhr.open('POST', sender.href);
// xhr.onloadend = function(e) {
// if (e.target.status == 200) {
// sender.parentElement.classList.add('faded');
// var li = sender.parentElement;
// if ('ontransitionend' in window) {
// li.addEventListener('transitionend', function(e) {
// this.users.splice(idx, 1);
// }.bind(this));
// li.classList.add('faded');
// } else {
// this.users.splice(idx, 1);
// }
// }
// }.bind(this);
// xhr.send();
}.bind(this));
}
});
</script>
</polymer-element>
</polymer-element> -->

Просмотреть файл

@ -1,4 +1,38 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/iron-ajax/iron-request.html">
<dom-module id="delete-link">
<template>
<content></content>
</template>
</dom-module>
<script>
Polymer({
is: 'delete-link',
extends: 'a',
listeners: {
'click': 'onClick'
},
onClick: function (e, details, sender) {
e.preventDefault();
var msg = this.getAttribute('message') || 'Remove?';
if (!confirm(msg)) {
return;
}
var xhr = document.createElement('iron-request');
xhr.send({url: e.target.href, method: 'POST'}).then(function(request) {
this.fire('ajaxdeleted', {response: request.response, xhr: request.xhr});
}.bind(this));
}
});
</script>
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="../bower_components/core-ajax/core-xhr.html">
<polymer-element name="delete-link" extends="a" on-click="{{onClick}}">
@ -14,7 +48,7 @@
if (!confirm(msg)) {
return;
}
var xhr = document.createElement('core-xhr');
xhr.request({url: sender.href, method: 'POST', callback: function(response, xhr) {
this.fire('ajaxdeleted', {response: response, xhr: xhr});
@ -23,3 +57,4 @@
});
</script>
</polymer-element>
-->

Просмотреть файл

@ -1,4 +1,4 @@
<link rel="import" href="../bower_components/polymer/polymer.html">
<!-- <link rel="import" href="../bower_components/polymer/polymer.html">
<polymer-element name="x-meter" attributes="value">
<template>
@ -30,3 +30,49 @@
});
</script>
</polymer-element>
-->
<link rel="import" href="../bower_components/polymer/polymer.html">
<dom-module id="x-meter">
<template>
<link rel="stylesheet" href="../css/elements/x-meter.css">
<div class="meter" id="meter" title="{{helpText}}">
<div style="{{computeStyle(value)}}"><span>{{valueFormmatted}}</span>%</div>
</div>
</template>
</dom-module>
<script>
Polymer({
is: 'x-meter',
properties: {
helpText: {
type: String,
value: ''
},
value: {
type: Number,
value: 0,
notify: true,
observer: 'valueChanged'
}
},
valueChanged: function () {
var MIN_VAL = 0.0001;
if (this.value <= MIN_VAL) {
this.valueFormmatted = '<=0.0001';
} else {
this.valueFormmatted = Number(this.value).toFixed(4);
}
this.value = Number(this.value).toFixed(4);
var MIN_FOR_AT_RISK = 0.03;
if (this.value <= MIN_FOR_AT_RISK) {
this.$.meter.classList.add('atrisk');
this.helpText = 'This feature may be at risk for removal. Usage is <= ' + MIN_FOR_AT_RISK + '%.';
}
},
computeStyle: function (value) {
return 'width:' + value + '%';
}
});
</script>

Просмотреть файл

@ -50,7 +50,7 @@
<link rel="apple-touch-icon" href="/static/img/chromium-128.png">
<link rel="apple-touch-icon-precomposed" href="/static/img/chromium-128.png">
<script src="/static/bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="/static/bower_components/webcomponentsjs/webcomponents-lite.min.js"></script>
<!-- <script src="/static/js/smoothscroll/dist/smoothscroll.js"></script> -->
<!-- <script src="/static/js/bower_components/smoothscroll/smoothscroll.js"></script> -->