UNPKG

spincycle

Version:

A reactive message router and object manager that lets clients subscribe to object property changes on the server

748 lines (655 loc) 23.5 kB
<!-- @license Copyright (c) 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt --> <link rel="import" href="../polymer/polymer.html"> <link rel="import" href="../hydrolysis/hydrolysis-analyzer.html"> <link rel="import" href="../iron-ajax/iron-ajax.html"> <link rel="import" href="../iron-doc-viewer/iron-doc-viewer.html"> <link rel="import" href="../iron-flex-layout/iron-flex-layout.html"> <link rel="import" href="../iron-icons/iron-icons.html"> <link rel="import" href="../iron-selector/iron-selector.html"> <link rel="import" href="../paper-header-panel/paper-header-panel.html"> <link rel="import" href="../paper-styles/color.html"> <link rel="import" href="../paper-styles/typography.html"> <link rel="import" href="../paper-toolbar/paper-toolbar.html"> <!-- Loads Polymer element and behavior documentation using [Hydrolysis](https://github.com/PolymerLabs/hydrolysis) and renders a complete documentation page including demos (if available). --> <dom-module id="iron-component-page"> <template> <style> :host { font-family: 'Roboto', 'Noto', sans-serif; @apply(--layout-fit); @apply(--layout); @apply(--layout-vertical); overflow: hidden; background: var(--paper-grey-50); } [hidden] { display: none !important; } p { max-width: 20em; } paper-header-panel { @apply(--layout-flex); background: var(--paper-grey-50); } paper-toolbar { --paper-toolbar-background: var(--paper-grey-50); --paper-toolbar-color: var(--paper-grey-800); flex-shrink: 0; } :host > paper-header-panel { opacity: 0; transition: opacity 0.5s; } :host(.loaded) > paper-header-panel { opacity: 1.0; } #content { display: block; background: var(--paper-grey-50); } paper-toolbar a { margin: 0 10px; cursor: pointer; } paper-toolbar a:last-child { margin-right: 0; } paper-toolbar a, paper-toolbar a iron-icon { font-weight: normal; color: var(--paper-grey-500); } paper-toolbar iron-icon { margin: -2px 5px 0 0; } paper-toolbar a.iron-selected, paper-toolbar a.iron-selected iron-icon { color: var(--paper-grey-800); } paper-toolbar a:hover, paper-toolbar a:hover iron-icon { color: var(--paper-pink-500); } select { cursor: pointer; } #demo iframe { @apply(--layout-fit); } #nodocs { background: var(--paper-grey-50); font-size: 24px; font-weight: 400; color: var(--paper-grey-400); } #demo iframe { border: 0; background: transparent; width: 100%; height: 100%; overflow-x: none; overflow-y: auto; } #view > * { display: none; } #view > .iron-selected { display: block; } #docs { max-width: var(--iron-component-page-max-width, 48em); @apply(--iron-component-page-container); padding: 20px; margin: 0 auto; } #active { font-size: 20px; font-family: Roboto, Noto; border: 0; background: transparent; } paper-toolbar a { font-size: 14px; text-transform: uppercase; cursor: pointer; } #cart-icon { margin-left: 10px; cursor: pointer; } #catalog-heading { margin: 4px 0 18px; } #catalog-heading h2 { color: var(--paper-grey-800); @apply(--paper-font-title); margin: 0; } #catalog-heading .version { color: var(--paper-grey-500); font-size: 18px; line-height: 24px; font-weight: 400; } #catalog-heading .version:before { content: "("; } #catalog-heading .version:after { content: ")"; } [catalog-only] { display: none; } :host([catalog]) [catalog-only] { display: block; } :host([catalog]) [catalog-hidden] { display: none; } .no-docs { @apply(--layout-horizontal); @apply(--layout-center-center); @apply(--layout-fit); } .docs-header { @apply(--layout-flex); } </style> <hydrolysis-analyzer id="analyzer" src="[[_srcUrl]]" transitive="[[transitive]]" clean analyzer="{{_hydroDesc}}" loading="{{_hydroLoading}}"></hydrolysis-analyzer> <iron-ajax id="ajax" url="[[docSrc]]" handle-as="json" on-response="_handleAjaxResponse" on-error="_handleError"></iron-ajax> <paper-header-panel id="headerPanel" mode="[[scrollMode]]"> <paper-toolbar catalog-hidden> <div class="docs-header"> <!-- TODO: Replace with paper-dropdown-menu when available --> <select id="active" value="[[active]]" on-change="_handleMenuItemSelected"> <template is="dom-repeat" items="[[docElements]]"> <option value="[[item.is]]">[[item.is]]</option> </template> <template is="dom-repeat" items="[[docBehaviors]]"> <option value="[[item.is]]">[[item.is]]</option> </template> </select> </div> <iron-selector attr-for-selected="view" selected="{{view}}" id="links" hidden$="[[!docDemos.length]]"> <a view="docs"><iron-icon icon="description"></iron-icon> Docs</a> <a view="[[_demoView(docDemos.0.path)]]"><iron-icon icon="visibility"></iron-icon> <span>Demo</span></a> </iron-selector> </paper-toolbar> <div id="content"> <iron-selector id="view" selected="[[_viewType(view)]]" attr-for-selected="id"> <div id="docs"> <div id="catalog-heading" catalog-only> <h2><span>[[active]]</span> <span class="version" hidden$="[[!version]]">[[version]]</span></h2> </div> <iron-doc-viewer prefix="[[_fragmentPrefix]]" id="viewer" descriptor="{{_activeDescriptor}}" on-iron-doc-viewer-component-selected="_handleComponentSelectedEvent"></iron-doc-viewer> <div id="nodocs" hidden$="[[_activeDescriptor]]" class="no-docs"> No documentation found. </div> </div> <div id="demo"></div> </iron-selector> </div> </paper-header-panel> </template> <script> (function() { // var hydrolysis = require('hydrolysis'); /** * @param {string} url * @return {string} `url` stripped of a file name, if one is present. This * considers URLs like "example.com/foo" to already be a base (no `.` is) * present in the final path part). */ function _baseUrl(url) { return url.match(/^(.*?)\/?([^\/]+\.[^\/]+)?$/)[1] + '/'; } Polymer({ is: 'iron-component-page', properties: { /** * The URL to an import that declares (or transitively imports) the * elements that you wish to see documented. * * If the URL is relative, it will be resolved relative to the master * document. * * If a `src` URL is not specified, it will resolve the name of the * directory containing this element, followed by `dirname.html`. For * example: * * `awesome-sauce/index.html`: * * <iron-doc-viewer></iron-doc-viewer> * * Would implicitly have `src="awesome-sauce.html"`. */ src: { type: String, observer: '_srcChanged', }, /** * The URL to a precompiled JSON descriptor. If you have precompiled * and stored a documentation set using Hydrolysis, you can load the * analyzer directly via AJAX by specifying this attribute. * * If a `doc-src` is not specified, it is ignored and the default * rules according to the `src` attribute are used. */ docSrc: { type: String, observer: '_srcChanged', }, /** * The relative root for determining paths to demos and default source * detection. */ base: { type: String, value: function() { // Don't include URL hash. return this.ownerDocument.baseURI.replace(/\#.*$/, ''); } }, /** * The element or behavior that will be displayed on the page. Defaults * to the element matching the name of the source file. */ active: { type: String, notify: true, value: '' }, /** * The current view. Can be `docs` or `demo`. */ view: { type: String, value: 'docs', notify: true }, /** * Whether _all_ dependencies should be loaded and documented. * * Turning this on will probably slow down the load process dramatically. */ transitive: { type: Boolean, value: false }, /** The Hydrolysis element descriptors that have been loaded. */ docElements: { type: Array, notify: true, readOnly: true, value: function() { return []; } }, /** The Hydrolysis behavior descriptors that have been loaded. */ docBehaviors: { type: Array, notify: true, readOnly: true, value: function() { return []; } }, /** * Demos for the currently selected element. */ docDemos: { type: Array, notify: true, readOnly: true }, /** * The scroll mode for the page. For details about the modes, * see the mode property in paper-header-panel. */ scrollMode: { type: String, value: 'waterfall' }, /** * The currently displayed element. * * @type {!hydrolysis.ElementDescriptor} */ _activeDescriptor: Object, _fragmentPrefix: String, /** * Toggle flag to be used when this element is being displayed in the * Polymer Elements catalog. */ catalog: { type: Boolean, value: false, reflectToAttribute: true }, /** * An optional version string. */ version: String, /** * The hydrolysis analyzer. * * @type {!hydrolysis.Analyzer} */ _analyzer: { type: Object, observer: '_analyzerChanged', }, _hydroDesc: { type: Object, observer: '_detectAnalyzer' }, _ajaxDesc: { type: Object, observer: '_detectAnalyzer' }, /** Whether the analyzer is loading source. */ _loading: { type: Boolean, observer: '_loadingChanged', }, _hydroLoading: { type: Boolean, observer: '_detectLoading' }, _ajaxLoading: { type: Boolean, observer: '_detectLoading' }, /** The complete URL to this component's demo. */ _demoUrl: { type: String, value: '', }, /** The complete URL to this component's source. */ _srcUrl: String, }, observers: [ '_updateFrameSrc(view, base)', '_activeChanged(active, _analyzer)' ], attached: function() { // In the catalog, let the catalog do all the routing if (!this.catalog) { this._setActiveFromHash(); this.listen(window, 'hashchange', '_setActiveFromHash'); } }, detached: function() { if (!this.catalog) { this.unlisten(window, 'hashchange', '_setActiveFromHash'); } }, ready: function() { var elements = this._loadJson(); if (elements) { this.docElements = elements; this._loading = false; } else { // Make sure our change handlers trigger in all cases. if (!this.src && !this.catalog) { this._srcChanged(); } } }, /** * Loads an array of hydrolysis element descriptors (as JSON) from the text * content of this element, if present. * * @return {Array<hydrolysis.ElementDescriptor>} The descriptors, or `null`. */ _loadJson: function() { var textContent = ''; Array.prototype.forEach.call(Polymer.dom(this).childNodes, function(node) { textContent = textContent + node.textContent; }); textContent = textContent.trim(); if (textContent === '') return null; try { var json = JSON.parse(textContent); if (!Array.isArray(json)) return []; return json; } catch(error) { console.error('Failure when parsing JSON:', textContent, error); throw error; } }, /** * Load the page identified in the fragment identifier. */ _setActiveFromHash: function(hash) { // hash is either element-name or element-name:{properties|methods|events} or // element-name:{property|method|event}.member-name var hash = window.location.hash; if (hash) { var elementDelimiter = hash.indexOf(':'); elementDelimiter = (elementDelimiter == -1) ? hash.length : elementDelimiter; var el = hash.slice(1, elementDelimiter); if (this.active != el) { this.active = el; } this.$.viewer.scrollToAnchor(hash); } }, _srcChanged: function() { var srcUrl; if (this.docSrc) { if (!this.$.ajax.lastRequest || (this.docSrc !== this.$.ajax.lastRequest.url && this.docSrc !== this._lastDocSrc)) { this._ajaxLoading = true; this._ajaxDesc = null; this._activeDescriptor = null; this.$.ajax.generateRequest(); } this._lastDocSrc = this.docSrc; return; } else if (this.src) { srcUrl = new URL(this.src, this.base).toString(); } else { var base = _baseUrl(this.base); srcUrl = new URL(base.match(/([^\/]*)\/$/)[1] + ".html", base).toString(); } // Rewrite gh-pages URLs to https://rawgit.com/ var match = srcUrl.match(/([^\/\.]+)\.github\.io\/([^\/]+)\/?([^\/]*)$/); if (match) { srcUrl = "https://cdn.rawgit.com/" + match[1] + "/" + match[2] + "/master/" + match[3]; } this._baseUrl = _baseUrl(srcUrl); this._srcUrl = srcUrl; if (!this._hydroLoading) this.$.analyzer.analyze(); }, _updateFrameSrc: function(view) { if (!view || view.indexOf("demo:") !== 0) return "about:blank"; var src = view.split(':')[1]; var demoSrc = new URL(src, this.base).toString(); var self = this; // If you use history.pushState with iframe.src = url, you will create 2 history entries, // but creating a new iframe dynamically will prevent it. if (this._iframe) { Polymer.dom(this.$.demo).removeChild(this._iframe); } this._iframe = document.createElement('iframe'); this._iframe.src = demoSrc; this._iframe.allowFullscreen = true; // Fixes iron-component-page/issues/80 // Scrollbar issue in desktop/mobile Safari that prevents the user // from scrolling the demos. In this case, we need to force layout // in the main document when the iframe content has been fully rendered. this._iframe.style.height = '0%'; this._iframe.addEventListener('load', function() { var win = self._iframe.contentWindow; if (win.HTMLImports) { win.HTMLImports.whenReady(function() { if (win.Polymer) { win.Polymer.RenderStatus.afterNextRender(self, function() { self._iframe.style.height = '100%'; }); } else { self._iframe.style.height = '100%'; } }); } else { self._iframe.style.height = '100%'; } }); Polymer.dom(this.$.demo).appendChild(this._iframe); }, _getDefaultActive: function() { var matchedPage; var url = this._srcUrl || this.base; var mainFile = url.replace(_baseUrl(this.base), ''); function findMatch(list) { for (var item, i = 0; i < list.length; i++) { item = list[i]; if (item && item.contentHref && item.contentHref.indexOf(mainFile) > 0) { return item; } } return null; } matchedPage = findMatch(this.docElements) || findMatch(this.docBehaviors); if (matchedPage) { return matchedPage.is; } else if (this.docElements.length > 0) { return this.docElements[0].is; } else if (this.docBehaviors.length > 0) { return this.docBehaviors[0].is; } return null; }, _findDescriptor: function(name) { if (!this._analyzer) return null; var descriptor = this._analyzer.elementsByTagName[name]; if (descriptor) return descriptor; for (var i = 0; i < this._analyzer.behaviors.length; i++) { if (this._analyzer.behaviors[i].is === name) { return this._analyzer.behaviors[i]; } } return null; }, _activeChanged: function(active, analyzer) { if (active === '') { this.active = this._getDefaultActive(); return; } this.async(function() { this.$.active.value = active; }); if (analyzer && analyzer.elementsByTagName) { this.$.headerPanel.scroller.scrollTop = 0; this._activeDescriptor = this._findDescriptor(active); if (this._activeDescriptor) { var hasDemo; var demos = this._activeDescriptor.demos; if (this.view && demos && demos.length) { var parts = this.view.split(':'); if (parts[0] == 'demo') { if (parts[1]) { hasDemo = demos.some(function(d, i) { if (d.path == parts[1]) { return true; } }); } if (!hasDemo) { this.view = 'demo:' + demos[0].path; hasDemo = true; } } } if (!hasDemo == undefined) { this.view = 'docs'; } if (this._activeDescriptor.is && !document.title) { document.title = this._activeDescriptor.is + " documentation"; } if (this._activeDescriptor.is && !this.catalog) { this._fragmentPrefix = this._activeDescriptor.is + ':'; } else { this._fragmentPrefix = ''; } // On initial load, scroll to the selected anchor (if any). // This probably shouldn't be required when we're running // in the catalog, but at the moment it is. this.$.viewer.scrollToAnchor(window.location.hash); } this._setDocDemos(this._activeDescriptor ? this._activeDescriptor.demos : []); } }, _loadingChanged: function() { this.toggleClass('loaded', !this._loading); }, _detectLoading: function() { this._loading = this.docSrc ? this._ajaxLoading : this._hydroLoading; }, _analyzerChanged: function() { var analyzer = this._analyzer; this._setDocElements(analyzer && analyzer.elements ? analyzer.elements : []); this._setDocBehaviors(analyzer && analyzer.behaviors ? analyzer.behaviors : []); if (!this._findDescriptor(this.active)) { this.active = this._getDefaultActive(); } }, _detectAnalyzer: function() { this._analyzer = this.docSrc ? this._ajaxDesc : this._hydroDesc; }, _handleMenuItemSelected: function(e) { if (e.target && e.target.value) { window.location.hash = '#' + e.target.value; } }, _handleAjaxResponse: function(e, req) { this._ajaxLoading = false; this._ajaxLastUrl = req.url; this._ajaxDesc = req.response; }, _handleError: function(e) { this.fire('iron-component-page-error', e.detail); }, _handleComponentSelectedEvent: function(ev) { var descriptor = this._findDescriptor(ev.detail); if (!descriptor) { console.warn("Could not navigate to ", ev.detail); } else { this.active = ev.detail; } }, /** * Renders this element into static HTML for offline use. * * This is mostly useful for debugging and one-off documentation generation. * If you want to integrate doc generation into your build process, you * probably want to be calling `hydrolysis.Analyzer.analyze()` directly. * * @return {string} The HTML for this element with all state baked in. */ marshal: function() { var jsonText = JSON.stringify(this.docElements || [], null, ' '); return '<' + this.is + '>\n' + jsonText.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '\n' + '</' + this.is + '>'; }, _demoView: function(path) { return "demo:" + path; }, _viewType: function(view) { return view ? view.split(":")[0] : null; } }); })(); </script> </dom-module>