@epa-wg/custom-element 0.0.17 → 0.0.19

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/custom-element.js CHANGED
@@ -7,12 +7,22 @@ const XSL_NS_URL = 'http://www.w3.org/1999/XSL/Transform'
7
7
 
8
8
  const attr = (el, attr)=> el.getAttribute?.(attr)
9
9
  , isText = e => e.nodeType === 3
10
- , create = ( tag, t = '', d=document ) => ( e => ((e.innerText = t||''),e) )((d.ownerDocument || d ).createElement( tag ))
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 ))
11
13
  , createText = ( d, t) => (d.ownerDocument || d ).createTextNode( t )
12
- , emptyNode = n=> { while(n.firstChild) n.firstChild.remove(); return n; }
14
+ , emptyNode = n => { while(n.firstChild) n.firstChild.remove(); n.getAttributeNames().map( a => n.removeAttribute(a) ); return n; }
13
15
  , createNS = ( ns, tag, t = '' ) => ( e => ((e.innerText = t||''),e) )(document.createElementNS( ns, tag ))
14
16
  , xslNs = x => ( x?.setAttribute('xmlns:xsl', XSL_NS_URL ), x )
15
- , xslHtmlNs = x => ( x?.setAttribute('xmlns:xhtml', HTML_NS_URL ), xslNs(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
+ }
16
26
 
17
27
  function
18
28
  ASSERT(x)
@@ -82,6 +92,30 @@ Json2Xml( o, tag )
82
92
  ret.push("/>");
83
93
  return ret.join('\n');
84
94
  }
95
+
96
+ export function
97
+ obj2node( o, tag, doc )
98
+ {
99
+ if( typeof o === 'function'){debugger}
100
+ if( typeof o === 'string' )
101
+ return create(tag,o,doc);
102
+
103
+ if( o instanceof Array )
104
+ { const ret = create('array');
105
+ o.map( ae => ret.append( obj2node(ae,tag,doc)) );
106
+ return ret
107
+ }
108
+ const ret = create(tag,'',doc);
109
+ for( let k in o )
110
+ if( isNode(o[k]) || typeof o[k] ==='function' || o[k] instanceof Window )
111
+ continue
112
+ else
113
+ if( typeof o[k] !== "object" )
114
+ ret.setAttribute(k, o[k] );
115
+ else
116
+ ret.append(obj2node(o[k], k, doc))
117
+ return ret;
118
+ }
85
119
  export function
86
120
  tagUid( node )
87
121
  { // {} to xsl:value-of
@@ -123,7 +157,7 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
123
157
  {
124
158
  if( templateNode.tagName === S || templateNode.documentElement?.tagName === S )
125
159
  return tagUid(templateNode)
126
- 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" >
160
+ 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" >
127
161
  <xsl:output method="xml" />
128
162
  <xsl:template match="/"><dce-root xmlns="${ HTML_NS_URL }"><xsl:apply-templates select="*"/></dce-root></xsl:template>
129
163
  <xsl:template match="*[name()='template']"><xsl:apply-templates mode="sanitize" select="*|text()"/></xsl:template>
@@ -141,6 +175,8 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
141
175
  , tc = (n =>
142
176
  {
143
177
  forEach$(n,'script', s=> s.remove() );
178
+ const xslRoot = n.content ?? n.firstElementChild?.content ?? n.body ?? n;
179
+ xslTags.forEach( tag => forEach$( xslRoot, tag, el=>toXsl(el,xslRoot) ) );
144
180
  const e = n.firstElementChild?.content || n.content
145
181
  , asXmlNode = r => {
146
182
  const d = xml2dom( '<xhtml/>' )
@@ -202,24 +238,38 @@ createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
202
238
  if( !fr )
203
239
  return console.error("transformation error",{ xml:tc.outerHTML, xsl: xmlString( sanitizeXsl ) });
204
240
  const params = [];
205
- [...fr.querySelectorAll('dce-root>param')].forEach(p=>
206
- { payload.append(p);
241
+ [...fr.querySelectorAll('dce-root>attribute')].forEach( a=>
242
+ {
243
+ const p = cloneAs(a,'xsl:param')
244
+ , name = attr(a,'name');
245
+ payload.append(p);
207
246
  let select = attr(p,'select')?.split('??')
208
247
  if( !select)
209
- { select = ['//'+attr(p, 'name'), `'${p.textContent}'`];
248
+ { select = ['//'+name, `'${p.textContent}'`];
210
249
  emptyNode(p);
250
+ p.setAttribute('name',name);
211
251
  }
212
- if( select?.length>1){
252
+ let val;
253
+ if( select?.length>1 ){
213
254
  p.removeAttribute('select');
214
255
  const c = $( xslDom, 'template[match="ignore"]>choose').cloneNode(true);
215
- c.firstElementChild.setAttribute('test',select[0]);
216
256
  emptyNode(c.firstElementChild).append( createText(c,'{'+select[0]+'}'));
217
257
  emptyNode(c.lastElementChild ).append( createText(c,'{'+select[1]+'}'));
218
- p.append(c)
219
- }
258
+ c.firstElementChild.setAttribute('test',select[0]);
259
+ p.append(c);
260
+ val = c.cloneNode(true);
261
+ }else
262
+ val=cloneAs(a,'xsl:value-of');
263
+ val.removeAttribute('name');
264
+ a.append(val);
265
+ a.removeAttribute('select');
220
266
  params.push(p)
221
267
  });
222
-
268
+ [...fr.querySelectorAll('[value]')].filter(el=>el.getAttribute('value').match( /\{(.*)\?\?(.*)\}/g )).forEach(el=>
269
+ { const v = attr(el,'value');
270
+ if(v)
271
+ el.setAttribute('value', evalCurly(v));
272
+ });
223
273
  for( const c of fr.childNodes )
224
274
  payload.append(xslDom.importNode(c,true))
225
275
 
@@ -276,18 +326,57 @@ deepEqual(a, b, O=false)
276
326
  return O
277
327
  return true;
278
328
  }
279
-
329
+ export const
330
+ assureSlices = ( root, names) =>
331
+ names.split('|').map(n=>n.trim()).map( xp =>
332
+ { if(xp.includes('/'))
333
+ { const ret = [], r = root.ownerDocument.evaluate( xp, root );
334
+ for( let n; n = r.iterateNext(); )
335
+ ret.push( n )
336
+ return ret
337
+ }
338
+ return [...root.childNodes].find(n=>n.localName === xp) || create(xp);
339
+ }).flat();
340
+
341
+ /**
342
+ *
343
+ * @param x slice node
344
+ * @param sliceNames slice name, xPath in /datadom/slice/
345
+ * @param ev Event obj
346
+ * @param dce
347
+ */
280
348
  export function
281
- injectSlice( x, s, data )
349
+ event2slice( x, sliceNames, ev, dce )
282
350
  {
283
- const isString = typeof data === 'string' ;
284
- const createXmlNode = ( tag, t = '' ) => ( e => ((e.append( createText(x, t||''))),e) )(x.ownerDocument.createElement( tag ))
285
- const el = isString
286
- ? createXmlNode(s, data)
287
- : document.adoptNode( xml2dom( Json2Xml( data, s ) ).documentElement);
288
- [...x.children].filter( e=>e.localName === s ).map( el=>el.remove() );
289
- el.data = data
290
- x.append(el);
351
+ // evaluate slices[]
352
+ // inject @attributes
353
+ // inject event
354
+ // evaluate slice-value
355
+ // slice[i] = slice-value
356
+ assureSlices(x,sliceNames).map( s =>
357
+ {
358
+ const d = x.ownerDocument
359
+ , el = ev.sliceEventSource
360
+ , sel = ev.sliceElement
361
+ , cleanSliceValue = ()=>[...s.childNodes].filter(n=>n.nodeType===3 || n.localName==='value').map(n=>n.remove());
362
+ el.getAttributeNames().map( a => s.setAttribute( a, attr(el,a) ) );
363
+ [...s.childNodes].filter(n=>n.localName==='event').map(n=>n.remove());
364
+ ev.type==='init' && cleanSliceValue();
365
+ s.append( obj2node( ev, 'event', d ) );
366
+ if( sel.hasAttribute('slice-value') )
367
+ { s.setAttribute('value', el.value );
368
+ const v = xPath( attr( sel, 'slice-value'),s );
369
+ cleanSliceValue();
370
+ s.append( createText( d, v ) );
371
+ }else
372
+ { const v = el.value || attr( sel, 'value' ) ;
373
+ cleanSliceValue();
374
+ if( isString(v) )
375
+ s.append( createText( d, v) );
376
+ else
377
+ s.append( obj2node(v,'value',s.ownerDocument) )
378
+ }
379
+ })
291
380
  }
292
381
 
293
382
  function forEach$( el, css, cb){
@@ -328,7 +417,10 @@ export function mergeAttr( from, to )
328
417
  return
329
418
  }
330
419
  for( let a of from.attributes)
331
- a.namespaceURI? to.setAttributeNS( a.namespaceURI, a.name, a.value ) : to.setAttribute( a.name, a.value )
420
+ { a.namespaceURI? to.setAttributeNS( a.namespaceURI, a.name, a.value ) : to.setAttribute( a.name, a.value )
421
+ if( a.name === 'value')
422
+ to.value = a.value
423
+ }
332
424
  }
333
425
  export function assureUnique(n, id=0)
334
426
  {
@@ -370,7 +462,7 @@ export function merge( parent, fromArr )
370
462
  { if( o.nodeValue !== e.nodeValue )
371
463
  o.nodeValue = e.nodeValue;
372
464
  }else
373
- { mergeAttr(o,e)
465
+ { mergeAttr(e,o)
374
466
  if( o.childNodes.length || e.childNodes.length )
375
467
  merge(o, e.childNodes)
376
468
  }
@@ -383,6 +475,54 @@ export function assureUID(n,attr)
383
475
  n.setAttribute(attr, crypto.randomUUID());
384
476
  return n.getAttribute(attr)
385
477
  }
478
+ export const evalCurly = s =>
479
+ { const exp = [...s?.matchAll( /([^{}]*)(\{)([^}]+)}([^{}]*)/g ) ].map(l=>`${l[1]}{${ xPathDefaults(l[3] )}}${l[4]}`);
480
+ return exp.join('');
481
+ }
482
+ export const xPathDefaults = x=>
483
+ { if(!x.trim())
484
+ return x;
485
+ const xx = x.split('??')
486
+ , a = xx.shift()
487
+ , b = xPathDefaults(xx.join('??'));
488
+
489
+ return xx.length ? `concat( ${a} , substring( ${b} , (1+string-length( ${b} )) * string-length( ${a} ) ) )`: x
490
+ // return xx.length ? `${a}|(${xPathDefaults(xx.join('??'))})[not(${a})]`: a
491
+ }
492
+ export const xPath = (x,root)=>
493
+ { x = xPathDefaults(x);
494
+
495
+ const it = root.ownerDocument.evaluate(x, root);
496
+ switch( it.resultType )
497
+ { case XPathResult.NUMBER_TYPE: return it.numberValue;
498
+ case XPathResult.STRING_TYPE: return it.stringValue;
499
+ }
500
+
501
+ let ret = '';
502
+ for( let n ;n=it.iterateNext(); )
503
+ ret += n.textContent;
504
+ return ret
505
+ }
506
+ 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(',');
507
+ export const toXsl = (el, defParent) => {
508
+ const x = create('xsl:'+el.localName);
509
+ for( let a of el.attributes )
510
+ x.setAttribute( a.name, a.value );
511
+ while(el.firstChild)
512
+ x.append(el.firstChild);
513
+ if( el.parentElement )
514
+ el.parentElement.replaceChild( x, el );
515
+ else
516
+ { const p = (el.parentElement || defParent)
517
+ , arr = [...p.childNodes];
518
+ arr.forEach((n, i) => {
519
+ if (n === el)
520
+ arr[i] = x;
521
+ });
522
+ p.replaceChildren(...arr);
523
+ }
524
+ };
525
+
386
526
  export class
387
527
  CustomElement extends HTMLElement
388
528
  {
@@ -405,19 +545,23 @@ CustomElement extends HTMLElement
405
545
 
406
546
  Object.defineProperty( this, "xsltString", { get: ()=>templateDocs.map( td => xmlString(td) ).join('\n') });
407
547
 
408
- const dce = this;
409
- const sliceNames = [...this.templateNode.querySelectorAll('[slice]')].map(e=>attr(e,'slice'));
548
+ const dce = this
549
+ , sliceNodes = [...this.templateNode.querySelectorAll('[slice]')]
550
+ , sliceNames = sliceNodes.map(e=>attr(e,'slice')).filter(n=>!n.includes('/')).filter((v, i, a)=>a.indexOf(v) === i)
551
+ , declaredAttributes = templateDocs.reduce( (ret,t) => { if( t.params ) ret.push( ...t.params ); return ret; }, [] );
552
+
410
553
  class DceElement extends HTMLElement
411
554
  {
412
- static get observedAttributes()
413
- { return templateDocs.reduce( (ret,t) =>
414
- { if( t.params ) ret.push( ...t.params.map(e=>attr(e,'name')) );
415
- return ret;
416
- }, [] );
417
- }
555
+ static get observedAttributes(){ return declaredAttributes.map( a=>attr(a,'name')); }
556
+ #inTransform = 0;
418
557
  connectedCallback()
419
- { if( this.firstElementChild?.tagName === 'TEMPLATE' )
420
- { const t = this.firstElementChild;
558
+ { let payload = this.childNodes;
559
+ if( this.firstElementChild?.tagName === 'TEMPLATE' )
560
+ {
561
+ const t = this.firstElementChild;
562
+ t.remove();
563
+ payload = t.content.childNodes;
564
+
421
565
  for( const n of [...t.content.childNodes] )
422
566
  if( n.localName === 'style' ){
423
567
  const id = assureUID(this,'data-dce-style')
@@ -428,9 +572,6 @@ CustomElement extends HTMLElement
428
572
  t.insertAdjacentElement('beforebegin',n);
429
573
  else if(n.nodeType===3)
430
574
  t.insertAdjacentText('beforebegin',n.data);
431
-
432
- t.remove();
433
-
434
575
  }
435
576
  const x = xml2dom( '<datadom/>' ).documentElement;
436
577
  const createXmlNode = ( tag, t = '' ) => ( e =>
@@ -438,11 +579,12 @@ CustomElement extends HTMLElement
438
579
  e.append( createText( x, t ))
439
580
  return e;
440
581
  })(x.ownerDocument.createElement( tag ))
441
- injectData( x, 'payload' , this.childNodes, assureSlot );
582
+ injectData( x, 'payload' , payload , assureSlot );
442
583
  this.innerHTML='';
443
584
  injectData( x, 'attributes' , this.attributes, e => createXmlNode( e.nodeName, e.value ) );
444
585
  injectData( x, 'dataset', Object.keys( this.dataset ), k => createXmlNode( k, this.dataset[ k ] ) );
445
- const sliceRoot = injectData( x, 'slice', sliceNames, k => createXmlNode( k, '' ) );
586
+ const sliceRoot = injectData( x, 'slice', sliceNames, k => createXmlNode( k, '' ) )
587
+ , sliceXPath = x => xPath(x, sliceRoot);
446
588
  this.xml = x;
447
589
 
448
590
  const sliceEvents=[];
@@ -450,10 +592,10 @@ CustomElement extends HTMLElement
450
592
  { const processed = {}
451
593
 
452
594
  for(let ev; ev = sliceEvents.pop(); )
453
- { const s = attr( ev.target, 'slice');
595
+ { const s = attr( ev.sliceElement, 'slice');
454
596
  if( processed[s] )
455
597
  continue;
456
- injectSlice( sliceRoot, s, 'object' === typeof ev.detail ? {...ev.detail}: ev.detail );
598
+ event2slice( sliceRoot, s, ev, this );
457
599
  processed[s] = ev;
458
600
  }
459
601
  Object.keys(processed).length !== 0 && transform();
@@ -462,10 +604,7 @@ CustomElement extends HTMLElement
462
604
 
463
605
  this.onSlice = ev=>
464
606
  { ev.stopPropagation?.();
465
- const s = attr( ev.target, 'slice')
466
- if( deepEqual( ev.detail, [...sliceRoot.children].find( e=>e.localName === s )?.data ) )
467
- return
468
-
607
+ ev.sliceEventSource = ev.currentTarget || ev.target;
469
608
  sliceEvents.push(ev);
470
609
  if( !timeoutID )
471
610
  timeoutID = setTimeout(()=>
@@ -474,7 +613,9 @@ CustomElement extends HTMLElement
474
613
  },10);
475
614
  };
476
615
  const transform = this.transform = ()=>
477
- {
616
+ { if(this.#inTransform){ debugger }
617
+ this.#inTransform = 1;
618
+
478
619
  const ff = xp.map( (p,i) =>
479
620
  { const f = p.transformToFragment(x.ownerDocument, document)
480
621
  if( !f )
@@ -487,35 +628,55 @@ CustomElement extends HTMLElement
487
628
  assureUnique(f);
488
629
  merge( this, f.childNodes )
489
630
  })
490
- const changeCb = el=>this.onSlice({ detail: el[attr(el,'slice-prop') || 'value'], target: el })
491
- , hasInitValue = el => el.hasAttribute('slice-prop') || el.hasAttribute('value') || el.value;
631
+
632
+ DceElement.observedAttributes.map( a =>
633
+ { let v = attr(this.firstElementChild,a);
634
+ if( v !== attr(this,a) )
635
+ { this.setAttribute( a, v );
636
+ this.#applyAttribute( a, v );
637
+ }
638
+ })
492
639
 
493
640
  forEach$( this,'[slice]', el =>
494
641
  { if( !el.dceInitialized )
495
642
  { el.dceInitialized = 1;
496
- el.addEventListener( attr(el,'slice-update')|| 'change', ()=>changeCb(el) )
497
- if( hasInitValue(el) )
498
- changeCb(el)
643
+ const evs = attr(el,'slice-event');
644
+ (evs || 'change')
645
+ .split(' ')
646
+ .forEach( t=> (el.localName==='slice'? el.parentElement : el)
647
+ .addEventListener( t, ev=>
648
+ { ev.sliceElement = el;
649
+ this.onSlice(ev)
650
+ } ));
651
+ if( !evs || evs.includes('init') )
652
+ { if( el.hasAttribute('slice-value') || el.hasAttribute('value') || el.value )
653
+ this.onSlice({type:'init', target: el, sliceElement:el })
654
+ else
655
+ el.value = sliceXPath( attr(el,'slice') )
656
+ }
499
657
  }
500
- })
658
+ });
659
+ this.#inTransform = 0;
501
660
  };
502
661
  transform();
503
662
  applySlices();
504
663
  }
505
- attributeChangedCallback(name, oldValue, newValue)
506
- { if( !this.xml )
507
- return;
508
- let a = this.xml.querySelector(`attributes>${name}`);
664
+ #applyAttribute(name, newValue)
665
+ { let a = this.xml.querySelector(`attributes>${name}`);
509
666
  if( a )
510
- emptyNode(a).append( createText(a,newValue));
667
+ emptyNode(a).append( createText(a,newValue) );
511
668
  else
512
669
  { a = create( name, newValue, this.xml );
513
- a.append( createText(a,newValue) );
514
670
  this.xml.querySelector('attributes').append( a );
515
671
  }
516
-
672
+ }
673
+ attributeChangedCallback(name, oldValue, newValue)
674
+ { if( !this.xml || this.#inTransform )
675
+ return;
676
+ this.#applyAttribute(name, newValue);
517
677
  this.transform(); // needs throttling
518
678
  }
679
+
519
680
  get dce(){ return dce }
520
681
  }
521
682
  if(tag)
package/demo/a.html CHANGED
@@ -4,24 +4,58 @@
4
4
  <head>
5
5
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
6
6
  <title>custom-element Declarative Custom Element implementation demo</title>
7
- <link rel="icon" href="./wc-square.svg" />
8
- <script type="module" src="../location-element.js"></script>
7
+ <link rel="icon" href="./wc-square.svg"/>
8
+ <script type="module" src="../http-request.js"></script>
9
9
  <script type="module" src="../custom-element.js"></script>
10
10
 
11
11
  <style>
12
12
  @import "./demo.css";
13
13
 
14
- button{ background: forestgreen; }
15
- table{ min-width: 16rem; }
16
- td{ border-bottom: 1px solid silver; }
17
- tfoot td{ border-bottom: none; }
18
- td,th{text-align: right; }
19
- caption{ padding: 1rem; font-weight: bolder; font-family: sans-serif; }
14
+ button {
15
+ background: forestgreen;
16
+ }
17
+
18
+ table {
19
+ min-width: 16rem;
20
+ }
21
+
22
+ td {
23
+ border-bottom: 1px solid silver;
24
+ }
25
+
26
+ tfoot td {
27
+ border-bottom: none;
28
+ }
29
+
30
+ td, th {
31
+ text-align: right;
32
+ }
33
+
34
+ caption {
35
+ padding: 1rem;
36
+ font-weight: bolder;
37
+ font-family: sans-serif;
38
+ }
20
39
  </style>
21
40
  </head>
22
41
  <body>
23
- <custom-element> my tag is {tag} </custom-element>
24
- <my-element></my-element>
42
+
43
+ <custom-element>
44
+ <template>
45
+ <button slice="clickcount"
46
+ slice-event="click"
47
+ slice-value="//clickcount + 1" >
48
+ +
49
+ </button>
50
+ <button slice="clickcount"
51
+ slice-event="click"
52
+ slice-value="//clickcount - 1" >
53
+ -
54
+ </button>
55
+ <input slice="clickcount" type="number" value="{//clickcount ?? 0}" />
56
+ {//clickcount}
57
+ </template>
58
+ </custom-element>
25
59
 
26
60
  </body>
27
61
  </html>