UNPKG

infamous

Version:

A CSS3D/WebGL UI library.

419 lines (341 loc) 15.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.initDeclarativeBase = initDeclarativeBase; exports.default = void 0; var _WebComponent = _interopRequireDefault(require("./WebComponent")); var _HTMLNode = _interopRequireDefault(require("./HTMLNode")); var _Utility = require("../core/Utility"); var _three = require("three"); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } /* global HTMLSlotElement */ var DeclarativeBase; exports.default = DeclarativeBase; const observers = new WeakMap(); initDeclarativeBase(); function initDeclarativeBase() { if (DeclarativeBase) return; /** * @implements {EventListener} */ exports.default = DeclarativeBase = _WebComponent.default.subclass('DeclarativeBase', ({ Super, Public, Private }) => ({ static: { define(name) { name = name || this.defaultElementName; customElements.define(name, this); this._definedElementName = name; }, get definedElementName() { return this._definedElementName || null; } }, // We use this to Override HTMLElement.prototype.attachShadow in v1, and // HTMLElement.prototype.createShadowRoot in v0, so that we can make the // connection between parent and child on the imperative side when the HTML side // is using shadow roots. // // TODO finish ShadowDOM compatibility! attachShadow(options, _method) { _method = _method || 'attachShadow'; if (!(this instanceof DeclarativeBase)) return Super(this)[_method](options); // In v0, shadow roots can be replaced, but in v1 calling attachShadow // on an element that already has a root throws. So, we can set this to // true, and if the try-catch passes then we know we have a v0 root and // that the root was just replaced. const oldRoot = this.shadowRoot; let root = null; try { root = Super(this)[_method](options); } catch (e) { throw e; } const privateThis = Private(this); privateThis._hasShadowRoot = true; if (oldRoot) { Private(this)._onV0ShadowRootReplaced(oldRoot); } const observer = (0, _Utility.observeChildren)(root, privateThis._shadowRootChildAdded.bind(privateThis), privateThis._shadowRootChildRemoved.bind(privateThis)); observers.set(root, observer); const { children } = this; for (let l = children.length, i = 0; i < l; i += 1) { if (!(children[i] instanceof DeclarativeBase)) continue; Private(children[i])._isPossiblyDistributed = true; } return root; }, createShadowRoot() { this.attachShadow(undefined, 'createShadowRoot'); }, childConnectedCallback(child) { // mirror the DOM connections in the imperative API's virtual scene graph. if (child instanceof _HTMLNode.default) { if (Private(this)._hasShadowRoot) Private(child)._isPossiblyDistributed = true; // If ImperativeBase#add was called first, child's // `parent` will already be set, so prevent recursion. if (child.parent) return; this.add(child); } else if (_Utility.hasShadowDomV0 && child instanceof HTMLContentElement && //getShadowRootVersion( (0, _Utility.getAncestorShadowRoot)(this) //) == 'v0' ) {// observe <content> elements. } else if (_Utility.hasShadowDomV1 && child instanceof HTMLSlotElement && //getShadowRootVersion( (0, _Utility.getAncestorShadowRoot)(this) //) == 'v1' ) { child.addEventListener('slotchange', this); Private(this)._handleDistributedChildren(child); } else { // if non-library content was added (div, img, etc). // TODO: replace this check with a more general one that // detects if anything is visible including from styling, not // just content. Perhaps make a specific API for defining that // a node should have DOM content, to make it clear. if (this instanceof _HTMLNode.default && !this.isDOMNode && (!(child instanceof Text) && !(child instanceof Comment) || child instanceof Text && child.textContent.trim().length > 0)) { Private(this)._possiblyCreateDOMPlane(); } } }, childDisconnectedCallback(child) { // mirror the connection in the imperative API's virtual scene graph. if (child instanceof _HTMLNode.default) { Private(child)._isPossiblyDistributed = false; // If ImperativeBase#remove was called first, child's // `parent` will already be null, so prevent recursion. if (!child.parent) return; this.remove(child); } else if (_Utility.hasShadowDomV0 && child instanceof HTMLContentElement && //getShadowRootVersion( (0, _Utility.getAncestorShadowRoot)(this) //) == 'v0' ) {// unobserve <content> element } else if (_Utility.hasShadowDomV1 && child instanceof HTMLSlotElement && //getShadowRootVersion( (0, _Utility.getAncestorShadowRoot)(this) //) == 'v1' ) { child.removeEventListener('slotchange', this); Private(this)._handleDistributedChildren(child); Private(this)._slotElementsAssignedNodes.delete(child); } else { // if non-library content was removed (div, img, etc). if (this instanceof _HTMLNode.default && !this.isDOMNode && (!(child instanceof Text) && !(child instanceof Comment) || child instanceof Text && child.textContent.trim().length > 0)) { Private(this)._possiblyDestroyDOMPlane(); } } }, // Traverses a tree while considering ShadowDOM disribution. traverse(isShadowChild) { console.log(isShadowChild ? 'distributedNode:' : 'node:', this); // in the future, the user will be use a pure-JS API with no HTML // DOM API. const hasHtmlApi = true; const { children } = this; for (let l = children.length, i = 0; i < l; i += 1) { // skip nodes that are possiblyDistributed, i.e. they have a parent // that has a ShadowRoot. if (!hasHtmlApi || !Private(children[i])._isPossiblyDistributed) children[i].traverse(); } const shadowChildren = Private(this)._shadowChildren; if (hasHtmlApi && shadowChildren) { for (let l = shadowChildren.length, i = 0; i < l; i += 1) shadowChildren[i].traverse(true); } }, // TODO: make setAttribute accept non-string values. setAttribute(attr, value) { //if (this.tagName.toLowerCase() == 'motor-scene') //console.log('setting attribute', arguments[1]) Super(this).setAttribute(attr, value); }, get threeDOMPlane() { return Private(this)._threeDOMPlane; }, private: { // true if this node has a shadow root (even if it is "closed", see // attachShadow method above). Once true always true because shadow // roots cannot be removed. _hasShadowRoot: false, // True when this node has a parent that has a shadow root. When // using the HTML API, Imperative API can look at this to determine // whether to render this node or not, in the case of WebGL. _isPossiblyDistributed: false, // A map of the slot elements that are children of this node and // their last-known assigned nodes. When a slotchange happens while // this node is in a shadow root and has a slot child, we can // detect what the difference is between the last known and the new // assignments, and notate the new distribution of child nodes. See // issue #40 for background on why we do this. _slotElementsAssignedNodes: new WeakMap(), // If this node is distributed into a shadow tree, this will // reference the parent of the <slot> or <content> element. // Basically, this node will render as a child of that parent node // in the flat tree. _shadowParent: null, // If this element has a child <slot> or <content> element while in // a shadow root, then this will be a Set of the nodes distributed // into the <slot> or <content>, and those nodes render relatively // to this node in the flat tree. We instantiate this later, only // when/if needed. _shadowChildren: null, // If this HTMLNode needs to be visible (f.e. it has non-library // HTML children like div, span, img, etc), then we store here a // reference to a WebGL plane that is aligned with the DOM element // in order to achieve "mixed mode" features like the DOM element // intersecting with WebGL meshes. _threeDOMPlane: null, _nonLibraryElementCount: 0, _shadowRootChildAdded(child) { // NOTE Logic here is similar to childConnectedCallback if (child instanceof DeclarativeBase) { Public(this).add(child); } else if (_Utility.hasShadowDomV0 && child instanceof HTMLContentElement) {// observe <content> elements. } else if (_Utility.hasShadowDomV1 && child instanceof HTMLSlotElement) { child.addEventListener('slotchange', this); this._handleDistributedChildren(child); } }, _shadowRootChildRemoved(child) { // NOTE Logic here is similar to childDisconnectedCallback if (child instanceof DeclarativeBase) { Public(this).remove(child); } else if (_Utility.hasShadowDomV0 && child instanceof HTMLContentElement) {// unobserve <content> element } else if (_Utility.hasShadowDomV1 && child instanceof HTMLSlotElement) { child.removeEventListener('slotchange', this); this._handleDistributedChildren(child); this._slotElementsAssignedNodes.delete(child); } }, // This method is part of the EventListener interface. handleEvent(event) { if (event.type == 'slotchange') { const slot = event.target; this._handleDistributedChildren(slot); } }, _handleDistributedChildren(slot) { const diff = this._getDistributedChildDifference(slot); const { added } = diff; for (let l = added.length, i = 0; i < l; i += 1) { const addedNode = added[i]; if (!(addedNode instanceof DeclarativeBase)) continue; // Keep track of the final distribution of a node. // // If the given slot is assigned to another // slot, then this logic will run again for the next slot on // that next slot's slotchange, so we remove the distributed // node from the previous shadowParent and add it to the next // one. If we don't do this, then the distributed node will // exist in multiple shadowChildren lists when there is a // chain of assigned slots. For more info, see // https://github.com/w3c/webcomponents/issues/611 const shadowParent = addedNode._shadowParent; if (shadowParent && shadowParent._shadowChildren) { const shadowChildren = shadowParent._shadowChildren; shadowChildren.splice(shadowChildren.indexOf(addedNode), 1); if (!shadowChildren.length) shadowParent._shadowChildren = null; } // The node is now distributed to `this` element. addedNode._shadowParent = this; if (!this._shadowChildren) this._shadowChildren = []; this._shadowChildren.add(addedNode); } const { removed } = diff; for (let l = removed.length, i = 0; i < l; i += 1) { const removedNode = removed[i]; if (!(removedNode instanceof DeclarativeBase)) continue; removedNode._shadowParent = null; this._shadowChildren.delete(removedNode); if (!this._shadowChildren.size) this._shadowChildren = null; } }, _getDistributedChildDifference(slot) { let previousNodes; if (this._slotElementsAssignedNodes.has(slot)) previousNodes = this._slotElementsAssignedNodes.get(slot);else previousNodes = []; const newNodes = slot.assignedNodes({ flatten: true }); // save the newNodes to be used as the previousNodes for next time. this._slotElementsAssignedNodes.set(slot, newNodes); const diff = { removed: [] }; for (let i = 0, l = previousNodes.length; i < l; i += 1) { const oldNode = previousNodes[i]; const newIndex = newNodes.indexOf(oldNode); // if it exists in the previousNodes but not the newNodes, then // the node was removed. if (!(newIndex >= 0)) { diff.removed.push(oldNode); } // otherwise the node wasn't added or removed. else { newNodes.splice(i, 1); } } // Remaining nodes in newNodes must have been added. diff.added = newNodes; return diff; }, _possiblyCreateDOMPlane() { if (!this._nonLibraryElementCount) this._createDOMPlane(); this._nonLibraryElementCount++; }, _possiblyDestroyDOMPlane() { this._nonLibraryElementCount--; if (!this._nonLibraryElementCount) this._destroyDOMPlane(); }, _createDOMPlane() { // We have to use a BoxGeometry instead of a // PlaneGeometry because Three.js is not capable of // casting shadows from Planes, at least until we find // another way. Unfortunately, this increases polygon // count by a factor of 6. See issue // https://github.com/mrdoob/three.js/issues/9315 const geometry = this._createDOMPlaneGeometry(); // TODO PERFORMANCE we can re-use a single material for // all the DOM planes rather than a new material per // plane. const material = new _three.MeshPhongMaterial({ opacity: 0.5, color: new _three.Color(0x111111), blending: _three.NoBlending //side : DoubleSide, }); const mesh = this._threeDOMPlane = new _three.Mesh(geometry, material); mesh.castShadow = true; mesh.receiveShadow = true; Public(this).threeObject3d.add(mesh); Public(this).on('sizechange', this._updateDOMPlaneOnSizeChange, this); }, _updateDOMPlaneOnSizeChange({ x, y, z }) { // TODO PERFORMANCE, destroying and creating a whole new geometry is // wasteful, but it works for now. Improve this. this._threeDOMPlane.geometry.dispose(); this._threeDOMPlane.geometry = this._createDOMPlaneGeometry(); }, _destroyDOMPlane() { const publicThis = Public(this); publicThis.threeObject3d.remove(this._threeDOMPlane); this._threeDOMPlane.geometry.dispose(); this._threeDOMPlane.material.dispose(); this._threeDOMPlane = null; publicThis.off('sizechange', this._updateDOMPlaneOnSizeChange); }, _createDOMPlaneGeometry() { const publicThis = Public(this); return new _three.BoxGeometry(publicThis._calculatedSize.x, publicThis._calculatedSize.y, 1); }, _onV0ShadowRootReplaced(oldRoot) { observers.get(oldRoot).disconnect(); observers.delete(oldRoot); const { childNodes } = oldRoot; for (let l = childNodes.length, i = 0; i < l; i += 1) { const child = childNodes[i]; if (!(child instanceof DeclarativeBase)) continue; // We should disconnect the imperative connection (f.e. so it is not // rendered in WebGL) Public(this).remove(child, true); } } } })); }