import {MountObserver} from 'mount-observer/MountObserver.js'; import {RoundaboutReady} from './ts-refs/trans-render/froop/types.js'; import { PropAttrQueryType, QuenitOfWork, Derivative, IMountOrchestrator, NumberExpression, InterpolatingExpression, DerivationCriteria, TransformerTarget, onMountStatusChange, RHS, AddEventListener, IfInstructions, UnitOfWork, QueryInfo, PropOrComputedProp, ITransformer, XForm, MarkedUpEventTarget, TransformOptions, LHS, WhereConditions, Info, ModificationUnitOfWork, YieldSettings } from './ts-refs/trans-render/types.js'; import { IMountObserver, MountContext, PipelineStage } from './ts-refs/mount-observer/types'; import {arr0} from './arr.js'; export {arr0} from './arr.js'; import {stdEvt} from './asmr/stdEvt.js'; export {UnitOfWork, ITransformer, EngagementCtx, XForm} from './ts-refs/trans-render/types.js'; export async function Transform( target: TransformerTarget, model: TProps & TMethods & RoundaboutReady, xform: XForm & Info, options?: TransformOptions ){ const xformer = new Transformer(target, model, xform, options!); await xformer.do(); return xformer; } export class Transformer extends EventTarget implements ITransformer{ #mountOrchestrators: Array> = []; #model: TProps & TMethods & RoundaboutReady; initializedMods: Set> = new Set(); get model(){ return this.#model; } async updateModel(newModel: TProps & TMethods & RoundaboutReady){ const {options} = this; const {propagator} = options; const {___props, ___nestedProps} = propagator!; if(___props === undefined){ this.#model = newModel; return; } if(___nestedProps === undefined){ Object.assign(this.#model, newModel); return; }else{ for(const prop in newModel){ if(___nestedProps.has(prop)){ const tbd = ___nestedProps.get(prop); const newVal = (newModel)[prop] if(tbd instanceof Transformer){ await tbd.updateModel(newVal); }else{ Object.assign(tbd, newVal); } } } } } constructor( public target: TransformerTarget, model: TProps & TMethods & RoundaboutReady, public xform: XForm & Info, public options: TransformOptions, ){ super(); this.#model = model; } async do(){ const {target, model, xform} = this; const info = xform as Info; let {options} = this; if(options === undefined){ options = {}; this.options = options; } let {propagator, useViewTransition} = options; if(propagator === undefined){ propagator = new EventTarget() as MarkedUpEventTarget; options.propagator = propagator; } if(propagator.___props === undefined){ propagator.___props = new Set(); } const uows : Array> = []; for(const key in xform){ if(key as any === '411') continue; let rhs = (xform[key as LHS]) as RHS; switch(typeof rhs){ case 'number': { if(rhs !== 0) throw 'NI'; const qi = await this.calcQI(key); const {hostPropToAttrMap, localPropCamelCase} = qi; const uow: QuenitOfWork = { o: hostPropToAttrMap!.map(x => x.name) as Array, d: 0, qi, q: key, s: localPropCamelCase }; uows.push(uow); break; } case 'string': { if(typeof model[rhs as keyof TProps] === 'function'){ const qi = await this.calcQI(key); const {hostPropToAttrMap} = qi; const uow: QuenitOfWork = { o: hostPropToAttrMap!.map(x => x.name) as Array, d: rhs as keyof TMethods & string, qi, q: key }; uows.push(uow); }else{ const uow: QuenitOfWork = { o: [rhs as keyof TProps & string], d: 0, q: key }; uows.push(uow); } } break; case 'object': { const rhses = arr0(rhs) as Array>; for(const rhsPart of rhses){ if(rhsPart.o !== undefined && !Array.isArray(rhsPart.o)){ rhsPart.o = [rhsPart.o]; } const uow: QuenitOfWork = { //d: 0, ...rhsPart!, q: key }; const {m, e, a, f, data} = uow; if((m || e || a || f) === undefined){ if(uow.o === undefined){ const qi = await this.calcQI(key); const {hostPropToAttrMap} = qi; if(hostPropToAttrMap !== undefined){ uow.o = hostPropToAttrMap.filter(x => x.name in (model as any)).map(x => x.name) as Array; } } const {o} = uow; if(o !== undefined && !(Array.isArray(o) && o.length === 0) && uow.d === undefined) uow.d = 0; } uows.push(uow); if(data !== undefined){ for(const x of data){ uows.push({ o: x, s: `?.dataset?.${x}`, q: key, d: 0, } as any as QuenitOfWork); } // const uowsToAdd = data.map(x => ({ // })) } } } break; } } for(const uow of uows){ let {q, qi, y, o: orig} = uow; if(qi === undefined) qi = await this.calcQI(q); if(!Array.isArray(orig)){ //qi.w = w; const {o, s} = qi; if(o!== undefined){ uow.o = o as PropOrComputedProp[]; } if(s !== undefined){ uow.s = s[0]; } if(y !== undefined){ uow.d = 0; } } const newProcessor = new MountOrchestrator(this, uow, qi); //for some reason, view transition logic doesn't work here await newProcessor.do(); this.#mountOrchestrators.push(newProcessor); await newProcessor.subscribe(); } } async calcQI(pqe: string){ if(pqe.startsWith('* ')){ const cssQuery = pqe.substring(2); return { css: cssQuery, cssQuery } as QueryInfo } if(pqe === ':root'){ return { isRootQry: true } as QueryInfo } if(!pqe.includes(' ')){ return { css: pqe, localName: pqe } as QueryInfo; } //TODO: dynamic import of the rest of this method, including other methods it calls. const qi: QueryInfo = {}; const asterSplit = pqe.split('*'); if(asterSplit.length === 2){ qi.cssQuery = asterSplit[1].trim(); } const tokens = pqe.split(' '); if(tokens.length === 1) throw 'NI'; let currentTokens = tokens; while(currentTokens.length > 0){ const [first, second, ...rest] = currentTokens; switch(first){ case '-s': { qi.localPropCamelCase = second; break; } default:{ if(qi.hostPropToAttrMap === undefined){ qi.hostPropToAttrMap = []; } qi.hostPropToAttrMap.push({ type: first as PropAttrQueryType, name: second, }) } } currentTokens = rest; } qi.css = await this.#calcCSS(qi); return qi; } async #calcCSS(qi: QueryInfo): Promise{ const {cssQuery} = qi; if(cssQuery !== undefined) return cssQuery; const {localName} = qi; if(localName !== undefined) return localName; const {hostPropToAttrMap, localPropCamelCase} = qi;; if(hostPropToAttrMap === undefined) throw 'NI'; qi.o = hostPropToAttrMap.map(x => x.name); if(localPropCamelCase !== undefined){ qi.s = [localPropCamelCase]; } let returnStr = hostPropToAttrMap.map(x => { const {name, type} = x; switch(type){ case '#': return `#${name}`; case '|': qi.outside = '[itemscope]'; return `[itemprop~="${name}"]`; case '%': return `[part~="${name}"]`; case '@': return `[name="${name}"]`; case '.': return `.${name}`; case '$': return `[itemscope][itemprop~="${name}"]`; case '-o': return `[-o~="${name}"]`; } }).join(''); if(localPropCamelCase !== undefined){ returnStr += `[-s~="${localPropCamelCase}"]` } return returnStr; } async doUpdate(matchingElement: Element, uow: UnitOfWork){ const {doUpdate} = await import('./trHelpers/doUpdate.js'); await doUpdate(this, matchingElement, uow); } async doIfs(matchingElement: Element, uow: UnitOfWork, i: IfInstructions): Promise{ const {doIfs} = await import('./trHelpers/doIfs.js'); return await doIfs(this, matchingElement, uow, i); } async engage(matchingElement: Element, type: onMountStatusChange, uow: UnitOfWork, observer: IMountObserver | undefined, mountContext: MountContext){ const {e} = uow; if(e === undefined) return const {Engage} = await import('./trHelpers/Engage.js'); await Engage(this, matchingElement, type, uow, mountContext); } async getDerivedVal(uow: UnitOfWork, d: Derivative, matchingElement: Element){ const {getDerivedVal} = await import('./trHelpers/getDerivedVal.js'); return await getDerivedVal(this, uow, d, matchingElement); } async getArrayVal(uow: UnitOfWork, u: NumberExpression | InterpolatingExpression){ const {getArrayVal} = await import('./trHelpers/getArrayVal.js'); return getArrayVal(this, uow, u); } async getComplexDerivedVal(uow: UnitOfWork, dc: DerivationCriteria){ const {getComplexDerivedVal} = await import('./trHelpers/getComplexDerivedVal.js'); return await getComplexDerivedVal(this, uow, dc); } getNumberUVal(uow: UnitOfWork, d: number){ const {o} = uow; const arrO = arr0(o); const propName = this.#getPropName(arrO, d); const pOrC = arrO[d]; const model = this.model as any; let val = model[propName as keyof TProps]; if(Array.isArray(pOrC)){ const c = pOrC[1]; if(typeof c === 'function'){ val = c(val); }else{ val = model[c](val); } } return val; } #getPropName(p: Array>, n: number){ const pOrC = p[n]; if(Array.isArray(pOrC)) return pOrC[0]; return pOrC; } } export class MountOrchestrator extends EventTarget implements IMountOrchestrator { #mountObserver: MountObserver | undefined; #matchingElements: WeakRef[] = []; #unitsOfWork: Array> constructor( public transformer: Transformer, uows: QuenitOfWork, public queryInfo: QueryInfo){ super(); this.#unitsOfWork = arr0(uows); } async do(){ const {transformer, queryInfo} = this; const {options, xform} = transformer; const {skipInit, useViewTransition, outside: oOutside} = options; const {isRootQry} = queryInfo; if(isRootQry){ const target = transformer.target as Element; this.#matchingElements.push(new WeakRef(target)); const {onMount} = await import('./trHelpers/onMount.js'); await onMount( transformer, this, target, this.#unitsOfWork, !!skipInit, {initializing: true}, this.#matchingElements ) return; } const info = xform as Info; const w = info?.[411]?.w; const x = w || ''; const {css, outside: iOutside} = queryInfo; const outside = iOutside || oOutside; const on = css + x;// transformer.calcCSS(queryInfo); const {assignGingerly: assigner} = await import('./lib/assignGingerly.js'); this.#mountObserver = new MountObserver({ on, outside, do:{ mount: async (matchingElement, observer, ctx) => { this.#matchingElements.push(new WeakRef(matchingElement)); const {onMount} = await import('./trHelpers/onMount.js'); if(!useViewTransition || !document.startViewTransition){ await onMount( transformer, this, matchingElement, this.#unitsOfWork, !!skipInit, ctx, this.#matchingElements, observer, this.#mountObserver!, ) }else{ document.startViewTransition(async () => { await onMount( transformer, this, matchingElement, this.#unitsOfWork, !!skipInit, ctx, this.#matchingElements, observer, this.#mountObserver!, ) }) } }, dismount: async(matchingElement, ctx, stage) => { for(const uow of this.#unitsOfWork){ this.#cleanUp(matchingElement); await transformer.engage(matchingElement, 'onDismount', uow, ctx, stage); } //TODO remove weak ref from matching elements; }, disconnect: async(matchingElement, ctx, stage) => { for(const uow of this.#unitsOfWork){ this.#cleanUp(matchingElement); await transformer.engage(matchingElement, 'onDisconnect', uow, ctx, stage); } } }, assigner, }); } async subscribe(){ for(const uow of this.#unitsOfWork){ let {o} = uow; const p = arr0(o) as string[]; const {target, options, model} = this.transformer; const {useViewTransition} = options; const propagator = ((model).propagator || options.propagator) as EventTarget; const propagatorIsReady = (model).propagator ? true : options.propagatorIsReady; for(const propName of p){ if(typeof propName !== 'string') throw 'NI'; if(!(propName in (model as any))) continue; if(!propagatorIsReady){ const propsSet = (propagator as MarkedUpEventTarget)!.___props; if(propsSet instanceof Set){ if(!propsSet.has(propName)){ const {subscribe} = await import('./lib/subscribe2.js'); await subscribe(model, propName, propagator!, true); } } } //I'm thinking this event handler doesn't access any memory, hence //risk of memory leaks seems really low. propagator!.addEventListener(propName, async e => { const all = this.#cleanUp(); for(const matchingElement of all){ if(!useViewTransition || !document.startViewTransition){ await this.doUpdate(matchingElement, uow); }else{ document.startViewTransition(async () => { await this.doUpdate(matchingElement, uow); }) } } }); } if(Array.isArray(target)){ throw 'NI'; }else{ this.#mountObserver?.observe(target); } } } #cleanUp(matchingElement?: Element){ const newRefs: WeakRef[] = []; const all: Element[] = []; for(const ref of this.#matchingElements){ const deref = ref.deref(); if(deref !== undefined && deref !== matchingElement){ newRefs.push(ref); all.push(deref); } } return all; } async doUpdate(matchingElement: Element, uow: UnitOfWork){ const {d, s, sa, ss, f} = uow; if(d !== undefined || s !== undefined || sa !== undefined || ss !== undefined || f !== undefined){ await this.transformer.doUpdate(matchingElement, uow); } } toStdEvt(a: keyof TMethods, matchingElement: Element): AddEventListener{ const on = stdEvt(matchingElement); return { on, do: a, } as AddEventListener; } }