@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/dist/bindings-api.js +1 -1
- package/dist/bindings-api.js.map +2 -2
- package/dist/context-api.js +1 -1
- package/dist/context-api.js.map +2 -2
- package/dist/html-bindings.js +54 -0
- package/dist/html-bindings.js.map +7 -0
- package/dist/html-imports.js +1 -1
- package/dist/html-imports.js.map +3 -3
- package/dist/html-namespaces.js +1 -1
- package/dist/html-namespaces.js.map +2 -2
- package/dist/main.js +38 -6
- package/dist/main.js.map +3 -3
- package/dist/scoped-css.js +1 -1
- package/dist/scoped-css.js.map +2 -2
- package/dist/scoped-js.js +4 -4
- package/dist/scoped-js.js.map +3 -3
- package/package.json +5 -5
- package/src/html-bindings/index.js +233 -0
- package/src/html-imports/_HTMLImportElement copy.js +217 -0
- package/src/html-imports/_HTMLImportElement.js +26 -17
- package/src/html-imports/_HTMLImportsProvider.js +2 -2
- package/src/html-imports/index.js +25 -44
- package/src/index.js +4 -4
- package/src/scoped-js/index.js +2 -2
- package/dist/html-bracelets.js +0 -2
- package/dist/html-bracelets.js.map +0 -7
- package/src/html-bracelets/AttrBracelet.js +0 -122
- package/src/html-bracelets/Bracelet.js +0 -82
- package/src/html-bracelets/HTMLBracelets.js +0 -68
- package/src/html-bracelets/TextBracelet.js +0 -69
- package/src/html-bracelets/index.js +0 -72
- /package/src/{html-bracelets → html-bindings}/targets.browser.js +0 -0
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.
|
|
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-
|
|
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.
|
|
40
|
-
"@webqit/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.
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
79
|
-
if ( originalsMatch
|
|
80
|
-
_( slottedElement ).set( 'original@imports', originalsMatch
|
|
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
|
-
|
|
89
|
+
const restore = () => {
|
|
90
90
|
priv.anchorNode.replaceWith( this.el );
|
|
91
|
-
|
|
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
|
-
|
|
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 (
|
|
140
|
-
|
|
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 (
|
|
168
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
} );
|