import {RoundaboutReady, BaseProps, PropInfo, PropInfoTypes, PropLookup, OConfig} from '../ts-refs/trans-render/froop/types.js'; export {OConfig} from '../ts-refs/trans-render/froop/types.js'; import {assignGingerly} from '../lib/assignGingerly.js'; import { RoundAbout } from './roundabout.js'; import { MountObserver } from 'mount-observer/MountObserver.js'; import {RRMixin} from './RRMixin.js'; const publicPrivateStore = Symbol(); export class O extends RRMixin(HTMLElement) implements RoundaboutReady{ propagator = new EventTarget(); [publicPrivateStore]: Partial = {}; async covertAssignment(obj: TProps){ const props = (this.constructor).props as PropLookup; const extObj: any = {}; for(const key in obj){ const prop = props[key]; const val = obj[key]; if(prop === undefined) throw 403; const {fawm, ip} = prop; if(ip){ (this.#internals)[key] = val; continue; } extObj[key] = val; if(fawm !== undefined){ (this.#internals)[fawm](obj[key]) } } await assignGingerly(this[publicPrivateStore], extObj); } constructor(){ super(); const internals = this.attachInternals(); this.#internals = internals; this.copyInternals(internals); } /** * Keep internals reference private, but allow subclasses to get a handle to the internal "singleton" */ copyInternals(internals: ElementInternals){} static observedAttributes: Array = []; async connectedCallback(){ if(this.RAController.signal.aborted){ this.RAController = new AbortController(); } const props = (this.constructor).props as PropLookup; this.#propUp(props); await this.#instantiateRoundaboutIfApplicable(); const customStatesToReflect = getComputedStyle(this).getPropertyValue('--custom-state-exports'); if(customStatesToReflect !== ''){ const {CustStSvc} = await import('./CustStSvc.js'); new CustStSvc(this, this.#internals, customStatesToReflect); } const attrsToReflect = getComputedStyle(this).getPropertyValue('--attrs-to-reflect'); const attrs = (this.constructor).attrs as {[key: string] : PropInfo}; const propsToReflect = Object.values(attrs).filter(p => p!== undefined && p.reflect) as PropInfo[]; if(attrsToReflect !== '' || propsToReflect.length > 0){ const {Reflector} = await import('./Reflector.js'); const r = new Reflector(this, attrsToReflect); } } disconnectedCallback(): any { this.RAController!.abort(); } #internals: ElementInternals; /** * provided for debugging purposes * so don't remove it even though no references to it other than initialization */ #roundabout: RoundAbout | undefined; get #config(){ return (this.constructor).config as OConfig; } async #instantiateRoundaboutIfApplicable(){ const config = this.#config; const {actions, compacts, infractions, handlers, positractions, isSleepless} = config; if((actions || compacts || infractions || handlers || positractions) !== undefined){ let mountObservers: Set | undefined; if(!isSleepless){ const {guid} = await import('mount-observer/MountObserver.js'); mountObservers = (this)[guid]; } const {roundabout} = await import('./roundabout.js'); const [vm, ra] = await roundabout({ vm: this, actions, compacts, handlers, positractions, mountObservers }, infractions); this.#roundabout = ra; } } #parseAttr(propInfo: PropInfo, name: string, ov: string | null, nv: string | null){ //TODO support memoized parse const {type} = propInfo; if(nv === null){ if(type === 'Boolean') return false; return undefined; } switch(type){ case 'Boolean': return true; case 'Number': return Number(nv); case 'Object': if (this.getAttribute('onload') === 'doEval') { return eval(`(${nv})`); } else { return JSON.parse(nv); } case 'RegExp': return new RegExp(nv); case 'String': return nv; } } /** * Needed for asynchronous loading * @param props Array of property names to "upgrade", without losing value set while element was Unknown * @param defaultValues: If property value not set, set it from the defaultValues lookup * @private */ #propUp(props: PropLookup){ for(const key in props){ const propInfo = props[key]!; let value = (this)[key]; if(value === undefined){ const {def, parse, attrName} = propInfo; if(parse && attrName){ value = this.#parseAttr(propInfo, attrName, null, this.getAttribute(attrName)); } if(value === undefined) value = def; if(value === undefined){ continue; } } if(value !== undefined){ const {fawm, ip} = propInfo; if(ip){ ((this as O).#internals)[key] = value; continue; }else{ if (this.hasOwnProperty(key)) { delete (this)[key]; } (this[publicPrivateStore])[key] = value; } if(fawm !== undefined){ ((this as O).#internals)[fawm](value); } //(this)[key] = value; } } this.proppedUp = true; } static addProps(newClass: {new(): O}, props: PropLookup){ const proto = newClass.prototype; for(const key in props){ if(key in proto) continue; const prop = props[key]!; const {ro, parse, attrName, farop, farom, ip, fawm, adjuster} = prop; if(ro || farop){ Object.defineProperty(proto, key, { get(){ if(farop){ //not sure this will work return ((this as O).#internals)[key]; }else if(farom){ return (this as O).#internals[farom]; } return this[publicPrivateStore][key]; }, enumerable: true, configurable: true, }); }else{ Object.defineProperty(proto, key, { get(){ if(ip){ return ((this as O).#internals)[key] } return this[publicPrivateStore][key]; }, set(nv: any){ if(ip){ ((this as O).#internals)[key] = nv; }else{ let adjustedNV = nv; if(adjuster !== undefined){ if(typeof adjuster === 'function'){ adjustedNV = adjuster(nv); }else{ adjustedNV = this[adjuster](nv); } } const ov = this[publicPrivateStore][key]; if(prop.dry && ov === adjustedNV) return; this[publicPrivateStore][key] = adjustedNV; } if(fawm !== undefined){ ((this as O).#internals)[fawm](nv); } (this as O).propagator.dispatchEvent(new Event(key)); }, enumerable: true, configurable: true, }); } if(parse && attrName){ this.observedAttributes.push(attrName); } } } #ignoreAttrChanges = false; set ignoreAttrChanges(nv: boolean){ this.#ignoreAttrChanges = nv; } attributeChangedCallback(name: string, oldVal: string | null, newVal: string | null){ if(!this.proppedUp || this.#ignoreAttrChanges) return; const attrs = (this.constructor).attrs as PropLookup; const propInfo = attrs[name]!; const val = this.#parseAttr(propInfo, name, oldVal, newVal); (this)[propInfo.propName!] = val; } static config: OConfig | undefined; static async bootUp(){ const config = this.config!; const {propDefaults, propInfo, wrappers} = config; const props = {...this.props as PropLookup}; const attrs = this.attrs; const states = this.states; Object.assign(props, propInfo); if(propDefaults !== undefined){ for(const key in propDefaults){ const def = propDefaults[key]; const propInfo = { ...defaultProp, def, propName: key } as PropInfo; this.setType(propInfo, def); if(propInfo.type !== 'Object' && def !== true){ propInfo.parse = true; const {camelToLisp} = await import('../lib/camelToLisp.js'); propInfo.attrName = camelToLisp(key); } props[key] = propInfo; if(propInfo.parse && propInfo.attrName){ attrs[key] = propInfo; } } } if(propInfo !== undefined){ for(const key in propInfo){ const prop = propInfo[key]!; const mergedPropInfo = { ...props[key], ...defaultProp, ...prop, propName: key } as PropInfo props[key] = mergedPropInfo; const {parse, attrName} = mergedPropInfo; if(parse && attrName){ attrs[attrName] = mergedPropInfo; } } } this.props = props; this.addProps(this, props); if(wrappers !== undefined){ const {addWrappers} = await import('./addWrappers.js'); await addWrappers(this, wrappers); } } static setType(prop: PropInfo, val: any){ if(val !== undefined){ if(val instanceof RegExp){ prop.type = 'RegExp'; }else{ let t: string = typeof(val); t = t[0].toUpperCase() + t.substr(1); prop.type = t as PropInfoTypes; } } } static props: PropLookup = {}; static attrs: PropLookup = {}; static states: PropLookup = {}; } export interface O extends BaseProps{} const defaultProp: PropInfo = { type: 'Object', dry: true, parse: false, }; const baseConfig: OConfig = { propDefaults:{ proppedUp: false, } };