UNPKG

infamous

Version:

A CSS3D/WebGL UI library.

226 lines (190 loc) 9.26 kB
/* global customElements */ import Class from 'lowclass' import Mixin from '../core/Mixin' import {native} from 'lowclass/native' import { observeChildren } from '../core/Utility' import jss from '../lib/jss' import documentReady from '@awaitbox/document-ready' import DefaultBehaviors from './behaviors/DefaultBehaviors' // Very very stupid hack needed for Safari in order for us to be able to extend // the HTMLElement class. See: // https://github.com/google/traceur-compiler/issues/1709 if (typeof window.HTMLElement != 'function') { const _HTMLElement = function HTMLElement(){} _HTMLElement.prototype = window.HTMLElement.prototype window.HTMLElement = _HTMLElement } function classExtendsHTMLElement(constructor) { if (!constructor) return false if (constructor === HTMLElement) return true else return classExtendsHTMLElement( constructor.__proto__ ) } /** * Creates a WebComponent base class dynamically, depending on which * HTMLElement class you want it to extend from. Extend from WebComponent when * making a new Custom Element class. * * @example * const WebComponent = WebComponentMixin(HTMLButtonElement) * class AwesomeButton extends WebComponent { ... } * * @param {Function} Base The class that the generated WebComponent * base class will extend from. */ export default Mixin(Base => { // the extra `class extends` is necessary here so that // babel-plugin-transform-builtin-classes can work properly. Base = Base || native( HTMLElement ) // XXX: In the future, possibly check for Element if other things besides // HTML are supported (f.e. SVGElements) if (!classExtendsHTMLElement(Base)) { throw new TypeError( 'The argument to WebComponent.mixin must be a constructor that extends from or is HTMLElement.' ) } // otherwise, create it. const WebComponent = Class('WebComponent').extends( DefaultBehaviors.mixin( Base ), ({ Super, Public, Private }) => ({ isConnected: false, constructor(...args) { // Throw an error if no Custom Elements v1 API exists. if (!('customElements' in window)) { // TODO: provide a link to the Docs. throw new Error(` Your browser does not support the Custom Elements API. You'll need to install a polyfill. See how at http://.... `) } const self = Super(this).constructor(...args) return self }, // Subclasses can implement these. childConnectedCallback(child) { }, childDisconnectedCallback(child) { }, connectedCallback() { if (Super(this).connectedCallback) Super(this).connectedCallback() this.isConnected = true if (!Private(this).initialized) { this.init() Private(this).initialized = true } }, async disconnectedCallback() { if (Super(this).disconnectedCallback) Super(this).disconnectedCallback() this.isConnected = false // Deferr to the next tick before cleaning up in case the // element is actually being re-attached somewhere else within this // same tick (detaching and attaching is synchronous, so by // deferring to the next tick we'll be able to know if the element // was re-attached or not in order to clean up or not). Note that // appendChild can be used to move an element to another parent // element, in which case connectedCallback and disconnectedCallback // both get called, and in which case we don't necessarily want to // clean up. If the element gets re-attached before the next tick // (for example, gets moved), then we want to preserve the // stuff that would be cleaned up by an extending class' deinit // method by not running the following this.deinit() call. await Promise.resolve() // deferr to the next tick. // As mentioned in the previous comment, if the element was not // re-attached in the last tick (for example, it was moved to // another element), then clean up. if (!this.isConnected && Private(this).initialized) { this.deinit() Private(this).initialized = false } }, /** * This method can be overridden by extending classes, it should return * JSS-compatible styling. See http://github.com/cssinjs/jss for * documentation. * @abstract */ getStyles() { return {} }, /** * Init is called exactly once, the first time this element is * connected into the DOM. When an element is disconnected then * connected right away within the same synchronous tick, init() is not * fired again. However, if an element is disconnected and the current * tick completes before the element is connected again, then deinit() * will be called (i.e. the element was not simply moved to a new * location, it was actually removed), then the next time that the * element is connected back into DOM init() will be called again. * * This is in contrast to connectedCallback and disconnectedCallback: * connectedCallback is guaranteed to always fire even if the elemet * was previously disconnected in the same synchronous tick. * * For example, ... * * Subclasses should extend this to add such logic. */ init() { if (!Private(this).style) Private(this).style = Private(this).createStyles() // Deferral needed in case the Custom Element classes are // registered after the elements are already defined in the // DOM (they're not yet upgraded). This means that children of this node // might be a `<motor-node>` but if they aren't upgraded yet then // their API won't be available to the logic inside the following // call to childConnectedCallback. The reason this happens is // because parents are upgraded first and their // connectedCallbacks fired before their children are // upgraded. // documentReady().then(() => { // implies a Promise.resolve() behavior if the DOM is already ready // Handle any nodes that may have been connected before `this` node // was created (f.e. child nodes that were connected before the // custom elements were registered and which would therefore not be // detected by the following MutationObserver). if (!Private(this).childObserver) { const children = this.childNodes for (let l=children.length, i=0; i<l; i+=1) { this.childConnectedCallback(children[i]) } Private(this).childObserver = observeChildren(this, this.childConnectedCallback, this.childDisconnectedCallback) } }) // fire this.attributeChangedCallback in case some attributes have // existed before the custom element was upgraded. if (!Private(this).initialAttributeChange && this.hasAttributes()) { const {attributes} = this for (let l=attributes.length, i=0; i<l; i+=1) this.attributeChangedCallback(attributes[i].name, null, attributes[i].value) } }, // TODO: when we make setAttribute accept non-strings, we need to move // logic from attributeChangedCallback attributeChangedCallback(...args) { if (Super(this).attributeChangedCallback) Super(this).attributeChangedCallback(...args) Private(this).initialAttributeChange = true }, /** * This is the reciprocal of init(). It will be called when an element * has been disconnected but not re-connected within the same tick. * * The reason that init() and deinit() exist is so that if an element is * moved from one place to another within the same synchronous tick, * that deinit and init logic will not fire unnecessarily. If logic is * needed in that case, then connectedCallback and disconnectedCallback * can be used directly instead. */ deinit() { // Nothing much at the moment, but extending classes can extend // this to add deintialization logic. Private(this).childObserver.disconnect() }, private: { style: null, initialized: false, initialAttributeChange: false, childObserver: null, createStyles() { const rule = jss.createRule(Public(this).getStyles()) rule.applyTo(Public(this)) return rule }, }, })) return WebComponent }, native( HTMLElement ))