@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/README.md +212 -217
- package/dist/bindings-api.js +1 -1
- package/dist/bindings-api.js.map +2 -2
- package/dist/context-api.js +1 -1
- package/dist/context-api.js.map +1 -1
- package/dist/data-binding.js +8 -8
- package/dist/data-binding.js.map +2 -2
- package/dist/html-imports.js +1 -1
- package/dist/html-imports.js.map +2 -2
- package/dist/main.js +25 -23
- package/dist/main.js.map +3 -3
- package/dist/main.lite.js +23 -21
- package/dist/main.lite.js.map +3 -3
- package/dist/namespaced-html.js +1 -1
- package/dist/namespaced-html.js.map +3 -3
- package/dist/scoped-css.js +5 -3
- package/dist/scoped-css.js.map +3 -3
- package/dist/scoped-js.js +1 -1
- package/dist/scoped-js.js.map +3 -3
- package/package.json +3 -3
- package/src/bindings-api/DOMBindingsContext.js +1 -1
- package/src/bindings-api/index.js +21 -21
- package/src/data-binding/index.js +4 -19
- package/src/html-imports/HTMLImportsContext.js +1 -3
- package/src/html-imports/index.js +7 -1
- package/src/index.js +3 -3
- package/src/namespaced-html/DOMNamingContext.js +54 -0
- package/src/namespaced-html/index.js +237 -60
- package/src/scoped-css/index.js +108 -16
- package/src/scoped-js/index.js +48 -25
- package/src/util.js +38 -0
- package/test/scoped-css.test.js +2 -2
- package/src/scoped-js/Hash.js +0 -26
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.
|
|
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
|
|
33
|
-
"preversion": "npm run
|
|
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
|
},
|
|
@@ -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 =
|
|
160
|
-
const [ left, right ] =
|
|
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 ] =
|
|
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
|
|
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',
|
|
15
|
-
api: { namespace: 'namespace',
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|