@webqit/oohtml 3.1.13 → 3.1.15

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.
@@ -2,7 +2,8 @@
2
2
  /**
3
3
  * @imports
4
4
  */
5
- import { _, _init } from '../util.js';
5
+ import DOMNamingContext from './DOMNamingContext.js';
6
+ import { _, _init, _splitOuter, _fromHash, _toHash } from '../util.js';
6
7
 
7
8
  /**
8
9
  * @init
@@ -11,16 +12,109 @@ import { _, _init } from '../util.js';
11
12
  */
12
13
  export default function init( $config = {} ) {
13
14
  const { config, window } = _init.call( this, 'namespaced-html', $config, {
14
- attr: { namespace: 'namespace', id: 'id', },
15
- api: { namespace: 'namespace', eagermode: true, },
15
+ attr: { namespace: 'namespace', lid: 'id', },
16
+ api: { namespace: 'namespace', },
17
+ tokens: { lidrefPrefix: '~', lidrefSeparator: ':' },
16
18
  target: { attr: ':target', event: ':target', scrolling: true },
17
19
  } );
18
- config.idSelector = `[${ window.CSS.escape( config.attr.id ) }]`;
20
+ config.lidSelector = `[${ window.CSS.escape( config.attr.lid ) }]`;
19
21
  config.namespaceSelector = `[${ window.CSS.escape( config.attr.namespace ) }]`;
22
+ window.webqit.DOMNamingContext = DOMNamingContext;
20
23
  exposeAPIs.call( window, config );
21
24
  realtime.call( window, config );
22
25
  }
23
26
 
27
+ /**
28
+ * @init
29
+ *
30
+ * @param Object config
31
+ *
32
+ * @return String
33
+ */
34
+ function lidUtil( config ) {
35
+ const { lidrefPrefix, lidrefSeparator, } = config.tokens;
36
+ return {
37
+ escape( str, mode = 1 ) { return [ ...str ].map( x => !/\w/.test( x ) ? ( mode === 2 ? `\\\\${ x }` : `\\${ x }` ) : x ).join( '' ); },
38
+ lidrefPrefix( escapeMode = 0 ) { return escapeMode ? this.escape( lidrefPrefix, escapeMode ) : lidrefPrefix; },
39
+ lidrefSeparator( escapeMode = 0 ) { return escapeMode ? this.escape( lidrefSeparator, escapeMode ) : lidrefSeparator; },
40
+ isUuid( str, escapeMode = 0 ) { return str.startsWith( this.lidrefPrefix( escapeMode ) ) && str.includes( this.lidrefSeparator( escapeMode ) ); },
41
+ isLidref( str, escapeMode = 0 ) { return str.startsWith( this.lidrefPrefix( escapeMode ) ) && !str.includes( this.lidrefSeparator( escapeMode ) ); },
42
+ toUuid( hash, lid, escapeMode = 0 ) { return `${ this.lidrefPrefix( escapeMode ) }${ hash }${ this.lidrefSeparator( escapeMode ) }${ lid }`; },
43
+ uuidToLid( str, escapeMode = 0 ) { return this.isUuid( str ) ? str.split( this.lidrefSeparator( escapeMode ) )[ 1 ] : str; },
44
+ uuidToLidref( str, escapeMode = 0 ) { return this.isUuid( str ) ? `${ this.lidrefPrefix( escapeMode ) }${ str.split( this.lidrefSeparator( escapeMode ) )[ 1 ] }` : str; },
45
+ }
46
+ }
47
+
48
+ /**
49
+ * @rewriteSelector
50
+ *
51
+ * @param String selectorText
52
+ * @param String namespaceUUID
53
+ * @param String scopeSelector
54
+ * @param Bool escapeMode
55
+ *
56
+ * @return String
57
+ */
58
+ export function rewriteSelector( selectorText, namespaceUUID, scopeSelector = null, escapeMode = 0 ) {
59
+ const window = this, { webqit: { oohtml: { configs: { NAMESPACED_HTML: config } } } } = window;
60
+ const $lidUtil = lidUtil( config );
61
+ // Match :scope and relative ID selector
62
+ const regex = new RegExp( `${ scopeSelector ? `:scope|` : '' }#(${ $lidUtil.lidrefPrefix( escapeMode + 1 ) })?([\\w]+${ $lidUtil.lidrefSeparator( escapeMode + 1 ) })?((?:[\\w-]|\\\\.)+)`, 'g' );
63
+ // Parse potentially combined selectors individually and categorise into categories per whether they have :scope or not
64
+ const [ cat1, cat2 ] = _splitOuter( selectorText, ',' ).reduce( ( [ cat1, cat2 ], selector ) => {
65
+ // The deal: match and replace
66
+ let quotesMatch, hadScopeSelector;
67
+ selector = selector.replace( regex, ( match, lidrefPrefixMatch, lidrefSeparatorMatch, id, index ) => {
68
+ if ( !quotesMatch ) { // Lazy: stuff
69
+ // Match strings between quotes (single or double) and use that qualify matches above
70
+ // The string: String.raw`She said, "Hello, John. I\"m your friend." or "you're he're" 'f\'"j\'"f'jfjf`;
71
+ // Should yield: `"Hello, John. I\\"m your friend."`, `"you're he're"`, `'f\\'"j\\'"f'`
72
+ quotesMatch = [ ...selector.matchAll( /(["'])(?:(?=(\\?))\2.)*?\1/g ) ];
73
+ }
74
+ if ( quotesMatch[ 0 ] )
75
+ // Qualify match
76
+ if ( quotesMatch.some( q => index > q.index && index + match.length < q.index + q[ 0 ].length ) ) return match;
77
+ // Replace :scope
78
+ if ( match === ':scope' ) {
79
+ hadScopeSelector = true;
80
+ return scopeSelector;
81
+ }
82
+ const isLidref = lidrefPrefixMatch && !lidrefSeparatorMatch;
83
+ const isUuid = lidrefPrefixMatch && lidrefSeparatorMatch;
84
+ if ( isUuid ) {
85
+ return `#${ $lidUtil.escape( match.replace( '#', '' ), 1 ) }`;
86
+ }
87
+ // Rewrite relative ID selector
88
+ let lowerBoundFactor = false;
89
+ if ( isLidref ) {
90
+ if ( config.attr.lid === 'id' && namespaceUUID ) {
91
+ return `#${ $lidUtil.toUuid( namespaceUUID, id, 1 ) }`;
92
+ }
93
+ // Fallback to attr-based
94
+ lowerBoundFactor = true;
95
+ }
96
+ // Rewrite absolute ID selector
97
+ let rewrite;
98
+ if ( config.attr.lid === 'id' ) {
99
+ rewrite = `[id^="${ $lidUtil.lidrefPrefix( escapeMode ) }"][id$="${ $lidUtil.lidrefSeparator( escapeMode ) }${ id }"]`;
100
+ } else {
101
+ rewrite = `:is(#${ id },[${ window.CSS.escape( config.attr.lid ) }="${ id }"])`;
102
+ }
103
+ return scopeSelector && lowerBoundFactor ? `:is(${ rewrite }):not(${ scopeSelector } [${ config.attr.namespace }] *)` : rewrite;
104
+ } );
105
+ // Category 2 has :scope and category 1 does not
106
+ return hadScopeSelector ? [ cat1, cat2.concat( selector ) ] : [ cat1.concat( selector ), cat2 ];
107
+ }, [ [], [] ] );
108
+ // Do the upgrade
109
+ let newSelectorText;
110
+ if ( scopeSelector && cat1.length ) {
111
+ newSelectorText = [ cat1.length > 1 ? `${ scopeSelector } :is(${ cat1.join( ', ' ) })` : `${ scopeSelector } ${ cat1[ 0 ] }`, cat2.join( ', ' ) ].filter( x => x ).join( ', ' );
112
+ } else {
113
+ newSelectorText = [ ...cat1, ...cat2 ].join( ', ' );
114
+ }
115
+ return newSelectorText;
116
+ }
117
+
24
118
  /**
25
119
  * Returns the "namespace" object associated with the given node.
26
120
  *
@@ -28,29 +122,25 @@ export default function init( $config = {} ) {
28
122
  *
29
123
  * @return Object
30
124
  */
31
- function getNamespaceObject( node, config ) {
32
- const window = this, { webqit: { Observer } } = window;
125
+ export function getNamespaceObject( node ) {
33
126
  if ( !_( node ).has( 'namespace' ) ) {
34
127
  const namespaceObj = Object.create( null );
35
- Observer.intercept( namespaceObj, 'get', ( event, receiver, next ) => {
36
- if ( Observer.has( namespaceObj, event.key ) || !config.api.eagermode ) return next();
37
- const selector = `[${ window.CSS.escape( config.attr.id ) }="${ event.key }"]`;
38
- const resultNode = Array.from( node.querySelectorAll( selector ) ).filter( idNode => {
39
- const ownerRoot = idNode.parentNode.closest( config.namespaceSelector );
40
- if ( node === window.document ) {
41
- // Only IDs without a scope actually belong to the document scope
42
- return !ownerRoot;
43
- }
44
- return ownerRoot === node;
45
- } )[ 0 ];
46
- if ( resultNode ) Observer.set( namespaceObj, event.key, resultNode );
47
- return next();
48
- } );
49
128
  _( node ).set( 'namespace', namespaceObj );
50
129
  }
51
130
  return _( node ).get( 'namespace' );
52
131
  }
53
132
 
133
+ /**
134
+ * @param Element node
135
+ *
136
+ * @return String
137
+ */
138
+ export function getNamespaceUUID( node ) {
139
+ const window = this, { webqit: { oohtml: { configs: { NAMESPACED_HTML: config } } } } = window;
140
+ const namespaceObj = getNamespaceObject( node instanceof window.Document ? node : ( node.closest( `[${ config.attr.namespace }]` ) || node.ownerDocument ) );
141
+ return _fromHash( namespaceObj ) || _toHash( namespaceObj );
142
+ }
143
+
54
144
  /**
55
145
  * Exposes Namespaced HTML with native APIs.
56
146
  *
@@ -65,10 +155,10 @@ function exposeAPIs( config ) {
65
155
  if ( config.api.namespace in window.Element.prototype ) { throw new Error( `The "Element" class already has a "${ config.api.namespace }" property!` ); }
66
156
  // Definitions
67
157
  Object.defineProperty( window.document, config.api.namespace, { get: function() {
68
- return Observer.proxy( getNamespaceObject.call( window, window.document, config ) );
158
+ return Observer.proxy( getNamespaceObject.call( window, window.document ) );
69
159
  } });
70
160
  Object.defineProperty( window.Element.prototype, config.api.namespace, { get: function() {
71
- return Observer.proxy( getNamespaceObject.call( window, this, config ) );
161
+ return Observer.proxy( getNamespaceObject.call( window, this ) );
72
162
  } } );
73
163
  }
74
164
 
@@ -80,50 +170,185 @@ function exposeAPIs( config ) {
80
170
  * @return Void
81
171
  */
82
172
  function realtime( config ) {
83
- const window = this, { webqit: { Observer, realdom } } = window;
84
- // ----------------
85
- const handle = ( target, entry, incoming ) => {
86
- const identifier = entry.getAttribute( config.attr.id );
87
- const ownerRoot = target.closest( config.namespaceSelector ) || _( entry ).get( 'ownerNamespace' ) || window.document;
88
- const namespaceObj = getNamespaceObject.call( window, ownerRoot, config );
89
- if ( incoming ) {
90
- if ( Observer.get( namespaceObj, identifier ) !== entry ) {
91
- _( entry ).set( 'ownerNamespace', ownerRoot );
92
- Observer.set( namespaceObj, identifier, entry );
173
+ const window = this, { webqit: { Observer, realdom, oohtml: { configs }, DOMNamingContext } } = window;
174
+
175
+ // ------------
176
+ // NAMESPACE
177
+ // ------------
178
+ window.document[ configs.CONTEXT_API.api.contexts ].attach( new DOMNamingContext );
179
+ realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.namespaceSelector, record => {
180
+ record.exits.forEach( entry => {
181
+ const contextsApi = entry[ configs.CONTEXT_API.api.contexts ];
182
+ const ctx = contextsApi.find( DOMNamingContext.kind );
183
+ if ( ctx ) { contextsApi.detach( ctx ); }
184
+ } );
185
+ record.entrants.forEach( entry => {
186
+ const contextsApi = entry[ configs.CONTEXT_API.api.contexts ];
187
+ if ( !contextsApi.find( DOMNamingContext.kind ) ) {
188
+ contextsApi.attach( new DOMNamingContext );
93
189
  }
94
- } else if ( Observer.get( namespaceObj, identifier ) === entry ) {
95
- _( entry ).delete( 'ownerNamespace' );
96
- Observer.deleteProperty( namespaceObj, identifier );
190
+ } );
191
+ }, { live: true, timing: 'sync', staticSensitivity: true } );
192
+
193
+ // ------------
194
+ // APIS
195
+ // ------------
196
+ // See https://wicg.github.io/aom/aria-reflection-explainer.html & https://github.com/whatwg/html/issues/3515 for the ARIA refelction properties idea
197
+ // See https://www.w3.org/TR/wai-aria-1.1/#attrs_relationships for the relational ARIA attributes
198
+ const idRefsAttrs = [ 'aria-owns', 'aria-controls', 'aria-labelledby', 'aria-describedby', 'aria-flowto', ];
199
+ const idRefAttrs = [ 'for', 'list', 'form', 'aria-activedescendant', 'aria-details', 'aria-errormessage', ];
200
+ const attrList = [ config.attr.lid, ...idRefsAttrs, ...idRefAttrs ];
201
+ const relMap = { id: 'id'/* just in case it's in attrList */, for: 'htmlFor', 'aria-owns': 'ariaOwns', 'aria-controls': 'ariaControls', 'aria-labelledby': 'ariaLabelledBy', 'aria-describedby': 'ariaDescribedBy', 'aria-flowto': 'ariaFlowto', 'aria-activedescendant': 'ariaActiveDescendant', 'aria-details': 'ariaDetails', 'aria-errormessage': 'ariaErrorMessage' };
202
+ const $lidUtil = lidUtil( config );
203
+
204
+ // Intercept getAttribute()
205
+ const getAttributeDescr = Object.getOwnPropertyDescriptor( window.Element.prototype, 'getAttribute' );
206
+ Object.defineProperty( window.Element.prototype, 'getAttribute', { ...getAttributeDescr, value( attrName ) {
207
+ const value = getAttributeDescr.value.call( this, attrName );
208
+ return !_( this, 'lock' ).get( attrName ) && attrList.includes( attrName ) ? ( attrName === 'id' ? $lidUtil.uuidToLid : $lidUtil.uuidToLidref ).call( $lidUtil, value ) : value;
209
+ } } );
210
+ // Intercept getElementById()
211
+ const getElementByIdDescr = Object.getOwnPropertyDescriptor( window.Document.prototype, 'getElementById' );
212
+ Object.defineProperty( window.Document.prototype, 'getElementById', { ...getElementByIdDescr, value( id ) {
213
+ return this.querySelector( `#${ id }`/* Will be rewritten at querySelector() */ );
214
+ } } );
215
+ // Intercept querySelector() and querySelectorAll()
216
+ for ( const queryApi of [ 'querySelector', 'querySelectorAll' ] ) {
217
+ for ( nodeApi of [ window.Document, window.Element ] ) {
218
+ const querySelectorDescr = Object.getOwnPropertyDescriptor( nodeApi.prototype, queryApi );
219
+ Object.defineProperty( nodeApi.prototype, queryApi, { ...querySelectorDescr, value( selector ) {
220
+ return querySelectorDescr.value.call( this, rewriteSelector.call( window, selector, getNamespaceUUID.call( window, this ) ) );
221
+ } } );
97
222
  }
223
+ }
224
+ // These APIs should return LIDREFS minus the hash part
225
+ for ( const attrName of attrList ) {
226
+ if ( !( attrName in relMap ) ) continue;
227
+ const domApis = attrName === 'for' ? [ window.HTMLLabelElement, window.HTMLOutputElement ] : [ window.Element ];
228
+ for ( const domApi of domApis ) {
229
+ const propertyDescr = Object.getOwnPropertyDescriptor( domApi.prototype, relMap[ attrName ] );
230
+ if ( !propertyDescr ) continue;
231
+ Object.defineProperty( domApi.prototype, relMap[ attrName ], { ...propertyDescr, get() {
232
+ return ( attrName === 'id' ? $lidUtil.uuidToLid : $lidUtil.uuidToLidref ).call( $lidUtil, propertyDescr.get.call( this, attrName ) || '' );
233
+ } } );
234
+ }
235
+ }
236
+ // Hide implementation details on the Attr node too.
237
+ const propertyDescr = Object.getOwnPropertyDescriptor( window.Attr.prototype, 'value' );
238
+ Object.defineProperty( window.Attr.prototype, 'value', { ...propertyDescr, get() {
239
+ const value = propertyDescr.get.call( this );
240
+ return attrList.includes( this.name ) ? ( this.name === 'id' ? $lidUtil.uuidToLid : $lidUtil.uuidToLidref ).call( $lidUtil, value ) : value;
241
+ } } );
242
+ if ( config.attr.lid !== 'id' ) {
243
+ // Reflect the custom [config.attr.lid] attribute
244
+ Object.defineProperty( window.Element.prototype, config.attr.lid, { configurable: true, enumerable: true, get() {
245
+ return this.getAttribute( config.attr.lid );
246
+ }, set( value ) {
247
+ return this.setAttribute( config.attr.lid, value );
248
+ } } );
249
+ }
250
+
251
+ // ------------
252
+ // LOCAL IDS & IDREFS
253
+ // ------------
254
+ const cleanupBinding = ( entry, attrName, oldValue ) => {
255
+ // Create or honour locking
256
+ if ( _( entry, 'lock' ).get( attrName ) ) return;
257
+ _( entry, 'lock' ).set( attrName, true );
258
+ // A function can be passed in to be called after having done the _( entry, 'lock' ).set( attrName, true ); flag
259
+ if ( typeof oldValue === 'function' ) oldValue = oldValue();
260
+ // Get down to work
261
+ const namespaceObj = _( entry ).get( 'ownerNamespace' );
262
+ if ( attrName === config.attr.lid ) {
263
+ const lid = $lidUtil.uuidToLid( oldValue );
264
+ if ( Observer.get( namespaceObj, lid ) === entry ) { Observer.deleteProperty( namespaceObj, lid ); }
265
+ } else {
266
+ const newAttrValue = oldValue.split( ' ' ).map( lid => ( lid = lid.trim() ) && $lidUtil.uuidToLidref( lid ) ).join( ' ' );
267
+ entry.setAttribute( attrName, newAttrValue );
268
+ }
269
+ // Release locking
270
+ _( entry, 'lock' ).delete( attrName );
98
271
  };
99
- realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.idSelector, record => {
100
- record.entrants.forEach( entry => handle( record.target, entry, true ) );
101
- record.exits.forEach( entry => handle( record.target, entry, false ) );
102
- }, { live: true, timing: 'sync', staticSensitivity: true } );
103
- // ----------------
104
- if ( true/*config.staticsensitivity*/ ) {
105
- realdom.realtime( window.document, 'attr' ).observe( config.attr.namespace, record => {
106
- const ownerRoot = record.target.parentNode?.closest( config.namespaceSelector ) || _( record.target ).get( 'ownerNamespace' ) || window.document;
107
- const ownerRootNamespaceObj = getNamespaceObject.call( window, ownerRoot, config );
108
- const namespaceObj = getNamespaceObject.call( window, record.target, config );
109
- if ( record.target.matches( config.namespaceSelector ) ) {
110
- for ( const [ key, entry ] of Object.entries( ownerRootNamespaceObj ) ) {
111
- if ( !record.target.contains( entry.parentNode ) ) continue;
112
- Observer.deleteProperty( ownerRootNamespaceObj, key );
113
- Observer.set( namespaceObj, key, entry );
114
- }
115
- } else {
116
- for ( const [ key, entry ] of Object.entries( namespaceObj ) ) {
117
- Observer.deleteProperty( namespaceObj, key );
118
- Observer.set( ownerRootNamespaceObj, key, entry );
119
- }
272
+ const setupBinding = ( entry, attrName, value ) => {
273
+ // Create or honour locking
274
+ if ( _( entry, 'lock' ).get( attrName ) ) return;
275
+ _( entry, 'lock' ).set( attrName, true );
276
+ // A function can be passed in to be called after having done the _( entry, 'lock' ).set( attrName, true ); flag
277
+ if ( typeof value === 'function' ) value = value();
278
+ // Get down to work
279
+ const namespaceObj = _( entry ).get( 'ownerNamespace' );
280
+ const namespaceUUID = _( entry ).get( 'namespaceUUID' );
281
+ if ( attrName === config.attr.lid ) {
282
+ const lid = $lidUtil.uuidToLid( value );
283
+ if ( Observer.get( namespaceObj, lid ) !== entry ) {
284
+ // Setup new namespace relationships
285
+ entry.setAttribute( 'id', $lidUtil.toUuid( namespaceUUID, lid ) );
286
+ Observer.set( namespaceObj, lid, entry );
120
287
  }
121
- }, { subtree: true, timing: 'sync' } );
122
- }
123
- // ----------------
288
+ } else {
289
+ const newAttrValue = value.split( ' ' ).map( lid => ( lid = lid.trim() ) && !$lidUtil.isLidref( lid ) ? lid : $lidUtil.toUuid( namespaceUUID, lid.replace( $lidUtil.lidrefPrefix(), '' ) ) ).join( ' ' );
290
+ entry.setAttribute( attrName, newAttrValue );
291
+ }
292
+ // Release locking
293
+ _( entry, 'lock' ).delete( attrName );
294
+ };
295
+
296
+ const cleanupAllBindings = ( entry, total = true ) => {
297
+ for ( const attrName of attrList ) {
298
+ if ( !entry.hasAttribute( attrName ) ) continue;
299
+ cleanupBinding( entry, attrName, () => entry.getAttribute( attrName ) );
300
+ }
301
+ _( entry ).delete( 'ownerNamespace' );
302
+ _( entry ).delete( 'namespaceUUID' );
303
+ if ( total ) {
304
+ _( entry ).get( 'namespaceBinding' ).abort();
305
+ _( entry ).delete( 'namespaceBinding' );
306
+ }
307
+ };
308
+ const setupAllBindings = ( entry ) => {
309
+ if ( !_( entry ).get( 'ownerNamespace' ) ) {
310
+ const request = { ...DOMNamingContext.createRequest(), live: true };
311
+ const binding = entry.parentNode[ configs.CONTEXT_API.api.contexts ].request( request, namespaceObj => {
312
+ // Cleanup of previous namespace?
313
+ if ( _( entry ).get( 'namespaceUUID' ) ) {
314
+ cleanupAllBindings( entry, false );
315
+ }
316
+ // Setup new namespace
317
+ _( entry ).set( 'ownerNamespace', namespaceObj );
318
+ _( entry ).set( 'namespaceUUID', _fromHash( namespaceObj ) || _toHash( namespaceObj ) );
319
+ setupAllBindings( entry );
320
+ } );
321
+ _( entry ).set( 'namespaceBinding', binding );
322
+ return;
323
+ }
324
+ for ( const attrName of attrList ) {
325
+ if ( !entry.hasAttribute( attrName ) ) continue;
326
+ setupBinding( entry, attrName, () => entry.getAttribute( attrName ) );
327
+ }
328
+ };
329
+
330
+ // DOM realtime
331
+ realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( `[${ attrList.map( attrName => window.CSS.escape( attrName ) ).join( '],[' ) }]`, record => {
332
+ record.exits.forEach( cleanupAllBindings );
333
+ record.entrants.forEach( setupAllBindings );
334
+ }, { live: true, timing: 'sync' } );
335
+ // Attr realtime
336
+ realdom.realtime( window.document, 'attr' ).observe( attrList, records => {
337
+ for ( const record of records ) {
338
+ if ( record.oldValue ) {
339
+ cleanupBinding( record.target, record.name, record.oldValue );
340
+ if ( record.value ) { setupBinding( record.target, record.name, record.value ); }
341
+ } else { setupAllBindings( entry ); }
342
+ }
343
+ }, { subtree: true, timing: 'sync', newValue: true, oldValue: true } );
344
+
345
+ // ------------
346
+ // TARGETS
347
+ // ------------
124
348
  let prevTarget;
125
349
  const activateTarget = () => {
126
- const path = window.location.hash?.substring( 1 ).split( '/' ).map( s => s.trim() ).filter( s => s ) || [];
350
+ if ( !window.location.hash?.startsWith( `#${ $lidUtil.lidrefPrefix() }` ) ) return;
351
+ const path = window.location.hash?.substring( `#${ $lidUtil.lidrefPrefix() }`.length ).split( '/' ).map( s => s.trim() ).filter( s => s ) || [];
127
352
  const currTarget = path.reduce( ( prev, segment ) => prev && prev[ config.api.namespace ][ segment ], window.document );
128
353
  if ( prevTarget && config.target.attr ) { prevTarget.toggleAttribute( config.target.attr, false ); }
129
354
  if ( currTarget && currTarget !== window.document ) {
@@ -133,6 +358,8 @@ function realtime( config ) {
133
358
  prevTarget = currTarget;
134
359
  }
135
360
  };
361
+ // "hash" realtime
136
362
  window.addEventListener( 'hashchange', activateTarget );
137
363
  realdom.ready( activateTarget );
364
+ // ----------------
138
365
  }
@@ -2,24 +2,51 @@
2
2
  /**
3
3
  * @imports
4
4
  */
5
- import { _init } from '../util.js';
5
+ import { rewriteSelector, getNamespaceUUID } from '../namespaced-html/index.js';
6
+ import { _init, _toHash } from '../util.js';
6
7
 
7
8
  /**
8
9
  * @init
9
10
  *
10
11
  * @param Object $config
11
12
  */
12
- export default function init( { advanced = {}, ...$config } ) {
13
+ export default function init({ advanced = {}, ...$config }) {
13
14
  const { config, window } = _init.call( this, 'scoped-css', $config, {
15
+ api: { styleSheets: 'styleSheets' },
14
16
  style: { retention: 'retain', mimeType: '', strategy: null },
15
17
  } );
16
- config.styleSelector = ( Array.isArray( config.style.mimeType ) ? config.style.mimeType : [ config.style.mimeType ] ).reduce( ( selector, mm ) => {
18
+ config.styleSelector = (Array.isArray( config.style.mimeType ) ? config.style.mimeType : [ config.style.mimeType ] ).reduce( ( selector, mm ) => {
17
19
  const qualifier = mm ? `[type=${ window.CSS.escape( mm ) }]` : '';
18
20
  return selector.concat( `style${ qualifier }[scoped]` );
19
21
  }, [] ).join( ',' );
22
+ window.webqit.oohtml.Style = {
23
+ compileCache: new Map,
24
+ };
25
+ exposeAPIs.call( window, config );
20
26
  realtime.call( window, config );
21
27
  }
22
28
 
29
+ /**
30
+ * Exposes Bindings with native APIs.
31
+ *
32
+ * @param Object config
33
+ *
34
+ * @return Void
35
+ */
36
+ function exposeAPIs( config ) {
37
+ const window = this, styleSheetsMap = new Map;
38
+ if ( config.api.styleSheets in window.Element.prototype ) { throw new Error( `The "Element" class already has a "${ config.api.styleSheets }" property!` ); }
39
+ Object.defineProperty( window.HTMLElement.prototype, config.api.styleSheets, { get: function() {
40
+ if ( !styleSheetsMap.has( this ) ) { styleSheetsMap.set( this, [] ); }
41
+ return styleSheetsMap.get( this );
42
+ }, } );
43
+ Object.defineProperty( window.HTMLStyleElement.prototype, 'scoped', {
44
+ configurable: true,
45
+ get() { return this.hasAttribute( 'scoped' ); },
46
+ set( value ) { this.toggleAttribute( 'scoped', value ); },
47
+ } );
48
+ }
49
+
23
50
  /**
24
51
  * Performs realtime capture of elements and builds their relationships.
25
52
  *
@@ -28,22 +55,93 @@ export default function init( { advanced = {}, ...$config } ) {
28
55
  * @return Void
29
56
  */
30
57
  function realtime( config ) {
31
- const window = this, { webqit: { realdom } } = window;
32
- if ( !window.HTMLScriptElement.supports ) { window.HTMLScriptElement.supports = () => false; }
33
- const handled = () => {};
34
- realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.styleSelector, record => {
58
+ const window = this, { webqit: { oohtml, realdom } } = window;
59
+ if ( !window.CSS.supports ) { window.CSS.supports = () => false; }
60
+ const handled = new WeakSet;
61
+ realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.styleSelector, record => {
35
62
  record.entrants.forEach( style => {
36
- if ( 'scoped' in style ) return handled( style );
37
- if ( config.style.strategy === '@scope' ) {
38
- Object.defineProperty( style, 'scoped', { value: style.hasAttribute( 'scoped' ) } );
39
- if ( style.hasAttribute( 'ref' ) ) return; // Server-rendered
40
- const uuid = `scoped${ uniqId() }`;
41
- style.setAttribute( 'ref', uuid );
42
- style.textContent = `@scope from (:has(> style[ref="${ uuid }"])) {\n${ style.textContent }\n}`;
63
+ if ( handled.has( style ) ) return;
64
+ handled.add( style );
65
+ if ( !style.scoped ) return;
66
+ style.parentNode[ config.api.styleSheets ].push( style );
67
+ // Do compilation
68
+ const sourceHash = _toHash( style.textContent );
69
+ const supportsHAS = CSS.supports( 'selector(:has(a,b))' );
70
+ const scopeSelector = supportsHAS ? `:has(> style[rand-${ sourceHash }])` : `[rand-${ sourceHash }]`;
71
+ const supportsScope = window.CSSScopeRule && false/* Disabled for buggy behaviour: rewriting selectorText within an @scope block invalidates the scoping */;
72
+ ( supportsHAS ? style : style.parentNode ).toggleAttribute( `rand-${ sourceHash }`, true );
73
+ if ( false ) {
74
+ let compiledSheet;
75
+ if ( !( compiledSheet = oohtml.Style.compileCache.get( sourceHash ) ) ) {
76
+ compiledSheet = createAdoptableStylesheet.call( window, style, null, supportsScope, scopeSelector );
77
+ oohtml.Style.compileCache.set( sourceHash, compiledSheet );
78
+ }
79
+ // Run now!!!
80
+ Object.defineProperty( style, 'sheet', { value: compiledSheet, configurable: true } );
81
+ style.textContent = '\n/*[ Shared style sheet ]*/\n';
82
+ } else {
83
+ const namespaceUUID = getNamespaceUUID.call( window, style );
84
+ upgradeSheet.call( this, style.sheet, namespaceUUID, !supportsScope && scopeSelector );
43
85
  }
44
86
  } );
45
- }, { live: true, timing: 'intercept', generation: 'entrants' } );
87
+ }, { live: true, timing: 'intercept', generation: 'entrants' } );
46
88
  // ---
47
89
  }
48
90
 
49
- const uniqId = () => (0|Math.random()*9e6).toString(36);
91
+ function createAdoptableStylesheet( style, namespaceUUID, supportsScope, scopeSelector ) {
92
+ const window = this, textContent = style.textContent;
93
+ let styleSheet, cssText = supportsScope ? `@scope (${ scopeSelector }) {\n${ textContent.trim() }\n}` : textContent.trim();
94
+ try {
95
+ styleSheet = new window.CSSStyleSheet;
96
+ styleSheet.replaceSync( cssText );
97
+ upgradeSheet.call( this, styleSheet, namespaceUUID, !supportsScope && scopeSelector );
98
+ document.adoptedStyleSheets.push( styleSheet );
99
+ } catch( e ) {
100
+ const style = window.document.createElement( 'style' );
101
+ window.document.body.appendChild( style );
102
+ style.textContent = cssText;
103
+ styleSheet = style.sheet;
104
+ upgradeSheet.call( this, styleSheet, namespaceUUID, !supportsScope && scopeSelector );
105
+ }
106
+ return styleSheet;
107
+ }
108
+
109
+ function upgradeSheet( styleSheet, namespaceUUID, scopeSelector = null ) {
110
+ const l = styleSheet?.cssRules.length || -1;
111
+ for ( let i = 0; i < l; ++i ) {
112
+ const cssRule = styleSheet.cssRules[ i ];
113
+ if ( cssRule instanceof CSSImportRule ) {
114
+ // Handle imported stylesheets
115
+ //upgradeSheet( cssRule.styleSheet, namespaceUUID, scopeSelector );
116
+ continue;
117
+ }
118
+ upgradeRule.call( this, cssRule, namespaceUUID, scopeSelector );
119
+ }
120
+ }
121
+
122
+ function upgradeRule( cssRule, namespaceUUID, scopeSelector = null ) {
123
+ if ( cssRule instanceof CSSStyleRule ) {
124
+ // Resolve relative IDs and scoping (for non-@scope browsers)
125
+ upgradeSelector.call( this, cssRule, namespaceUUID, scopeSelector );
126
+ return;
127
+ }
128
+ if ( [ window.CSSScopeRule, window.CSSMediaRule, window.CSSContainerRule, window.CSSSupportsRule, window.CSSLayerBlockRule ].some( type => type && cssRule instanceof type ) ) {
129
+ // Parse @rule blocks
130
+ const l = cssRule.cssRules.length;
131
+ for ( let i = 0; i < l; ++i ) {
132
+ upgradeRule.call( this, cssRule.cssRules[ i ], namespaceUUID, scopeSelector );
133
+ }
134
+ }
135
+ }
136
+
137
+ function upgradeSelector( cssRule, namespaceUUID, scopeSelector = null ) {
138
+ const newSelectorText = rewriteSelector.call( this, cssRule.selectorText, namespaceUUID, scopeSelector, 1 );
139
+ cssRule.selectorText = newSelectorText;
140
+ // Parse nested blocks. (CSS nesting)
141
+ if ( cssRule.cssRules ) {
142
+ const l = cssRule.cssRules.length;
143
+ for ( let i = 0; i < l; ++i ) {
144
+ upgradeSelector.call( this, cssRule.cssRules[ i ], namespaceUUID, /* Nesting has nothing to do with scopeSelector */ );
145
+ }
146
+ }
147
+ }