@webqit/oohtml 1.10.3 → 2.0.0
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/.gitignore +3 -3
- package/LICENSE +20 -20
- package/README.md +399 -396
- package/dist/context-api.js +2 -0
- package/dist/context-api.js.map +7 -0
- package/dist/html-imports.js +1 -2
- package/dist/html-imports.js.map +3 -3
- package/dist/html-modules.js +1 -2
- package/dist/html-modules.js.map +3 -3
- package/dist/main.js +26 -14
- package/dist/main.js.map +3 -3
- package/dist/namespaced-html.js +1 -2
- package/dist/namespaced-html.js.map +3 -3
- package/dist/scoped-js.js +27 -0
- package/dist/scoped-js.js.map +7 -0
- package/dist/state-api.js +1 -2
- package/dist/state-api.js.map +3 -3
- package/package.json +76 -76
- package/src/context-api/HTMLContext.js +158 -0
- package/src/context-api/HTMLContextManager.js +77 -0
- package/src/context-api/_ContextRequestEvent.js +26 -0
- package/src/context-api/index.js +53 -0
- package/src/{namespaced-html/browser-entry.js → context-api/targets.browser.js} +9 -9
- package/src/html-imports/_HTMLImportElement.js +216 -0
- package/src/html-imports/index.js +92 -557
- package/src/{browser-entry.js → html-imports/targets.browser.js} +10 -10
- package/src/html-modules/HTMLExportsManager.js +191 -0
- package/src/html-modules/_HTMLImportsContext.js +114 -0
- package/src/html-modules/index.js +133 -384
- package/src/{html-imports/browser-entry.js → html-modules/targets.browser.js} +9 -9
- package/src/index.js +34 -39
- package/src/namespaced-html/index.js +130 -144
- package/src/namespaced-html/targets.browser.js +10 -0
- package/src/scoped-js/index.js +382 -0
- package/src/scoped-js/targets.browser.js +10 -0
- package/src/state-api/index.js +55 -142
- package/src/state-api/targets.browser.js +10 -0
- package/src/{html-modules/browser-entry.js → targets.browser.js} +10 -10
- package/src/util.js +20 -180
- package/test/imports.test.js +194 -0
- package/test/index.js +119 -0
- package/test/modules.test.js +198 -0
- package/test/namespaced-html.test.js +50 -0
- package/test/scoped-js.js +57 -0
- package/test/state-api.test.js +34 -0
- package/test/test.html +69 -0
- package/dist/subscript.js +0 -15
- package/dist/subscript.js.map +0 -7
- package/src/state-api/browser-entry.js +0 -10
- package/src/subscript/Element.js +0 -103
- package/src/subscript/browser-entry.js +0 -10
- package/src/subscript/index.js +0 -70
- package/test/all.test.js +0 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @imports
|
|
4
|
+
*/
|
|
5
|
+
import Observer from '@webqit/observer';
|
|
6
|
+
import { getModulesObject } from './index.js';
|
|
7
|
+
import { _ } from '../util.js';
|
|
8
|
+
|
|
9
|
+
export default class HTMLExportsManager {
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* @instance
|
|
13
|
+
*/
|
|
14
|
+
static instance( host, params ) {
|
|
15
|
+
return _( host ).get( 'exportsmanager::instance' ) || new this( host, params );;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @constructor
|
|
20
|
+
*/
|
|
21
|
+
constructor( host, params, parent = null, level = 0 ) {
|
|
22
|
+
_( host ).get( `exportsmanager::instance` )?.dispose();
|
|
23
|
+
_( host ).set( `exportsmanager::instance`, this );
|
|
24
|
+
this.host = host;
|
|
25
|
+
this.params = params;
|
|
26
|
+
this.parent = parent;
|
|
27
|
+
this.level = level;
|
|
28
|
+
this.modules = getModulesObject( this.host );
|
|
29
|
+
this.exportId = ( this.host.getAttribute( this.params.template.attr.exportid ) || '' ).trim();
|
|
30
|
+
this.validateExportId( this.exportId );
|
|
31
|
+
const dom = params.window.wq.dom;
|
|
32
|
+
// ----------
|
|
33
|
+
this.realtimeA = dom.realtime( this.host.content ).children( record => {
|
|
34
|
+
this.export( record.entrants, true );
|
|
35
|
+
this.export( record.exits, false );
|
|
36
|
+
}, { live: true, timing: 'sync' } );
|
|
37
|
+
// ----------
|
|
38
|
+
this.realtimeB = dom.realtime( this.host ).attr( [ 'src', 'loading' ], ( ...args ) => this.evaluateLoading( ...args ), {
|
|
39
|
+
live: true,
|
|
40
|
+
atomic: true,
|
|
41
|
+
timing: 'sync',
|
|
42
|
+
lifecycleSignals: true
|
|
43
|
+
} );
|
|
44
|
+
// ----------
|
|
45
|
+
this.realtimeC = this.evalInheritance();
|
|
46
|
+
// ----------
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validates export ID.
|
|
51
|
+
*
|
|
52
|
+
* @param String exportId
|
|
53
|
+
*
|
|
54
|
+
* @returns Void
|
|
55
|
+
*/
|
|
56
|
+
validateExportId( exportId ) {
|
|
57
|
+
if ( [ '@', '#', ':' ].some( token => exportId.includes( token ) ) ) {
|
|
58
|
+
throw new Error( `The export ID "${ exportId }" contains an invalid character.` );
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Maps module contents as exports.
|
|
64
|
+
*
|
|
65
|
+
* @param Array entries
|
|
66
|
+
* @param Bool isConnected
|
|
67
|
+
*
|
|
68
|
+
* @returns Void
|
|
69
|
+
*/
|
|
70
|
+
export( entries, isConnected ) {
|
|
71
|
+
const fragmentsExports = new Map;
|
|
72
|
+
entries.forEach( entry => {
|
|
73
|
+
if ( entry.nodeType !== 1 ) return;
|
|
74
|
+
if ( entry.matches( this.params.templateSelector ) ) {
|
|
75
|
+
if ( isConnected ) {
|
|
76
|
+
const moduleExport = new HTMLExportsManager( entry, this.params, this.host, this.level + 1 );
|
|
77
|
+
if ( moduleExport.exportId ) { Observer.set( this.modules, moduleExport.exportId, entry ); }
|
|
78
|
+
} else {
|
|
79
|
+
const moduleExport = HTMLModulesGraph.instance( entry, this.params );
|
|
80
|
+
if ( moduleExport.exportId ) { Observer.deleteProperty( this.modules, moduleExport.exportId ); }
|
|
81
|
+
moduleExport.dispose();
|
|
82
|
+
}
|
|
83
|
+
} else {
|
|
84
|
+
const exportId = ( entry.getAttribute( this.params.export.attr.exportid ) || '' ).trim() || 'default';
|
|
85
|
+
this.validateExportId( exportId );
|
|
86
|
+
if ( !fragmentsExports.has( exportId ) ) { fragmentsExports.set( exportId, [] ); }
|
|
87
|
+
fragmentsExports.get( exportId ).push( entry );
|
|
88
|
+
}
|
|
89
|
+
} );
|
|
90
|
+
// ----------------
|
|
91
|
+
fragmentsExports.forEach( ( fragments, exportId ) => {
|
|
92
|
+
let existingFragments = Observer.get( this.modules, `#${ exportId }` );
|
|
93
|
+
if ( isConnected ) {
|
|
94
|
+
existingFragments = new Set( ( existingFragments ? [ ...existingFragments ] : [] ).concat( fragments ) );
|
|
95
|
+
} else if ( existingFragments ) {
|
|
96
|
+
fragments.forEach( el => existingFragments.delete( el ) );
|
|
97
|
+
}
|
|
98
|
+
if ( !isConnected && !existingFragments.size ) { Observer.deleteProperty( this.modules, `#${ exportId }` ); }
|
|
99
|
+
else { Observer.set( this.modules, `#${ exportId }`, existingFragments ) }
|
|
100
|
+
} );
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Evaluates remote content loading.
|
|
105
|
+
*
|
|
106
|
+
* @param AbortSignal signal
|
|
107
|
+
*
|
|
108
|
+
* @returns Void
|
|
109
|
+
*/
|
|
110
|
+
evaluateLoading( [ record1, record2 ], { signal } ) {
|
|
111
|
+
const src = ( record1.value || '' ).trim();
|
|
112
|
+
if ( !src ) return;
|
|
113
|
+
const loading = ( record2.value || '' ).trim();
|
|
114
|
+
if ( loading === 'lazy' ) {
|
|
115
|
+
const interception = Observer.intercept( this.modules, 'get', async ( descriptor, recieved, next ) => {
|
|
116
|
+
await this.load( src, true );
|
|
117
|
+
interception.remove();
|
|
118
|
+
return next();
|
|
119
|
+
}, { signal } );
|
|
120
|
+
} else { this.load( src ); }
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Fetches a module's "src".
|
|
125
|
+
*
|
|
126
|
+
* @param String src
|
|
127
|
+
*
|
|
128
|
+
* @return Promise
|
|
129
|
+
*/
|
|
130
|
+
load( src ) {
|
|
131
|
+
const window = this.params.window;
|
|
132
|
+
if ( this.host.content.children.length ) return;
|
|
133
|
+
// Ongoing request?
|
|
134
|
+
if ( this.fetchInFlight?.src === src ) return this.fetchInFlight.request;
|
|
135
|
+
this.fetchInFlight?.controller.abort();
|
|
136
|
+
// The promise
|
|
137
|
+
const controller = new AbortController();
|
|
138
|
+
const fire = ( type, detail ) => this.host.dispatchEvent( new window.CustomEvent( type, { detail } ) );
|
|
139
|
+
const request = window.fetch( src, { signal: controller.signal } ).then( response => {
|
|
140
|
+
return response.ok ? response.text() : Promise.reject( response.statusText );
|
|
141
|
+
}).then( content => {
|
|
142
|
+
this.host.innerHTML = content.trim(); // IMPORTANT: .trim()
|
|
143
|
+
fire( 'load' );
|
|
144
|
+
return this.host;
|
|
145
|
+
} ).catch( e => {
|
|
146
|
+
console.error( `Error fetching the bundle at "${ src }": ${ e.message }` );
|
|
147
|
+
this.fetchInFlight = null;
|
|
148
|
+
fire( 'loaderror' );
|
|
149
|
+
return this.host;
|
|
150
|
+
} );
|
|
151
|
+
this.fetchInFlight = { src, request, controller };
|
|
152
|
+
return request;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Evaluates module inheritance.
|
|
157
|
+
*
|
|
158
|
+
* @returns Void|AbortController
|
|
159
|
+
*/
|
|
160
|
+
evalInheritance( ) {
|
|
161
|
+
let inheritedIds;
|
|
162
|
+
if ( this.parent && ( inheritedIds = ( this.host.getAttribute( this.params.template.attr.inherits ) || '' ).trim() )
|
|
163
|
+
&& ( inheritedIds = inheritedIds.split( ' ' ).map( id => id.trim() ) ).length ) {
|
|
164
|
+
const parentExportsObj = getModulesObject( this.parent );
|
|
165
|
+
return Observer.get( parentExportsObj, inheritedIds, records => {
|
|
166
|
+
records.forEach( record => {
|
|
167
|
+
if ( [ 'get'/*initial get*/, 'set', 'defineProperty' ].includes( record.type ) ) {
|
|
168
|
+
Observer[ record.type.replace( 'get', 'set' ) ]( this.modules, record.key, record.value );
|
|
169
|
+
} else if ( record.type === 'deleteProperty' ) {
|
|
170
|
+
Observer.deleteProperty( this.modules, record.key );
|
|
171
|
+
}
|
|
172
|
+
} );
|
|
173
|
+
}, { live: true } );
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Disposes the instance and its processes.
|
|
179
|
+
*
|
|
180
|
+
* @returns Void
|
|
181
|
+
*/
|
|
182
|
+
dispose() {
|
|
183
|
+
this.realtimeA.disconnect();
|
|
184
|
+
this.realtimeB.disconnect();
|
|
185
|
+
this.realtimeC?.abort();
|
|
186
|
+
Object.entries( this.modules ).forEach( ( [ key, entry ] ) => {
|
|
187
|
+
if ( key.startsWith( '#' ) ) return;
|
|
188
|
+
HTMLExportsManager.instance( entry ).dispose();
|
|
189
|
+
} );
|
|
190
|
+
}
|
|
191
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @imports
|
|
4
|
+
*/
|
|
5
|
+
import Observer from '@webqit/observer';
|
|
6
|
+
import { HTMLContextManager, HTMLContext } from '../context-api/index.js';
|
|
7
|
+
import { getModulesObject } from './index.js';
|
|
8
|
+
import { _ } from '../util.js';
|
|
9
|
+
|
|
10
|
+
export default class _HTMLImportsContext extends HTMLContext {
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @createRequest
|
|
14
|
+
*/
|
|
15
|
+
static createRequest( fields = {} ) {
|
|
16
|
+
const request = { type: 'HTMLModules', ...fields };
|
|
17
|
+
if ( !request.name && request.detail?.startsWith( '/' ) ) { request.name = 'root'; }
|
|
18
|
+
else if ( request.detail?.startsWith( '@' ) ) {
|
|
19
|
+
const [ contextName, detail ] = request.detail.split( ':' ).map( s => s.trim() );
|
|
20
|
+
request.name = contextName.slice( 1 );
|
|
21
|
+
request.detail = detail;
|
|
22
|
+
}
|
|
23
|
+
return request;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @modules
|
|
28
|
+
*/
|
|
29
|
+
get modules() {
|
|
30
|
+
return getModulesObject( this.host );
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @handle()
|
|
35
|
+
*/
|
|
36
|
+
handle( event ) {
|
|
37
|
+
// Any existing event.request.controller? Abort!
|
|
38
|
+
event.request.controller?.abort();
|
|
39
|
+
|
|
40
|
+
// Parse and translate detail
|
|
41
|
+
if ( ( event.request.detail || '' ).trim() === '/' ) return event.respondWith( this.modules );
|
|
42
|
+
const $params = this.constructor.params;
|
|
43
|
+
let path = ( event.request.detail || '' ).split( /\/|(?<=\w)(?=\W)/g ).map( x => x.trim() ).filter( x => x );
|
|
44
|
+
if ( path.length ) { path = path.join( `/${ $params.template.api.modules }/` )?.split( '/' ) || []; }
|
|
45
|
+
// No detail?
|
|
46
|
+
if ( !path.length ) return event.respondWith();
|
|
47
|
+
|
|
48
|
+
// We'll now fulfill request
|
|
49
|
+
const params = { live: event.request.live, descripted: true, midwayResults: true };
|
|
50
|
+
// Find a way to resolve request against two sources
|
|
51
|
+
event.request.controller = Observer.deep( this.modules, path, Observer.get, ( result, { signal } = {} ) => {
|
|
52
|
+
if ( !result.value && this.host.isConnected === false ) return; // Subtree is being disposed
|
|
53
|
+
if ( result.value || !this.altModules ) return event.respondWith( result.value );
|
|
54
|
+
// This superModules binding is automatically aborted by the injected control.signal; see below
|
|
55
|
+
return Observer.deep( this.altModules, path, Observer.get, result => {
|
|
56
|
+
return event.respondWith( result.value );
|
|
57
|
+
}, { signal, ...params } );
|
|
58
|
+
}, params );
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* @startRealtime()
|
|
63
|
+
*/
|
|
64
|
+
realtimeSources( host ) {
|
|
65
|
+
this.host = host;
|
|
66
|
+
// ----------------
|
|
67
|
+
const update = () => {
|
|
68
|
+
for ( const subscriptionEvent of this.subscriptions ) {
|
|
69
|
+
this.handle( subscriptionEvent );
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
// ----------------
|
|
73
|
+
const $params = this.constructor.params;
|
|
74
|
+
if ( !this.host.matches || !$params.context.attr.importscontext ) return;
|
|
75
|
+
// Any existing this.refdSourceController? Abort!
|
|
76
|
+
this.refdSourceController?.disconnect();
|
|
77
|
+
const dom = this.host.ownerDocument.defaultView.wq.dom;
|
|
78
|
+
this.refdSourceController = dom.realtime( this.host ).attr( $params.context.attr.importscontext, ( record, { signal } ) => {
|
|
79
|
+
// No importscontext attr set. But we're still watching
|
|
80
|
+
if ( !record.value ) {
|
|
81
|
+
this.altModules = undefined;
|
|
82
|
+
return update();
|
|
83
|
+
}
|
|
84
|
+
// This superModules contextrequest is automatically aborted by the injected signal below
|
|
85
|
+
const request = this.constructor.createRequest( { detail: record.value.trim(), live: true, signal } );
|
|
86
|
+
HTMLContextManager.instance( this.host ).ask( request, response => {
|
|
87
|
+
this.altModules = !( response && Object.getPrototypeOf( response ) ) ? response : getModulesObject( response );
|
|
88
|
+
update();
|
|
89
|
+
} );
|
|
90
|
+
}, { live: true, timing: 'sync', lifecycleSignals: true } );
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* @initialize()
|
|
95
|
+
*/
|
|
96
|
+
initialize( host ) {
|
|
97
|
+
// If host has importscontext attr, compute that
|
|
98
|
+
this.realtimeSources( host );
|
|
99
|
+
// Now, listen for contextrequest and contextclaim events
|
|
100
|
+
// And process own claim
|
|
101
|
+
return super.initialize( host );
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @dispose()
|
|
106
|
+
*/
|
|
107
|
+
dispose( host ) {
|
|
108
|
+
// Stop listening for sources
|
|
109
|
+
this.refdSourceController?.disconnect();
|
|
110
|
+
// Now, stop listening for contextrequest and contextclaim events
|
|
111
|
+
// And relinquish own subscribers to owner context
|
|
112
|
+
return super.dispose( host );
|
|
113
|
+
}
|
|
114
|
+
}
|