3
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7
Code distributed by Google as part of the polymer project is also
8
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
11
<link rel="import" href="../polymer/polymer.html">
12
<link rel="import" href="../hydrolysis/hydrolysis-analyzer.html">
13
<link rel="import" href="../iron-ajax/iron-ajax.html">
14
<link rel="import" href="../iron-doc-viewer/iron-doc-viewer.html">
15
<link rel="import" href="../iron-flex-layout/iron-flex-layout.html">
16
<link rel="import" href="../iron-icons/iron-icons.html">
17
<link rel="import" href="../iron-selector/iron-selector.html">
18
<link rel="import" href="../paper-header-panel/paper-header-panel.html">
19
<link rel="import" href="../paper-styles/color.html">
20
<link rel="import" href="../paper-styles/typography.html">
21
<link rel="import" href="../paper-toolbar/paper-toolbar.html">
24
Loads Polymer element and behavior documentation using
25
[Hydrolysis](https://github.com/PolymerLabs/hydrolysis) and renders a complete
26
documentation page including demos (if available).
29
<dom-module id="iron-component-page">
33
font-family: 'Roboto', 'Noto', sans-serif;
36
@apply(--layout-vertical);
38
background: var(--paper-grey-50);
46
@apply(--layout-flex);
47
background: var(--paper-grey-50);
51
--paper-toolbar-background: var(--paper-grey-50);
52
--paper-toolbar-color: var(--paper-grey-800);
56
:host > paper-header-panel {
58
transition: opacity 0.5s;
61
:host(.loaded) > paper-header-panel {
67
background: var(--paper-grey-50);
75
paper-toolbar a:last-child {
79
paper-toolbar a, paper-toolbar a iron-icon {
81
color: var(--paper-grey-500);
84
paper-toolbar iron-icon {
88
paper-toolbar a.iron-selected, paper-toolbar a.iron-selected iron-icon {
89
color: var(--paper-grey-800);
92
paper-toolbar a:hover, paper-toolbar a:hover iron-icon {
93
color: var(--paper-pink-500);
101
@apply(--layout-fit);
105
background: var(--paper-grey-50);
108
color: var(--paper-grey-400);
113
background: transparent;
124
#view > .iron-selected {
129
max-width: var(--iron-component-page-max-width, 48em);
130
@apply(--iron-component-page-container);
137
font-family: Roboto, Noto;
139
background: transparent;
144
text-transform: uppercase;
157
#catalog-heading h2 {
158
color: var(--paper-grey-800);
159
@apply(--paper-font-title);
163
#catalog-heading .version {
164
color: var(--paper-grey-500);
169
#catalog-heading .version:before {
172
#catalog-heading .version:after {
180
:host([catalog]) [catalog-only] {
183
:host([catalog]) [catalog-hidden] {
188
@apply(--layout-horizontal);
189
@apply(--layout-center-center);
190
@apply(--layout-fit);
194
@apply(--layout-flex);
198
<hydrolysis-analyzer id="analyzer" src="[[_srcUrl]]" transitive="[[transitive]]" clean analyzer="{{_hydroDesc}}" loading="{{_hydroLoading}}"></hydrolysis-analyzer>
199
<iron-ajax id="ajax" url="[[docSrc]]" handle-as="json" on-response="_handleAjaxResponse" on-error="_handleError"></iron-ajax>
201
<paper-header-panel id="headerPanel" mode="[[scrollMode]]">
202
<paper-toolbar catalog-hidden>
203
<div class="docs-header">
204
<!-- TODO: Replace with paper-dropdown-menu when available -->
205
<select id="active" value="[[active]]" on-change="_handleMenuItemSelected">
206
<template is="dom-repeat" items="[[docElements]]">
207
<option value="[[item.is]]">[[item.is]]</option>
209
<template is="dom-repeat" items="[[docBehaviors]]">
210
<option value="[[item.is]]">[[item.is]]</option>
214
<iron-selector attr-for-selected="view" selected="{{view}}" id="links" hidden$="[[!docDemos.length]]">
215
<a view="docs"><iron-icon icon="description"></iron-icon> Docs</a>
216
<a view="[[_demoView(docDemos.0.path)]]"><iron-icon icon="visibility"></iron-icon> <span>Demo</span></a>
220
<iron-selector id="view" selected="[[_viewType(view)]]" attr-for-selected="id">
222
<div id="catalog-heading" catalog-only>
223
<h2><span>[[active]]</span> <span class="version" hidden$="[[!version]]">[[version]]</span></h2>
225
<iron-doc-viewer prefix="[[_fragmentPrefix]]" id="viewer" descriptor="{{_activeDescriptor}}"
226
on-iron-doc-viewer-component-selected="_handleComponentSelectedEvent"></iron-doc-viewer>
227
<div id="nodocs" hidden$="[[_activeDescriptor]]" class="no-docs">
228
No documentation found.
231
<div id="demo"></div>
234
</paper-header-panel>
239
// var hydrolysis = require('hydrolysis');
242
* @param {string} url
243
* @return {string} `url` stripped of a file name, if one is present. This
244
* considers URLs like "example.com/foo" to already be a base (no `.` is)
245
* present in the final path part).
247
function _baseUrl(url) {
248
return url.match(/^(.*?)\/?([^\/]+\.[^\/]+)?$/)[1] + '/';
252
is: 'iron-component-page',
256
* The URL to an import that declares (or transitively imports) the
257
* elements that you wish to see documented.
259
* If the URL is relative, it will be resolved relative to the master
262
* If a `src` URL is not specified, it will resolve the name of the
263
* directory containing this element, followed by `dirname.html`. For
266
* `awesome-sauce/index.html`:
268
* <iron-doc-viewer></iron-doc-viewer>
270
* Would implicitly have `src="awesome-sauce.html"`.
274
observer: '_srcChanged',
278
* The URL to a precompiled JSON descriptor. If you have precompiled
279
* and stored a documentation set using Hydrolysis, you can load the
280
* analyzer directly via AJAX by specifying this attribute.
282
* If a `doc-src` is not specified, it is ignored and the default
283
* rules according to the `src` attribute are used.
287
observer: '_srcChanged',
291
* The relative root for determining paths to demos and default source
297
// Don't include URL hash.
298
return this.ownerDocument.baseURI.replace(/\#.*$/, '');
303
* The element or behavior that will be displayed on the page. Defaults
304
* to the element matching the name of the source file.
313
* The current view. Can be `docs` or `demo`.
322
* Whether _all_ dependencies should be loaded and documented.
324
* Turning this on will probably slow down the load process dramatically.
331
/** The Hydrolysis element descriptors that have been loaded. */
341
/** The Hydrolysis behavior descriptors that have been loaded. */
352
* Demos for the currently selected element.
361
* The scroll mode for the page. For details about the modes,
362
* see the mode property in paper-header-panel.
370
* The currently displayed element.
372
* @type {!hydrolysis.ElementDescriptor}
374
_activeDescriptor: Object,
376
_fragmentPrefix: String,
379
* Toggle flag to be used when this element is being displayed in the
380
* Polymer Elements catalog.
385
reflectToAttribute: true
389
* An optional version string.
394
* The hydrolysis analyzer.
396
* @type {!hydrolysis.Analyzer}
400
observer: '_analyzerChanged',
404
observer: '_detectAnalyzer'
408
observer: '_detectAnalyzer'
411
/** Whether the analyzer is loading source. */
414
observer: '_loadingChanged',
418
observer: '_detectLoading'
422
observer: '_detectLoading'
425
/** The complete URL to this component's demo. */
431
/** The complete URL to this component's source. */
436
'_updateFrameSrc(view, base)',
437
'_activeChanged(active, _analyzer)'
440
attached: function() {
441
// In the catalog, let the catalog do all the routing
443
this._setActiveFromHash();
444
this.listen(window, 'hashchange', '_setActiveFromHash');
448
detached: function() {
450
this.unlisten(window, 'hashchange', '_setActiveFromHash');
455
var elements = this._loadJson();
457
this.docElements = elements;
458
this._loading = false;
460
// Make sure our change handlers trigger in all cases.
461
if (!this.src && !this.catalog) {
468
* Loads an array of hydrolysis element descriptors (as JSON) from the text
469
* content of this element, if present.
471
* @return {Array<hydrolysis.ElementDescriptor>} The descriptors, or `null`.
473
_loadJson: function() {
474
var textContent = '';
475
Array.prototype.forEach.call(Polymer.dom(this).childNodes, function(node) {
476
textContent = textContent + node.textContent;
478
textContent = textContent.trim();
479
if (textContent === '') return null;
482
var json = JSON.parse(textContent);
483
if (!Array.isArray(json)) return [];
486
console.error('Failure when parsing JSON:', textContent, error);
492
* Load the page identified in the fragment identifier.
495
_setActiveFromHash: function(hash) {
496
// hash is either element-name or element-name:{properties|methods|events} or
497
// element-name:{property|method|event}.member-name
498
var hash = window.location.hash;
500
var elementDelimiter = hash.indexOf(':');
501
elementDelimiter = (elementDelimiter == -1) ? hash.length : elementDelimiter;
502
var el = hash.slice(1, elementDelimiter);
503
if (this.active != el) {
506
this.$.viewer.scrollToAnchor(hash);
510
_srcChanged: function() {
513
if (!this.$.ajax.lastRequest || (this.docSrc !== this.$.ajax.lastRequest.url && this.docSrc !== this._lastDocSrc)) {
514
this._ajaxLoading = true;
515
this._ajaxDesc = null;
516
this._activeDescriptor = null;
517
this.$.ajax.generateRequest();
519
this._lastDocSrc = this.docSrc;
521
} else if (this.src) {
522
srcUrl = new URL(this.src, this.base).toString();
524
var base = _baseUrl(this.base);
525
srcUrl = new URL(base.match(/([^\/]*)\/$/)[1] + ".html", base).toString();
528
// Rewrite gh-pages URLs to https://rawgit.com/
529
var match = srcUrl.match(/([^\/\.]+)\.github\.io\/([^\/]+)\/?([^\/]*)$/);
531
srcUrl = "https://cdn.rawgit.com/" + match[1] + "/" + match[2] + "/master/" + match[3];
534
this._baseUrl = _baseUrl(srcUrl);
535
this._srcUrl = srcUrl;
536
if (!this._hydroLoading) this.$.analyzer.analyze();
539
_updateFrameSrc: function(view) {
540
if (!view || view.indexOf("demo:") !== 0) return "about:blank";
542
var src = view.split(':')[1];
543
var demoSrc = new URL(src, this.base).toString();
545
// If you use history.pushState with iframe.src = url, you will create 2 history entries,
546
// but creating a new iframe dynamically will prevent it.
549
Polymer.dom(this.$.demo).removeChild(this._iframe);
552
this._iframe = document.createElement('iframe');
553
this._iframe.src = demoSrc;
554
Polymer.dom(this.$.demo).appendChild(this._iframe);
557
_getDefaultActive: function() {
559
var url = this._srcUrl || this.base;
560
var mainFile = url.replace(_baseUrl(this.base), '');
562
function findMatch(list) {
563
for (var item, i = 0; i < list.length; i++) {
565
if (item && item.contentHref && item.contentHref.indexOf(mainFile) > 0) {
572
matchedPage = findMatch(this.docElements) || findMatch(this.docBehaviors);
575
return matchedPage.is;
576
} else if (this.docElements.length > 0) {
577
return this.docElements[0].is;
578
} else if (this.docBehaviors.length > 0) {
579
return this.docBehaviors[0].is;
584
_findDescriptor: function(name) {
585
if (!this._analyzer) return null;
587
var descriptor = this._analyzer.elementsByTagName[name];
588
if (descriptor) return descriptor;
590
for (var i = 0; i < this._analyzer.behaviors.length; i++) {
591
if (this._analyzer.behaviors[i].is === name) {
592
return this._analyzer.behaviors[i];
598
_activeChanged: function(active, analyzer) {
600
this.active = this._getDefaultActive();
603
this.async(function() { this.$.active.value = active; });
604
if (analyzer && analyzer.elementsByTagName) {
605
this.$.headerPanel.scroller.scrollTop = 0;
606
this._activeDescriptor = this._findDescriptor(active);
607
if (this._activeDescriptor) {
609
var demos = this._activeDescriptor.demos;
610
if (this.view && demos && demos.length) {
611
var parts = this.view.split(':');
612
if (parts[0] == 'demo') {
614
hasDemo = demos.some(function(d, i) {
615
if (d.path == parts[1]) {
621
this.view = 'demo:' + demos[0].path;
626
if (!hasDemo == undefined) {
629
if (this._activeDescriptor.is && !document.title) {
630
document.title = this._activeDescriptor.is + " documentation";
632
if (this._activeDescriptor.is && !this.catalog) {
633
this._fragmentPrefix = this._activeDescriptor.is + ':';
635
this._fragmentPrefix = '';
637
// On initial load, scroll to the selected anchor (if any).
638
// This probably shouldn't be required when we're running
639
// in the catalog, but at the moment it is.
640
this.$.viewer.scrollToAnchor(window.location.hash);
642
this._setDocDemos(this._activeDescriptor ? this._activeDescriptor.demos : []);
646
_loadingChanged: function() {
647
this.toggleClass('loaded', !this._loading);
650
_detectLoading: function() {
651
this._loading = this.docSrc ? this._ajaxLoading : this._hydroLoading;
654
_analyzerChanged: function() {
655
var analyzer = this._analyzer;
656
this._setDocElements(analyzer && analyzer.elements ? analyzer.elements : []);
657
this._setDocBehaviors(analyzer && analyzer.behaviors ? analyzer.behaviors : []);
659
if (!this._findDescriptor(this.active)) {
660
this.active = this._getDefaultActive();
664
_detectAnalyzer: function() {
665
this._analyzer = this.docSrc ? this._ajaxDesc : this._hydroDesc;
668
_handleMenuItemSelected: function(e) {
669
if (e.target && e.target.value) {
670
window.location.hash = '#' + e.target.value;
674
_handleAjaxResponse: function(e, req) {
675
this._ajaxLoading = false;
676
this._ajaxLastUrl = req.url;
677
this._ajaxDesc = req.response;
680
_handleError: function(e) {
681
this.fire('iron-component-page-error', e.detail);
684
_handleComponentSelectedEvent: function(ev) {
685
var descriptor = this._findDescriptor(ev.detail);
687
console.warn("Could not navigate to ", ev.detail);
690
this.active = ev.detail;
695
* Renders this element into static HTML for offline use.
697
* This is mostly useful for debugging and one-off documentation generation.
698
* If you want to integrate doc generation into your build process, you
699
* probably want to be calling `hydrolysis.Analyzer.analyze()` directly.
701
* @return {string} The HTML for this element with all state baked in.
703
marshal: function() {
704
var jsonText = JSON.stringify(this.docElements || [], null, ' ');
705
return '<' + this.is + '>\n' +
706
jsonText.replace(/</g, '<').replace(/>/g, '>') + '\n' +
707
'</' + this.is + '>';
710
_demoView: function(path) {
711
return "demo:" + path;
714
_viewType: function(view) {
715
return view ? view.split(":")[0] : null;