UNPKG

@twobirds/microcomponents

Version:

Micro Components Organization Class

422 lines (368 loc) 11.2 kB
'use strict'; import { LooseObject, debounce, deepEqual, flattenObject, copyGettersSetters, } from './helpers.js'; import { DC, HTMLMcElement, McEvent } from './MC.js'; function isObservableObject(value: any): boolean { return value != null && typeof value === 'object'; } type Callback = (value: any) => void; function getPlaceholders(target: HTMLElement): string[] { let placeholders: Set<string> = new Set(), regEx = /\{[^\{\}]*\}/g; // scan attributes target .getAttributeNames() .filter((attr) => target.getAttribute(attr)?.match(regEx)) // extract placeholder(s) .forEach((attr) => { let phs: Set<string> = new Set(target.getAttribute(attr)?.match(regEx)); phs.forEach((placeholder) => placeholders.add(placeholder.replace(/[\{\}]/g, '')) ); }); // scan children text nodes [...target.childNodes] .filter((childNode) => { return ( // is text node and has placeholders childNode.nodeType === 3 && ((childNode as any).nodeValue || '').match(regEx) ); }) .forEach((childNode) => { ((childNode as any).nodeValue || '') .match(regEx) .forEach((placeholder: string) => { placeholders.add(placeholder.replace(/[\{\}]/g, '')); }); }); // extract {} and return result return [...placeholders].map((placeholder) => placeholder.replace(/[\{\}]/g, '') ); } const inputs = [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement]; class Placeholders extends DC { data: LooseObject = {}; oldDisplay?: string = ''; constructor(target: HTMLElement) { super(target); let that = this; getPlaceholders(target).forEach((property) => (that.data[property] = '')); observe(that); that.#attachCallbacks(); delete that._mc; // cleanup, not needed } #attachCallbacks() { let that = this, target = that.target, hide: boolean = false; //console.log('attachCallbacks', that.target); // scan attributes target .getAttributeNames() .filter((attr: string) => target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g) ) // extract placeholder(s) .forEach((attr: string) => { let phs: Set<string> = new Set( target.getAttribute(attr)?.match(/\{[^\{\}]*\}/g) ); // attach callback that.data.observe( ((template: string) => (data: LooseObject) => { let result: string = template; phs.forEach((placeholder) => { result = result.replace( placeholder, data[placeholder.replace(/[\{\}]/g, '')] ); }); target.setAttribute(attr, result); //console.log('cb setAttribute()', data, template, attr, result); })(target.getAttribute(attr)) ); }); // scan children text nodes [...target.childNodes] .filter((childNode) => { return ( // is text node and has placeholders childNode.nodeType === 3 && ((childNode as any).nodeValue || '').match(/\{[^\{\}]*\}/g) ); }) .forEach((childNode) => { hide = true; let phs: Set<string> = new Set( childNode.nodeValue.match(/\{[^\{\}]*\}/g) ); // attach callback that.data.observe( ((template: string) => (data: LooseObject) => { let result: string = template; //console.log('text node "', template, '" callbach data:', data); phs.forEach((placeholder) => { result = result.replace( placeholder, data[placeholder.replace(/[\{\}]/g, '')] ); }); if ( Object.keys(data).some((key) => { return !!data[key]; }) ) { if (!!that.oldDisplay) { target.style = that.oldDisplay; } else { target.removeAttribute('style'); } delete that.oldDisplay; } childNode.nodeValue = result; })(childNode.nodeValue) ); // hide for now that.oldDisplay = target.getAttribute('style)') || ''; if ( hide && !inputs.includes((target as any).constructor) && target.tagName !== 'FIELDSET' ) { target.style.display = 'none'; } }); } onData(ev: McEvent) { let that = this, data: LooseObject = {}; Object.keys(ev.data).forEach((key) => { if (that.data.hasOwnProperty(key)) data[key] = ev.data[key]; }); //console.log('onData()', ev, data, that.data); if (JSON.stringify(data) !== JSON.stringify(that.data)) Object.assign(that.data, data); // will trigger callbacks } } function getPlaceholderElements( target: HTMLElement | DocumentFragment | ShadowRoot ): HTMLElement[] { let elements: Array<HTMLElement> = ( target instanceof HTMLElement ? [target] : [] ) .concat([...(target.querySelectorAll('*') as unknown as HTMLElement[])]) .filter((e) => !!getPlaceholders(e as HTMLElement).length) .filter((e) => e.tagName !== 'STYLE'); //console.log('getPlaceholderElements', elements); return elements as HTMLElement[]; } function makePlaceholders( element: HTMLElement | DocumentFragment | ShadowRoot ): void { //console.log('makePlaceholders', element); getPlaceholderElements(element).forEach((e: HTMLElement) => { if ((e as HTMLMcElement)?._mc?._placeholders) return; DC.add(e, '_placeholders', new Placeholders(e)); }); } export type Observable = { observe: (cb: Callback) => void; notify: () => void; bind: (target: HTMLElement | DocumentFragment | ShadowRoot ) => void; callbacks: Array<Function>; }; type Class<T> = new (...args: any[]) => T; // repository for observable class wrappers const classRepo: LooseObject = {}; // creates a new observable class wrapper function makeObservable(val: any): Class<typeof val>{ const Constructor = val.constructor, ConstructorName: string = Constructor.name; let ObservableClass = class extends Constructor { #notify: boolean = true; #callbacks: Array<Function> = []; constructor(val: any) { super(val); (this.constructor as LooseObject).callbacks = []; if (isObservableObject(val)) { // you cannot bind native values to the dom Object.getOwnPropertyNames(val).forEach((key) => { this[key] = val[key]; }); } } get callbacks(): Array<Function> { return this.#callbacks } set callbacks( callbacks: Array<Function> ){ this.#callbacks = callbacks } observe(f: Function) { //console.log('observe'); this.#callbacks.push(f); return this; } bind(target: HTMLElement | DocumentFragment | ShadowRoot) { const that = this, iso = isObservableObject(that.valueOf()); //console.log('bind', iso, target); if (iso) { makePlaceholders(target); const placeholders: unknown = [ ...target.querySelectorAll('[_mc]'), ].filter((e) => !!(e as LooseObject)?._mc?._placeholders); //console.log('placeholders:', target, placeholders); const func = function callback(data: LooseObject) { // avoid bubbling for speed let d = flattenObject(data); //console.log('inside callback', target, d); (placeholders as DC[]).forEach((e) => { // console.log('trigger callback', e); if (!e) return; (e as any)._mc.trigger('data', d); }); }; (that as any).#callbacks.push(func); that.notify(); } else { console.warn( 'IGNORED - cannot bind a non-Object to the dom, IS:', typeof that.valueOf(), that ); } return this; } notify(notify?: boolean) { let that = this, data = isObservableObject(that.valueOf()) && that.valueOf() instanceof Array === false ? structuredClone(Object.assign({}, that)) : that.valueOf(); if (notify !== undefined) { that.#notify = notify; // return; } //if (that.#notify) console.log('notify', this, data); (that as any).#callbacks.forEach((f: Function) => { // console.log( // (this.constructor as any).callbacks, // 'notify: exec callback', // f, // data // ); f(data); }); return that; } }; (classRepo as any)[ConstructorName] = ObservableClass; // console.log( 'classRepo:', classRepo ); return ObservableClass; } function getConstructor( val: any ): Class<typeof val> { let key: string = val.constructor.name; return classRepo[key] || makeObservable( val ); } export const observe = ( target: LooseObject, propertyName?: string ): LooseObject => { let keys = !propertyName ? [...Object.getOwnPropertyNames(target)] : [propertyName]; keys.forEach((key) => { if ( target.hasOwnProperty(key) && typeof target[key] !== 'function' && !target?.[key]?.constructor?.prototype?.observe ) { // console.log( 'co propname', key, typeof target[key], target[key] ); if (key[0] === '_' || target?.[key] === undefined) return target; let val: any = target[key]; let o = new ( getConstructor(val) as Class<typeof val> )( val ); Object.defineProperty(target, key, { enumerable: true, get() { if (isObservableObject(o.valueOf())) { let check = structuredClone(Object.assign({}, o)); setTimeout( debounce(function checkChanges() { let state = structuredClone(Object.assign({}, o)); //console.log( 'check changes', state, check ); if (!deepEqual(state, check)) { //console.log( '-> not equal!' ); o.notify(); } }, 0), 0 ); } return o; }, set(val: any) { //console.log( 'SET:', isObservableObject( val.valueOf() ), 'set:', val, 'to', isObservableObject( o.valueOf() ), o, ); if (typeof val === typeof o.valueOf()) { let iso = isObservableObject(o.valueOf()) && o.valueOf() instanceof Array === false; //console.log('target iso:', iso); if (iso) { let old = structuredClone(Object.assign({}, o)); //val = structuredClone(Object.assign({}, val)); //let callbacks = (o.constructor as LooseObject).callbacks; if (o?.constructor?.prototype?.observe === undefined ) { o = new ( getConstructor(val) as Class<typeof val> )( structuredClone(Object.assign({}, val)) ); copyGettersSetters(val, o); o.seal(); } Object.assign(o, val); // console.log('after set o=', o, 'check', old); // (o.constructor as LooseObject).callbacks = callbacks; setTimeout( // check for changes debounce(function checkChanges() { let state = structuredClone(Object.assign({}, o)); //console.log( 'check changes', state, check ); if (!deepEqual(state, old)) { //console.log( '-> not equal!' ); o.notify(); } }, 0), 0 ); } else { // simple native var let old = o.valueOf(), callbacks = (o as Observable).callbacks; // console.log( 'sO', o, o.valueOf(), 'val', val ); if (val !== old) { // console.log( '-> sO new val', val ); o = new ( getConstructor(val) as Class<typeof val> )( val ); (o as Observable).callbacks = callbacks; o.notify(); } } } else { console.warn( 'IGNORED - cannot change observable types, IS:', typeof o.valueOf(), o, '- CANNOT BECOME:', typeof val ); } }, }); // replace old property with proxy (observable) target[key] = val; }; }); return target; };