@epa-wg/custom-element 0.0.20 → 0.0.22

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.
@@ -0,0 +1,763 @@
1
+ const XSL_NS_URL = 'http://www.w3.org/1999/XSL/Transform'
2
+ , HTML_NS_URL = 'http://www.w3.org/1999/xhtml'
3
+ , EXSL_NS_URL = 'http://exslt.org/common'
4
+ , DCE_NS_URL ="urn:schemas-epa-wg:dce";
5
+
6
+ // const log = x => console.debug( new XMLSerializer().serializeToString( x ) );
7
+
8
+ const attr = (el, attr)=> el.getAttribute?.(attr)
9
+ , isText = e => e.nodeType === 3
10
+ , isString = s => typeof s === 'string'
11
+ , isNode = e => e && typeof e.nodeType === 'number'
12
+ , create = ( tag, t = '', d=document ) => ( e => ((t && e.append(createText(d.ownerDocument||d, t))),e) )((d.ownerDocument || d ).createElement( tag ))
13
+ , createText = ( d, t) => (d.ownerDocument || d ).createTextNode( t )
14
+ , removeChildren = n => { while(n.firstChild) n.firstChild.remove(); return n; }
15
+ , emptyNode = n => { n.getAttributeNames().map( a => n.removeAttribute(a) ); return removeChildren(n); }
16
+ , xslNs = x => ( x?.setAttribute('xmlns:xsl', XSL_NS_URL ), x )
17
+ , xslHtmlNs = x => ( x?.setAttribute('xmlns:xhtml', HTML_NS_URL ), xslNs(x) )
18
+ , cloneAs = (p,tag) =>
19
+ { const px = p.ownerDocument.createElementNS(p.namespaceURI,tag);
20
+ for( let a of p.attributes)
21
+ px.setAttribute(a.name, a.value);
22
+ while( p.firstChild )
23
+ px.append(p.firstChild);
24
+ return px;
25
+ };
26
+
27
+ function
28
+ ASSERT(x)
29
+ {
30
+ // if(!x)
31
+ // debugger
32
+ }
33
+ export function
34
+ xml2dom( xmlString )
35
+ {
36
+ return new DOMParser().parseFromString( xmlString, "application/xml" )
37
+ }
38
+ export function
39
+ xmlString(doc){ return new XMLSerializer().serializeToString( doc ) }
40
+
41
+ function
42
+ injectData( root, sectionName, arr, cb )
43
+ { const create = ( tag ) => root.ownerDocument.createElement( tag );
44
+ const inject = ( tag, parent, s ) =>
45
+ { parent.append( s = create( tag,'',parent.ownerDocument ) );
46
+ return s;
47
+ };
48
+ const l = inject( sectionName, root );
49
+ [ ...arr ].forEach( e => l.append( cb( e, root.ownerDocument ) ) );
50
+ return l;
51
+ }
52
+
53
+ function
54
+ assureSlot( e, doc )
55
+ {
56
+ if( !e.slot )
57
+ {
58
+ if( !e.setAttribute )
59
+ e = create( 'span', e.textContent.replaceAll( '\n', '' ), doc );
60
+ e.setAttribute( 'slot', '' )
61
+ }
62
+ return e;
63
+ }
64
+
65
+ export function
66
+ obj2node( o, tag, doc )
67
+ { const t = typeof o;
68
+ if( t === 'string' )
69
+ return create(tag,o,doc);
70
+ if( t === 'number' )
71
+ return create(tag,''+o,doc);
72
+
73
+ if( o instanceof Array )
74
+ { const ret = create('array','',doc);
75
+ o.map( ae => ret.append( obj2node(ae,tag,doc)) );
76
+ return ret
77
+ }
78
+ if( o instanceof FormData )
79
+ { const ret = create('form-data','',doc);
80
+ for( const p of o )
81
+ ret.append( obj2node(p[1],p[0],doc) );
82
+ return ret
83
+ }
84
+ const ret = create(tag,'',doc);
85
+ for( let k in o )
86
+ if( isNode(o[k]) || typeof o[k] ==='function' || o[k] instanceof Window )
87
+ continue
88
+ else
89
+ if( typeof o[k] !== "object" )
90
+ ret.setAttribute(k, o[k] );
91
+ else
92
+ ret.append(obj2node(o[k], k, doc))
93
+ return ret;
94
+ }
95
+ export function
96
+ tagUid( node )
97
+ { // {} to xsl:value-of
98
+ forEach$(node,'*',d => [...d.childNodes].filter( e=>e.nodeType === 3 ).forEach( e=>
99
+ { if( e.parentNode.localName === 'style' )
100
+ return;
101
+ const m = e.data.matchAll( /{([^}]*)}/g );
102
+ if(m)
103
+ { let l = 0
104
+ , txt = t => createText(e,t||'')
105
+ , tt = [];
106
+ [...m].forEach(t=>
107
+ { if( t.index > l )
108
+ tt.push( txt( t.input.substring( l, t.index ) ))
109
+ const v = node.querySelector('value-of').cloneNode();
110
+ v.setAttribute('select', t[1] );
111
+ tt.push(v);
112
+ l = t.index+t[0].length;
113
+ })
114
+ if( l < e.data.length)
115
+ tt.push( txt( e.data.substring(l,e.data.length) ));
116
+ if( tt.length )
117
+ { for( let t of tt )
118
+ d.insertBefore(t,e);
119
+ d.removeChild(e);
120
+ }
121
+ }
122
+ }));
123
+
124
+ if( 'all' in node ) {
125
+ let i= 1;
126
+ for( let e of node.all )
127
+ e.setAttribute && !e.tagName.startsWith('xsl:') && e.setAttribute('data-dce-id', '' + i++)
128
+ }
129
+ return node
130
+ }
131
+ export function
132
+ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
133
+ {
134
+ if( templateNode.tagName === S || templateNode.documentElement?.tagName === S )
135
+ return tagUid(templateNode)
136
+ const sanitizeXsl = xml2dom(`<xsl:stylesheet version="1.0" xmlns:xsl="${ XSL_NS_URL }" xmlns:xhtml="${ HTML_NS_URL }" xmlns:exsl="${EXSL_NS_URL}" exclude-result-prefixes="exsl" >
137
+ <xsl:output method="xml" />
138
+ <xsl:template match="/"><dce-root xmlns="${ HTML_NS_URL }"><xsl:apply-templates select="*"/></dce-root></xsl:template>
139
+ <xsl:template match="*[name()='template']"><xsl:apply-templates mode="sanitize" select="*|text()"/></xsl:template>
140
+ <xsl:template match="*"><xsl:apply-templates mode="sanitize" select="*|text()"/></xsl:template>
141
+ <xsl:template match="*[name()='svg']|*[name()='math']"><xsl:apply-templates mode="sanitize" select="."/></xsl:template>
142
+ <xsl:template mode="sanitize" match="*[count(text())=1 and count(*)=0]"><xsl:copy><xsl:apply-templates mode="sanitize" select="@*"/><xsl:value-of select="text()"></xsl:value-of></xsl:copy></xsl:template>
143
+ <xsl:template mode="sanitize" match="xhtml:*[count(text())=1 and count(*)=0]"><xsl:element name="{local-name()}"><xsl:apply-templates mode="sanitize" select="@*"/><xsl:value-of select="text()"></xsl:value-of></xsl:element></xsl:template>
144
+ <xsl:template mode="sanitize" match="*|@*"><xsl:copy><xsl:apply-templates mode="sanitize" select="*|@*|text()"/></xsl:copy></xsl:template>
145
+ <xsl:template mode="sanitize" match="text()[normalize-space(.) = '']"/>
146
+ <xsl:template mode="sanitize" match="text()"><dce-text><xsl:copy/></dce-text></xsl:template>
147
+ <xsl:template mode="sanitize" match="xsl:value-of|*[name()='slot']"><dce-text><xsl:copy><xsl:apply-templates mode="sanitize" select="*|@*|text()"/></xsl:copy></dce-text></xsl:template>
148
+ <xsl:template mode="sanitize" match="xhtml:*"><xsl:element name="{local-name()}"><xsl:apply-templates mode="sanitize" select="*|@*|text()"/></xsl:element></xsl:template>
149
+ </xsl:stylesheet>`)
150
+ const sanitizeProcessor = new XSLTProcessor()
151
+ , tc = (n =>
152
+ {
153
+ forEach$(n,'script', s=> s.remove() );
154
+ const xslRoot = n.content ?? n.firstElementChild?.content ?? n.body ?? n;
155
+ xslTags.forEach( tag => forEach$( xslRoot, tag, el=>toXsl(el,xslRoot) ) );
156
+ const e = n.firstElementChild?.content || n.content
157
+ , asXmlNode = r => {
158
+ const d = xml2dom( '<xhtml/>' )
159
+ , n = d.importNode(r, true);
160
+ d.replaceChild(n,d.documentElement);
161
+ return xslHtmlNs(n);
162
+ };
163
+ if( e )
164
+ { const t = create('div');
165
+ [ ...e.childNodes ].map( c => t.append(c.cloneNode(true)) )
166
+ return asXmlNode(t)
167
+ }
168
+ return asXmlNode(n.documentElement || n.body || n)
169
+ })(templateNode)
170
+ , xslDom = xml2dom(
171
+ `<xsl:stylesheet version="1.0"
172
+ xmlns:xsl="${ XSL_NS_URL }"
173
+ xmlns:xhtml="${ HTML_NS_URL }"
174
+ xmlns:dce="urn:schemas-epa-wg:dce"
175
+ xmlns:exsl="http://exslt.org/common"
176
+ exclude-result-prefixes="exsl"
177
+ >
178
+ <xsl:template match="ignore">
179
+ <xsl:choose>
180
+ <xsl:when test="//attr">{//attr}</xsl:when>
181
+ <xsl:otherwise>{def}</xsl:otherwise>
182
+ </xsl:choose><xsl:value-of select="."></xsl:value-of></xsl:template>
183
+ <xsl:template mode="payload" match="attributes"></xsl:template>
184
+ <xsl:template match="/">
185
+ <xsl:apply-templates mode="payload" select="/datadom/attributes"/>
186
+ </xsl:template>
187
+
188
+ <xsl:template match="@*|node()" mode="copy-html">
189
+ <xsl:copy><xsl:apply-templates select="@*|node()" mode="copy-html"/></xsl:copy>
190
+ </xsl:template>
191
+ <xsl:template match="node()[starts-with(name(),'xhtml:')]" mode="copy-html">
192
+ <xsl:element name="{local-name()}"><xsl:apply-templates select="@*|node()" mode="copy-html"/></xsl:element>
193
+ </xsl:template>
194
+
195
+
196
+
197
+ <xsl:template name="slot" >
198
+ <xsl:param name="slotname" />
199
+ <xsl:param name="defaultvalue" />
200
+ <xsl:choose>
201
+ <xsl:when test="//payload/*[@slot=$slotname]">
202
+ <xsl:apply-templates mode="copy-html" select="//payload/*[@slot=$slotname]"/>
203
+ </xsl:when>
204
+ <xsl:otherwise>
205
+ <xsl:apply-templates mode="copy-html" select="$defaultvalue"/>
206
+ </xsl:otherwise>
207
+ </xsl:choose>
208
+ </xsl:template>
209
+ <xsl:variable name="js-injected-body">
210
+ <xsl:call-template name="slot" >
211
+ <xsl:with-param name="slotname" select="''"/>
212
+ <xsl:with-param name="defaultvalue"/>
213
+ </xsl:call-template>
214
+ </xsl:variable>
215
+ </xsl:stylesheet>`
216
+ );
217
+
218
+ sanitizeProcessor.importStylesheet( sanitizeXsl );
219
+
220
+ const fr = sanitizeProcessor.transformToFragment(tc, document)
221
+ , $ = (e,css) => e.querySelector(css)
222
+ , payload = $( xslDom, 'template[mode="payload"]');
223
+ if( !fr )
224
+ return console.error("transformation error",{ xml:tc.outerHTML, xsl: xmlString( sanitizeXsl ) });
225
+ const params = [];
226
+ [...fr.querySelectorAll('dce-root>attribute')].forEach( a=>
227
+ {
228
+ const p = cloneAs(a,'xsl:param')
229
+ , name = attr(a,'name');
230
+ payload.append(p);
231
+ let select = attr(p,'select')?.split('??')
232
+ if( !select)
233
+ { select = ['//'+name, `'${p.textContent}'`];
234
+ emptyNode(p);
235
+ p.setAttribute('name',name);
236
+ }
237
+ let val;
238
+ if( select?.length>1 ){
239
+ p.removeAttribute('select');
240
+ const c = $( xslDom, 'template[match="ignore"]>choose').cloneNode(true);
241
+ emptyNode(c.firstElementChild).append( createText(c,'{'+select[0]+'}'));
242
+ emptyNode(c.lastElementChild ).append( createText(c,'{'+select[1]+'}'));
243
+ c.firstElementChild.setAttribute('test',select[0]);
244
+ p.append(c);
245
+ val = c.cloneNode(true);
246
+ }else
247
+ val=cloneAs(a,'xsl:value-of');
248
+ val.removeAttribute('name');
249
+ a.append(val);
250
+ a.removeAttribute('select');
251
+ params.push(p)
252
+ });
253
+ [...fr.querySelectorAll('[value]')].filter(el=>el.getAttribute('value').match( /\{(.*)\?\?(.*)\}/g )).forEach(el=>
254
+ { const v = attr(el,'value');
255
+ if(v)
256
+ el.setAttribute('value', evalCurly(v));
257
+ });
258
+ for( const c of fr.childNodes )
259
+ payload.append(xslDom.importNode(c,true))
260
+
261
+ const embeddedTemplates = [...payload.querySelectorAll('template')];
262
+ embeddedTemplates.forEach(t=>payload.ownerDocument.documentElement.append(t));
263
+
264
+ const slotCall = $(xslDom,'call-template[name="slot"]')
265
+ , slot2xsl = s =>
266
+ { const v = slotCall.cloneNode(true)
267
+ , name = attr(s,'name') || '';
268
+ name && v.firstElementChild.setAttribute('select',`'${ name }'`)
269
+ for( let c of s.childNodes)
270
+ v.lastElementChild.append(c)
271
+ return v
272
+ }
273
+
274
+ forEach$( payload,'slot', s => s.parentNode.replaceChild( slot2xsl(s), s ) )
275
+
276
+ const ret = tagUid(xslDom)
277
+ ret.params = params;
278
+ return ret;
279
+ }
280
+ export async function
281
+ xhrTemplate(src)
282
+ {
283
+ const dom = await new Promise((resolve,reject)=>
284
+ { const xhr = new XMLHttpRequest();
285
+ xhr.open("GET", src);
286
+ xhr.responseType = "document";
287
+ // xhr.overrideMimeType("text/xml");
288
+ xhr.onload = () =>
289
+ { if( xhr.readyState === xhr.DONE && xhr.status === 200 )
290
+ resolve( xhr.responseXML || create('div', xhr.responseText ) )
291
+ reject(xhr.statusText)
292
+ };
293
+ xhr.addEventListener("error", ev=>reject(ev) );
294
+
295
+ xhr.send();
296
+ })
297
+ return dom
298
+ }
299
+ export function
300
+ deepEqual(a, b, O=false)
301
+ {
302
+ if( a === b )
303
+ return true;
304
+
305
+ if( (typeof a !== "object" || a === null) || (typeof b !== "object" || b === null)
306
+ || Object.keys(a).length !== Object.keys(b).length )
307
+ return O;
308
+
309
+ for( let k in a )
310
+ if( !(k in b) || !deepEqual( a[k], b[k] ) )
311
+ return O
312
+ return true;
313
+ }
314
+ export const
315
+ assureSlices = ( root, names) =>
316
+ names.split('|').filter(s=>s).map(n=>n.trim()).map( xp =>
317
+ { const append = n=> (root.append(n),n);
318
+ if(xp.includes('/'))
319
+ { const ret = [], r = root.ownerDocument.evaluate( xp, root );
320
+ for( let n; n = r.iterateNext(); )
321
+ ret.push( n )
322
+ return ret
323
+ }
324
+
325
+ return [...root.childNodes].find(n=>n.localName === xp) || append( create(xp,'',root.ownerDocument) );
326
+ }).flat();
327
+
328
+ /**
329
+ *
330
+ * @param x slice node
331
+ * @param sliceNames slice name, xPath in /datadom/slice/
332
+ * @param ev Event obj
333
+ * @param dce
334
+ */
335
+ export function
336
+ event2slice( x, sliceNames, ev, dce )
337
+ {
338
+ if( ev.sliceProcessed )
339
+ return
340
+ ev.sliceProcessed = 1;
341
+ // evaluate slices[]
342
+ // inject @attributes
343
+ // inject event
344
+ // evaluate slice-value
345
+ // slice[i] = slice-value
346
+ return assureSlices( x, sliceNames ?? '' ).map( s =>
347
+ {
348
+ const d = x.ownerDocument
349
+ , el = ev.sliceEventSource
350
+ , sel = ev.sliceElement
351
+ , cleanSliceValue = ()=>[...s.childNodes].filter(n=>n.nodeType===3 || n.localName==='value' || n.localName==='form-data').map(n=>n.remove());
352
+ el.getAttributeNames().map( a => s.setAttribute( a, attr(el,a) ) );
353
+ [...s.childNodes].filter(n=>n.localName==='event').map(n=>n.remove());
354
+ if( 'validationMessage' in el )
355
+ s.setAttribute('validation-message', el.validationMessage);
356
+ ev.type==='init' && cleanSliceValue();
357
+ s.append( obj2node( ev, 'event', d ) );
358
+ if( sel.hasAttribute('slice-value') )
359
+ { if( el.value === undefined)
360
+ s.removeAttribute('value')
361
+ else
362
+ s.setAttribute('value', el.value );
363
+ const v = xPath( attr( sel, 'slice-value'),s );
364
+ cleanSliceValue();
365
+ s.append( createText( d, v ) );
366
+ }else
367
+ { if( 'elements' in el )
368
+ { cleanSliceValue();
369
+ s.append( obj2node(new FormData(el),'value', s.ownerDocument) )
370
+ return s
371
+ }
372
+ const v = el.value ?? attr( sel, 'value' ) ;
373
+ cleanSliceValue();
374
+ if( v === null || v === undefined )
375
+ [...s.childNodes].filter(n=>n.localName!=='event').map(n=>n.remove());
376
+ else
377
+ if( isString(v) )
378
+ s.append( createText( d, v) );
379
+ else
380
+ s.append( obj2node(v,'value',s.ownerDocument) )
381
+ }
382
+ return s
383
+ })
384
+ }
385
+
386
+ function forEach$( el, css, cb){
387
+ if( el.querySelectorAll )
388
+ [...el.querySelectorAll(css)].forEach(cb)
389
+ }
390
+ const loadTemplateRoots = async ( src, dce )=>
391
+ {
392
+ if( !src || !src.trim() )
393
+ return [dce]
394
+ if( src.startsWith('#') )
395
+ return ( n =>
396
+ { if(!n) return []
397
+ const a = n.querySelectorAll(src)
398
+ if( a.length )
399
+ return [...a]
400
+ const r = n.getRootNode();
401
+ return r===n ? []: getByHashId(r)
402
+ })(dce.parentElement)
403
+ try
404
+ { // todo cache
405
+ const dom = await xhrTemplate(src)
406
+ const hash = new URL(src, location).hash
407
+ if( hash )
408
+ { const ret = dom.querySelectorAll(hash);
409
+ if( ret.length )
410
+ return [...ret]
411
+ return [dce]
412
+ }
413
+ return [dom]
414
+ }catch (error){ return [dce]}
415
+ }
416
+ export function mergeAttr( from, to )
417
+ { if( isText(from) )
418
+ {
419
+ if( !isText(to) ){ debugger }
420
+ return
421
+ }
422
+ for( let a of from.attributes)
423
+ { a.namespaceURI? to.setAttributeNS( a.namespaceURI, a.name, a.value ) : to.setAttribute( a.name, a.value )
424
+ if( a.name === 'value')
425
+ to.value = a.value
426
+ }
427
+ }
428
+ export function assureUnique(n, id=0)
429
+ {
430
+ const m = {}
431
+ for( const e of n.childNodes )
432
+ {
433
+ const a = attr(e,'data-dce-id') || e.dceId || 0;
434
+ if( !m[a] )
435
+ { if( !a )
436
+ { m[a] = e.dceId = ++id;
437
+ if( e.setAttribute )
438
+ e.setAttribute('data-dce-id', e.dceId )
439
+ }else
440
+ m[a] = 1;
441
+ }else
442
+ { const v = e.dceId = a + '-' + m[a]++;
443
+ if( e.setAttribute )
444
+ e.setAttribute('data-dce-id', v )
445
+ }
446
+ e.childNodes.length && assureUnique(e)
447
+ }
448
+ }
449
+ export function merge( parent, fromArr )
450
+ {
451
+ if(!fromArr.length)
452
+ return removeChildren(parent);
453
+ const id2old = {};
454
+ for( let c of parent.childNodes)
455
+ { ASSERT( !id2old[c.dceId] );
456
+ if( isText(c) )
457
+ { ASSERT( c.data.trim() );
458
+ id2old[c.dceId || 0] = c;
459
+ } else
460
+ id2old[attr(c, 'data-dce-id') || 0] = c;
461
+ }
462
+ for( let e of [...fromArr] )
463
+ { const k = attr(e, 'data-dce-id') || e.dceId;
464
+ const o = id2old[ k ];
465
+ if( o )
466
+ { if( isText(e) )
467
+ { if( o.nodeValue !== e.nodeValue )
468
+ o.nodeValue = e.nodeValue;
469
+ }else
470
+ { mergeAttr(e,o)
471
+ if( o.childNodes.length || e.childNodes.length )
472
+ merge(o, e.childNodes)
473
+ }
474
+ delete id2old[ k ]
475
+ }else
476
+ parent.append( e )
477
+ }
478
+ for( let v of Object.values(id2old) )
479
+ v.remove();
480
+ }
481
+ export function assureUID(n,attr)
482
+ { if( !n.hasAttribute(attr) )
483
+ n.setAttribute(attr, crypto.randomUUID());
484
+ return n.getAttribute(attr)
485
+ }
486
+ export const evalCurly = s =>
487
+ { const exp = [...s?.matchAll( /([^{}]*)(\{)([^}]+)}([^{}]*)/g ) ].map(l=>`${l[1]}{${ xPathDefaults(l[3] )}}${l[4]}`);
488
+ return exp.join('');
489
+ }
490
+ export const xPathDefaults = x=>
491
+ { if(!x.trim())
492
+ return x;
493
+ const xx = x.split('??')
494
+ , a = xx.shift()
495
+ , b = xPathDefaults(xx.join('??'));
496
+
497
+ return xx.length ? `concat( ${a} , substring( ${b} , (1+string-length( ${b} )) * string-length( ${a} ) ) )`: x
498
+ // return xx.length ? `${a}|(${xPathDefaults(xx.join('??'))})[not(${a})]`: a
499
+ }
500
+ export const xPath = (x,root)=>
501
+ {
502
+ const xx = x.split('??');
503
+ if( xx.length > 1 )
504
+ return xPath(xx[0], root) || xPath(xx[1], root);
505
+
506
+ x = xPathDefaults(x);
507
+
508
+ const it = root.ownerDocument.evaluate(x, root);
509
+ switch( it.resultType )
510
+ { case XPathResult.NUMBER_TYPE: return it.numberValue;
511
+ case XPathResult.STRING_TYPE: return it.stringValue;
512
+ case XPathResult.BOOLEAN_TYPE: return it.booleanValue;
513
+ }
514
+
515
+ let ret = '';
516
+ for( let n ;n=it.iterateNext(); )
517
+ ret += n.textContent;
518
+ return ret
519
+ }
520
+ export const xslTags = 'stylesheet,transform,import,include,strip-space,preserve-space,output,key,decimal-format,namespace-alias,template,value-of,copy-of,number,apply-templates,apply-imports,for-each,sort,if,choose,when,otherwise,attribute-set,call-template,with-param,variable,param,text,processing-instruction,element,attribute,comment,copy,message,fallback'.split(',');
521
+ export const toXsl = (el, defParent) => {
522
+ const x = create('xsl:'+el.localName);
523
+ for( let a of el.attributes )
524
+ x.setAttribute( a.name, a.value );
525
+ while(el.firstChild)
526
+ x.append(el.firstChild);
527
+ if( el.parentElement )
528
+ el.parentElement.replaceChild( x, el );
529
+ else
530
+ { const p = (el.parentElement || defParent)
531
+ , arr = [...p.childNodes];
532
+ arr.forEach((n, i) => {
533
+ if (n === el)
534
+ arr[i] = x;
535
+ });
536
+ p.replaceChildren(...arr);
537
+ }
538
+ };
539
+
540
+ export class
541
+ CustomElement extends HTMLElement
542
+ {
543
+ static observedAttributes = ['src','tag','hidden'];
544
+ async connectedCallback()
545
+ {
546
+ const templateRoots = await loadTemplateRoots( attr( this, 'src' ), this )
547
+ , tag = attr( this, 'tag' )
548
+ , tagName = tag ? tag : 'dce-'+crypto.randomUUID();
549
+
550
+ for( const t of templateRoots )
551
+ forEach$(t.templateNode||t.content||t, 'style',s=>{
552
+ const slot = s.closest('slot');
553
+ const sName = slot ? `slot[name="${slot.name}"]`:'';
554
+ s.innerHTML = `${tagName} ${sName}{${s.innerHTML}}`;
555
+ this.append(s);
556
+ })
557
+ const templateDocs = templateRoots.map( n => createXsltFromDom( n ) )
558
+ , xp = templateDocs.map( (td, p) =>{ p = new XSLTProcessor(); p.importStylesheet( td ); return p })
559
+
560
+ Object.defineProperty( this, "xsltString", { get: ()=>templateDocs.map( td => xmlString(td) ).join('\n') });
561
+
562
+ const dce = this
563
+ , sliceNodes = [...this.templateNode.querySelectorAll('[slice]')]
564
+ , sliceNames = sliceNodes.map(e=>attr(e,'slice')).filter(n=>!n.includes('/')).filter((v, i, a)=>a.indexOf(v) === i)
565
+ , declaredAttributes = templateDocs.reduce( (ret,t) => { if( t.params ) ret.push( ...t.params ); return ret; }, [] );
566
+
567
+ class DceElement extends HTMLElement
568
+ {
569
+ static get observedAttributes(){ return declaredAttributes.map( a=>attr(a,'name')); }
570
+ #inTransform = 0;
571
+ connectedCallback()
572
+ { let payload = [...this.childNodes];
573
+ if( this.firstElementChild?.tagName === 'TEMPLATE' )
574
+ {
575
+ if( this.firstElementChild !== this.lastElementChild )
576
+ { console.error('payload should have TEMPLATE as only child', this.outerHTML ) }
577
+ const t = this.firstElementChild;
578
+ t.remove();
579
+ payload = t.content.childNodes;
580
+
581
+ for( const n of [...t.content.childNodes] )
582
+ if( n.localName === 'style' ){
583
+ const id = assureUID(this,'data-dce-style')
584
+ n.innerHTML= `${tagName}[data-dce-style="${id}"]{${n.innerHTML}}`;
585
+ this.insertAdjacentElement('afterbegin',n);
586
+ }else
587
+ if(n.nodeType===1)
588
+ t.insertAdjacentElement('beforebegin',n);
589
+ else if(n.nodeType===3)
590
+ t.insertAdjacentText('beforebegin',n.data);
591
+ }else
592
+ this.innerHTML='';
593
+ const x = xml2dom( `<datadom
594
+ xmlns:xsl="${ XSL_NS_URL }"
595
+ xmlns="${ HTML_NS_URL }"
596
+ xmlns:xhtml="${ HTML_NS_URL }"
597
+ xmlns:dce="urn:schemas-epa-wg:dce"
598
+ />` ).documentElement;
599
+ const createXmlNode = ( tag, t = '' ) => ( e =>
600
+ { if( t )
601
+ e.append( createText( x, t ))
602
+ return e;
603
+ })(x.ownerDocument.createElement( tag ))
604
+
605
+ injectData( x, 'payload' , payload , assureSlot );
606
+ const attrsRoot = injectData( x, 'attributes' , this.attributes, e => createXmlNode( e.nodeName, e.value ) );
607
+ injectData( x, 'dataset', Object.keys( this.dataset ), k => createXmlNode( k, this.dataset[ k ] ) );
608
+ const sliceRoot = injectData( x, 'slice', sliceNames, k => createXmlNode( k, '' ) )
609
+ , sliceXPath = x => xPath(x, sliceRoot);
610
+ this.xml = x;
611
+
612
+ const sliceEvents=[];
613
+ const applySlices = ()=>
614
+ { const processed = {}
615
+
616
+ for(let ev; ev = sliceEvents.pop(); )
617
+ { const s = attr( ev.sliceElement, 'slice');
618
+ if( processed[s] )
619
+ continue;
620
+ event2slice( sliceRoot, s, ev, this );
621
+ processed[s] = ev;
622
+ }
623
+ Object.keys(processed).length !== 0 && transform();
624
+ }
625
+ let timeoutID;
626
+
627
+ this.onSlice = ev=>
628
+ { sliceEvents.push(ev);
629
+ if( !timeoutID )
630
+ timeoutID = setTimeout(()=>
631
+ { applySlices();
632
+ timeoutID =0;
633
+ },1);
634
+ };
635
+ const transform = this.transform = ()=>
636
+ { if(this.#inTransform){ debugger }
637
+ this.#inTransform = 1;
638
+
639
+ const ff = xp.map( (p,i) =>
640
+ { const f = p.transformToFragment(x.ownerDocument, document)
641
+ if( !f )
642
+ console.error( "XSLT transformation error. xsl:\n", xmlString(templateDocs[i]), '\nxml:\n', xmlString(x) );
643
+ return f
644
+ });
645
+ ff.map( f =>
646
+ { if( !f )
647
+ return;
648
+ assureUnique(f);
649
+ merge( this, f.childNodes )
650
+ })
651
+
652
+ DceElement.observedAttributes.map( a =>
653
+ { let v = attr(this.firstElementChild,a);
654
+ if( v !== attr(this,a) )
655
+ { this.setAttribute( a, v );
656
+ this.#applyAttribute( a, v );
657
+ }
658
+ })
659
+
660
+ forEach$( this,'[slice],[slice-event]', el =>
661
+ { if( !el.dceInitialized )
662
+ { el.dceInitialized = 1;
663
+ let evs = attr(el,'slice-event');
664
+ if( attr(el,'custom-validity') )
665
+ evs += ' change submit';
666
+
667
+ [...new Set((evs || 'change') .split(' '))]
668
+ .forEach( t=> (el.localName==='slice'? el.parentElement : el)
669
+ .addEventListener( t, ev=>
670
+ { ev.sliceElement = el;
671
+ ev.sliceEventSource = ev.currentTarget || ev.target;
672
+ const slices = event2slice( sliceRoot, attr( ev.sliceElement, 'slice')||'', ev, this );
673
+
674
+ forEach$(this,'[custom-validity]',el =>
675
+ { if( !el.setCustomValidity )
676
+ return;
677
+ const x = attr( el, 'custom-validity' );
678
+ try
679
+ { const v = x && xPath( x, attrsRoot );
680
+ el.setCustomValidity( v === true? '': v === false ? 'invalid' : v );
681
+ }catch(err)
682
+ { console.error(err, 'xPath', x) }
683
+ })
684
+ const x = attr(el,'custom-validity')
685
+ , v = x && xPath( x, attrsRoot )
686
+ , msg = v === true? '' : v;
687
+ if( x )
688
+ { el.setCustomValidity ? el.setCustomValidity( msg ) : ( el.validationMessage = msg );
689
+ slices.map( s => s.setAttribute('validation-message', msg ) );
690
+ if( ev.type === 'submit' )
691
+ { if( v === true )
692
+ return;
693
+ setTimeout(transform,1)
694
+ if( !!v === v )
695
+ { v || ev.preventDefault();
696
+ return v;
697
+ }
698
+ if( v )
699
+ { ev.preventDefault();
700
+ return !1
701
+ }
702
+ return ;
703
+ }else
704
+ setTimeout(transform,1)
705
+ }
706
+ this.onSlice(ev);
707
+ } ));
708
+ if( !evs || evs.includes('init') )
709
+ { if( el.hasAttribute('slice-value') || el.hasAttribute('value') || el.value )
710
+ this.onSlice({type:'init', target: el, sliceElement:el, sliceEventSource:el })
711
+ else
712
+ el.value = sliceXPath( attr(el,'slice') )
713
+ }
714
+ }
715
+ });
716
+ this.#inTransform = 0;
717
+ };
718
+ transform();
719
+ applySlices();
720
+ }
721
+ #applyAttribute(name, newValue)
722
+ { let a = this.xml.querySelector(`attributes>${name}`);
723
+ if( a )
724
+ emptyNode(a).append( createText(a,newValue) );
725
+ else
726
+ { a = create( name, newValue, this.xml );
727
+ this.xml.querySelector('attributes').append( a );
728
+ }
729
+ }
730
+ attributeChangedCallback(name, oldValue, newValue)
731
+ { if( !this.xml || this.#inTransform )
732
+ return;
733
+ this.#applyAttribute(name, newValue);
734
+ this.transform(); // needs throttling
735
+ }
736
+
737
+ get dce(){ return dce }
738
+ }
739
+ const registerTag = tag =>
740
+ {
741
+ if( window.customElements.get(tag) !== DceElement )
742
+ window.customElements.define( tag, DceElement);
743
+ };
744
+ if(tag)
745
+ registerTag(tag);
746
+ else
747
+ { const t = tagName;
748
+ this.setAttribute('tag', t );
749
+ registerTag(t);
750
+ const el = document.createElement(t);
751
+ this.getAttributeNames().forEach(a=>el.setAttribute(a,this.getAttribute(a)));
752
+ el.append(...[...this.childNodes].filter( e => e.localName!=='style') );
753
+ this.append(el);
754
+ }
755
+ }
756
+ get templateNode(){ return this.firstElementChild?.tagName === 'TEMPLATE'? this.firstElementChild.content : this }
757
+ get dce(){ return this }
758
+
759
+ get xslt(){ return xml2dom( this.xsltString ) }
760
+ }
761
+
762
+ window.customElements.define( 'custom-element', CustomElement );
763
+ export default CustomElement;