@webqit/oohtml 2.1.57 → 2.1.59

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "wicg-proposal"
15
15
  ],
16
16
  "homepage": "https://webqit.io/tooling/oohtml",
17
- "version": "2.1.57",
17
+ "version": "2.1.59",
18
18
  "license": "MIT",
19
19
  "repository": {
20
20
  "type": "git",
@@ -29,19 +29,19 @@
29
29
  "scripts": {
30
30
  "test": "mocha --extension .test.js --exit",
31
31
  "test:coverage": "c8 --reporter=text-lcov npm run test | coveralls",
32
- "build": "esbuild main=src/targets.browser.js context-api=src/context-api/targets.browser.js bindings-api=src/bindings-api/targets.browser.js html-bracelets=src/html-bracelets/targets.browser.js html-namespaces=src/html-namespaces/targets.browser.js html-imports=src/html-imports/targets.browser.js scoped-js=src/scoped-js/targets.browser.js scoped-css=src/scoped-css/targets.browser.js --bundle --minify --sourcemap --outdir=dist",
32
+ "build": "esbuild main=src/targets.browser.js context-api=src/context-api/targets.browser.js bindings-api=src/bindings-api/targets.browser.js html-imports=src/html-imports/targets.browser.js html-bindings=src/html-bindings/targets.browser.js html-namespaces=src/html-namespaces/targets.browser.js scoped-js=src/scoped-js/targets.browser.js scoped-css=src/scoped-css/targets.browser.js --bundle --minify --sourcemap --outdir=dist",
33
33
  "preversion": "npm run build && git add -A dist",
34
34
  "postversion": "npm publish",
35
35
  "postpublish": "git push && git push --tags"
36
36
  },
37
37
  "dependencies": {
38
38
  "@webqit/observer": "^2.2.9",
39
- "@webqit/realdom": "^2.1.17",
40
- "@webqit/stateful-js": "file:../stateful-js",
39
+ "@webqit/realdom": "^2.1.19",
40
+ "@webqit/stateful-js": "^3.0.27",
41
41
  "@webqit/util": "^0.8.11"
42
42
  },
43
43
  "devDependencies": {
44
- "@webqit/oohtml-ssr": "^1.2.16",
44
+ "@webqit/oohtml-ssr": "^1.2.17",
45
45
  "chai": "^4.3.4",
46
46
  "coveralls": "^3.1.1",
47
47
  "esbuild": "^0.14.43",
@@ -0,0 +1,233 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import Observer from '@webqit/observer';
6
+ import _HTMLBindingsProvider from '../bindings-api/_HTMLBindingsProvider.js';
7
+ import { StatefulAsyncFunction } from '@webqit/stateful-js/async';
8
+ import { _, _init } from '../util.js';
9
+
10
+ /**
11
+ * Initializes DOM Parts.
12
+ *
13
+ * @param $config Object
14
+ *
15
+ * @return Void
16
+ */
17
+ export default function init( $config = {} ) {
18
+ const { config, window } = _init.call( this, 'html-bindings', $config, {
19
+ attr: { bindings: 'bindings', itemIndex: 'data-index' },
20
+ tokens: { nodeType: 'processing-instruction', tagStart: '?{', tagEnd: '}?', stateStart: '; [=', stateEnd: ']' },
21
+ staticsensitivity: true,
22
+ isomorphic: true,
23
+ } );
24
+ config.CONTEXT_API = window.webqit.oohtml.configs.CONTEXT_API;
25
+ config.BINDINGS_API = window.webqit.oohtml.configs.BINDINGS_API;
26
+ config.HTML_IMPORTS = window.webqit.oohtml.configs.HTML_IMPORTS;
27
+ config.attrSelector = `[${ window.CSS.escape( config.attr.bindings ) }]`;
28
+ const discreteBindingsMatch = ( start, end ) => {
29
+ const starting = `starts-with(., "${ start }")`;
30
+ const ending = `substring(., string-length(.) - string-length("${ end }") + 1) = "${ end }"`;
31
+ return `${ starting } and ${ ending }`;
32
+ }
33
+ config.discreteBindingsSelector = `comment()[${ discreteBindingsMatch( config.tokens.tagStart, config.tokens.tagEnd ) }]`;
34
+ window.webqit.Observer = Observer;
35
+ realtime.call( window, config );
36
+ }
37
+
38
+ /**
39
+ * Performs realtime capture of elements and their attributes
40
+ *
41
+ * @param Object config
42
+ *
43
+ * @return Void
44
+ */
45
+ function realtime( config ) {
46
+ const window = this, { realdom } = window.webqit;
47
+ // ----------------
48
+ realdom.realtime( window.document ).subtree( `(${ config.discreteBindingsSelector })`, record => {
49
+ cleanup.call( this, ...record.exits );
50
+ mountDiscreteBindings.call( this, config, ...record.entrants );
51
+ }, { live: true } );
52
+ realdom.realtime( window.document ).subtree( config.attrSelector, record => {
53
+ cleanup.call( this, ...record.exits );
54
+ mountInlineBindings.call( this, config, ...record.entrants );
55
+ }, { live: true, timing: 'sync', staticSensitivity: config.staticsensitivity } );
56
+ }
57
+
58
+ function createDynamicScope( config, root ) {
59
+ if ( _( root ).has( 'htBindings' ) ) return _( root ).get( 'htBindings' );
60
+ const scope = {}, abortController = new AbortController;
61
+ scope.$set__ = function( node, prop, val ) {
62
+ node && ( node[ prop ] = val );
63
+ }
64
+ Observer.intercept( scope, {
65
+ get: ( e, recieved, next ) => {
66
+ if ( !( e.key in scope ) ) {
67
+ const request = _HTMLBindingsProvider.createRequest( { detail: e.key, live: true, signal: abortController.signal } );
68
+ root[ config.CONTEXT_API.api.context ].request( request, value => {
69
+ Observer.set( scope, e.key, value );
70
+ } );
71
+ }
72
+ return next( scope[ e.key ] ?? ( e.key in globalThis ? globalThis[ e.key ] : undefined ) );
73
+ },
74
+ has: ( e, recieved, next ) => { return next( true ); }
75
+ } );
76
+ const instance = { scope, abortController, htBindings: new Map };
77
+ _( root ).set( 'htBindings', instance );
78
+ return instance;
79
+ }
80
+
81
+ function cleanup( ...entries ) {
82
+ for ( const node of entries ) {
83
+ const root = node.nodeName === '#text' ? node.parentNode : node;
84
+ const { htBindings, abortController } = _( root ).get( 'htBindings' ) || {};
85
+ if ( !htBindings?.has( node ) ) return;
86
+ htBindings.get( node ).state.dispose();
87
+ htBindings.get( node ).signals.forEach( s => s.abort() );
88
+ htBindings.delete( node );
89
+ if ( !htBindings.size ) {
90
+ abortController.abort();
91
+ _( root ).delete( 'htBindings' );
92
+ }
93
+ }
94
+ }
95
+
96
+ async function mountDiscreteBindings( config, ...entries ) {
97
+ const window = this;
98
+ const patternMatch = str => {
99
+ const tagStart = config.tokens.tagStart.split( '' ).map( x => `\\${ x }` ).join( '' );
100
+ const tagEnd = config.tokens.tagEnd.split( '' ).map( x => `\\${ x }` ).join( '' );
101
+ const stateStart = config.tokens.stateStart.split( '' ).map( x => x === ' ' ? `(?:\\s+)?` : `\\${ x }` ).join( '' );
102
+ const stateEnd = config.tokens.stateEnd.split( '' ).map( x => `\\${ x }` ).join( '' );
103
+ const pattern = `^${ tagStart }(.*?)(?:${ stateStart }(\\d+)${ stateEnd }(?:\\s+)?)?${ tagEnd }$`;
104
+ const [ /*raw*/, expr, span ] = str.match( new RegExp( pattern ) );
105
+ return { raw: str, expr, span: parseInt( span ?? 0 ) };
106
+ };
107
+
108
+ const instances = entries.reduce( ( instances, node ) => {
109
+ if ( node.isBound ) return instances;
110
+ const template = patternMatch( node.nodeValue );
111
+ let textNode = node;
112
+ if ( template.span ) {
113
+ textNode = node.nextSibling;
114
+ if ( textNode?.nodeName !== '#text' || textNode.nodeValue.length < template.span ) return instances;
115
+ if ( textNode.nodeValue.length > template.span ) { textNode.splitText( template.span ); }
116
+ } else {
117
+ textNode = node.ownerDocument.createTextNode( '' );
118
+ node.after( textNode );
119
+ }
120
+ textNode.isBound = true;
121
+ let anchorNode = node;
122
+ if ( window.webqit.env !== 'server' ) {
123
+ anchorNode.remove();
124
+ anchorNode = null;
125
+ }
126
+ return instances.concat( { textNode, template, anchorNode } );
127
+ }, [] );
128
+
129
+ for ( const { textNode, template, anchorNode } of instances ) {
130
+ const { scope: env, htBindings } = createDynamicScope( config, textNode.parentNode );
131
+ let source = '';
132
+ source += `let content = ((${ template.expr }) ?? '') + '';`;
133
+ source += `$set__(this, 'nodeValue', content);`;
134
+ if ( anchorNode ) { source += `$set__($anchorNode__, 'nodeValue', \`${ config.tokens.tagStart }${ template.expr }${ config.tokens.stateStart }\` + content.length + \`${ config.tokens.stateEnd } ${ config.tokens.tagEnd }\`);`; }
135
+ const compiled = new StatefulAsyncFunction( '$signals__', `$anchorNode__`, source, { env } );
136
+ const signals = [];
137
+ htBindings.set( textNode, { compiled, signals, state: await compiled.call( textNode, signals, anchorNode ), } );
138
+ }
139
+ }
140
+
141
+ async function mountInlineBindings( config, ...entries ) {
142
+ for ( const node of entries ) {
143
+ const source = parseInlineBindings( config, node.getAttribute( config.attr.bindings ) );
144
+ const { scope: env, htBindings } = createDynamicScope( config, node );
145
+ const compiled = new StatefulAsyncFunction( '$signals__', source, { env } );
146
+ const signals = [];
147
+ htBindings.set( node, { compiled, signals, state: await compiled.call( node, signals ), } );
148
+ }
149
+ }
150
+
151
+ const parseCache = new Map;
152
+ function parseInlineBindings( config, str ) {
153
+ if ( parseCache.has( str ) ) return parseCache.get( str );
154
+ const validation = {};
155
+ const source = splitOuter( str, ';' ).map( str => {
156
+ const [ left, right ] = splitOuter( str, ':' ).map( x => x.trim() );
157
+ const token = left[ 0 ], param = left.slice( 1 ).trim();
158
+ const $expr = `(${ right })`, $$expr = `(${ $expr } ?? '')`;
159
+ if ( token === '&' ) return `this.style[\`${ param }\`] = ${ $$expr };`;
160
+ if ( token === '%' ) return `this.classList.toggle(\`${ param }\`, !!${ $expr });`;
161
+ if ( token === '~' ) {
162
+ if ( param.endsWith( '?' ) ) return `this.toggleAttribute(\`${ param.substring( 0, -1 ).trim() }\`, !!${ $expr });`;
163
+ return `this.setAttribute(\`${ param }\`, ${ $$expr });`;
164
+ }
165
+ if ( token === '@' ) {
166
+ if ( validation[ param ] ) throw new Error( `Duplicate binding: ${ left }.` );
167
+ validation[ param ] = true;
168
+ if ( param === 'text' ) return `$set__(this, 'textContent', ${ $$expr });`;
169
+ if ( param === 'html' ) return `this.setHTML(${ $$expr });`;
170
+ if ( param === 'items' ) {
171
+ const [ iterationSpec, importSpec ] = splitOuter( right, '/' );
172
+ if ( !importSpec ) throw new Error( `Invalid ${ token }items spec: ${ str }; no import specifier.` );
173
+ let [ raw, production, kind, iteratee ] = iterationSpec.trim().match( /(.*?[\)\s+])(of|in)([\(\{\[\s+].*)/i ) || [];
174
+ if ( !raw ) throw new Error( `Invalid ${ token }items spec: ${ str }.` );
175
+ if ( production.startsWith( '(' ) ) {
176
+ production = production.trim().slice( 1, -1 ).split( ',' ).map( x => x.trim() );
177
+ } else { production = [ production ]; }
178
+ if ( production.length > ( kind === 'in' ? 3 : 2 ) ) throw new Error( `Invalid ${ token }items spec: ${ str }.` );
179
+ const indices = kind === 'in' ? production[ 2 ] : ( production[ 1 ] || '$index__' );
180
+ return `
181
+ let $iteratee__ = ${ iteratee };
182
+ let $import__ = this.${ config.HTML_IMPORTS.context.api.import }( ${ importSpec.trim() }, true );
183
+ $signals__.push( $import__ );
184
+
185
+ if ( $import__.value && $iteratee__ ) {
186
+ let $existing__ = new Map;
187
+ this.querySelectorAll( '[${ config.attr.itemIndex }]' ).forEach( x => {
188
+ $existing__.set( x.getAttribute( '${ config.attr.itemIndex }' ), x );
189
+ } );
190
+ ${ indices ? `let ${ indices } = -1;` : '' }
191
+ for ( let ${ production[ 0 ] } ${ kind } $iteratee__ ) {
192
+ ${ indices ? `${ indices } ++;` : '' }
193
+ ${ kind === 'in' && production[ 1 ] ? `let ${ production[ 1 ] } = $iteratee__[ ${ production[ 0 ] } ];` : '' }
194
+ const $itemBinding__ = { ${ production.join( ', ' ) } };
195
+
196
+ const $key___ = ( ${ kind === 'in' ? production[ 0 ] : indices } ) + '';
197
+ let $itemNode__ = $existing__.get( $key___ );
198
+ if ( $itemNode__ ) {
199
+ $existing__.delete( $key___ );
200
+ } else {
201
+ $itemNode__ = ( Array.isArray( $import__.value ) ? $import__.value[ 0 ] : ( $import__.value instanceof window.HTMLTemplateElement ? $import__.value.content.firstElementChild : $import__.value ) ).cloneNode( true );
202
+ $itemNode__.setAttribute( "${ config.attr.itemIndex }", $key___ );
203
+ this.appendChild( $itemNode__ );
204
+ }
205
+
206
+ $itemNode__.${ config.BINDINGS_API.api.bind }( $itemBinding__ );
207
+ if ( ${ kind === 'in' ? `!( ${ production[ 0 ] } in $iteratee__ )` : `typeof ${ production[ 0 ] } === 'undefined'` } ) { $itemNode__.remove(); }
208
+ }
209
+ $existing__.forEach( x => x.remove() );
210
+ $existing__.clear();
211
+ }`;
212
+ }
213
+ }
214
+ if ( str.trim() ) throw new Error( `Invalid binding: ${ str }.` );
215
+ } ).join( `\n` );
216
+ parseCache.set( str, source );
217
+ return source;
218
+ }
219
+
220
+ export function splitOuter( str, delim ) {
221
+ return [ ...str ].reduce( ( [ quote, depth, splits, skip ], x ) => {
222
+ if ( !quote && depth === 0 && ( Array.isArray( delim ) ? delim : [ delim ] ).includes( x ) ) {
223
+ return [ quote, depth, [ '' ].concat( splits ) ];
224
+ }
225
+ if ( !quote && [ '(', '[', '{' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) depth++;
226
+ if ( !quote && [ ')', ']', '}' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) depth--;
227
+ if ( [ '"', "'", '`' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) {
228
+ quote = quote === x ? null : ( quote || x );
229
+ }
230
+ splits[ 0 ] += x;
231
+ return [ quote, depth, splits ]
232
+ }, [ null, 0, [ '' ] ] )[ 2 ].reverse();
233
+ }
@@ -0,0 +1,217 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import _HTMLImportsContext from './_HTMLImportsProvider.js';
6
+ import { _ } from '../util.js';
7
+
8
+ /**
9
+ * Creates the HTMLImportElement class.
10
+ *
11
+ * @param Object config
12
+ *
13
+ * @return HTMLImportElement
14
+ */
15
+ export default function( config ) {
16
+ const window = this, { realdom } = window.webqit;
17
+ const BaseElement = config.import.tagName.includes( '-' ) ? window.HTMLElement : class {};
18
+ class HTMLImportElement extends BaseElement {
19
+
20
+ /**
21
+ * @instance
22
+ *
23
+ * @param HTMLElement node
24
+ *
25
+ * @returns
26
+ */
27
+ static instance( node ) {
28
+ if ( config.import.tagName.includes( '-' ) && ( node instanceof this ) ) return node;
29
+ return _( node ).get( 'import::instance' ) || new this( node );
30
+ }
31
+
32
+ /**
33
+ * @constructor
34
+ */
35
+ constructor( ...args ) {
36
+ super();
37
+ // --------
38
+ const el = args[ 0 ] || this;
39
+ _( el ).set( 'import::instance', this );
40
+ Object.defineProperty( this, 'el', { get: () => el, configurable: false } );
41
+
42
+ const priv = {};
43
+ Object.defineProperty( this, '#', { get: () => priv, configurable: false } );
44
+ priv.slottedElements = new Set;
45
+
46
+ priv.setAnchorNode = anchorNode => {
47
+ priv.anchorNode = anchorNode;
48
+ _( anchorNode ).set( 'anchoredNode@imports', this.el );
49
+ };
50
+
51
+ priv.importRequest = ( callback, signal = null ) => {
52
+ const request = _HTMLImportsContext.createRequest( { detail: priv.moduleRef && !priv.moduleRef.includes( '#' ) ? priv.moduleRef + '#' : priv.moduleRef, live: signal && true, signal } );
53
+ ( this.el.isConnected ? this.el.parentNode : priv.anchorNode.parentNode )[ config.CONTEXT_API.api.context ].request( request, response => {
54
+ callback( ( response instanceof window.HTMLTemplateElement ? [ ...response.content.children ] : (
55
+ Array.isArray( response ) ? response : response && [ response ]
56
+ ) ) || [] );
57
+ } );
58
+ };
59
+
60
+ priv.hydrate = ( anchorNode, slottedElements ) => {
61
+ // ----------------
62
+ priv.moduleRef = ( this.el.getAttribute( config.import.attr.moduleref ) || '' ).trim();
63
+ priv.setAnchorNode( anchorNode );
64
+ priv.autoRestore( () => {
65
+ slottedElements.forEach( slottedElement => {
66
+ priv.slottedElements.add( slottedElement );
67
+ _( slottedElement ).set( 'slot@imports', this.el );
68
+ } );
69
+ } );
70
+ // ----------------
71
+ priv.hydrationImportRequest = new AbortController;
72
+ priv.importRequest( fragments => {
73
+ if ( priv.originalsRemapped ) { return this.fill( fragments ); }
74
+ const identifiersMap = fragments.map( fragment => ( { el: fragment, fragmentDef: fragment.getAttribute( config.template.attr.fragmentdef ) || '', tagName: fragment.tagName, } ) );
75
+ slottedElements.forEach( slottedElement => {
76
+ const tagName = slottedElement.tagName, fragmentDef = slottedElement.getAttribute( config.template.attr.fragmentdef ) || '';
77
+ const originalsMatch = identifiersMap.filter( fragmentIdentifiers => tagName === fragmentIdentifiers.tagName && fragmentDef === fragmentIdentifiers.fragmentDef );
78
+ if ( originalsMatch.length !== 1 ) return;
79
+ _( slottedElement ).set( 'original@imports', originalsMatch[ 0 ].el );
80
+ } );
81
+ priv.originalsRemapped = true;
82
+ }, priv.hydrationImportRequest.signal );
83
+ };
84
+
85
+ priv.autoRestore = ( callback = null ) => {
86
+ priv.autoRestoreRealtime?.disconnect();
87
+ if ( callback ) callback();
88
+ if ( !priv.slottedElements.size ) {
89
+ priv.anchorNode.replaceWith( this.el );
90
+ return;
91
+ }
92
+ const autoRestoreRealtime = realdom.realtime( window.document ).observe( [ ...priv.slottedElements ], record => {
93
+ record.exits.forEach( outgoingNode => {
94
+ _( outgoingNode ).delete( 'slot@imports' );
95
+ priv.slottedElements.delete( outgoingNode );
96
+ } );
97
+ if ( !priv.slottedElements.size ) {
98
+ autoRestoreRealtime.disconnect();
99
+ // At this point, ignore if this is a removal involving the whole parent node
100
+ if ( !record.target.isConnected ) return;
101
+ priv.anchorNode.replaceWith( this.el );
102
+ }
103
+ }, { subtree: true, timing: 'sync', generation: 'exits' } );
104
+ priv.autoRestoreRealtime = autoRestoreRealtime;
105
+ };
106
+
107
+ priv.connectedCallback = () => {
108
+ // In case this is DOM node relocation or induced reinsertion into the DOM
109
+ if ( priv.slottedElements.size ) throw new Error( `Illegal reinsertion into the DOM; import slot is not empty!` );
110
+ // Totally initialize this instance?
111
+ if ( !priv.anchorNode ) { priv.setAnchorNode( this.createAnchorNode() ); }
112
+ if ( priv.moduleRefRealtime ) return;
113
+ priv.moduleRefRealtime = realdom.realtime( this.el ).attr( config.import.attr.moduleref, ( record, { signal } ) => {
114
+ priv.moduleRef = record.value;
115
+ // Below, we ignore first restore from hydration
116
+ priv.importRequest( fragments => !priv.hydrationImportRequest && this.fill( fragments ), signal );
117
+ }, { live: true, timing: 'sync', lifecycleSignals: true } );
118
+ // Must come after
119
+ priv.hydrationImportRequest?.abort();
120
+ priv.hydrationImportRequest = null;
121
+ };
122
+
123
+ priv.disconnectedCallback = () => {
124
+ priv.hydrationImportRequest?.abort();
125
+ priv.hydrationImportRequest = null;
126
+ if ( priv.anchorNode.isConnected ) return;
127
+ priv.moduleRefRealtime?.disconnect();
128
+ priv.moduleRefRealtime = null;
129
+ };
130
+ }
131
+
132
+ /**
133
+ * Creates the slot's anchor node.
134
+ *
135
+ * @return Element
136
+ */
137
+ createAnchorNode() {
138
+ if ( !config.isomorphic ) { return window.document.createTextNode( '' ) }
139
+ return window.document.createComment( this.el.outerHTML );
140
+ }
141
+
142
+ /**
143
+ * Fills the slot with slottableElements
144
+ *
145
+ * @param Iterable slottableElements
146
+ *
147
+ * @return void
148
+ */
149
+ fill( slottableElements ) {
150
+ if ( Array.isArray( slottableElements ) ) { slottableElements = new Set( slottableElements ) }
151
+ this[ '#' ].autoRestore( () => {
152
+ this[ '#' ].slottedElements.forEach( slottedElement => {
153
+ const slottedElementOriginal = _( slottedElement ).get( 'original@imports' );
154
+ // If still available in source, simply leave unchanged
155
+ // otherwise remove it from slot... to reflect this change
156
+ if ( slottableElements.has( slottedElementOriginal ) ) {
157
+ slottableElements.delete( slottedElementOriginal );
158
+ } else {
159
+ this[ '#' ].slottedElements.delete( slottedElement );
160
+ // This removal will not be caught
161
+ slottedElement.remove();
162
+ }
163
+ } );
164
+ // Make sure anchor node is what's in place...
165
+ // not the import element itslef - but all only when we have slottableElements.size
166
+ if ( this.el.isConnected && slottableElements.size ) {
167
+ this.el.replaceWith( this[ '#' ].anchorNode );
168
+ }
169
+ // Insert slottables now
170
+ slottableElements.forEach( slottableElement => {
171
+ // Clone each slottable element and give it a reference to its original
172
+ const slottableElementClone = slottableElement.cloneNode( true );
173
+ // The folllowing references must be set before adding to DODM
174
+ if ( !slottableElementClone.hasAttribute( config.template.attr.fragmentdef ) ) {
175
+ slottableElementClone.toggleAttribute( config.template.attr.fragmentdef, true );
176
+ }
177
+ _( slottableElementClone ).set( 'original@imports', slottableElement );
178
+ _( slottableElementClone ).set( 'slot@imports', this.el );
179
+ this[ '#' ].slottedElements.add( slottableElementClone );
180
+ this[ '#' ].anchorNode.before( slottableElementClone );
181
+ } );
182
+ } );
183
+ }
184
+
185
+ /**
186
+ * Empty slot.
187
+ *
188
+ * @return void
189
+ */
190
+ empty() { this[ '#' ].slottedElements.forEach( slottedElement => slottedElement.remove() ); }
191
+
192
+ /**
193
+ * Returns the slot's anchorNode.
194
+ *
195
+ * @return array
196
+ */
197
+ get anchorNode() { return this[ '#' ].anchorNode; }
198
+
199
+ /**
200
+ * Returns the slot's module reference, if any.
201
+ *
202
+ * @return string
203
+ */
204
+ get moduleRef() { return this[ '#' ].moduleRef; }
205
+
206
+ /**
207
+ * Returns the slot's slotted elements.
208
+ *
209
+ * @return array
210
+ */
211
+ get slottedElements() { return this[ '#' ].slottedElements; }
212
+ }
213
+ if ( config.import.tagName.includes( '-' ) ) {
214
+ customElements.define( config.import.tagName, HTMLImportElement );
215
+ }
216
+ return HTMLImportElement;
217
+ }
@@ -2,7 +2,6 @@
2
2
  /**
3
3
  * @imports
4
4
  */
5
- import { HTMLContext } from '../context-api/index.js';
6
5
  import _HTMLImportsContext from './_HTMLImportsProvider.js';
7
6
  import { _ } from '../util.js';
8
7
 
@@ -46,12 +45,12 @@ export default function( config ) {
46
45
 
47
46
  priv.setAnchorNode = anchorNode => {
48
47
  priv.anchorNode = anchorNode;
49
- _( anchorNode ).set( 'anchoredNode@imports', this.el );
48
+ return anchorNode;
50
49
  };
51
50
 
52
51
  priv.importRequest = ( callback, signal = null ) => {
53
52
  const request = _HTMLImportsContext.createRequest( { detail: priv.moduleRef && !priv.moduleRef.includes( '#' ) ? priv.moduleRef + '#' : priv.moduleRef, live: signal && true, signal } );
54
- HTMLContext.instance( this.el.isConnected ? this.el.parentNode : priv.anchorNode.parentNode ).request( request, response => {
53
+ ( this.el.isConnected ? this.el.parentNode : priv.anchorNode.parentNode )[ config.CONTEXT_API.api.context ].request( request, response => {
55
54
  callback( ( response instanceof window.HTMLTemplateElement ? [ ...response.content.children ] : (
56
55
  Array.isArray( response ) ? response : response && [ response ]
57
56
  ) ) || [] );
@@ -61,7 +60,7 @@ export default function( config ) {
61
60
  priv.hydrate = ( anchorNode, slottedElements ) => {
62
61
  // ----------------
63
62
  priv.moduleRef = ( this.el.getAttribute( config.import.attr.moduleref ) || '' ).trim();
64
- priv.setAnchorNode( anchorNode );
63
+ anchorNode.replaceWith( priv.setAnchorNode( this.createAnchorNode() ) );
65
64
  priv.autoRestore( () => {
66
65
  slottedElements.forEach( slottedElement => {
67
66
  priv.slottedElements.add( slottedElement );
@@ -72,12 +71,13 @@ export default function( config ) {
72
71
  priv.hydrationImportRequest = new AbortController;
73
72
  priv.importRequest( fragments => {
74
73
  if ( priv.originalsRemapped ) { return this.fill( fragments ); }
75
- const identifiersMap = fragments.map( fragment => ( { el: fragment, fragmentDef: fragment.getAttribute( config.template.attr.fragmentdef ) || '', tagName: fragment.tagName, } ) );
74
+ const identifiersMap = fragments.map( ( fragment, i ) => ( { el: fragment, fragmentDef: fragment.getAttribute( config.template.attr.fragmentdef ) || '', tagName: fragment.tagName, i } ) );
75
+ let i = -1;
76
76
  slottedElements.forEach( slottedElement => {
77
77
  const tagName = slottedElement.tagName, fragmentDef = slottedElement.getAttribute( config.template.attr.fragmentdef ) || '';
78
- const originalsMatch = identifiersMap.filter( fragmentIdentifiers => tagName === fragmentIdentifiers.tagName && fragmentDef === fragmentIdentifiers.fragmentDef );
79
- if ( originalsMatch.length !== 1 ) return;
80
- _( slottedElement ).set( 'original@imports', originalsMatch[ 0 ].el );
78
+ const originalsMatch = ( i ++, identifiersMap.find( fragmentIdentifiers => fragmentIdentifiers.tagName === tagName && fragmentIdentifiers.fragmentDef === fragmentDef && fragmentIdentifiers.i === i ) );
79
+ if ( !originalsMatch ) return; // Or should we throw integrity error?
80
+ _( slottedElement ).set( 'original@imports', originalsMatch.el );
81
81
  } );
82
82
  priv.originalsRemapped = true;
83
83
  }, priv.hydrationImportRequest.signal );
@@ -86,10 +86,12 @@ export default function( config ) {
86
86
  priv.autoRestore = ( callback = null ) => {
87
87
  priv.autoRestoreRealtime?.disconnect();
88
88
  if ( callback ) callback();
89
- if ( !priv.slottedElements.size ) {
89
+ const restore = () => {
90
90
  priv.anchorNode.replaceWith( this.el );
91
- return;
92
- }
91
+ priv.anchorNode = null;
92
+ this.el.setAttribute( 'data-nodecount', 0 );
93
+ };
94
+ if ( !priv.slottedElements.size ) return restore();
93
95
  const autoRestoreRealtime = realdom.realtime( window.document ).observe( [ ...priv.slottedElements ], record => {
94
96
  record.exits.forEach( outgoingNode => {
95
97
  _( outgoingNode ).delete( 'slot@imports' );
@@ -99,7 +101,7 @@ export default function( config ) {
99
101
  autoRestoreRealtime.disconnect();
100
102
  // At this point, ignore if this is a removal involving the whole parent node
101
103
  if ( !record.target.isConnected ) return;
102
- priv.anchorNode.replaceWith( this.el );
104
+ restore();
103
105
  }
104
106
  }, { subtree: true, timing: 'sync', generation: 'exits' } );
105
107
  priv.autoRestoreRealtime = autoRestoreRealtime;
@@ -109,7 +111,6 @@ export default function( config ) {
109
111
  // In case this is DOM node relocation or induced reinsertion into the DOM
110
112
  if ( priv.slottedElements.size ) throw new Error( `Illegal reinsertion into the DOM; import slot is not empty!` );
111
113
  // Totally initialize this instance?
112
- if ( !priv.anchorNode ) { priv.setAnchorNode( this.createAnchorNode() ); }
113
114
  if ( priv.moduleRefRealtime ) return;
114
115
  priv.moduleRefRealtime = realdom.realtime( this.el ).attr( config.import.attr.moduleref, ( record, { signal } ) => {
115
116
  priv.moduleRef = record.value;
@@ -136,8 +137,12 @@ export default function( config ) {
136
137
  * @return Element
137
138
  */
138
139
  createAnchorNode() {
139
- if ( !config.isomorphic ) { return window.document.createTextNode( '' ) }
140
- return window.document.createComment( this.el.outerHTML );
140
+ if ( window.webqit.env !== 'server' ) { return window.document.createTextNode( '' ) }
141
+ const escapeElement = window.document.createElement( 'div' );
142
+ escapeElement.textContent = this.el.outerHTML;
143
+ const anchorNode = window.document.createComment( escapeElement.innerHTML );
144
+ _( anchorNode ).set( 'isAnchorNode', true );
145
+ return anchorNode;
141
146
  }
142
147
 
143
148
  /**
@@ -149,6 +154,8 @@ export default function( config ) {
149
154
  */
150
155
  fill( slottableElements ) {
151
156
  if ( Array.isArray( slottableElements ) ) { slottableElements = new Set( slottableElements ) }
157
+ // This state must be set before the diffing below and the serialization done at createAnchorNode()
158
+ this.el.setAttribute( 'data-nodecount', slottableElements.size );
152
159
  this[ '#' ].autoRestore( () => {
153
160
  this[ '#' ].slottedElements.forEach( slottedElement => {
154
161
  const slottedElementOriginal = _( slottedElement ).get( 'original@imports' );
@@ -164,8 +171,10 @@ export default function( config ) {
164
171
  } );
165
172
  // Make sure anchor node is what's in place...
166
173
  // not the import element itslef - but all only when we have slottableElements.size
167
- if ( this.el.isConnected && slottableElements.size ) {
168
- this.el.replaceWith( this[ '#' ].anchorNode );
174
+ if ( slottableElements.size ) {
175
+ const currentAnchorNode = this[ '#' ].anchorNode;
176
+ const anchorNode = this[ '#' ].setAnchorNode( this.createAnchorNode() );
177
+ ( this.el.isConnected ? this.el : currentAnchorNode ).replaceWith( anchorNode );
169
178
  }
170
179
  // Insert slottables now
171
180
  slottableElements.forEach( slottableElement => {
@@ -3,7 +3,7 @@
3
3
  * @imports
4
4
  */
5
5
  import Observer from '@webqit/observer';
6
- import { HTMLContext, HTMLContextProvider } from '../context-api/index.js';
6
+ import { HTMLContextProvider } from '../context-api/index.js';
7
7
  import { getModulesObject } from './index.js';
8
8
  import { _ } from '../util.js';
9
9
 
@@ -87,7 +87,7 @@ export default class _HTMLImportsProvider extends HTMLContextProvider {
87
87
  }
88
88
  // This superModules contextrequest is automatically aborted by the injected signal below
89
89
  const request = this.constructor.createRequest( { detail: record.value.trim(), live: true, signal, superContextOnly: true } );
90
- HTMLContext.instance( this.host ).request( request, response => {
90
+ this.host[ $config.CONTEXT_API.api.context ].request( request, response => {
91
91
  this.contextModules = !( response && Object.getPrototypeOf( response ) ) ? response : getModulesObject( response );
92
92
  update();
93
93
  } );