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