@webqit/oohtml 3.2.0 → 4.0.1

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.2.0",
17
+ "version": "4.0.1",
18
18
  "license": "MIT",
19
19
  "repository": {
20
20
  "type": "git",
@@ -29,14 +29,14 @@
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 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",
32
+ "build": "esbuild main=src/index.js main.lite=src/index.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 test && 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
- "@webqit/quantum-js": "^4.2.11",
39
- "@webqit/realdom": "^2.1.22",
38
+ "@webqit/quantum-js": "^4.5.0",
39
+ "@webqit/realdom": "^2.1.23",
40
40
  "@webqit/util": "^0.8.11"
41
41
  },
42
42
  "devDependencies": {
@@ -13,11 +13,11 @@ import { _, _init, _splitOuter } from '../util.js';
13
13
  */
14
14
  export default function init( $config = {} ) {
15
15
  const { config, window } = _init.call( this, 'data-binding', $config, {
16
- attr: { expr: 'expr', itemIndex: 'data-key' },
16
+ attr: { render: 'render', itemIndex: 'data-key' },
17
17
  tokens: { nodeType: 'processing-instruction', tagStart: '?{', tagEnd: '}?', stateStart: '; [=', stateEnd: ']' },
18
18
  } );
19
19
  ( { CONTEXT_API: config.CONTEXT_API, BINDINGS_API: config.BINDINGS_API, HTML_IMPORTS: config.HTML_IMPORTS } = window.webqit.oohtml.configs );
20
- config.attrSelector = `[${ window.CSS.escape( config.attr.expr ) }]`;
20
+ config.attrSelector = `[${ window.CSS.escape( config.attr.render ) }]`;
21
21
  const discreteBindingsMatch = ( start, end ) => {
22
22
  const starting = `starts-with(., "${ start }")`;
23
23
  const ending = `substring(., string-length(.) - string-length("${ end }") + 1) = "${ end }"`;
@@ -90,7 +90,7 @@ function cleanup( ...entries ) {
90
90
  }
91
91
 
92
92
  async function mountDiscreteBindings( config, ...entries ) {
93
- const window = this, { webqit: { QuantumAsyncFunction } } = window;
93
+ const window = this;
94
94
  const patternMatch = str => {
95
95
  const tagStart = config.tokens.tagStart.split( '' ).map( x => `\\${ x }` ).join( '' );
96
96
  const tagEnd = config.tokens.tagEnd.split( '' ).map( x => `\\${ x }` ).join( '' );
@@ -144,7 +144,7 @@ function compileDiscreteBindings( config, str ) {
144
144
 
145
145
  async function mountInlineBindings( config, ...entries ) {
146
146
  for ( const node of entries ) {
147
- const compiled = compileInlineBindings( config, node.getAttribute( config.attr.expr ) );
147
+ const compiled = compileInlineBindings( config, node.getAttribute( config.attr.render ) );
148
148
  const { scope, bindings } = createDynamicScope.call( this, config, node );
149
149
  const signals = [];
150
150
  Object.defineProperty( node, '$oohtml_internal_databinding_signals', { value: signals, configurable: true } );
package/src/index.js CHANGED
@@ -1,28 +1,10 @@
1
-
2
1
  /**
3
2
  * @imports
4
3
  */
5
- import NamespacedHTML from './namespaced-html/index.js';
6
- import ScopedCSS from './scoped-css/index.js';
7
- import ScopedJS from './scoped-js/index.js';
8
- import ContextAPI from './context-api/index.js';
9
- import BindingsAPI from './bindings-api/index.js';
10
- import HTMLImports from './html-imports/index.js';
11
- import DataBinding from './data-binding/index.js';
4
+ import * as QuantumJS from `@webqit/quantum-js`;
5
+ import init from './init.js';
12
6
 
13
7
  /**
14
8
  * @init
15
9
  */
16
- export default function init( QuantumJS, configs = {} ) {
17
- if ( !this.webqit ) { this.webqit = {}; }
18
- Object.assign( this.webqit, QuantumJS );
19
- // --------------
20
- ContextAPI.call( this, ( configs.CONTEXT_API || {} ) );
21
- BindingsAPI.call( this, ( configs.BINDINGS_API || {} ) ); // Depends on ContextAPI
22
- NamespacedHTML.call( this, ( configs.NAMESPACED_HTML || {} ) ); // Depends on ContextAPI
23
- HTMLImports.call( this, ( configs.HTML_IMPORTS || {} ) ); // Depends on ContextAPI
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
- // --------------
28
- }
10
+ init.call( window, QuantumJS );
@@ -3,7 +3,7 @@
3
3
  * @imports
4
4
  */
5
5
  import * as QuantumJS from `@webqit/quantum-js/lite`;
6
- import init from './index.js';
6
+ import init from './init.js';
7
7
 
8
8
  /**
9
9
  * @init
package/src/init.js ADDED
@@ -0,0 +1,28 @@
1
+
2
+ /**
3
+ * @imports
4
+ */
5
+ import NamespacedHTML from './namespaced-html/index.js';
6
+ import ScopedCSS from './scoped-css/index.js';
7
+ import ScopedJS from './scoped-js/index.js';
8
+ import ContextAPI from './context-api/index.js';
9
+ import BindingsAPI from './bindings-api/index.js';
10
+ import HTMLImports from './html-imports/index.js';
11
+ import DataBinding from './data-binding/index.js';
12
+
13
+ /**
14
+ * @init
15
+ */
16
+ export default function init( QuantumJS, configs = {} ) {
17
+ if ( !this.webqit ) { this.webqit = {}; }
18
+ Object.assign( this.webqit, QuantumJS );
19
+ // --------------
20
+ ContextAPI.call( this, ( configs.CONTEXT_API || {} ) );
21
+ BindingsAPI.call( this, ( configs.BINDINGS_API || {} ) ); // Depends on ContextAPI
22
+ NamespacedHTML.call( this, ( configs.NAMESPACED_HTML || {} ) ); // Depends on ContextAPI
23
+ HTMLImports.call( this, ( configs.HTML_IMPORTS || {} ) ); // Depends on ContextAPI
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
+ // --------------
28
+ }
@@ -3,7 +3,7 @@
3
3
  * @imports
4
4
  */
5
5
  import DOMNamingContext from './DOMNamingContext.js';
6
- import { _, _init, _splitOuter, _fromHash, _toHash } from '../util.js';
6
+ import { _, _init, _splitOuter, _fromHash, _toHash, getInternalAttrInteraction, internalAttrInteraction } from '../util.js';
7
7
 
8
8
  /**
9
9
  * @init
@@ -15,52 +15,91 @@ export default function init( $config = {} ) {
15
15
  attr: { namespace: 'namespace', lid: 'id', },
16
16
  api: { namespace: 'namespace', },
17
17
  tokens: { lidrefPrefix: '~', lidrefSeparator: ':' },
18
- target: { attr: ':target', event: ':target', scrolling: true },
18
+ target: { className: ':target', eventName: ':target', scrolling: true },
19
19
  } );
20
20
  config.lidSelector = `[${ window.CSS.escape( config.attr.lid ) }]`;
21
21
  config.namespaceSelector = `[${ window.CSS.escape( config.attr.namespace ) }]`;
22
+ config.documentNamespaceUUID = getNamespaceUUID( getOwnNamespaceObject.call( window, window.document ) );
22
23
  window.webqit.DOMNamingContext = DOMNamingContext;
23
24
  exposeAPIs.call( window, config );
24
25
  realtime.call( window, config );
25
26
  }
26
27
 
28
+ /**
29
+ * @init
30
+ *
31
+ * @param Object config
32
+ *
33
+ * @return String
34
+ */
35
+ function lidUtil( config ) {
36
+ const { lidrefPrefix, lidrefSeparator, } = config.tokens;
37
+ return {
38
+ escape( str, mode = 1 ) { return [ ...str ].map( x => !/\w/.test( x ) ? ( mode === 2 ? `\\\\${ x }` : `\\${ x }` ) : x ).join( '' ); },
39
+ lidrefPrefix( escapeMode = 0 ) { return escapeMode ? this.escape( lidrefPrefix, escapeMode ) : lidrefPrefix; },
40
+ lidrefSeparator( escapeMode = 0 ) { return escapeMode ? this.escape( lidrefSeparator, escapeMode ) : lidrefSeparator; },
41
+ isUuid( str, escapeMode = 0 ) { return str.startsWith( this.lidrefPrefix( escapeMode ) ) && str.includes( this.lidrefSeparator( escapeMode ) ); },
42
+ isLidref( str, escapeMode = 0 ) { return str.startsWith( this.lidrefPrefix( escapeMode ) ) && !str.includes( this.lidrefSeparator( escapeMode ) ); },
43
+ toUuid( hash, lid, escapeMode = 0 ) { return hash === config.documentNamespaceUUID ? lid : `${ this.lidrefPrefix( escapeMode ) }${ hash }${ this.lidrefSeparator( escapeMode ) }${ lid }`; },
44
+ uuidToId( str, escapeMode = 0 ) { return this.isUuid( str ) ? str.split( this.lidrefSeparator( escapeMode ) )[ 1 ] : str; },
45
+ uuidToLidref( str, escapeMode = 0 ) { return this.isUuid( str ) ? `${ this.lidrefPrefix( escapeMode ) }${ str.split( this.lidrefSeparator( escapeMode ) )[ 1 ] }` : str; },
46
+ }
47
+ }
48
+
27
49
  /**
28
50
  * @rewriteSelector
29
51
  *
30
52
  * @param String selectorText
53
+ * @param String namespaceUUID
31
54
  * @param String scopeSelector
32
- * @param Bool isCssSelector
55
+ * @param Bool escapeMode
56
+ *
57
+ * @return String
33
58
  */
34
- export function rewriteSelector( selectorText, scopeSelector = null, isCssSelector = false ) {
59
+ export function rewriteSelector( selectorText, namespaceUUID, scopeSelector = null, escapeMode = 0 ) {
35
60
  const window = this, { webqit: { oohtml: { configs: { NAMESPACED_HTML: config } } } } = window;
36
- const { lidrefPrefix, lidrefSeparator } = config.tokens;
61
+ const $lidUtil = lidUtil( config );
37
62
  // 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' );
63
+ const regex = new RegExp( `${ scopeSelector ? `:scope|` : '' }#(${ $lidUtil.lidrefPrefix( escapeMode + 1 ) })?([\\w]+${ $lidUtil.lidrefSeparator( escapeMode + 1 ) })?((?:[\\w-]|\\\\.)+)`, 'g' );
39
64
  // Parse potentially combined selectors individually and categorise into categories per whether they have :scope or not
40
65
  const [ cat1, cat2 ] = _splitOuter( selectorText, ',' ).reduce( ( [ cat1, cat2 ], selector ) => {
41
66
  // The deal: match and replace
42
67
  let quotesMatch, hadScopeSelector;
43
- selector = selector.replace( regex, ( match, unused/**/, index ) => {
68
+ selector = selector.replace( regex, ( match, lidrefPrefixMatch, lidrefSeparatorMatch, id, index ) => {
44
69
  if ( !quotesMatch ) { // Lazy: stuff
45
70
  // Match strings between quotes (single or double) and use that qualify matches above
46
71
  // The string: String.raw`She said, "Hello, John. I\"m your friend." or "you're he're" 'f\'"j\'"f'jfjf`;
47
72
  // Should yield: `"Hello, John. I\\"m your friend."`, `"you're he're"`, `'f\\'"j\\'"f'`
48
73
  quotesMatch = [ ...selector.matchAll( /(["'])(?:(?=(\\?))\2.)*?\1/g ) ];
49
74
  }
75
+ if ( quotesMatch[ 0 ] )
50
76
  // Qualify match
51
- if ( quotesMatch.some( q => index > q.index && index + match.length < q.index + match.length ) ) return match;
77
+ if ( quotesMatch.some( q => index > q.index && index + match.length < q.index + q[ 0 ].length ) ) return match;
52
78
  // Replace :scope
53
79
  if ( match === ':scope' ) {
54
80
  hadScopeSelector = true;
55
81
  return scopeSelector;
56
82
  }
57
- // Replace relative ID selector
58
- const lidref = match.replace( `#${ !isCssSelector ? lidrefPrefix : [ ...lidrefPrefix ].map( x => !/\w/.test( x ) ? `\\${ x }` : x ).join( '' ) }`, '' );
83
+ const isLidref = lidrefPrefixMatch && !lidrefSeparatorMatch;
84
+ const isUuid = lidrefPrefixMatch && lidrefSeparatorMatch;
85
+ if ( isUuid ) {
86
+ return `#${ $lidUtil.escape( match.replace( '#', '' ), 1 ) }`;
87
+ }
88
+ // Rewrite relative ID selector
89
+ if ( isLidref ) {
90
+ if ( config.attr.lid === 'id' && namespaceUUID && namespaceUUID !== config.documentNamespaceUUID ) {
91
+ return `#${ $lidUtil.toUuid( namespaceUUID, id, 1 ) }`;
92
+ }
93
+ // Fallback to attr-based
94
+ }
95
+ // Rewrite absolute ID selector
96
+ let rewrite;
59
97
  if ( config.attr.lid === 'id' ) {
60
- return `[id^="${ lidrefPrefix }"][id$="${ lidrefSeparator }${ lidref }"]`;
98
+ rewrite = `:is(#${ id },[id^="${ $lidUtil.lidrefPrefix( escapeMode ) }"][id$="${ $lidUtil.lidrefSeparator( escapeMode ) }${ id }"])`;
61
99
  } else {
62
- return `[${ window.CSS.escape( config.attr.lid ) }="${ lidref }"]`;
100
+ rewrite = `:is(#${ id },[${ window.CSS.escape( config.attr.lid ) }="${ id }"])`;
63
101
  }
102
+ return isLidref ? `:is(${ rewrite }):not(${ scopeSelector ? scopeSelector + ' ' : '' }[${ config.attr.namespace }] *)` : rewrite;
64
103
  } );
65
104
  // Category 2 has :scope and category 1 does not
66
105
  return hadScopeSelector ? [ cat1, cat2.concat( selector ) ] : [ cat1.concat( selector ), cat2 ];
@@ -76,13 +115,11 @@ export function rewriteSelector( selectorText, scopeSelector = null, isCssSelect
76
115
  }
77
116
 
78
117
  /**
79
- * Returns the "namespace" object associated with the given node.
80
- *
81
118
  * @param Element node
82
119
  *
83
120
  * @return Object
84
121
  */
85
- function getNamespaceObject( node ) {
122
+ export function getOwnNamespaceObject( node ) {
86
123
  if ( !_( node ).has( 'namespace' ) ) {
87
124
  const namespaceObj = Object.create( null );
88
125
  _( node ).set( 'namespace', namespaceObj );
@@ -90,6 +127,26 @@ function getNamespaceObject( node ) {
90
127
  return _( node ).get( 'namespace' );
91
128
  }
92
129
 
130
+ /**
131
+ * @param Element node
132
+ * @param Bool forID
133
+ *
134
+ * @return Object
135
+ */
136
+ export function getOwnerNamespaceObject( node, forID = false ) {
137
+ const window = this, { webqit: { oohtml: { configs: { NAMESPACED_HTML: config } } } } = window;
138
+ return getOwnNamespaceObject( node instanceof window.Document ? node : ( ( forID ? node.parentNode : node )?.closest( `[${ config.attr.namespace }]` ) || node.ownerDocument ) );
139
+ }
140
+
141
+ /**
142
+ * @param Object namespaceObj
143
+ *
144
+ * @return String
145
+ */
146
+ export function getNamespaceUUID( namespaceObj ) {
147
+ return _fromHash( namespaceObj ) || _toHash( namespaceObj );
148
+ }
149
+
93
150
  /**
94
151
  * Exposes Namespaced HTML with native APIs.
95
152
  *
@@ -104,10 +161,10 @@ function exposeAPIs( config ) {
104
161
  if ( config.api.namespace in window.Element.prototype ) { throw new Error( `The "Element" class already has a "${ config.api.namespace }" property!` ); }
105
162
  // Definitions
106
163
  Object.defineProperty( window.document, config.api.namespace, { get: function() {
107
- return Observer.proxy( getNamespaceObject.call( window, window.document ) );
164
+ return Observer.proxy( getOwnNamespaceObject.call( window, window.document ) );
108
165
  } });
109
166
  Object.defineProperty( window.Element.prototype, config.api.namespace, { get: function() {
110
- return Observer.proxy( getNamespaceObject.call( window, this ) );
167
+ return Observer.proxy( getOwnNamespaceObject.call( window, this ) );
111
168
  } } );
112
169
  }
113
170
 
@@ -120,25 +177,6 @@ function exposeAPIs( config ) {
120
177
  */
121
178
  function realtime( config ) {
122
179
  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
180
 
143
181
  // ------------
144
182
  // APIS
@@ -149,48 +187,62 @@ function realtime( config ) {
149
187
  const idRefAttrs = [ 'for', 'list', 'form', 'aria-activedescendant', 'aria-details', 'aria-errormessage', ];
150
188
  const attrList = [ config.attr.lid, ...idRefsAttrs, ...idRefAttrs ];
151
189
  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;
190
+ const $lidUtil = lidUtil( config );
191
+ const uuidsToLidrefs = ( node, attrName, getter ) => {
192
+ if ( !getInternalAttrInteraction( node, attrName ) && _( node, 'attrOriginals' ).has( attrName ) ) {
193
+ return _( node, 'attrOriginals' ).get( attrName );
194
+ }
195
+ const value = getter();
196
+ if ( getInternalAttrInteraction( node, attrName ) ) return value;
197
+ return value && value.split( ' ' ).map( x => ( x = x.trim() ) && ( attrName === config.attr.lid ? $lidUtil.uuidToId : $lidUtil.uuidToLidref ).call( $lidUtil, x ) ).join( ' ' );
198
+ };
157
199
 
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
200
  // 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
201
+ const getElementByIdDescr = Object.getOwnPropertyDescriptor( window.Document.prototype, 'getElementById' );
202
+ Object.defineProperty( window.Document.prototype, 'getElementById', { ...getElementByIdDescr, value( id ) {
203
+ return this.querySelector( `#${ id }` ); // To be rewritten at querySelector()
170
204
  } } );
171
205
  // Intercept querySelector() and querySelectorAll()
172
206
  for ( const queryApi of [ 'querySelector', 'querySelectorAll' ] ) {
173
207
  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 ) );
208
+ const querySelectorDescr = Object.getOwnPropertyDescriptor( nodeApi.prototype, queryApi );
209
+ Object.defineProperty( nodeApi.prototype, queryApi, { ...querySelectorDescr, value( selector ) {
210
+ return querySelectorDescr.value.call( this, rewriteSelector.call( window, selector, getNamespaceUUID( getOwnNamespaceObject.call( window, this ) ) ) );
177
211
  } } );
178
212
  }
179
213
  }
214
+ // Intercept getAttribute()
215
+ const getAttributeDescr = Object.getOwnPropertyDescriptor( window.Element.prototype, 'getAttribute' );
216
+ Object.defineProperty( window.Element.prototype, 'getAttribute', { ...getAttributeDescr, value( attrName ) {
217
+ const getter = () => getAttributeDescr.value.call( this, attrName );
218
+ return attrList.includes( attrName ) && !_( this, 'lock' ).get( attrName ) ? uuidsToLidrefs( this, attrName, getter ) : getter();
219
+ } } );
220
+ // Hide implementation details on the Attr node too.
221
+ const propertyDescr = Object.getOwnPropertyDescriptor( window.Attr.prototype, 'value' );
222
+ Object.defineProperty( window.Attr.prototype, 'value', { ...propertyDescr, get() {
223
+ const getter = () => propertyDescr.get.call( this );
224
+ return attrList.includes( this.name ) ? uuidsToLidrefs( this.ownerElement, this.name, getter ) : getter();
225
+ } } );
226
+ const propertyDescr2 = Object.getOwnPropertyDescriptor( window.Node.prototype, 'nodeValue' );
227
+ Object.defineProperty( window.Node.prototype, 'nodeValue', { ...propertyDescr2, get() {
228
+ const getter = () => propertyDescr2.get.call( this );
229
+ return this instanceof window.Attr && attrList.includes( this.name ) ? uuidsToLidrefs( this.ownerElement, this.name, getter ) : getter();
230
+ } } );
180
231
  // These APIs should return LIDREFS minus the hash part
181
232
  for ( const attrName of attrList ) {
182
233
  if ( !( attrName in relMap ) ) continue;
183
234
  const domApis = attrName === 'for' ? [ window.HTMLLabelElement, window.HTMLOutputElement ] : [ window.Element ];
184
235
  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 ) || '' );
236
+ const propertyDescr = Object.getOwnPropertyDescriptor( domApi.prototype, relMap[ attrName ] );
237
+ if ( !propertyDescr ) continue;
238
+ Object.defineProperty( domApi.prototype, relMap[ attrName ], { ...propertyDescr, get() {
239
+ const getter = () => propertyDescr.get.call( this, attrName );
240
+ return uuidsToLidrefs( this, attrName, getter );
189
241
  } } );
190
242
  }
191
243
  }
192
- // Reflect the custom [config.attr.lid] attribute
193
244
  if ( config.attr.lid !== 'id' ) {
245
+ // Reflect the custom [config.attr.lid] attribute
194
246
  Object.defineProperty( window.Element.prototype, config.attr.lid, { configurable: true, enumerable: true, get() {
195
247
  return this.getAttribute( config.attr.lid );
196
248
  }, set( value ) {
@@ -201,94 +253,105 @@ function realtime( config ) {
201
253
  // ------------
202
254
  // LOCAL IDS & IDREFS
203
255
  // ------------
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 );
256
+ const attrChange = ( entry, attrName, value, callback ) => {
257
+ return internalAttrInteraction( entry, attrName, () => {
258
+ if ( typeof value === 'function' ) value = value();
259
+ return callback( value );
260
+ } );
221
261
  };
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 );
262
+ const setupBinding = ( entry, attrName, value, newNamespaceObj = null ) => {
263
+ attrChange( entry, attrName, value, value => {
264
+ const isLidAttr = attrName === config.attr.lid;
265
+ const namespaceObj = newNamespaceObj || getOwnerNamespaceObject( entry, isLidAttr );
266
+ const namespaceUUID = getNamespaceUUID( namespaceObj );
267
+ if ( isLidAttr ) {
268
+ const id = $lidUtil.uuidToId( value );
269
+ if ( Observer.get( namespaceObj, id ) !== entry ) {
270
+ const uuid = $lidUtil.toUuid( namespaceUUID, id );
271
+ if ( uuid !== value ) { entry.setAttribute( 'id', uuid ); }
272
+ Observer.set( namespaceObj, id, entry );
273
+ }
274
+ } else {
275
+ _( entry, 'attrOriginals' ).set( attrName, value ); // Save original before rewrite
276
+ const newAttrValue = value.split( ' ' ).map( idref => ( idref = idref.trim() ) && !$lidUtil.isLidref( idref ) ? idref : $lidUtil.toUuid( namespaceUUID, idref.replace( $lidUtil.lidrefPrefix(), '' ) ) ).join( ' ' );
277
+ entry.setAttribute( attrName, newAttrValue );
278
+ _( namespaceObj ).set( 'idrefs', _( namespaceObj ).get( 'idrefs' ) || new Set );
279
+ _( namespaceObj ).get( 'idrefs' ).add( entry );
237
280
  }
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 );
241
- }
242
- // Release locking
243
- _( entry, 'lock' ).delete( attrName );
244
- };
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
- }
281
+ } );
257
282
  };
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 );
283
+ const cleanupBinding = ( entry, attrName, oldValue, prevNamespaceObj = null ) => {
284
+ attrChange( entry, attrName, oldValue, oldValue => {
285
+ const isLidAttr = attrName === config.attr.lid;
286
+ const namespaceObj = prevNamespaceObj || getOwnerNamespaceObject( entry, isLidAttr );
287
+ if ( isLidAttr ) {
288
+ const id = $lidUtil.uuidToId( oldValue );
289
+ if ( Observer.get( namespaceObj, id ) === entry ) {
290
+ Observer.deleteProperty( namespaceObj, id );
265
291
  }
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
- }
292
+ } else {
293
+ const newAttrValue = _( entry, 'attrOriginals' ).get( attrName );// oldValue.split( ' ' ).map( lid => ( lid = lid.trim() ) && $lidUtil.uuidToLidref( lid ) ).join( ' ' );
294
+ entry.setAttribute( attrName, newAttrValue );
295
+ _( namespaceObj ).get( 'idrefs' ).delete( entry );
296
+ }
297
+ } );
278
298
  };
279
299
 
300
+ // ------------
301
+ // NAMESPACE
302
+ // ------------
303
+ window.document[ configs.CONTEXT_API.api.contexts ].attach( new DOMNamingContext );
304
+ realdom.realtime( window.document ).subtree/*instead of observe(); reason: jsdom timing*/( config.namespaceSelector, record => {
305
+ const reAssociate = ( entry, attrName, oldNamespaceObj, newNamespaceObj ) => {
306
+ if ( !entry.hasAttribute( attrName ) ) return;
307
+ const attrValue = () => entry.getAttribute( attrName );
308
+ cleanupBinding( entry, attrName, attrValue/* Current resolved value as-is */, oldNamespaceObj );
309
+ if ( entry.isConnected ) { setupBinding( entry, attrName, _( entry, 'attrOriginals' ).get( attrName )/* Saved original value */ || attrValue/* Lest it's ID */, newNamespaceObj ); }
310
+ };
311
+ record.exits.forEach( entry => {
312
+ const namespaceObj = getOwnNamespaceObject( entry );
313
+ // Detach ID and IDREF associations
314
+ for ( const node of new Set( [ ...Object.values( namespaceObj ), ..._( namespaceObj ).get( 'idrefs' ) ] ) ) {
315
+ for ( const attrName of attrList ) { reAssociate( node, attrName, namespaceObj ); }
316
+ }
317
+ // Detach ID associations
318
+ const contextsApi = entry[ configs.CONTEXT_API.api.contexts ];
319
+ const ctx = contextsApi.find( DOMNamingContext.kind );
320
+ // Detach namespace instance
321
+ if ( ctx ) { contextsApi.detach( ctx ); }
322
+ } );
323
+ record.entrants.forEach( entry => {
324
+ // Claim ID and IDREF associations
325
+ let newSuperNamespaceObj;
326
+ const superNamespaceObj = getOwnerNamespaceObject( entry, true );
327
+ for ( const node of new Set( [ ...Object.values( superNamespaceObj ), ...( _( superNamespaceObj ).get( 'idrefs' ) || [] ) ] ) ) {
328
+ if ( ( newSuperNamespaceObj = getOwnerNamespaceObject( node, true ) ) === superNamespaceObj ) continue;
329
+ for ( const attrName of attrList ) { reAssociate( node, attrName, superNamespaceObj, newSuperNamespaceObj ); }
330
+ }
331
+ // Attach namespace instance
332
+ const contextsApi = entry[ configs.CONTEXT_API.api.contexts ];
333
+ if ( !contextsApi.find( DOMNamingContext.kind ) ) { contextsApi.attach( new DOMNamingContext ); }
334
+ } );
335
+ }, { live: true, timing: 'sync', staticSensitivity: true } );
336
+
280
337
  // DOM realtime
281
338
  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 );
339
+ for ( const attrName of attrList ) {
340
+ record.exits.forEach( entry => {
341
+ if ( !entry.hasAttribute( attrName ) ) return;
342
+ cleanupBinding( entry, attrName, () => entry.getAttribute( attrName )/* Current resolved value as-is */ );
343
+ } );
344
+ record.entrants.forEach( entry => {
345
+ if ( !entry.hasAttribute( attrName ) ) return;
346
+ setupBinding( entry, attrName, () => entry.getAttribute( attrName )/* Raw value (as-is) that will be saved as original */ );
347
+ } );
348
+ }
284
349
  }, { live: true, timing: 'sync' } );
285
350
  // Attr realtime
286
351
  realdom.realtime( window.document, 'attr' ).observe( attrList, records => {
287
352
  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 ); }
353
+ if ( record.oldValue ) { cleanupBinding( record.target, record.name, record.oldValue/* Current resolved value as-is */ ); }
354
+ if ( record.value ) { setupBinding( record.target, record.name, record.value/* Raw value (as-is) that will be saved as original */ ); }
292
355
  }
293
356
  }, { subtree: true, timing: 'sync', newValue: true, oldValue: true } );
294
357
 
@@ -297,13 +360,13 @@ function realtime( config ) {
297
360
  // ------------
298
361
  let prevTarget;
299
362
  const activateTarget = () => {
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 ) || [];
363
+ if ( !window.location.hash?.startsWith( `#${ $lidUtil.lidrefPrefix() }` ) ) return;
364
+ const path = window.location.hash?.substring( `#${ $lidUtil.lidrefPrefix() }`.length ).split( '/' ).map( s => s.trim() ).filter( s => s ) || [];
302
365
  const currTarget = path.reduce( ( prev, segment ) => prev && prev[ config.api.namespace ][ segment ], window.document );
303
- if ( prevTarget && config.target.attr ) { prevTarget.toggleAttribute( config.target.attr, false ); }
366
+ if ( prevTarget && config.target.className ) { prevTarget.classList.toggle( config.target.className, false ); }
304
367
  if ( currTarget && currTarget !== window.document ) {
305
- if ( config.target.attr ) { currTarget.toggleAttribute( config.target.attr, true ); }
306
- if ( config.target.event ) { currTarget.dispatchEvent( new window.CustomEvent( config.target.event ) ); }
368
+ if ( config.target.className ) { currTarget.classList.toggle( config.target.className, true ); }
369
+ if ( config.target.eventName ) { currTarget.dispatchEvent( new window.CustomEvent( config.target.eventName ) ); }
307
370
  if ( config.target.scrolling && path.length > 1 ) { currTarget.scrollIntoView(); }
308
371
  prevTarget = currTarget;
309
372
  }