diff --git a/src/custom-element.js b/src/custom-element.js
index ece2a69..0176b52 100644
--- a/src/custom-element.js
+++ b/src/custom-element.js
@@ -1,27 +1,36 @@
-const XML_DECLARATION = ''
-, XSL_NS_URL = 'http://www.w3.org/1999/XSL/Transform'
-, DCE_NS_URL ="urn:schemas-epa-wg:dce";
+const XSL_NS_URL = 'http://www.w3.org/1999/XSL/Transform'
+, HTML_NS_URL = 'http://www.w3.org/1999/xhtml'
+, EXSL_NS_URL = 'http://exslt.org/common'
+, DCE_NS_URL ="urn:schemas-epa-wg:dce";
// const log = x => console.debug( new XMLSerializer().serializeToString( x ) );
-const attr = (el, attr)=> el.getAttribute(attr)
+const attr = (el, attr)=> el.getAttribute?.(attr)
+, isText = e => e.nodeType === 3
, create = ( tag, t = '' ) => ( e => ((e.innerText = t||''),e) )(document.createElement( tag ))
-, createNS = ( ns, tag, t = '' ) => ( e => ((e.innerText = t||''),e) )(document.createElementNS( ns, tag ));
+, createText = ( d, t) => (d.ownerDocument || d ).createTextNode( t )
+, createNS = ( ns, tag, t = '' ) => ( e => ((e.innerText = t||''),e) )(document.createElementNS( ns, tag ))
+, xslNs = x => ( x?.setAttribute('xmlns:xsl', XSL_NS_URL ), x )
+, xslHtmlNs = x => ( x?.setAttribute('xmlns:xhtml', HTML_NS_URL ), xslNs(x) );
+ function
+ASSERT(x)
+{ if(!x)
+ debugger
+}
function
xml2dom( xmlString )
{
- return new DOMParser().parseFromString( XML_DECLARATION + xmlString, "application/xml" )
+ return new DOMParser().parseFromString( xmlString, "application/xml" )
}
function
xmlString(doc){ return new XMLSerializer().serializeToString( doc ) }
function
injectData( root, sectionName, arr, cb )
-{
+{ const create = ( tag ) => root.ownerDocument.createElement( tag );
const inject = ( tag, parent, s ) =>
- {
- parent.append( s = createNS( DCE_NS_URL, tag ) );
+ { parent.append( s = create( tag ) );
return s;
};
const l = inject( sectionName, root );
@@ -71,70 +80,133 @@ Json2Xml( o, tag )
ret.push("/>");
return ret.join('\n');
}
+ export function
+tagUid( node )
+{ // {} to xsl:value-of
+ forEach$(node,'*',d => [...d.childNodes].filter( e=>e.nodeType === 3 ).forEach( e=>
+ { if( e.parentNode.localName === 'style' )
+ return;
+ const m = e.data.matchAll( /{([^}]*)}/g );
+ if(m)
+ { let l = 0
+ , txt = t => createText(e,t||'')
+ , tt = [];
+ [...m].forEach(t=>
+ { if( t.index > l )
+ tt.push( txt( t.input.substring( l, t.index ) ))
+ const v = e.ownerDocument.createElement('xsl:value-of');
+ v.setAttribute('select', t[1] );
+ tt.push(v);
+ l = t.index+t[0].length;
+ })
+ if( l < e.data.length)
+ tt.push( txt( e.data.substring(l,e.data.length) ));
+ if( tt.length )
+ { for( let t of tt )
+ d.insertBefore(t,e);
+ d.removeChild(e);
+ }
+ }
+ }));
+ if( 'all' in node ) {
+ let i= 1;
+ for( let e of node.all )
+ e.setAttribute && !e.tagName.startsWith('xsl:') && e.setAttribute('data-dce-id', '' + i++)
+ }
+ return node
+}
export function
createXsltFromDom( templateNode, S = 'xsl:stylesheet' )
{
if( templateNode.tagName === S || templateNode.documentElement?.tagName === S )
- return templateNode
- const dom = xml2dom(
-`
+
+
+
+
+
+
+
+
+
+
+
+
+ `)
+ const sanitizeProcessor = new XSLTProcessor()
+ , tc = (n =>
+ {
+ forEach$(n,'script', s=> s.remove() );
+ const e = n.firstElementChild?.content || n.content
+ , asXmlNode = r => xslHtmlNs(xml2dom( '' ).importNode(r, true));
+ if( e )
+ { const t = create('div');
+ [ ...e.childNodes ].map( c => t.append(c.cloneNode(true)) )
+ return asXmlNode(t)
+ }
+ return asXmlNode(n.documentElement || n.body || n)
+ })(templateNode)
+ , xslDom = xml2dom(
+ `
-
-
+
-
- \t
-
+
-
+
-
-
+ `
- );
+ );
- const attrsTemplate = dom.documentElement.lastElementChild.previousElementSibling
- , getTemplateRoot = n => n.documentElement || n.firstElementChild?.content || n.content || n.body || n
- , tc = getTemplateRoot(templateNode)
- , cc = tc?.childNodes || [];
- if( (tc instanceof CustomElement) || tc.nodeType===11) {
- for( let c of cc )
- attrsTemplate.append(dom.importNode(c,true))
- }else
- {
- attrsTemplate.append(dom.importNode(tc,true))
- }
+ sanitizeProcessor.importStylesheet( sanitizeXsl );
+
+ const fr = sanitizeProcessor.transformToFragment(tc, document)
+ , $ = (e,css) => e.querySelector(css)
+ , payload = $( xslDom, 'template[mode="payload"]');
+ if( !fr )
+ return console.error("transformation error",{ xml:tc.outerHTML, xsl: xmlString( sanitizeXsl ) });
+
+ for( const c of fr.childNodes )
+ payload.append(xslDom.importNode(c,true))
+
+ const embeddedTemplates = [...payload.querySelectorAll('template')];
+ embeddedTemplates.forEach(t=>payload.ownerDocument.documentElement.append(t));
- const slot2xsl = s =>
- { const v = dom.firstElementChild.lastElementChild.lastElementChild.cloneNode(true);
- v.firstElementChild.setAttribute('select',`'${s.name}'`)
+ const slotCall = $(xslDom,'call-template[name="slot"]')
+ , slot2xsl = s =>
+ { const v = slotCall.cloneNode(true)
+ , name = attr(s,'name') || '';
+ name && v.firstElementChild.setAttribute('select',`'${ name }'`)
for( let c of s.childNodes)
v.lastElementChild.append(c)
return v
}
- for( const s of attrsTemplate.querySelectorAll('slot') )
- s.parentNode.replaceChild( slot2xsl(s), s )
+ forEach$( payload,'slot', s => s.parentNode.replaceChild( slot2xsl(s), s ) )
- // apply bodyXml changes
- return dom
+ return tagUid(xslDom)
}
export async function
xhrTemplate(src)
@@ -175,9 +247,9 @@ deepEqual(a, b, O=false)
injectSlice( x, s, data )
{
const isString = typeof data === 'string' ;
-
+ const createXmlNode = ( tag, t = '' ) => ( e => ((e.append( createText(x, t||''))),e) )(x.ownerDocument.createElement( tag ))
const el = isString
- ? create(s, data)
+ ? createXmlNode(s, data)
: document.adoptNode( xml2dom( Json2Xml( data, s ) ).documentElement);
[...x.children].filter( e=>e.localName === s ).map( el=>el.remove() );
el.data = data
@@ -186,8 +258,7 @@ injectSlice( x, s, data )
function forEach$( el, css, cb){
if( el.querySelectorAll )
- for( let n of el.querySelectorAll(css) )
- cb(n)
+ [...el.querySelectorAll(css)].forEach(cb)
}
const getByHashId = ( n, id )=> ( p => n===p? null: (p && ( p.querySelector(id) || getByHashId(p,id) ) ))( n.getRootNode() )
const loadTemplateRoots = async ( src, dce )=>
@@ -216,6 +287,64 @@ const loadTemplateRoots = async ( src, dce )=>
return [dom]
}catch (error){ return [dce]}
}
+export function mergeAttr( from, to )
+{ if( isText(from) )
+ {
+ if( !isText(to) ){ debugger }
+ return
+ }
+ for( let a of from.attributes)
+ a.namespaceURI? to.setAttributeNS( a.namespaceURI, a.name, a.value ) : to.setAttribute( a.name, a.value )
+}
+export function assureUnique(n, id=0)
+{
+ const m = {}
+ for( const e of n.childNodes )
+ {
+ const a = attr(e,'data-dce-id') || e.dceId || 0;
+ if( !m[a] )
+ { if( !a )
+ { m[a] = e.dceId = ++id;
+ if( e.setAttribute )
+ e.setAttribute('data-dce-id', e.dceId )
+ }else
+ m[a] = 1;
+ }else
+ { const v = e.dceId = a + '-' + m[a]++;
+ if( e.setAttribute )
+ e.setAttribute('data-dce-id', v )
+ }
+ e.childNodes.length && assureUnique(e)
+ }
+}
+export function merge( parent, fromArr )
+{
+ const id2old = {};
+ for( let c of parent.childNodes)
+ { ASSERT( !id2old[c.dceId] );
+ if( isText(c) )
+ { ASSERT( c.data.trim() );
+ id2old[c.dceId || 0] = c;
+ } else
+ id2old[attr(c, 'data-dce-id') || 0] = c;
+ }
+ for( let e of [...fromArr] )
+ {
+ const o = id2old[ attr(e, 'data-dce-id') || e.dceId ];
+ if( o )
+ { if( isText(e) )
+ { if( o.nodeValue !== e.nodeValue )
+ o.nodeValue = e.nodeValue;
+ }else
+ { mergeAttr(o,e)
+ if( o.childNodes.length || e.childNodes.length )
+ merge(o, e.childNodes)
+ }
+ }else
+ parent.append( e )
+ }
+}
+
export class
CustomElement extends HTMLElement
{
@@ -225,7 +354,7 @@ CustomElement extends HTMLElement
, templateDocs = templateRoots.map( n => createXsltFromDom( n ) )
, xp = templateDocs.map( (td, p) =>{ p = new XSLTProcessor(); p.importStylesheet( td ); return p })
- Object.defineProperty( this, "xsltString", { get: ()=>xp.map( td => xmlString(td) ).join('\n') });
+ Object.defineProperty( this, "xsltString", { get: ()=>templateDocs.map( td => xmlString(td) ).join('\n') });
const tag = attr( this, 'tag' );
const dce = this;
@@ -233,13 +362,18 @@ CustomElement extends HTMLElement
class DceElement extends HTMLElement
{
connectedCallback()
- { const x = createNS( DCE_NS_URL,'datadom' );
+ { const x = xml2dom( '' ).documentElement;
+ const createXmlNode = ( tag, t = '' ) => ( e =>
+ { if( t )
+ e.append( createText( x, t ))
+ return e;
+ })(x.ownerDocument.createElement( tag ))
injectData( x, 'payload' , this.childNodes, assureSlot );
- injectData( x, 'attributes' , this.attributes, e => create( e.nodeName, e.value ) );
- injectData( x, 'dataset', Object.keys( this.dataset ), k => create( k, this.dataset[ k ] ) );
- const sliceRoot = injectData( x, 'slice', sliceNames, k => create( k, '' ) );
+ this.innerHTML='';
+ injectData( x, 'attributes' , this.attributes, e => createXmlNode( e.nodeName, e.value ) );
+ injectData( x, 'dataset', Object.keys( this.dataset ), k => createXmlNode( k, this.dataset[ k ] ) );
+ const sliceRoot = injectData( x, 'slice', sliceNames, k => createXmlNode( k, '' ) );
this.xml = x;
- const slices = {};
const sliceEvents=[];
const applySlices = ()=>
@@ -249,7 +383,7 @@ CustomElement extends HTMLElement
{ const s = attr( ev.target, 'slice');
if( processed[s] )
continue;
- injectSlice( sliceRoot, s, ev.detail );
+ injectSlice( sliceRoot, s, 'object' === typeof ev.detail ? {...ev.detail}: ev.detail );
processed[s] = ev;
}
Object.keys(processed).length !== 0 && transform();
@@ -271,17 +405,28 @@ CustomElement extends HTMLElement
};
const transform = ()=>
{
- const ff = xp.map( p => p.transformToFragment(x, document) );
- this.innerHTML = '';
+ const ff = xp.map( (p,i) =>
+ { const f = p.transformToFragment(x, document)
+ if( !f )
+ console.error( "XSLT transformation error. xsl:\n", xmlString(templateDocs[i]), '\nxml:\n', xmlString(x) );
+ return f
+ });
ff.map( f =>
- { [ ...f.childNodes ].forEach( e => this.append( e ) );
+ { if( !f )
+ return;
+ assureUnique(f)
+ merge( this, f.childNodes )
+ })
+ const changeCb = el=>this.onSlice({ detail: el[attr(el,'slice-prop') || 'value'], target: el })
+ , hasInitValue = el => el.hasAttribute('slice-prop') || el.hasAttribute('value') || el.value;
- forEach$( this,'[slice]', el =>
- { if( 'function' === typeof el.sliceInit )
- { const s = attr( el,'slice' );
- slices[s] = el.sliceInit( slices[s] );
- }
- })
+ forEach$( this,'[slice]', el =>
+ { if( !el.dceInitialized )
+ { el.dceInitialized = 1;
+ el.addEventListener( attr(el,'slice-update')|| 'change', ()=>changeCb(el) )
+ if( hasInitValue(el) )
+ changeCb(el)
+ }
})
};
transform();
diff --git a/src/demo/http-request.html b/src/demo/http-request.html
index 9f62d93..c1396ea 100644
--- a/src/demo/http-request.html
+++ b/src/demo/http-request.html
@@ -35,7 +35,7 @@
description="load the list of pokemons">
Should display 6 image buttons with pokemon name
-
+
+ https://unpkg.com/pokeapi-sprites@2.0.2/sprites/pokemon/other/dream-world
- https://unpkg.com/pokeapi-sprites@2.0.2/sprites/pokemon/other/dream-world
-
-
-
-
-Content of //slice/request_slice is filled by request and response
-from ${url}
+
+
+
+ Content of //slice/request_slice is filled by request and response
+ from ${url}
-