UNPKG

@webqit/oohtml

Version:

A suite of new DOM features that brings language support for modern UI development paradigms: a component-based architecture, data binding, and reactivity.

229 lines (211 loc) 11 kB
/** * @imports */ import HTMLImportsContext from './HTMLImportsContext.js'; import { _, env } from '../util.js'; /** * Creates the HTMLImportElement class. * * @param Object config * * @return HTMLImportElement */ export default function() { const { window } = env, { webqit } = window, { realdom, oohtml: { configs } } = webqit; if ( webqit.HTMLImportElement ) return webqit.HTMLImportElement; const BaseElement = configs.HTML_IMPORTS.elements.import.includes( '-' ) ? window.HTMLElement : class {}; class HTMLImportElement extends BaseElement { /** * @instance * * @param HTMLElement node * * @returns */ static instance( node ) { if ( configs.HTML_IMPORTS.elements.import.includes( '-' ) && ( node instanceof this ) ) return node; return _( node ).get( 'import::instance' ) || new this( node ); } /** * @constructor */ constructor( ...args ) { super(); // -------- const el = args[ 0 ] || this; _( el ).set( 'import::instance', this ); Object.defineProperty( this, 'el', { get: () => el, configurable: false } ); const priv = {}; Object.defineProperty( this, '#', { get: () => priv, configurable: false } ); priv.slottedElements = new Set; priv.setAnchorNode = anchorNode => { priv.anchorNode = anchorNode; return anchorNode; }; priv.live = callback => { if ( priv.liveImportsRealtime ) throw new Error(`Import element already in live mode.`); const parentNode = this.el.isConnected ? this.el.parentNode : priv.anchorNode.parentNode; priv.liveImportsRealtime = realdom.realtime( this.el ).attr( configs.HTML_IMPORTS.attr.ref, ( record, { signal } ) => { priv.moduleRef = record.value; const moduleRef = priv.moduleRef.includes( '#' ) ? priv.moduleRef : `${ priv.moduleRef }#`/* for live children */; const request = { ...HTMLImportsContext.createRequest( moduleRef ), live: signal && true, signal, diff: !moduleRef.endsWith('#') }; parentNode[ configs.CONTEXT_API.api.contexts ].request( request, response => { callback( ( response instanceof window.HTMLTemplateElement ? [ ...response.content.children ] : ( Array.isArray( response ) ? response : response && [ response ] ) ) || [] ); } ); }, { live: true, timing: 'sync', lifecycleSignals: true } ); priv.autoDestroyRealtime = realdom.realtime( window.document ).track( parentNode, () => { priv.die(); }, { subtree: 'cross-roots', timing: 'sync', generation: 'exits' } ); }; priv.die = () => { priv.autoDestroyRealtime?.disconnect(); priv.liveImportsRealtime?.disconnect(); priv.liveImportsRealtime = null; }; priv.hydrate = ( anchorNode, slottedElements ) => { anchorNode.replaceWith( priv.setAnchorNode( this.createAnchorNode() ) ); priv.live( fragments => { // The default action if ( priv.originalsRemapped ) return this.fill( fragments ); // Initial remap action const identifiersMap = fragments.map( ( fragment, i ) => ( { el: fragment, fragmentDef: fragment.getAttribute( configs.HTML_IMPORTS.attr.fragmentdef ) || '', tagName: fragment.tagName, i } ) ); slottedElements.forEach( ( slottedElement, i ) => { const tagName = slottedElement.tagName, fragmentDef = slottedElement.getAttribute( configs.HTML_IMPORTS.attr.fragmentdef ) || ''; const originalsMatch = ( i ++, identifiersMap.find( fragmentIdentifiers => fragmentIdentifiers.tagName === tagName && fragmentIdentifiers.fragmentDef === fragmentDef && fragmentIdentifiers.i === i ) ); if ( originalsMatch ) _( slottedElement ).set( 'original@imports', originalsMatch.el ); // Or should we throw integrity error here? _( slottedElement ).set( 'slot@imports', this.el ); priv.slottedElements.add( slottedElement ); } ); priv.originalsRemapped = true; }); }; priv.autoRestore = ( callback = null ) => { priv.autoRestoreRealtime?.disconnect(); if ( callback ) callback(); const restore = () => { if (this.el.isConnected) return; this.el.setAttribute( 'data-nodecount', 0 ); priv.internalMutation = true; priv.anchorNode.replaceWith( this.el ); priv.internalMutation = false; priv.setAnchorNode( null ); }; if ( !priv.slottedElements.size ) return restore(); const autoRestoreRealtime = realdom.realtime( priv.anchorNode.parentNode ).observe( [ ...priv.slottedElements ], record => { record.exits.forEach( outgoingNode => { _( outgoingNode ).delete( 'slot@imports' ); priv.slottedElements.delete( outgoingNode ); } ); if ( !priv.slottedElements.size ) { autoRestoreRealtime.disconnect(); // At this point, ignore if this is a removal involving the whole parent node if ( !record.target.isConnected ) return; restore(); } }, { subtree: 'cross-roots', timing: 'sync', generation: 'exits' } ); priv.autoRestoreRealtime = autoRestoreRealtime; }; priv.connectedCallback = () => { if ( priv.internalMutation ) return; priv.live( fragments => this.fill( fragments ) ); }; priv.disconnectedCallback = () => { if ( priv.internalMutation ) return; priv.die(); }; } /** * Creates the slot's anchor node. * * @return Element */ createAnchorNode() { if ( window.webqit.env !== 'server' ) { return window.document.createTextNode( '' ) } const escapeElement = window.document.createElement( 'div' ); escapeElement.textContent = this.el.outerHTML; const anchorNode = window.document.createComment( escapeElement.innerHTML ); _( anchorNode ).set( 'isAnchorNode', true ); return anchorNode; } /** * Fills the slot with slottableElements * * @param Iterable slottableElements * * @return void */ fill( slottableElements, r ) { if (!this.el.isConnected && (!this[ '#' ].anchorNode || !this[ '#' ].anchorNode.isConnected)) { // LiveImports must be responding to an event that just removed the subtree from DOM return; } if ( Array.isArray( slottableElements ) ) { slottableElements = new Set( slottableElements ) } // This state must be set before the diffing below and the serialization done at createAnchorNode() this.el.setAttribute( 'data-nodecount', slottableElements.size ); this[ '#' ].autoRestore( () => { this[ '#' ].slottedElements.forEach( slottedElement => { const slottedElementOriginal = _( slottedElement ).get( 'original@imports' ); // If still available in source, simply leave unchanged // otherwise remove it from slot... to reflect this change if ( slottableElements.has( slottedElementOriginal ) ) { slottableElements.delete( slottedElementOriginal ); } else { this[ '#' ].slottedElements.delete( slottedElement ); // This removal will not be caught slottedElement.remove(); } } ); // Make sure anchor node is what's in place... // not the import element itslef - but all only when we have slottableElements.size if ( slottableElements.size && this.el.isConnected ) { const newAnchorNode = this[ '#' ].setAnchorNode( this.createAnchorNode() ); this[ '#' ].internalMutation = true; this.el.replaceWith( newAnchorNode ); this[ '#' ].internalMutation = false; } // Insert slottables now slottableElements.forEach( slottableElement => { // Clone each slottable element and give it a reference to its original const slottableElementClone = slottableElement.cloneNode( true ); // The folllowing references must be set before adding to DODM if ( !slottableElementClone.hasAttribute( configs.HTML_IMPORTS.attr.fragmentdef ) ) { slottableElementClone.toggleAttribute( configs.HTML_IMPORTS.attr.fragmentdef, true ); } _( slottableElementClone ).set( 'original@imports', slottableElement ); _( slottableElementClone ).set( 'slot@imports', this.el ); this[ '#' ].slottedElements.add( slottableElementClone ); this[ '#' ].anchorNode.before( slottableElementClone ); } ); } ); } /** * Empty slot. * * @return void */ empty() { this[ '#' ].slottedElements.forEach( slottedElement => slottedElement.remove() ); } /** * Returns the slot's anchorNode. * * @return array */ get anchorNode() { return this[ '#' ].anchorNode; } /** * Returns the slot's module reference, if any. * * @return string */ get moduleRef() { return this[ '#' ].moduleRef; } /** * Returns the slot's slotted elements. * * @return array */ get slottedElements() { return this[ '#' ].slottedElements; } } if ( configs.HTML_IMPORTS.elements.import.includes( '-' ) ) { customElements.define( configs.HTML_IMPORTS.elements.import, HTMLImportElement ); } webqit.HTMLImportElement = HTMLImportElement; return HTMLImportElement; }