@webqit/oohtml 2.1.56 → 2.1.58
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 +86 -32
- package/dist/html-bindings.js +54 -0
- package/dist/html-bindings.js.map +7 -0
- package/dist/html-bracelets.js +1 -1
- package/dist/html-bracelets.js.map +3 -3
- package/dist/main.js +38 -6
- package/dist/main.js.map +3 -3
- package/dist/scoped-js.js +4 -4
- package/dist/scoped-js.js.map +3 -3
- package/package.json +3 -3
- package/src/html-bindings/index.js +233 -0
- package/src/index.js +3 -3
- package/src/html-bracelets/AttrBracelet.js +0 -122
- package/src/html-bracelets/Bracelet.js +0 -82
- package/src/html-bracelets/HTMLBracelets.js +0 -68
- package/src/html-bracelets/TextBracelet.js +0 -69
- package/src/html-bracelets/index.js +0 -72
- /package/src/{html-bracelets → html-bindings}/targets.browser.js +0 -0
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"wicg-proposal"
|
|
15
15
|
],
|
|
16
16
|
"homepage": "https://webqit.io/tooling/oohtml",
|
|
17
|
-
"version": "2.1.
|
|
17
|
+
"version": "2.1.58",
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
@@ -29,7 +29,7 @@
|
|
|
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/targets.browser.js context-api=src/context-api/targets.browser.js bindings-api=src/bindings-api/targets.browser.js html-
|
|
32
|
+
"build": "esbuild main=src/targets.browser.js context-api=src/context-api/targets.browser.js bindings-api=src/bindings-api/targets.browser.js html-bindings=src/html-bindings/targets.browser.js html-namespaces=src/html-namespaces/targets.browser.js html-imports=src/html-imports/targets.browser.js scoped-js=src/scoped-js/targets.browser.js scoped-css=src/scoped-css/targets.browser.js --bundle --minify --sourcemap --outdir=dist",
|
|
33
33
|
"preversion": "npm run build && git add -A dist",
|
|
34
34
|
"postversion": "npm publish",
|
|
35
35
|
"postpublish": "git push && git push --tags"
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@webqit/observer": "^2.2.9",
|
|
39
39
|
"@webqit/realdom": "^2.1.17",
|
|
40
|
-
"@webqit/stateful-js": "
|
|
40
|
+
"@webqit/stateful-js": "^3.0.27",
|
|
41
41
|
"@webqit/util": "^0.8.11"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
|
|
2
|
+
/**
|
|
3
|
+
* @imports
|
|
4
|
+
*/
|
|
5
|
+
import Observer from '@webqit/observer';
|
|
6
|
+
import { HTMLContext } from '../context-api/index.js';
|
|
7
|
+
import _HTMLBindingsProvider from '../bindings-api/_HTMLBindingsProvider.js';
|
|
8
|
+
import { StatefulAsyncFunction } from '@webqit/stateful-js/async';
|
|
9
|
+
import { _, _init } from '../util.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initializes DOM Parts.
|
|
13
|
+
*
|
|
14
|
+
* @param $config Object
|
|
15
|
+
*
|
|
16
|
+
* @return Void
|
|
17
|
+
*/
|
|
18
|
+
export default function init( $config = {} ) {
|
|
19
|
+
const { config, window } = _init.call( this, 'html-bindings', $config, {
|
|
20
|
+
attr: { bind: 'bind', itemIndex: 'data-index' },
|
|
21
|
+
tokens: { nodeType: 'processing-instruction', tagStart: '?{', tagEnd: '}?', stateStart: '; [=', stateEnd: ']' },
|
|
22
|
+
staticsensitivity: true,
|
|
23
|
+
isomorphic: true,
|
|
24
|
+
} );
|
|
25
|
+
config.api = {
|
|
26
|
+
bind: window.webqit.oohtml.configs.BINDINGS_API.api.bind,
|
|
27
|
+
import: window.webqit.oohtml.configs.HTML_IMPORTS.context.api.import,
|
|
28
|
+
};
|
|
29
|
+
config.attrSelector = `[${ window.CSS.escape( config.attr.bind ) }]`;
|
|
30
|
+
const braceletMatch = ( start, end ) => {
|
|
31
|
+
const starting = `starts-with(., "${ start }")`;
|
|
32
|
+
const ending = `substring(., string-length(.) - string-length("${ end }") + 1) = "${ end }"`;
|
|
33
|
+
return `${ starting } and ${ ending }`;
|
|
34
|
+
}
|
|
35
|
+
config.braceletSelector = `comment()[${ braceletMatch( config.tokens.tagStart, config.tokens.tagEnd ) }]`;
|
|
36
|
+
window.webqit.Observer = Observer;
|
|
37
|
+
realtime.call( window, config );
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Performs realtime capture of elements and their attributes
|
|
42
|
+
*
|
|
43
|
+
* @param Object config
|
|
44
|
+
*
|
|
45
|
+
* @return Void
|
|
46
|
+
*/
|
|
47
|
+
function realtime( config ) {
|
|
48
|
+
const window = this, { realdom } = window.webqit;
|
|
49
|
+
// ----------------
|
|
50
|
+
realdom.realtime( window.document ).subtree( `(${ config.braceletSelector })`, record => {
|
|
51
|
+
cleanup.call( this, ...record.exits );
|
|
52
|
+
mountBracelets.call( this, config, ...record.entrants );
|
|
53
|
+
}, { live: true } );
|
|
54
|
+
realdom.realtime( window.document ).subtree( config.attrSelector, record => {
|
|
55
|
+
cleanup.call( this, ...record.exits );
|
|
56
|
+
mountInlineSubscript.call( this, config, ...record.entrants );
|
|
57
|
+
}, { live: true, timing: 'sync', staticSensitivity: config.staticsensitivity } );
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function createDynamicScope( root ) {
|
|
61
|
+
if ( _( root ).has( 'subscripts' ) ) return _( root ).get( 'subscripts' );
|
|
62
|
+
const scope = {}, abortController = new AbortController;
|
|
63
|
+
scope.$set = function( node, prop, val ) {
|
|
64
|
+
node && ( node[ prop ] = val );
|
|
65
|
+
}
|
|
66
|
+
Observer.intercept( scope, {
|
|
67
|
+
get: ( e, recieved, next ) => {
|
|
68
|
+
if ( !( e.key in scope ) ) {
|
|
69
|
+
const request = _HTMLBindingsProvider.createRequest( { detail: e.key, live: true, signal: abortController.signal } );
|
|
70
|
+
HTMLContext.instance( root ).request( request, value => {
|
|
71
|
+
Observer.set( scope, e.key, value );
|
|
72
|
+
} );
|
|
73
|
+
}
|
|
74
|
+
return next( scope[ e.key ] ?? ( e.key in globalThis ? globalThis[ e.key ] : undefined ) );
|
|
75
|
+
},
|
|
76
|
+
has: ( e, recieved, next ) => { return next( true ); }
|
|
77
|
+
} );
|
|
78
|
+
const instance = { scope, abortController, subscripts: new Map };
|
|
79
|
+
_( root ).set( 'subscripts', instance );
|
|
80
|
+
return instance;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function cleanup( ...entries ) {
|
|
84
|
+
for ( const node of entries ) {
|
|
85
|
+
const root = node.nodeName === '#text' ? node.parentNode : node;
|
|
86
|
+
const { subscripts, abortController } = _( root ).get( 'subscripts' ) || {};
|
|
87
|
+
if ( !subscripts?.has( node ) ) return;
|
|
88
|
+
subscripts.get( node ).state.dispose();
|
|
89
|
+
subscripts.get( node ).signals.forEach( s => s.abort() );
|
|
90
|
+
subscripts.delete( node );
|
|
91
|
+
if ( !subscripts.size ) {
|
|
92
|
+
abortController.abort();
|
|
93
|
+
_( root ).delete( 'subscripts' );
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function mountBracelets( config, ...entries ) {
|
|
99
|
+
const window = this;
|
|
100
|
+
const patternMatch = str => {
|
|
101
|
+
const tagStart = config.tokens.tagStart.split( '' ).map( x => `\\${ x }` ).join( '' );
|
|
102
|
+
const tagEnd = config.tokens.tagEnd.split( '' ).map( x => `\\${ x }` ).join( '' );
|
|
103
|
+
const stateStart = config.tokens.stateStart.split( '' ).map( x => x === ' ' ? `(?:\\s+)?` : `\\${ x }` ).join( '' );
|
|
104
|
+
const stateEnd = config.tokens.stateEnd.split( '' ).map( x => `\\${ x }` ).join( '' );
|
|
105
|
+
const pattern = `^${ tagStart }(.*?)(?:${ stateStart }(\\d+)${ stateEnd }(?:\\s+)?)?${ tagEnd }$`;
|
|
106
|
+
const [ /*raw*/, expr, span ] = str.match( new RegExp( pattern ) );
|
|
107
|
+
return { raw: str, expr, span: parseInt( span ?? 0 ) };
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
const instances = entries.reduce( ( instances, node ) => {
|
|
111
|
+
if ( node.isBracelet ) return instances;
|
|
112
|
+
const template = patternMatch( node.nodeValue );
|
|
113
|
+
let textNode = node;
|
|
114
|
+
if ( template.span ) {
|
|
115
|
+
textNode = node.nextSibling;
|
|
116
|
+
if ( textNode?.nodeName !== '#text' || textNode.nodeValue.length < template.span ) return instances;
|
|
117
|
+
if ( textNode.nodeValue.length > template.span ) { textNode.splitText( template.span ); }
|
|
118
|
+
} else if ( node.nextSibling ) {
|
|
119
|
+
textNode = node.parentNode.insertBefore( node.ownerDocument.createTextNode( '' ), node.nextSibling );
|
|
120
|
+
} else { textNode = node.parentNode.appendChild( node.ownerDocument.createTextNode( '' ) ); }
|
|
121
|
+
textNode.isBracelet = true;
|
|
122
|
+
let stateNode = node;
|
|
123
|
+
if ( window.webqit.env !== 'server' ) {
|
|
124
|
+
stateNode.remove();
|
|
125
|
+
stateNode = null;
|
|
126
|
+
}
|
|
127
|
+
return instances.concat( { textNode, template, stateNode } );
|
|
128
|
+
}, [] );
|
|
129
|
+
|
|
130
|
+
for ( const { textNode, template, stateNode } of instances ) {
|
|
131
|
+
const { scope: env, subscripts } = createDynamicScope( textNode.parentNode );
|
|
132
|
+
let source = '';
|
|
133
|
+
source += `let content = ((${ template.expr }) ?? '') + '';`;
|
|
134
|
+
source += `$set(this, 'nodeValue', content);`;
|
|
135
|
+
if ( stateNode ) { source += `$set($stateNode__, 'nodeValue', \`${ config.tokens.tagStart }${ template.expr }${ config.tokens.stateStart }\` + content.length + \`${ config.tokens.stateEnd } ${ config.tokens.tagEnd }\`);`; }
|
|
136
|
+
const compiled = new StatefulAsyncFunction( '$signals__', `$stateNode__`, source, { env } );
|
|
137
|
+
const signals = [];
|
|
138
|
+
subscripts.set( textNode, { compiled, signals, state: await compiled.call( textNode, signals, stateNode ), } );
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
async function mountInlineSubscript( config, ...entries ) {
|
|
143
|
+
for ( const node of entries ) {
|
|
144
|
+
const source = parseInlineBindings( config, node.getAttribute( config.attr.bind ) );
|
|
145
|
+
const { scope: env, subscripts } = createDynamicScope( node );
|
|
146
|
+
const compiled = new StatefulAsyncFunction( '$signals__', source, { env } );
|
|
147
|
+
const signals = [];
|
|
148
|
+
subscripts.set( node, { compiled, signals, state: await compiled.call( node, signals ), } );
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const parseCache = new Map;
|
|
153
|
+
function parseInlineBindings( config, str ) {
|
|
154
|
+
if ( parseCache.has( str ) ) return parseCache.get( str );
|
|
155
|
+
const validation = {};
|
|
156
|
+
const source = splitOuter( str, ';' ).map( str => {
|
|
157
|
+
const [ left, right ] = splitOuter( str, ':' ).map( x => x.trim() );
|
|
158
|
+
const token = left[ 0 ], param = left.slice( 1 ).trim();
|
|
159
|
+
const $expr = `(${ right })`, $$expr = `(${ $expr } ?? '')`;
|
|
160
|
+
if ( token === '&' ) return `this.style[\`${ param }\`] = ${ $$expr };`;
|
|
161
|
+
if ( token === '%' ) return `this.classList.toggle(\`${ param }\`, !!${ $expr });`;
|
|
162
|
+
if ( token === '@' ) {
|
|
163
|
+
if ( param.endsWith( '?' ) ) return `this.toggleAttribute(\`${ param.substring( 0, -1 ).trim() }\`, !!${ $expr });`;
|
|
164
|
+
return `this.setAttribute(\`${ param }\`, ${ $$expr });`;
|
|
165
|
+
}
|
|
166
|
+
if ( token === '~' ) {
|
|
167
|
+
if ( validation[ param ] ) throw new Error( `Duplicate binding: ${ left }.` );
|
|
168
|
+
validation[ param ] = true;
|
|
169
|
+
if ( param === 'text' ) return `$set(this, 'textContent', ${ $$expr });`;
|
|
170
|
+
if ( param === 'html' ) return `this.setHTML(${ $$expr });`;
|
|
171
|
+
if ( param === 'items' ) {
|
|
172
|
+
const [ iterationSpec, importSpec ] = splitOuter( right, '/' );
|
|
173
|
+
let [ raw, production, kind, iteratee ] = iterationSpec.trim().match( /(.*?[\)\s+])(of|in)([\(\{\[\s+].*)/i ) || [];
|
|
174
|
+
if ( !raw ) throw new Error( `Invalid ${ token }items spec: ${ str }.` );
|
|
175
|
+
if ( production.startsWith( '(' ) ) {
|
|
176
|
+
production = production.trim().slice( 1, -1 ).split( ',' ).map( x => x.trim() );
|
|
177
|
+
} else { production = [ production ]; }
|
|
178
|
+
if ( production.length > ( kind === 'in' ? 3 : 2 ) ) throw new Error( `Invalid ${ token }items spec: ${ str }.` );
|
|
179
|
+
const indices = kind === 'in' ? production[ 2 ] : ( production[ 1 ] || '$index__' );
|
|
180
|
+
return `
|
|
181
|
+
let $iteratee__ = ${ iteratee };
|
|
182
|
+
let $import__ = this.${ config.api.import }( ${ importSpec.trim() }, true );
|
|
183
|
+
$signals__.push( $import__ );
|
|
184
|
+
|
|
185
|
+
if ( $import__.value && $iteratee__ ) {
|
|
186
|
+
let $existing__ = new Map;
|
|
187
|
+
this.querySelectorAll( '[${ config.attr.itemIndex }]' ).forEach( x => {
|
|
188
|
+
$existing__.set( x.getAttribute( '${ config.attr.itemIndex }' ), x );
|
|
189
|
+
} );
|
|
190
|
+
${ indices ? `let ${ indices } = -1;` : '' }
|
|
191
|
+
for ( let ${ production[ 0 ] } ${ kind } $iteratee__ ) {
|
|
192
|
+
${ indices ? `${ indices } ++;` : '' }
|
|
193
|
+
${ kind === 'in' && production[ 1 ] ? `let ${ production[ 1 ] } = $iteratee__[ ${ production[ 0 ] } ];` : '' }
|
|
194
|
+
const $itemBinding__ = { ${ production.join( ', ' ) } };
|
|
195
|
+
|
|
196
|
+
const $key___ = ( ${ kind === 'in' ? production[ 0 ] : indices } ) + '';
|
|
197
|
+
let $itemNode__ = $existing__.get( $key___ );
|
|
198
|
+
if ( $itemNode__ ) {
|
|
199
|
+
$existing__.delete( $key___ );
|
|
200
|
+
} else {
|
|
201
|
+
$itemNode__ = ( Array.isArray( $import__.value ) ? $import__.value[ 0 ] : ( $import__.value.content ? $import__.value.content.firstElementChild : $import__.value ) ).cloneNode( true );
|
|
202
|
+
$itemNode__.setAttribute( "${ config.attr.itemIndex }", $key___ );
|
|
203
|
+
this.appendChild( $itemNode__ );
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
$itemNode__.${ config.api.bind }( $itemBinding__ );
|
|
207
|
+
if ( ${ kind === 'in' ? `!( ${ production[ 0 ] } in $iteratee__ )` : `typeof ${ production[ 0 ] } === 'undefined'` } ) { $itemNode__.remove(); }
|
|
208
|
+
}
|
|
209
|
+
$existing__.forEach( x => x.remove() );
|
|
210
|
+
$existing__.clear();
|
|
211
|
+
}`;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if ( str.trim() ) throw new Error( `Invalid binding: ${ str }.` );
|
|
215
|
+
} ).join( `\n` );
|
|
216
|
+
parseCache.set( str, source );
|
|
217
|
+
return source;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function splitOuter( str, delim ) {
|
|
221
|
+
return [ ...str ].reduce( ( [ quote, depth, splits, skip ], x ) => {
|
|
222
|
+
if ( !quote && depth === 0 && ( Array.isArray( delim ) ? delim : [ delim ] ).includes( x ) ) {
|
|
223
|
+
return [ quote, depth, [ '' ].concat( splits ) ];
|
|
224
|
+
}
|
|
225
|
+
if ( !quote && [ '(', '[', '{' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) depth++;
|
|
226
|
+
if ( !quote && [ ')', ']', '}' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) depth--;
|
|
227
|
+
if ( [ '"', "'", '`' ].includes( x ) && !splits[ 0 ].endsWith( '\\' ) ) {
|
|
228
|
+
quote = quote === x ? null : ( quote || x );
|
|
229
|
+
}
|
|
230
|
+
splits[ 0 ] += x;
|
|
231
|
+
return [ quote, depth, splits ]
|
|
232
|
+
}, [ null, 0, [ '' ] ] )[ 2 ].reverse();
|
|
233
|
+
}
|
package/src/index.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import Observer from '@webqit/observer';
|
|
6
6
|
import ContextAPI from './context-api/index.js';
|
|
7
7
|
import BindingsAPI from './bindings-api/index.js';
|
|
8
|
-
import
|
|
8
|
+
import HTMLBindings from './html-bindings/index.js';
|
|
9
9
|
import HTMLNamespaces from './html-namespaces/index.js';
|
|
10
10
|
import HTMLImports from './html-imports/index.js';
|
|
11
11
|
import ScopedCSS from './scoped-css/index.js';
|
|
@@ -24,9 +24,9 @@ export default function init( configs = {} ) {
|
|
|
24
24
|
// --------------
|
|
25
25
|
ContextAPI.call( this, ( configs.CONTEXT_API || {} ) );
|
|
26
26
|
BindingsAPI.call( this, ( configs.BINDINGS_API || {} ) );
|
|
27
|
-
|
|
27
|
+
HTMLImports.call( this, ( configs.HTML_IMPORTS || {} ) ); // Depends ContextAPI
|
|
28
|
+
HTMLBindings.call( this, ( configs.HTML_BRACELETS || {} ) ); // Depends ContextAPI, BindingsAPI, HTMLImports
|
|
28
29
|
HTMLNamespaces.call( this, ( configs.HTML_NAMESPACES || {} ) );
|
|
29
|
-
HTMLImports.call( this, ( configs.HTML_IMPORTS || {} ) );
|
|
30
30
|
ScopedCSS.call( this, ( configs.SCOPED_CSS || {} ) );
|
|
31
31
|
ScopedJS.call( this, ( configs.SCOPED_JS || {} ) );
|
|
32
32
|
// --------------
|
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @imports
|
|
4
|
-
*/
|
|
5
|
-
import HTMLBracelets from './HTMLBracelets.js';
|
|
6
|
-
import Bracelet from './Bracelet.js';
|
|
7
|
-
import { _ } from '../util.js';
|
|
8
|
-
|
|
9
|
-
export default class AttrBracelet extends Bracelet {
|
|
10
|
-
static get query() { return `@*[${ this.tokens.contains }]`; }
|
|
11
|
-
|
|
12
|
-
static parse( ...attrs ) {
|
|
13
|
-
return attrs.reduce( ( attrs, attr ) => {
|
|
14
|
-
return attrs.concat( [ ...attr.nodeValue.matchAll( new RegExp( this.tokens.regex, 'g' ) ) ].reduce( ( bracelets, match ) => {
|
|
15
|
-
const bracelet = new this( attr, match[ 0 ], match.index, match[ 1 ].trim(), attr.nodeName === 'class' || match[ 0 ] === attr.nodeValue.trim() );
|
|
16
|
-
const prev = bracelets.slice( -1 )[ 0 ];
|
|
17
|
-
if ( prev ) { prev._nextSibling = bracelet; }
|
|
18
|
-
return bracelets.concat( bracelet );
|
|
19
|
-
}, [] ) );
|
|
20
|
-
}, [] );
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
static mount( ...bracelets ) {
|
|
24
|
-
for ( const bracelet of bracelets ) {
|
|
25
|
-
// Add to attr-specific registry
|
|
26
|
-
let attrBraceletsRegistry = _( bracelet.ownerElement ).get( 'attr-bracelets' );
|
|
27
|
-
if ( !attrBraceletsRegistry ) {
|
|
28
|
-
attrBraceletsRegistry = new Map;
|
|
29
|
-
attrBraceletsRegistry.active = [];
|
|
30
|
-
_( bracelet.ownerElement ).set( 'attr-bracelets', attrBraceletsRegistry );
|
|
31
|
-
}
|
|
32
|
-
let attrBracelets = attrBraceletsRegistry.get( bracelet.attr.nodeName );
|
|
33
|
-
if ( !attrBracelets ) {
|
|
34
|
-
attrBracelets = new Set;
|
|
35
|
-
attrBraceletsRegistry.set( bracelet.attr.nodeName, attrBracelets );
|
|
36
|
-
}
|
|
37
|
-
attrBracelets.add( bracelet );
|
|
38
|
-
attrBraceletsRegistry.active[ 0 ]?._nested.add( bracelet );
|
|
39
|
-
// Add to general registry
|
|
40
|
-
HTMLBracelets.instance( bracelet.ownerElement ).add( bracelet );
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
static cleanup( ...attrs ) {
|
|
45
|
-
for ( const attr of attrs ) {
|
|
46
|
-
// Remove from attr-specific registry
|
|
47
|
-
const attrBraceletsRegistry = _( attr.ownerElement ).get( 'attr-bracelets' );
|
|
48
|
-
attrBraceletsRegistry?.get( attr.nodeName )?.forEach( bracelet => {
|
|
49
|
-
bracelet.disconnect();
|
|
50
|
-
// Remove from general registry
|
|
51
|
-
HTMLBracelets.instance( bracelet.ownerElement ).delete( bracelet );
|
|
52
|
-
} );
|
|
53
|
-
attrBraceletsRegistry?.delete( attr.nodeName );
|
|
54
|
-
if ( attrBraceletsRegistry && !attrBraceletsRegistry.size ) { _( attr.ownerElement ).delete( 'attr-bracelets' ); }
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
constructor( attr, _value, startIndex, expr, booleanAble ) {
|
|
59
|
-
super();
|
|
60
|
-
const $refs = [], $expr = this.parseExpr( expr, $refs );
|
|
61
|
-
const ownerElement = attr.ownerElement; // Hard save. Deleted attributes don't retain .ownerElement
|
|
62
|
-
Object.defineProperties( this, {
|
|
63
|
-
type: { get: () => 'attr' },
|
|
64
|
-
expr: { get: () => $expr },
|
|
65
|
-
refs: { get: () => $refs },
|
|
66
|
-
attr: { get: () => attr },
|
|
67
|
-
ownerElement: { get: () => ownerElement },
|
|
68
|
-
originalValue: { value: _value },
|
|
69
|
-
_value: { value: _value, writable: true },
|
|
70
|
-
_dirty: { value: false, writable: true },
|
|
71
|
-
_startIndex: { value: undefined, writable: true },
|
|
72
|
-
_endIndex: { value: undefined, writable: true },
|
|
73
|
-
_nextSibling: { value: undefined, writable: true },
|
|
74
|
-
_booleanAble: { value: booleanAble, writable: true },
|
|
75
|
-
_nested: { value: new Set },
|
|
76
|
-
} );
|
|
77
|
-
if ( this.attr.nodeName === 'class' && ( this.expr.length !== 1 || this.expr[ 0 ].type !== 'ref' ) ) {
|
|
78
|
-
throw new Error( `Invalid bracelet for the class attribute: "${ this.originalValue }"` );
|
|
79
|
-
}
|
|
80
|
-
this.startIndex = startIndex;
|
|
81
|
-
}
|
|
82
|
-
get isBoolean() { return this._booleanAble && typeof this._value === 'boolean'; }
|
|
83
|
-
|
|
84
|
-
get startIndex() { return this._startIndex; }
|
|
85
|
-
set startIndex( value ) {
|
|
86
|
-
this._startIndex = value;
|
|
87
|
-
this.endIndex = this._startIndex + this.value.length;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
get endIndex() { return this._endIndex; }
|
|
91
|
-
set endIndex( value ) {
|
|
92
|
-
if ( value === this._endIndex ) return;
|
|
93
|
-
if ( this.nextSibling ) { this.nextSibling.startIndex += value - this._endIndex; }
|
|
94
|
-
this._endIndex = value;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
get value() { return this._value; }
|
|
98
|
-
set value( value ) {
|
|
99
|
-
if ( this.disconnected || value === this._value ) return;
|
|
100
|
-
this._value = value;
|
|
101
|
-
this._dirty = true;
|
|
102
|
-
// Set attribute; but first disconnect any "nested"
|
|
103
|
-
this._nested.forEach( p => p.disconnect() );
|
|
104
|
-
const attrBraceletsRegistry = _( this.ownerElement ).get( 'attr-bracelets' );
|
|
105
|
-
attrBraceletsRegistry.active.unshift( this );
|
|
106
|
-
if ( this.isBoolean && this.attr.nodeName !== 'class' ) {
|
|
107
|
-
this.ownerElement.toggleAttribute( this.attr.nodeName, value );
|
|
108
|
-
} else {
|
|
109
|
-
if ( this.isBoolean && this.attr.nodeName === 'class' ) {
|
|
110
|
-
value = value ? this.expr[ 0 ].value.join( '' ) : '';
|
|
111
|
-
}
|
|
112
|
-
this.ownerElement.setAttribute( this.attr.nodeName, this.attr.nodeValue.substring( 0, this.startIndex ) + value + this.attr.nodeValue.substring( this.endIndex ) );
|
|
113
|
-
}
|
|
114
|
-
attrBraceletsRegistry.active.shift();
|
|
115
|
-
// Reindex
|
|
116
|
-
const newEndIndex = this.startIndex + value.length;
|
|
117
|
-
if ( newEndIndex !== this.endIndex ) { this.endIndex = newEndIndex; }
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
get nextSibling() { return this._nextSibling; }
|
|
121
|
-
get dirty() { return this._dirty; }
|
|
122
|
-
}
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
export default class Bracelet {
|
|
3
|
-
static tokens = {
|
|
4
|
-
startTag: `{`, endTag: `}`,
|
|
5
|
-
get contains () { return `contains(., "${ this.startTag }") and contains(substring-after(., "${ this.startTag }"), "${ this.endTag }")`; },
|
|
6
|
-
get startsAndEnds() { return `starts-with(., "${ this.startTag }") and substring(., string-length(.) - string-length("${ this.endTag }") + 1) = "${ this.endTag }"`; },
|
|
7
|
-
get regex() {
|
|
8
|
-
const startTag = this.startTag.split( '' ).map( s => '\\' + s );
|
|
9
|
-
const endTag = this.endTag.split( '' ).map( s => '\\' + s );
|
|
10
|
-
return `${ startTag.join( '' ) }([^${ startTag[ 0 ] }]+)${ endTag.join( '' ) }`;
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
parseExpr( expr, refs ) {
|
|
15
|
-
const tokens = expr.match( /[\+\-\*\/&|\?\:\=\<\>\%]+|(['"])[^'"]*\1|\[[^\]]*\]|\([^)]*\)|\S+/g ).map( s => s.trim() );
|
|
16
|
-
return tokens.map( s => {
|
|
17
|
-
let meta = {};
|
|
18
|
-
if ( s[ 0 ] === '!' ) { meta = { negation: true }; s = s.slice( 1 ); }
|
|
19
|
-
if ( /^\(.*\)$/.test( s ) ) return { type: 'expr', value: this.parseExpr( s.slice( 1, -1 ), refs ), ...meta }; // must be before the operator test
|
|
20
|
-
if ( [ '"', "'" ].includes( s[ 0 ] ) ) return { type: 'literal', value: /(['"])(.*?)\1/g.exec( s )[ 2 ], ...meta }; // must be before the operator test
|
|
21
|
-
if ( /[\+\-\*\/&|\?\:\=\<\>\%]/.test( s ) ) return { type: 'operator', value: s, ...meta };
|
|
22
|
-
if ( !isNaN( s ) ) return { type: 'literal', value: parseFloat( s ), ...meta };
|
|
23
|
-
const ref = s.match( /[^\.\[\]]+/g );
|
|
24
|
-
refs.push( ref );
|
|
25
|
-
return { type: 'ref', value: ref, ...meta };
|
|
26
|
-
} );
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
renderExpr( expr, bindings ) {
|
|
30
|
-
return expr.reduce( ( [ prev, operator, state ], token ) => {
|
|
31
|
-
// No further evaluations allowed?
|
|
32
|
-
if ( state === 'end' ) return [ prev, null, 'end' ];
|
|
33
|
-
// Mode has been consequent and we've now hit the alternate block?
|
|
34
|
-
if ( state === 'consequent' && token.type === 'operator' && token.value === ':' ) return [ prev, null, 'end' ];
|
|
35
|
-
// Always return operators at this level
|
|
36
|
-
if ( token.type === 'operator' ) return [ prev, token, state ];
|
|
37
|
-
// Still expecting to hit the alternate block?
|
|
38
|
-
if ( state === 'alternate' && operator?.value !== ':' ) return [ null, null, 'alternate' ];
|
|
39
|
-
// Main...
|
|
40
|
-
let value, render = ( token, val ) => token.negation ? !val : val;
|
|
41
|
-
switch ( token.type ) {
|
|
42
|
-
case 'ref': value = render( token, bindings[ token.value.join( '.' ) ].value ); break;
|
|
43
|
-
case 'expr': value = render( token, this.renderExpr( token.value, bindings ) ); break;
|
|
44
|
-
default: value = render( token, token.value );
|
|
45
|
-
}
|
|
46
|
-
switch ( operator?.value ) {
|
|
47
|
-
case '-': return [ prev - value, null, state ];
|
|
48
|
-
case '+': return [ prev + value, null, state ];
|
|
49
|
-
case '/': return [ prev / value, null, state ];
|
|
50
|
-
case '*': return [ prev * value, null, state ];
|
|
51
|
-
case '%': return [ prev % value, null, state ];
|
|
52
|
-
case '===': return [ prev === value, null, state ];
|
|
53
|
-
case '==': return [ render( operator, prev == value ), null, state ];
|
|
54
|
-
case '>=': return [ prev >= value, null, state ];
|
|
55
|
-
case '<=': return [ prev <= value, null, state ];
|
|
56
|
-
case '>': return [ prev > value, null, state ];
|
|
57
|
-
case '<': return [ prev < value, null, state ];
|
|
58
|
-
case '||': return [ prev || value, null, state ];
|
|
59
|
-
case '&&': return [ prev && value, null, state ];
|
|
60
|
-
case '?': return prev ? [ value, null, 'consequent' ] : [ null, null, 'alternate' ];
|
|
61
|
-
case '??': return [ prev ?? value, null, state ];
|
|
62
|
-
default: return [ value ];
|
|
63
|
-
}
|
|
64
|
-
}, [ null, null ] )[ 0 ];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
render( bindings ) {
|
|
68
|
-
let value = this.renderExpr( this.expr, bindings );
|
|
69
|
-
if ( typeof value === 'undefined' ) {
|
|
70
|
-
value = this.originalValue;
|
|
71
|
-
if ( !this.dirty ) {
|
|
72
|
-
if ( this._booleanAble ) { value = false; }
|
|
73
|
-
else return;
|
|
74
|
-
}
|
|
75
|
-
this.value = value;
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
this.value = value;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
disconnect() { this.disconnected = true; }
|
|
82
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @imports
|
|
4
|
-
*/
|
|
5
|
-
import { HTMLContext } from '../context-api/index.js';
|
|
6
|
-
import _HTMLBindingsProvider from '../bindings-api/_HTMLBindingsProvider.js';
|
|
7
|
-
import Bracelet from './Bracelet.js';
|
|
8
|
-
import { _ } from '../util.js';
|
|
9
|
-
|
|
10
|
-
export default class HTMLBracelets extends Set {
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* @instance
|
|
14
|
-
*/
|
|
15
|
-
static instance( host ) {
|
|
16
|
-
return _( host ).get( 'bracelets::instance' ) || new this( host );
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @constructor
|
|
21
|
-
*/
|
|
22
|
-
constructor( host ) {
|
|
23
|
-
super();
|
|
24
|
-
_( host ).get( `bracelets::instance` )?.dispose();
|
|
25
|
-
_( host ).set( `bracelets::instance`, this );
|
|
26
|
-
const priv = { host, bindings: Object.create( null ) };
|
|
27
|
-
Object.defineProperty( this, '#', { get: () => priv } );
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
add( bracelet ) {
|
|
31
|
-
if ( !( bracelet instanceof Bracelet ) ) throw new Error( `Argument must be instance of Bracelet.` );
|
|
32
|
-
const returnValue = super.add( bracelet );
|
|
33
|
-
const bindings = this[ '#' ].bindings;
|
|
34
|
-
bracelet.refs.forEach( path => {
|
|
35
|
-
const $path = path.join( '.' );
|
|
36
|
-
if ( !( $path in bindings ) ) {
|
|
37
|
-
bindings[ $path ] = { subs: new Set, controller: new AbortController };
|
|
38
|
-
const request = _HTMLBindingsProvider.createRequest( { detail: path, live: true, signal: bindings[ $path ].signal } );
|
|
39
|
-
HTMLContext.instance( this[ '#' ].host ).request( request, value => {
|
|
40
|
-
bindings[ $path ].value = value;
|
|
41
|
-
bindings[ $path ].subs.forEach( bracelet => bracelet.render( bindings ) );
|
|
42
|
-
} );
|
|
43
|
-
}
|
|
44
|
-
bindings[ $path ].subs.add( bracelet );
|
|
45
|
-
} );
|
|
46
|
-
bracelet.render( bindings );
|
|
47
|
-
return returnValue;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
delete( bracelet ) {
|
|
51
|
-
if ( !( bracelet instanceof Bracelet ) ) throw new Error( `Argument must be instance of Bracelet.` );
|
|
52
|
-
const returnValue = super.delete( bracelet );
|
|
53
|
-
const bindings = this[ '#' ].bindings;
|
|
54
|
-
bracelet.refs.forEach( path => {
|
|
55
|
-
const $path = path.join( '.' );
|
|
56
|
-
bindings[ $path ].subs.delete( bracelet );
|
|
57
|
-
if ( !bindings[ $path ].subs.size ) {
|
|
58
|
-
bindings[ $path ].controller.abort();
|
|
59
|
-
delete bindings[ $path ];
|
|
60
|
-
}
|
|
61
|
-
} );
|
|
62
|
-
return returnValue;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
clear() {
|
|
66
|
-
for ( const bracelet of this ) { this.delete( bracelet ); }
|
|
67
|
-
}
|
|
68
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
/**
|
|
3
|
-
* @imports
|
|
4
|
-
*/
|
|
5
|
-
import HTMLBracelets from './HTMLBracelets.js';
|
|
6
|
-
import Bracelet from './Bracelet.js';
|
|
7
|
-
import { _ } from '../util.js';
|
|
8
|
-
|
|
9
|
-
export default class TextBracelet extends Bracelet {
|
|
10
|
-
static get query() { return `text()[not(ancestor::script) and not(ancestor::style) and ${ this.tokens.contains }]`; }
|
|
11
|
-
|
|
12
|
-
static parse( ...nodes ) {
|
|
13
|
-
return nodes.reduce( ( nodes, node ) => {
|
|
14
|
-
let $node = node, $rest = node, startIndex, endIndex;
|
|
15
|
-
while ( $rest && ( startIndex = $rest.nodeValue.indexOf( this.tokens.startTag ) ) > -1 ) {
|
|
16
|
-
if ( startIndex > 0 ) { $node = $rest.splitText( startIndex ); }
|
|
17
|
-
if ( ( endIndex = $node.nodeValue.indexOf( this.tokens.endTag ) + this.tokens.endTag.length ) !== $node.nodeValue.length ) {
|
|
18
|
-
$rest = $node.splitText( endIndex );
|
|
19
|
-
} else { $rest = null; }
|
|
20
|
-
nodes.push( new this( $node ) );
|
|
21
|
-
}
|
|
22
|
-
return nodes;
|
|
23
|
-
}, [] );
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
static mount( ...bracelets ) {
|
|
27
|
-
for ( const bracelet of bracelets ) {
|
|
28
|
-
_( bracelet.node ).set( 'text-bracelet', bracelet );
|
|
29
|
-
HTMLBracelets.instance( bracelet.ownerElement ).add( bracelet );
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
static cleanup( ...nodes ) {
|
|
34
|
-
for ( const node of nodes ) {
|
|
35
|
-
const bracelet = _( node ).get( 'text-bracelet' );
|
|
36
|
-
if ( !bracelet ) continue;
|
|
37
|
-
bracelet.disconnect();
|
|
38
|
-
HTMLBracelets.instance( bracelet.ownerElement ).delete( bracelet );
|
|
39
|
-
_( node ).delete( 'text-bracelet' );
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
constructor( node ) {
|
|
44
|
-
super();
|
|
45
|
-
const expr = [ ...node.nodeValue.match( new RegExp( this.constructor.tokens.regex ) ) ][ 1 ].trim();
|
|
46
|
-
const $refs = [], $expr = this.parseExpr( expr, $refs );
|
|
47
|
-
Object.defineProperties( this, {
|
|
48
|
-
_value: { value: node.nodeValue, writable: true },
|
|
49
|
-
_dirty: { value: false, writable: true },
|
|
50
|
-
type: { get: () => 'text' },
|
|
51
|
-
expr: { get: () => $expr },
|
|
52
|
-
refs: { get: () => $refs },
|
|
53
|
-
node: { get: () => node },
|
|
54
|
-
ownerElement: { get: () => node.parentNode },
|
|
55
|
-
originalValue: { value: node.nodeValue },
|
|
56
|
-
} );
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
get value() { return this._value; }
|
|
60
|
-
set value( value ) {
|
|
61
|
-
if ( this.disconnected || value === this._value ) return;
|
|
62
|
-
this._value = value;
|
|
63
|
-
this._dirty = true;
|
|
64
|
-
this.node.nodeValue = value;
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
get nextSibling() { return this.node.nextSibling; }
|
|
68
|
-
get dirty() { return this._dirty; }
|
|
69
|
-
}
|