@webqit/oohtml 3.1.13 → 3.2.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/package.json CHANGED
@@ -14,7 +14,7 @@
14
14
  "wicg-proposal"
15
15
  ],
16
16
  "homepage": "https://webqit.io/tooling/oohtml",
17
- "version": "3.1.13",
17
+ "version": "3.2.0",
18
18
  "license": "MIT",
19
19
  "repository": {
20
20
  "type": "git",
@@ -29,8 +29,8 @@
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/api.global.js main.lite=src/api.global.lite.js namespaced-html=src/namespaced-html/targets.browser.js scoped-css=src/scoped-css/targets.browser.js scoped-js=src/scoped-js/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 data-binding=src/data-binding/targets.browser.js --bundle --minify --sourcemap --outdir=dist",
33
- "preversion": "npm run test && npm run build && git add -A dist",
32
+ "build": "esbuild main=src/api.global.js main.lite=src/api.global.lite.js context-api=src/context-api/targets.browser.js bindings-api=src/bindings-api/targets.browser.js namespaced-html=src/namespaced-html/targets.browser.js html-imports=src/html-imports/targets.browser.js data-binding=src/data-binding/targets.browser.js scoped-css=src/scoped-css/targets.browser.js scoped-js=src/scoped-js/targets.browser.js --bundle --minify --sourcemap --outdir=dist",
33
+ "preversion": "npm run build && git add -A dist",
34
34
  "postversion": "npm publish",
35
35
  "postpublish": "git push && git push --tags"
36
36
  },
@@ -52,5 +52,5 @@ export default class DOMBindingsContext extends DOMContext {
52
52
  /**
53
53
  * @unsubscribed()
54
54
  */
55
- unsubscribed( event ) { event._controller?.abort(); }
55
+ unsubscribed( event ) { event._controller?.abort(); }
56
56
  }
@@ -55,27 +55,6 @@ function getBindings( config, node ) {
55
55
  return _( node ).get( 'bindings' );
56
56
  }
57
57
 
58
- /**
59
- * Exposes Bindings with native APIs.
60
- *
61
- * @param Object config
62
- * @param document|Element target
63
- * @param Object bindings
64
- * @param Object params
65
- *
66
- * @return Void
67
- */
68
- function applyBindings( config, target, bindings, { merge, diff, namespace } = {} ) {
69
- const window = this, { webqit: { Observer } } = window;
70
- const bindingsObj = getBindings.call( this, config, target );
71
- const $params = { diff, namespace };
72
- const exitingKeys = merge ? [] : Observer.ownKeys( bindingsObj, $params ).filter( key => !( key in bindings ) );
73
- return Observer.batch( bindingsObj, () => {
74
- if ( exitingKeys.length ) { Observer.deleteProperties( bindingsObj, exitingKeys, $params ); }
75
- return Observer.set( bindingsObj, bindings, $params );
76
- }, $params );
77
- }
78
-
79
58
  /**
80
59
  * Exposes Bindings with native APIs.
81
60
  *
@@ -104,3 +83,24 @@ function exposeAPIs( config ) {
104
83
  return Observer.proxy( getBindings.call( window, config, this ) );
105
84
  } } );
106
85
  }
86
+
87
+ /**
88
+ * Exposes Bindings with native APIs.
89
+ *
90
+ * @param Object config
91
+ * @param document|Element target
92
+ * @param Object bindings
93
+ * @param Object params
94
+ *
95
+ * @return Void
96
+ */
97
+ function applyBindings( config, target, bindings, { merge, diff, namespace } = {} ) {
98
+ const window = this, { webqit: { Observer } } = window;
99
+ const bindingsObj = getBindings.call( this, config, target );
100
+ const $params = { diff, namespace };
101
+ const exitingKeys = merge ? [] : Observer.ownKeys( bindingsObj, $params ).filter( key => !( key in bindings ) );
102
+ return Observer.batch( bindingsObj, () => {
103
+ if ( exitingKeys.length ) { Observer.deleteProperties( bindingsObj, exitingKeys, $params ); }
104
+ return Observer.set( bindingsObj, bindings, $params );
105
+ }, $params );
106
+ }
@@ -2,7 +2,7 @@
2
2
  /**
3
3
  * @imports
4
4
  */
5
- import { _, _init } from '../util.js';
5
+ import { _, _init, _splitOuter } from '../util.js';
6
6
 
7
7
  /**
8
8
  * Initializes DOM Parts.
@@ -156,8 +156,8 @@ const inlineParseCache = new Map;
156
156
  function compileInlineBindings( config, str ) {
157
157
  if ( inlineParseCache.has( str ) ) return inlineParseCache.get( str );
158
158
  const validation = {};
159
- const source = splitOuter( str, ';' ).map( str => {
160
- const [ left, right ] = splitOuter( str, ':' ).map( x => x.trim() );
159
+ const source = _splitOuter( str, ';' ).map( str => {
160
+ const [ left, right ] = _splitOuter( str, ':' ).map( x => x.trim() );
161
161
  const directive = left[ 0 ], param = left.slice( 1 ).trim();
162
162
  const arg = `(${ right })`, $arg = `(${ arg } ?? '')`;
163
163
  if ( directive === '&' ) {
@@ -175,7 +175,7 @@ function compileInlineBindings( config, str ) {
175
175
  if ( param === 'text' ) return `$assign__(this, 'textContent', ${ $arg });`;
176
176
  if ( param === 'html' ) return `$exec__(this, 'setHTML', ${ $arg });`;
177
177
  if ( param === 'items' ) {
178
- const [ iterationSpec, importSpec ] = splitOuter( right, '/' );
178
+ const [ iterationSpec, importSpec ] = _splitOuter( right, '/' );
179
179
  if ( !importSpec ) throw new Error( `Invalid ${ directive }items spec: ${ str }; no import specifier.` );
180
180
  let [ raw, production, kind, iteratee ] = iterationSpec.trim().match( /(.*?[\)\s+])(of|in)([\(\{\[\s+].*)/i ) || [];
181
181
  if ( !raw ) throw new Error( `Invalid ${ directive }items spec: ${ str }.` );
@@ -227,19 +227,4 @@ function compileInlineBindings( config, str ) {
227
227
  return compiled;
228
228
  }
229
229
 
230
- export function splitOuter( str, delim ) {
231
- return [ ...str ].reduce( ( [ quote, depth, splits, skip ], x ) => {
232
- if ( !quote && depth === 0 && ( Array.isArray( delim ) ? delim : [ delim ] ).includes( x ) ) {
233
- return [ quote, depth, [ '' ].concat( splits ) ];
234
- }
235
- if ( !quote && [ '(', '[', '{' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) depth++;
236
- if ( !quote && [ ')', ']', '}' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) depth--;
237
- if ( [ '"', "'", '`' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) {
238
- quote = quote === x ? null : ( quote || x );
239
- }
240
- splits[ 0 ] += x;
241
- return [ quote, depth, splits ]
242
- }, [ null, 0, [ '' ] ] )[ 2 ].reverse();
243
- }
244
-
245
230
  const escDouble = str => str.replace(/"/g, '\\"');
@@ -44,11 +44,9 @@ export default class HTMLImportsContext extends DOMContext {
44
44
 
45
45
  // Parse and translate detail
46
46
  if ( ( event.detail || '' ).trim() === '/' ) return event.respondWith( this.localModules );
47
- const $config = this.configs.HTML_IMPORTS;
48
47
  let path = ( event.detail || '' ).split( /\/|(?<=\w)(?=#)/g ).map( x => x.trim() ).filter( x => x );
49
- if ( path.length ) { path = path.join( `/${ $config.api.defs }/` )?.split( '/' ) || []; }
50
- // No detail?
51
48
  if ( !path.length ) return event.respondWith();
49
+ path = path.join( `/${ this.configs.HTML_IMPORTS.api.defs }/` )?.split( '/' ) || [];
52
50
 
53
51
 
54
52
  // We'll now fulfill request
@@ -73,6 +73,11 @@ function exposeAPIs( config ) {
73
73
  Object.defineProperty( window.HTMLTemplateElement.prototype, config.api.def, { get: function() {
74
74
  return this.getAttribute( config.attr.def );
75
75
  } } );
76
+ Object.defineProperty( window.HTMLTemplateElement.prototype, 'scoped', {
77
+ configurable: true,
78
+ get() { return this.hasAttribute( 'scoped' ); },
79
+ set( value ) { this.toggleAttribute( 'scoped', value ); },
80
+ } );
76
81
  Object.defineProperty( window.document, config.api.import, { value: function( ref, live = false, callback = null ) {
77
82
  return importRequest( window.document, ...arguments );
78
83
  } } );
@@ -102,6 +107,7 @@ function exposeAPIs( config ) {
102
107
  */
103
108
  function realtime( config ) {
104
109
  const window = this, { webqit: { Observer, realdom, oohtml: { configs }, HTMLImportElement, HTMLImportsContext } } = window;
110
+
105
111
  // ------------
106
112
  // MODULES
107
113
  // ------------
@@ -124,7 +130,6 @@ function realtime( config ) {
124
130
  realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( [ config.templateSelector, config.importsContextSelector ], record => {
125
131
  record.entrants.forEach( entry => {
126
132
  if ( entry.matches( config.templateSelector ) ) {
127
- Object.defineProperty( entry, 'scoped', { value: entry.hasAttribute( 'scoped' ) } );
128
133
  const htmlModule = HTMLModule.instance( entry );
129
134
  htmlModule.ownerContext = entry.scoped ? record.target : window.document;
130
135
  const ownerContextModulesObj = getDefs( htmlModule.ownerContext );
@@ -148,6 +153,7 @@ function realtime( config ) {
148
153
  }
149
154
  } );
150
155
  }, { live: true, timing: 'sync', staticSensitivity: true } );
156
+
151
157
  // ------------
152
158
  // IMPORTS
153
159
  // ------------
package/src/index.js CHANGED
@@ -17,12 +17,12 @@ export default function init( QuantumJS, configs = {} ) {
17
17
  if ( !this.webqit ) { this.webqit = {}; }
18
18
  Object.assign( this.webqit, QuantumJS );
19
19
  // --------------
20
- NamespacedHTML.call( this, ( configs.NAMESPACED_HTML || {} ) );
21
- ScopedCSS.call( this, ( configs.SCOPED_CSS || {} ) );
22
- ScopedJS.call( this, ( configs.SCOPED_JS || {} ) );
23
20
  ContextAPI.call( this, ( configs.CONTEXT_API || {} ) );
24
21
  BindingsAPI.call( this, ( configs.BINDINGS_API || {} ) ); // Depends on ContextAPI
22
+ NamespacedHTML.call( this, ( configs.NAMESPACED_HTML || {} ) ); // Depends on ContextAPI
25
23
  HTMLImports.call( this, ( configs.HTML_IMPORTS || {} ) ); // Depends on ContextAPI
26
24
  DataBinding.call( this, ( configs.DATA_BINDING || {} ) ); // Depends on ContextAPI, BindingsAPI, HTMLImports
25
+ ScopedCSS.call( this, ( configs.SCOPED_CSS || {} ) ); // Depends on NamespacedHTML
26
+ ScopedJS.call( this, ( configs.SCOPED_JS || {} ) );
27
27
  // --------------
28
28
  }
@@ -0,0 +1,54 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import DOMContext from '../context-api/DOMContext.js';
6
+ import { env } from '../util.js';
7
+
8
+ export default class DOMNamingContext extends DOMContext {
9
+
10
+ static kind = 'namespace';
11
+
12
+ /**
13
+ * @createRequest
14
+ */
15
+ static createRequest( detail = null ) {
16
+ const request = super.createRequest();
17
+ if ( detail?.startsWith( '@' ) ) {
18
+ const [ targetContext, ...detail ] = detail.slice( 1 ).split( '/' ).map( s => s.trim() );
19
+ request.targetContext = targetContext;
20
+ request.detail = detail.join( '/' );
21
+ } else { request.detail = detail; }
22
+ return request;
23
+ }
24
+
25
+ /**
26
+ * @namespaceObj
27
+ */
28
+ get namespaceObj() { return this.host[ this.configs.NAMESPACED_HTML.api.namespace ]; }
29
+
30
+ /**
31
+ * @handle()
32
+ */
33
+ handle( event ) {
34
+ const { window: { webqit: { Observer } } } = env;
35
+ // Any existing event._controller? Abort!
36
+ event._controller?.abort();
37
+
38
+ // Parse and translate detail
39
+ if ( !( event.detail || '' ).trim() ) return event.respondWith( Observer.unproxy( this.namespaceObj ) );
40
+ let path = ( event.detail || '' ).split( '/' ).map( x => x.trim() ).filter( x => x );
41
+ if ( !path.length ) return event.respondWith();
42
+ path = path.join( `/${ this.configs.NAMESPACED_HTML.api.namespace }/` )?.split( '/' ) || [];
43
+
44
+ event._controller = Observer.reduce( this.namespaceObj, path, Observer.get, descriptor => {
45
+ if ( this.disposed ) return; // If already scheduled but aborted as in provider unmounting
46
+ event.respondWith( descriptor.value );
47
+ }, { live: event.live, signal: event.signal, descripted: true } );
48
+ }
49
+
50
+ /**
51
+ * @unsubscribed()
52
+ */
53
+ unsubscribed( event ) { event._controller?.abort(); }
54
+ }
@@ -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,69 @@ 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
+ * @rewriteSelector
29
+ *
30
+ * @param String selectorText
31
+ * @param String scopeSelector
32
+ * @param Bool isCssSelector
33
+ */
34
+ export function rewriteSelector( selectorText, scopeSelector = null, isCssSelector = false ) {
35
+ const window = this, { webqit: { oohtml: { configs: { NAMESPACED_HTML: config } } } } = window;
36
+ const { lidrefPrefix, lidrefSeparator } = config.tokens;
37
+ // Match :scope and relative ID selector
38
+ const regex = new RegExp( `${ scopeSelector ? `:scope|` : '' }#${ [ ...lidrefPrefix ].map( x => !/\w/.test( x ) ? ( !isCssSelector ? `\\${ x }` : `\\\\${ x }` ) : x ).join( '' ) }([\\w-]|\\\\.)+`, 'g' );
39
+ // Parse potentially combined selectors individually and categorise into categories per whether they have :scope or not
40
+ const [ cat1, cat2 ] = _splitOuter( selectorText, ',' ).reduce( ( [ cat1, cat2 ], selector ) => {
41
+ // The deal: match and replace
42
+ let quotesMatch, hadScopeSelector;
43
+ selector = selector.replace( regex, ( match, unused/**/, index ) => {
44
+ if ( !quotesMatch ) { // Lazy: stuff
45
+ // Match strings between quotes (single or double) and use that qualify matches above
46
+ // The string: String.raw`She said, "Hello, John. I\"m your friend." or "you're he're" 'f\'"j\'"f'jfjf`;
47
+ // Should yield: `"Hello, John. I\\"m your friend."`, `"you're he're"`, `'f\\'"j\\'"f'`
48
+ quotesMatch = [ ...selector.matchAll( /(["'])(?:(?=(\\?))\2.)*?\1/g ) ];
49
+ }
50
+ // Qualify match
51
+ if ( quotesMatch.some( q => index > q.index && index + match.length < q.index + match.length ) ) return match;
52
+ // Replace :scope
53
+ if ( match === ':scope' ) {
54
+ hadScopeSelector = true;
55
+ return scopeSelector;
56
+ }
57
+ // Replace relative ID selector
58
+ const lidref = match.replace( `#${ !isCssSelector ? lidrefPrefix : [ ...lidrefPrefix ].map( x => !/\w/.test( x ) ? `\\${ x }` : x ).join( '' ) }`, '' );
59
+ if ( config.attr.lid === 'id' ) {
60
+ return `[id^="${ lidrefPrefix }"][id$="${ lidrefSeparator }${ lidref }"]`;
61
+ } else {
62
+ return `[${ window.CSS.escape( config.attr.lid ) }="${ lidref }"]`;
63
+ }
64
+ } );
65
+ // Category 2 has :scope and category 1 does not
66
+ return hadScopeSelector ? [ cat1, cat2.concat( selector ) ] : [ cat1.concat( selector ), cat2 ];
67
+ }, [ [], [] ] );
68
+ // Do the upgrade
69
+ let newSelectorText;
70
+ if ( scopeSelector && cat1.length ) {
71
+ newSelectorText = [ cat1.length > 1 ? `${ scopeSelector } :is(${ cat1.join( ', ' ) })` : `${ scopeSelector } ${ cat1[ 0 ] }`, cat2.join( ', ' ) ].filter( x => x ).join( ', ' );
72
+ } else {
73
+ newSelectorText = [ ...cat1, ...cat2 ].join( ', ' );
74
+ }
75
+ return newSelectorText;
76
+ }
77
+
24
78
  /**
25
79
  * Returns the "namespace" object associated with the given node.
26
80
  *
@@ -28,24 +82,9 @@ export default function init( $config = {} ) {
28
82
  *
29
83
  * @return Object
30
84
  */
31
- function getNamespaceObject( node, config ) {
32
- const window = this, { webqit: { Observer } } = window;
85
+ function getNamespaceObject( node ) {
33
86
  if ( !_( node ).has( 'namespace' ) ) {
34
87
  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
88
  _( node ).set( 'namespace', namespaceObj );
50
89
  }
51
90
  return _( node ).get( 'namespace' );
@@ -65,10 +104,10 @@ function exposeAPIs( config ) {
65
104
  if ( config.api.namespace in window.Element.prototype ) { throw new Error( `The "Element" class already has a "${ config.api.namespace }" property!` ); }
66
105
  // Definitions
67
106
  Object.defineProperty( window.document, config.api.namespace, { get: function() {
68
- return Observer.proxy( getNamespaceObject.call( window, window.document, config ) );
107
+ return Observer.proxy( getNamespaceObject.call( window, window.document ) );
69
108
  } });
70
109
  Object.defineProperty( window.Element.prototype, config.api.namespace, { get: function() {
71
- return Observer.proxy( getNamespaceObject.call( window, this, config ) );
110
+ return Observer.proxy( getNamespaceObject.call( window, this ) );
72
111
  } } );
73
112
  }
74
113
 
@@ -80,50 +119,186 @@ function exposeAPIs( config ) {
80
119
  * @return Void
81
120
  */
82
121
  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 );
122
+ const window = this, { webqit: { Observer, realdom, oohtml: { configs }, DOMNamingContext } } = window;
123
+ const { lidrefPrefix, lidrefSeparator } = config.tokens;
124
+
125
+ // ------------
126
+ // NAMESPACE
127
+ // ------------
128
+ window.document[ configs.CONTEXT_API.api.contexts ].attach( new DOMNamingContext );
129
+ realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.namespaceSelector, record => {
130
+ record.exits.forEach( entry => {
131
+ const contextsApi = entry[ configs.CONTEXT_API.api.contexts ];
132
+ const ctx = contextsApi.find( DOMNamingContext.kind );
133
+ if ( ctx ) { contextsApi.detach( ctx ); }
134
+ } );
135
+ record.entrants.forEach( entry => {
136
+ const contextsApi = entry[ configs.CONTEXT_API.api.contexts ];
137
+ if ( !contextsApi.find( DOMNamingContext.kind ) ) {
138
+ contextsApi.attach( new DOMNamingContext );
139
+ }
140
+ } );
141
+ }, { live: true, timing: 'sync', staticSensitivity: true } );
142
+
143
+ // ------------
144
+ // APIS
145
+ // ------------
146
+ // See https://wicg.github.io/aom/aria-reflection-explainer.html & https://github.com/whatwg/html/issues/3515 for the ARIA refelction properties idea
147
+ // See https://www.w3.org/TR/wai-aria-1.1/#attrs_relationships for the relational ARIA attributes
148
+ const idRefsAttrs = [ 'aria-owns', 'aria-controls', 'aria-labelledby', 'aria-describedby', 'aria-flowto', ];
149
+ const idRefAttrs = [ 'for', 'list', 'form', 'aria-activedescendant', 'aria-details', 'aria-errormessage', ];
150
+ const attrList = [ config.attr.lid, ...idRefsAttrs, ...idRefAttrs ];
151
+ 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' };
152
+ const isUuid = str => str.startsWith( lidrefPrefix ) && str.includes( lidrefSeparator );
153
+ const isLidref = str => str.startsWith( lidrefPrefix ) && !str.includes( lidrefSeparator );
154
+ const toUuid = ( hash, lid ) => `${ lidrefPrefix }${ hash }${ lidrefSeparator }${ lid }`;
155
+ const uuidToLid = str => isUuid( str ) ? str.split( lidrefSeparator )[ 1 ] : str;
156
+ const uuidToLidref = str => isUuid( str ) ? `${ lidrefPrefix }${ str.split( lidrefSeparator )[ 1 ] }` : str;
157
+
158
+ // Intercept getAttribute()
159
+ const getAttribute = Object.getOwnPropertyDescriptor( window.Element.prototype, 'getAttribute' );
160
+ Object.defineProperty( window.Element.prototype, 'getAttribute', { ...getAttribute, value( attrName ) {
161
+ const value = getAttribute.value.call( this, attrName );
162
+ return !_( this, 'lock' ).get( attrName ) && attrList.includes( attrName ) ? ( attrName === 'id' ? uuidToLid : uuidToLidref )( value ) : value;
163
+ } } );
164
+ // Intercept getElementById()
165
+ const getElementById = Object.getOwnPropertyDescriptor( window.Document.prototype, 'getElementById' );
166
+ Object.defineProperty( window.Document.prototype, 'getElementById', { ...getElementById, value( id ) {
167
+ if ( !isLidref( id ) ) return getElementById.value.call( this, id );
168
+ const node = this.querySelector( rewriteSelector.call( window, `#${ id }` ) );
169
+ return node;// !node?.closest( config.namespaceSelector ) ? node : null; // Cool, but not consistent with querySelector(All)() results
170
+ } } );
171
+ // Intercept querySelector() and querySelectorAll()
172
+ for ( const queryApi of [ 'querySelector', 'querySelectorAll' ] ) {
173
+ for ( nodeApi of [ window.Document, window.Element ] ) {
174
+ const querySelector = Object.getOwnPropertyDescriptor( nodeApi.prototype, queryApi );
175
+ Object.defineProperty( nodeApi.prototype, queryApi, { ...querySelector, value( selector ) {
176
+ return querySelector.value.call( this, rewriteSelector.call( window, selector ) );
177
+ } } );
178
+ }
179
+ }
180
+ // These APIs should return LIDREFS minus the hash part
181
+ for ( const attrName of attrList ) {
182
+ if ( !( attrName in relMap ) ) continue;
183
+ const domApis = attrName === 'for' ? [ window.HTMLLabelElement, window.HTMLOutputElement ] : [ window.Element ];
184
+ for ( const domApi of domApis ) {
185
+ const idReflection = Object.getOwnPropertyDescriptor( domApi.prototype, relMap[ attrName ] );
186
+ if ( !idReflection ) continue;
187
+ Object.defineProperty( domApi.prototype, relMap[ attrName ], { ...idReflection, get() {
188
+ return ( attrName === 'id' ? uuidToLid : uuidToLidref )( idReflection.get.call( this, attrName ) || '' );
189
+ } } );
190
+ }
191
+ }
192
+ // Reflect the custom [config.attr.lid] attribute
193
+ if ( config.attr.lid !== 'id' ) {
194
+ Object.defineProperty( window.Element.prototype, config.attr.lid, { configurable: true, enumerable: true, get() {
195
+ return this.getAttribute( config.attr.lid );
196
+ }, set( value ) {
197
+ return this.setAttribute( config.attr.lid, value );
198
+ } } );
199
+ }
200
+
201
+ // ------------
202
+ // LOCAL IDS & IDREFS
203
+ // ------------
204
+ const cleanupBinding = ( entry, attrName, oldValue ) => {
205
+ // Create or honour locking
206
+ if ( _( entry, 'lock' ).get( attrName ) ) return;
207
+ _( entry, 'lock' ).set( attrName, true );
208
+ // A function can be passed in to be called after having done the _( entry, 'lock' ).set( attrName, true ); flag
209
+ if ( typeof oldValue === 'function' ) oldValue = oldValue();
210
+ // Get down to work
211
+ const namespaceObj = _( entry ).get( 'ownerNamespace' );
212
+ if ( attrName === config.attr.lid ) {
213
+ const lid = uuidToLid( oldValue );
214
+ if ( Observer.get( namespaceObj, lid ) === entry ) { Observer.deleteProperty( namespaceObj, lid ); }
215
+ } else {
216
+ const newAttrValue = oldValue.split( ' ' ).map( lid => ( lid = lid.trim() ) && uuidToLidref( lid ) ).join( ' ' );
217
+ entry.setAttribute( attrName, newAttrValue );
218
+ }
219
+ // Release locking
220
+ _( entry, 'lock' ).delete( attrName );
221
+ };
222
+ const setupBinding = ( entry, attrName, value ) => {
223
+ // Create or honour locking
224
+ if ( _( entry, 'lock' ).get( attrName ) ) return;
225
+ _( entry, 'lock' ).set( attrName, true );
226
+ // A function can be passed in to be called after having done the _( entry, 'lock' ).set( attrName, true ); flag
227
+ if ( typeof value === 'function' ) value = value();
228
+ // Get down to work
229
+ const namespaceObj = _( entry ).get( 'ownerNamespace' );
230
+ const namespaceUUID = _( entry ).get( 'namespaceUUID' );
231
+ if ( attrName === config.attr.lid ) {
232
+ const lid = uuidToLid( value );
233
+ if ( Observer.get( namespaceObj, lid ) !== entry ) {
234
+ // Setup new namespace relationships
235
+ entry.setAttribute( 'id', toUuid( namespaceUUID, lid ) );
236
+ Observer.set( namespaceObj, lid, entry );
93
237
  }
94
- } else if ( Observer.get( namespaceObj, identifier ) === entry ) {
95
- _( entry ).delete( 'ownerNamespace' );
96
- Observer.deleteProperty( namespaceObj, identifier );
238
+ } else {
239
+ const newAttrValue = value.split( ' ' ).map( lid => ( lid = lid.trim() ) && !isLidref( lid ) ? lid : toUuid( namespaceUUID, lid.replace( lidrefPrefix, '' ) ) ).join( ' ' );
240
+ entry.setAttribute( attrName, newAttrValue );
97
241
  }
242
+ // Release locking
243
+ _( entry, 'lock' ).delete( attrName );
98
244
  };
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 );
245
+
246
+ const cleanupAllBindings = ( entry, total = true ) => {
247
+ for ( const attrName of attrList ) {
248
+ if ( !entry.hasAttribute( attrName ) ) continue;
249
+ cleanupBinding( entry, attrName, () => entry.getAttribute( attrName ) );
250
+ }
251
+ _( entry ).delete( 'ownerNamespace' );
252
+ _( entry ).delete( 'namespaceUUID' );
253
+ if ( total ) {
254
+ _( entry ).get( 'namespaceBinding' ).abort();
255
+ _( entry ).delete( 'namespaceBinding' );
256
+ }
257
+ };
258
+ const setupAllBindings = ( entry ) => {
259
+ if ( !_( entry ).get( 'ownerNamespace' ) ) {
260
+ const request = { ...DOMNamingContext.createRequest(), live: true };
261
+ const binding = entry.parentNode[ configs.CONTEXT_API.api.contexts ].request( request, namespaceObj => {
262
+ // Cleanup of previous namespace?
263
+ if ( _( entry ).get( 'namespaceUUID' ) ) {
264
+ cleanupAllBindings( entry, false );
119
265
  }
120
- }
121
- }, { subtree: true, timing: 'sync' } );
122
- }
123
- // ----------------
266
+ // Setup new namespace
267
+ _( entry ).set( 'ownerNamespace', namespaceObj );
268
+ _( entry ).set( 'namespaceUUID', _fromHash( namespaceObj ) || _toHash( namespaceObj ) );
269
+ setupAllBindings( entry );
270
+ } );
271
+ _( entry ).set( 'namespaceBinding', binding );
272
+ return;
273
+ }
274
+ for ( const attrName of attrList ) {
275
+ if ( !entry.hasAttribute( attrName ) ) continue;
276
+ setupBinding( entry, attrName, () => entry.getAttribute( attrName ) );
277
+ }
278
+ };
279
+
280
+ // DOM realtime
281
+ realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( `[${ attrList.map( attrName => window.CSS.escape( attrName ) ).join( '],[' ) }]`, record => {
282
+ record.exits.forEach( cleanupAllBindings );
283
+ record.entrants.forEach( setupAllBindings );
284
+ }, { live: true, timing: 'sync' } );
285
+ // Attr realtime
286
+ realdom.realtime( window.document, 'attr' ).observe( attrList, records => {
287
+ for ( const record of records ) {
288
+ if ( record.oldValue ) {
289
+ cleanupBinding( record.target, record.name, record.oldValue );
290
+ if ( record.value ) { setupBinding( record.target, record.name, record.value ); }
291
+ } else { setupAllBindings( entry ); }
292
+ }
293
+ }, { subtree: true, timing: 'sync', newValue: true, oldValue: true } );
294
+
295
+ // ------------
296
+ // TARGETS
297
+ // ------------
124
298
  let prevTarget;
125
299
  const activateTarget = () => {
126
- const path = window.location.hash?.substring( 1 ).split( '/' ).map( s => s.trim() ).filter( s => s ) || [];
300
+ if ( !window.location.hash?.startsWith( `#${ lidrefPrefix }` ) ) return;
301
+ const path = window.location.hash?.substring( `#${ lidrefPrefix }`.length ).split( '/' ).map( s => s.trim() ).filter( s => s ) || [];
127
302
  const currTarget = path.reduce( ( prev, segment ) => prev && prev[ config.api.namespace ][ segment ], window.document );
128
303
  if ( prevTarget && config.target.attr ) { prevTarget.toggleAttribute( config.target.attr, false ); }
129
304
  if ( currTarget && currTarget !== window.document ) {
@@ -133,6 +308,8 @@ function realtime( config ) {
133
308
  prevTarget = currTarget;
134
309
  }
135
310
  };
311
+ // "hash" realtime
136
312
  window.addEventListener( 'hashchange', activateTarget );
137
313
  realdom.ready( activateTarget );
314
+ // ----------------
138
315
  }