/* ts interfaces */ export interface LooseObject { [key: string | symbol]: any; } export interface TbTarget extends HTMLElement{ // tb: Function; _tb: Object; } /* ts types */ type singleSelector = string | HTMLElement | HTMLElement[] | HTMLCollection | NodeList | DomSelector | TbClass | TbSelector | undefined; type selection = singleSelector | singleSelector[]; /* helpers */ // the selectors must be negated for use!!! const _makeLoadData = ( e: HTMLElement ): Array | false => { let tagName = e?.localName ?? '', isAttribute = e.getAttribute?.('is') || '', isUBE = isAttribute ? true : false, isCE = tagName.indexOf('-') !== -1 ? true : false, ubeSelector = isUBE ? `${tagName}[is=\"${isAttribute}\"]:defined` : '', ceSelector = isCE ? tagName + ':defined' : '', fileBaseName = isUBE ? isAttribute : isCE ? tagName : '', fileName = fileBaseName.length ? '/' + fileBaseName.split('-').join('/') + '.js' : '', selector = isUBE ? ubeSelector : isCE ? ceSelector : '' ; //console.log( 'makeLoadData()', tagName, isUBE, isCE, ubeSelector, ceSelector, fileBaseName, ' => ', (isUBE || isCE) ? [ e, selector, fileName ] : false ); return (isUBE || isCE) ? [ e, selector, fileName, fileBaseName ] : false; } // currently loading scripts const loadingScripts: Set = new Set(); const _loadRequirements = ( node: HTMLTemplateElement | HTMLElement ): Promise => { // return Promise.resolve(); let undefinedSet: Set = new Set(), element: any = node instanceof HTMLTemplateElement ? (node as HTMLTemplateElement).content : node, head = document.head, allPromises: Array = [], e: HTMLElement; ; if ( element instanceof HTMLTemplateElement === false ){ // if node itself is undefined let data: any = _makeLoadData(element); //console.log('data', element, data) if ( data && !data[0].matches( data[1] ) ){ // console.log( '+file:', data ); undefinedSet.add( data ); } } // if some of the childNodes are undefined Array.from( element.childNodes ).forEach(( node )=>{ let data: any = _makeLoadData(node as HTMLElement); //console.log('data', element, data) if ( data && !data[0].matches( data[1] ) ){ // console.log( '+file:', data ); undefinedSet.add( data ); } }); if ( !!undefinedSet.size ){ // there are requirements //console.log( 'require set', [ ...undefinedSet ] ); undefinedSet.forEach( (data) => { if ( !getCustomElementConstructor( data[3] ) && !loadingScripts.has( data[2]) ){ loadingScripts.add( data[2] ); //console.info( '%cloading:', 'color:blue;', data[2], Array.from(loadingScripts) ); const se: HTMLScriptElement = document.createElement('script'); se.setAttribute( 'src', data[2] ); se.setAttribute( 'blocking', 'render' ); se.setAttribute( 'type', 'module' ); let s = (data as any)[2]; se.onload = () => { // console.info('loaded', s); loadingScripts.delete( s ); }; head.append( se ); let e = (data as Array)[0]; //allPromises.push( customElements.whenDefined( e ) ); } else { return ; } }); } return Promise.all( allPromises ); } const _htmlToElements = ( html: string ): HTMLElement[] => { let template = document.createElement('template'); html = html .replace( /<([A-Za-z0-9\-]*)([^>\/]*)(\/>)/gi, "<$1$2>"); // replace XHTML closing tags by full closing tags template.innerHTML = html; template.content.normalize(); if ( template.content.childNodes.length ){ _loadRequirements(template); } //// console.log( '_htmlToElements', isHtml, template.content, ret ); return ( Array.from(template.content.childNodes) as HTMLElement[] ); } const _getElementList = ( selector: singleSelector, rootNode: HTMLElement | Document ): HTMLElement[] => { //// console.log('_getElementList(', ...arguments,')'); let result: Set = new Set(); ; if (!selector) { // no selector given, or a falsy value return []; } else if (typeof selector === 'string') { selector = selector .replace( /\r/g,'') .replace( /\n/g,'') .replace( /\t/g,'') .replace( />\s*<') .trim(); if ( !selector.match( /^<.*>$/ ) ){ // it is not a HTML string, but a simple string --> it is regarded a CSS selector const nodeList: NodeList = rootNode.querySelectorAll( selector ); nodeList.forEach( (node) => { result.add( node as HTMLElement ); }); return [ ...result ] as HTMLElement[]; } else { // assume HTML string // return html content as a set of nodes return _htmlToElements( selector ); } } else if ( selector instanceof HTMLElement ) { // selector is a dom node result.add( selector ); } else if ( selector instanceof DomSelector ) { // selector is a $d() result set selector.forEach( e => { result.add( e ); }); } else if ( selector instanceof TbSelector ) { // selector is a tb() result set selector.forEach( e => { result.add( e.target ); }); } else if ( selector instanceof TbClass ) { // selector is a tb() result set result.add( (selector as any).target ); } else if ( selector instanceof NodeList ){ selector.forEach( n => { result.add( n as HTMLElement ); }); } else if ( selector instanceof HTMLCollection ){ Array.from(selector).forEach( e => { result.add( e as HTMLElement ); }); } else if ( selector instanceof Array ) { selector.forEach( e => { result.add( e as HTMLElement ); }); } //// console.log( 'result', [ ...result ] ); return [ ...result ] as HTMLElement[]; } const _addEvent = ( domNode: Node, eventName: string, handler: EventListenerOrEventListenerObject | null, capture: boolean = false, once: boolean = false ) => { let options: LooseObject = {} ; if ( capture ){ options.capture = capture; } if ( once ){ options.once = once; } domNode.addEventListener( eventName, handler, options ); } const _removeEvent = ( domNode: Node, eventName: string, handler: EventListenerOrEventListenerObject | null ) => { domNode.removeEventListener( eventName, handler ); } /* Object.defineProperty( HTMLElement.prototype, 'tb', { value: function( definitionClass?: { new(...args: any[]): any; }, data: any = {} ): TbClass | TbSelector { if ( !definitionClass ){ return $d(this).$t(); } if ( !definitionClass.name ) console.warn( `no classname given for ${definitionClass.toString().substring( 0, 200 )} ...` ) return cTb( this, definitionClass, data ) as TbClass; } } ); */ const getId = (): string => { let i: string; if ( i = crypto?.randomUUID() ){ return i; } return ( 'id-' + new Date().getTime() + '-' + Math.random().toString().replace(/\./, '') ); } export const debounce = ( func: Function, milliseconds: number ): Function => { let timeout: number; return () => { clearTimeout( timeout ); timeout = setTimeout( function(){ func( ...arguments ); }, milliseconds ); }; }; /* helper classes */ export interface TbEvent{ // TBD } export class TbEvent extends Event{ #name: string = 'l'; #data: any; #bubble: string = 'l'; #stopped: boolean = false; #immediateStopped: boolean = false; constructor( name: string, data: any, bubble: string ){ super( name ); let that = this ; that.#name = `[TB]${name}`; that.#bubble = bubble || 'l'; that.#data = data || {}; that.#stopped = that.#immediateStopped = false; } get name(): string { return this.#name; } get data(): any { return this.#data; } get bubble(): string { return this.#bubble; } get __stopped__(): boolean { return this.#stopped; } get __immediateStopped__(): boolean { return this.#immediateStopped; } stopPropagation(): TbEvent{ let that = this; that.#stopped = true; return that; } stopImmediatePropagation(): TbEvent{ let that = this; that.#stopped = true; that.#immediateStopped = true; return that; } preventDefault(): TbEvent{ let that = this; that.preventDefault(); return that; } } function isObject( value: any ): boolean { return value != null && typeof value === 'object'; } export const nameSpace = ( ns: string, obj: LooseObject, val: any = null, orig?: any ): any => { let leftOver: Array = ns.split('.'); if ( !orig ){ orig = obj; } // console.log( 'start', ns, obj, val, leftOver ); let last: string = leftOver.shift(), // this property name lns: string = leftOver.join('.') // the leftover namespace ; if ( val !== null && last.length && obj[last] === undefined ){ // extend if val given and ns leftover... // console.log( 'add prop', last ); obj[last] = {}; } let lob: any = obj[last] // the leftover value or obj ; // console.log( 'lob', lob, 'last:', last, 'lns:', lns, 'obj:', obj ); if ( lns.length === 0 ){ // this should be the value to set or get if ( val !== null ){ obj[last] = val; return orig; } return obj[last]; } if ( isObject( obj[last] ) ){ return nameSpace( lns, obj[last], val, orig ); } else { if (val){ return orig; } return lob[last]; } } const deepEqual = ( o1: any, o2:any): boolean => { const k1 = Object.keys(o1); const k2 = Object.keys(o2); if (k1.length !== k2.length) { return false; } for (const key of k1) { const v1 = o1[key], v2 = o2[key], recurse = isObject(v1) && isObject(v2); ; if ( ( recurse && !deepEqual(v1, v2) ) || (!recurse && v1 !== v2) ) { return false; } } return true; } export const parse = function( what: string | Object | any[], parseThis: Object ){ var args = Array.from(arguments); if (!args.length){ console.error('no arguments give to parse'); return; } if (args.length === 1){ return args[1]; } else if (args.length > 2) { while (args.length > 1){ args[0] = parse( args[0], args[1]); args.splice(1, 1); } return args[0]; } // implicit else: exactly 2 arguments if ( typeof what === 'string' ){ var vars = what.match( /\{[^\{\}]*\}/g ); if ( !!vars ) { vars .forEach( function (pPropname) { var propname = pPropname.substr(1, pPropname.length - 2), value = nameSpace( propname, parseThis ); if ( typeof value !== 'undefined' ){ what = (what as string).replace( pPropname, value ); } } ); } } else if ( isObject(what) ){ switch ( what.constructor ){ case Object: Object .keys( what ) .forEach( function( pKey ){ if ( what.hasOwnProperty( pKey ) ){ ( what as LooseObject )[ pKey ] = parse( ( what as LooseObject )[ pKey ], parseThis ); } } ); break; case Array: ( what as Array ) .forEach( function( pValue, pKey, original ){ original[ pKey ] = parse( (what as any)[ pKey ], parseThis ); } ); break; } } return what; }; export interface TbClass{ target: HTMLElement; config: any; nameSpace: string; } type TbStoreFactory = LooseObject & Function; const observable = function( value: any ): TbStoreFactory { let observedValue = value, enableNotify = true, timeout: number, initalIsObject = isObject(value); // console.log('observable Factory', value ); // make observable function to return in the end let observableFunction: LooseObject = function( p1: any, p2: any ){ function notify(){ if ( !enableNotify ) { return; } observableFunction.lastChanged = (new Date()).getTime(); // needed for tb.idle() return observableFunction.notify(); } //console.log('observableFunction', p1, p2 ); if ( typeof p1 !== 'undefined' ){ //console.log(' p1 =', p1 ); if( !!observedValue && initalIsObject ) { //console.log(' isObject(observedValue)', initalIsObject ); if ( typeof p1 === 'string' ) { // p1 is a property name //console.log(' property p1', p1 ); if ( p2 ) { // and a value is given //console.log(' value p2', p2 ); // value has changed, p1 must be key or namespace ( key1.key2 etc ) for object property if ( p1.indexOf('.') > -1 ){ // its a namespace nameSpace( p1, observedValue, p2 ); } else { // it is a simple property observedValue[p1] = p2; } clearTimeout(timeout); timeout=setTimeout( notify, 0 ); // console.log(' setter ', p1, p2 ); } else { // it is a getter //console.log('--getter p1', p1, observedValue ); return nameSpace( p1, observedValue ); } } else if ( typeof p1 === 'object' && typeof p2 === 'undefined' ){ //console.log('--object', p1 ); if ( observableFunction.isObject ){ observedValue = p1; } else { observedValue = p1[ Object.keys( p1 )[0] ]; } clearTimeout(timeout); timeout=setTimeout( notify, 0 ); } else { console.warn('observable() set value: parameter 1 should be a string or object if observed data is an object!'); } } else { // observed value is not an object, rather a plain value if ( typeof p1 !== 'undefined' ){ // set plain value // console.log('--set plain value', p1 ); // value has changed observedValue = p1; clearTimeout(timeout); timeout=setTimeout( notify, 0 ); } else { // it is a getter //console.log('--get plain value', observedValue ); return observedValue; } } } else { //console.log('-get observed value', observedValue ); return observedValue; } // console.log('-return observableFunction' ); // it was a setter functionality, so return the observable itself for chaining // getters return the value directly (see above) return observableFunction; }; observableFunction.isObject = initalIsObject; observableFunction.lastChanged = (new Date()).getTime(); // needed for tb.idle() // list of all callbacks to trigger on observedValue change observableFunction.notifiers = []; observableFunction.bound = []; // function used to execute all callbacks observableFunction.notify = function(){ // execute all callbacks ['notifiers', 'bound'].forEach( ( type ) => { observableFunction[type].forEach( ( func: LooseObject, key:string ) => { if ( typeof func === 'function' ){ observedValue = func( observedValue ); if ( (func as LooseObject).once ){ observableFunction.notifiers.splice(key,1); } } else { observableFunction.notifiers.splice(key,1); } } ); }); return observableFunction; // chaining }; // enable/disable notifications observableFunction.enableNotify = function( pEnableNotify = true ){ enableNotify = pEnableNotify; return observableFunction; // chaining }; // function used to add a callbacks observableFunction.observe = function( func: LooseObject, once: boolean ){ if ( typeof func === 'function' ){ (func as LooseObject).once = once || false; observableFunction.notifiers.push( func ); } return observableFunction; // chaining }; observableFunction.bindDom = function( element: HTMLElement ){ //console.log('bind:', element); function walk( element: Node ){ //console.log('-walk:', element); if ( !!element['nodeType'] && element.nodeType === 3 ){ // text node //console.log('--type: textNode(3)'); var placeholders = Array.from( element.nodeValue && element.nodeValue.match( /\{[^\{\}]*\}/g ) || []); if ( placeholders ){ //console.log('--placeholders:', placeholders); var f=(function( template: string ){ return function( values: any ){ var t, changed = false; if ( placeholders ){ if ( observableFunction.isObject ){ placeholders.forEach(function(placeholder){ if ( ( f as LooseObject).values[placeholder] !== values[placeholder] ){ (f as LooseObject).values[placeholder] = values[placeholder]; changed = true; } }); } else { // it is a simple value (f as LooseObject).values = values; changed = true; }; } if (changed){ // only reflow if changed t = template; //console.log('--template parse', t, (f as LooseObject).values ); if ( observableFunction.isObject ){ element.nodeValue = parse( t, (f as LooseObject).values ); } else { // it is a simple value -> replace every placeholder whatever the name wi element.nodeValue = t.replace( /\{.*\}/g ,(f as LooseObject).values ); }; } return values; }; })( element.nodeValue || '' ); (f as LooseObject).values = observableFunction.isObject ? {} : undefined; placeholders = Array.from( placeholders ).map((pKey) => pKey.replace(/[\{\}]/g, '').trim()); let initial: any = observableFunction.isObject ? {} : undefined; placeholders.forEach(function(pKey){ if ( observableFunction.isObject ){ initial[pKey] = ""; } }); observableFunction.bound.push(f); //console.log( `--bound`, observableFunction.bound ); (observableFunction as Function)(initial); } //console.log('--childNodes:', element.childNodes ); Array.from( element.childNodes ) .forEach(function( childNode ){ walk( childNode ); }); observableFunction.notify(); } if ( !!element['nodeType'] && element.nodeType === 1 ){ // HTML element //console.log('--type: HTMLElement(1)'); Array.from( (element as HTMLElement).attributes ) .forEach( function( attributeNode ){ var placeholders = Array.from( attributeNode.value && attributeNode.value.match( /\{[^\{\}]*\}/g ) || []); if ( placeholders ){ //console.log( `--[${attributeNode.value}] placeholders:`, placeholders ); var f=(function( template: string ){ return function( values: any ){ var t, changed = false; if ( placeholders ){ placeholders.forEach(function(pKey){ if ( ( f as LooseObject).values[pKey] !== values[pKey] ){ (f as LooseObject).values[pKey] = values[pKey]; changed = true; } }); } if (changed){ // only reflow if changed t = template; element.nodeValue = parse( t, (f as LooseObject).values ); } return values; }; })( attributeNode.value || '' ); (f as LooseObject).values = {}; placeholders = Array.from( placeholders ).map((pKey) => pKey.replace(/[\{\}]/g, '').trim()); let initial: LooseObject = {}; placeholders.forEach(function(pKey){ initial[pKey] = ""; }); observableFunction.bound.push(f); // console.log( `--bound`, observableFunction.bound ); (observableFunction as Function)(initial); } } ); // console.log('--childNodes:', element.childNodes ); Array.from( element.childNodes ) .forEach(function( childNode ){ walk( childNode ); }); observableFunction.notify(); } } walk( element ); return observableFunction; // chaining }; return (observableFunction as Function); }; export function store( name: string, value?: any, target: Object = {} ) { let obs = observable( value ), propName: string = name ; // console.log( '.store() - obs:', propName, obs() ); const iso = isObject( value ); const getFunction = iso ? (): any => { // console.log( 'get prop from object:', propName ); return obs(propName); } : (): any => { if ( value instanceof Function) return; // TBD hotfix // console.log( 'get other val:' ); return obs(); } const setFunction = iso ? ( value: any ) => { // console.log( 'set prop in object:', propName, value ); obs( propName, value ); } : ( value: any ) => { if ( value instanceof Function) return; // TBD hotfix //console.log( 'set other val:', value ); obs( value ); } Object.defineProperty( target, propName, { enumerable: true, configurable: true, get: getFunction, set: setFunction } ); return obs; } export class TbClass{ constructor( target: HTMLElement, data?: any){ Object.defineProperty( this, 'target', { value: target, writable: false, configurable: false } ); Object.defineProperty( this, 'config', { value: data instanceof Object ? Object.seal(data) : data, writable: false, configurable: false } ); } children( nameSpace?: string ): TbSelector{ let that = this, id: string = getId(), elements = $d( (that as any).target ); ; // generate temp attribute elements.attr( 'data-tempid', id ); let selector = $d((that as any).target) .descendants() .$t() .filter( e => $t(e).parent().$d().attr('data-tempid') === id ); // remove temporary attribute elements.attr( 'data-tempid', null ); if ( nameSpace ){ selector.filter( ( e: TbClass ) => ( e as LooseObject).nameSpace = nameSpace ); } return selector; } descendants( nameSpace?: string ): TbSelector{ let that = this, id: string = getId(), elements = $d( (that as any).target ); ; // generate temp attribute elements.attr( 'data-tempid', id ); let selector = $d((that as any).target) .descendants() .$t(); // remove temporary attribute elements.attr( 'data-tempid', null ); if ( nameSpace ){ selector.filter( ( e: TbClass ) => ( e as LooseObject).nameSpace = nameSpace ); } return selector; } off( name: string, cb: Function ){ let that = this, eventNames = name.indexOf(' ') > -1 ? name.split(' ') : [ name ] ; eventNames.forEach( (name) => { $d( (that as any).target ).off( `[TB]${name}`, cb ); }) return this; } on( name: string, cb: Function, once: boolean = false ): Function{ let that = this, eventNames = name.indexOf(' ') > -1 ? name.split(' ') : [ name ] ; function wrapper(){ wrapper.cb.apply(that, Array.from(arguments) ); }; wrapper.cb = cb; eventNames.forEach( (n) => { _addEvent( (that as any).target, `[TB]${name}`, (wrapper as EventListenerOrEventListenerObject), false, once ); }) return wrapper; } one( name: string, cb: Function ): Function{ let that = this, eventNames = name.indexOf(' ') > -1 ? name.split(' ') : [ name ] ; function wrapper(){ wrapper.cb.apply(that, Array.from(arguments) ); }; wrapper.cb = cb; eventNames.forEach( (n) => { _addEvent( (that as any).target, `[TB]${name}`, (wrapper as EventListenerOrEventListenerObject), false, true ); }) return wrapper; } parent( nameSpace?: string ): TbSelector{ let that = this, result: TbSelector ; result = $d((that as any).target) .parents() .filter( e => e.tb().length ) .first() .$t(); if ( nameSpace ){ result.filter( ( e: TbClass ) => ( e as LooseObject).nameSpace = nameSpace ); } return result; } parents( nameSpace?: string ): TbSelector{ let that = this, result: TbSelector ; result = $d((that as any).target) .parents() .filter( e => e.tb().length ) .$t(); if ( nameSpace ){ result.filter( ( e: TbClass ) => ( e as LooseObject).nameSpace = nameSpace ); } return result; } store( name: string, value: any ): TbStoreFactory { let myStore = store( name, value, this ); ( this as LooseObject)[name] = myStore; return myStore; } trigger( ev: TbEvent | string, data: any = {}, bubble: string = 'l' ): TbClass{ let that = this, tbEvent: TbEvent = ev instanceof TbEvent ? ev : new TbEvent( ev as string, data, bubble ); ; // if event __stopped__ , handling is cancelled if ( (ev as TbEvent).__stopped__ || (ev as TbEvent).__immediateStopped__ ) { return that; } $d( (that as any).target ).trigger( tbEvent.name, data ); // if event __stopped__ , handling is cancelled if ( !!tbEvent.__stopped__ ) { return that; } setTimeout( () => { // bubble up if ( tbEvent.bubble.indexOf('u') > -1 ){ (that as any) .parent() .trigger( new TbEvent( tbEvent.name, tbEvent.data, 'lu' )); } // bubble down if ( tbEvent.bubble.indexOf('d') > -1 ){ (that as any) .children() .forEach( (tbInstance: TbClass) => { tbInstance.trigger( new TbEvent( tbEvent.name, tbEvent.data, 'ld' ) ); }) ; } }, 0 ); return that; } } class TbNop extends TbClass{ constructor( target: HTMLElement, data?: any ){ super( target, data ); } } export interface DomSelector{ }; /* selector classes */ export class DomSelector{ #set: Set = new Set(); public get set(): Set { return this.#set; } public get array(): HTMLElement[] { return Array.from( this.#set ); } public set set( set: Set ) { this.#set = set; return; } public get length(): number { return this.#set.size; } constructor( selection?: selection, element: any = document ){ let that = this, undefinedTags: Set = new Set(); // // console.log('DomSelector constructor', ...arguments); if ( !selection ) { return that; } if ( selection instanceof Array ){ selection.forEach( (select) => { _getElementList( select, element ).forEach( (e) => { if ( e.localName.indexOf('-') !== -1 || e.getAttribute('is') ) _loadRequirements( e ); that.#set.add( e ); }); }); } else { _getElementList( selection, element ).forEach( (e) => { if ( e.localName.indexOf('-') !== -1 || e.getAttribute('is') ) _loadRequirements( e ); that.#set.add( e ); }); } } $t( nameSpace?: string ): TbSelector { let result = new TbSelector() ; this.#set.forEach( (e: HTMLElement) => { if ( (e as TbTarget)._tb ){ Object.values( (e as TbTarget)._tb ).forEach( (tb: TbClass) => { result.set.add( tb ); }); } }); if ( nameSpace ) { result.filter( tb => (tb as LooseObject).nameSpace === nameSpace ); } return result; } add( selection: selection, target?: HTMLElement ): DomSelector { let that = this ; $d( selection, target ).forEach( element => { that.#set.add( element ); }); return that; } after( selection: selection, target?: HTMLElement ): DomSelector { let that = this, after = $d( selection, target ); ; if ( !after.length ) return that; after .first() .forEach( element => { that.array.reverse().forEach( (e: HTMLElement) => { e.after( element ); }); }); return that; } append( selection: selection, target?: HTMLElement ): DomSelector { let that = this, addTo = that.array['0'], append = $d( selection, target ) ; if ( addTo ){ append.forEach( (e: HTMLElement) => { addTo.append( e ); }); } return that; } appendTo( selection: selection, target?: HTMLElement ): DomSelector { let that = this, appendTo = $d( selection, target ).array['0'] ; if ( appendTo ){ that.forEach( (e: HTMLElement) => { appendTo.append( e ); }); } return that; } attr(): LooseObject; attr( obj: LooseObject ): DomSelector; attr( name: string ): string | null; attr( first?: string | LooseObject, second?: string | object | null ): DomSelector; attr( first?: unknown, second?: unknown ): DomSelector | LooseObject | string | null { let that = this, attributes: LooseObject = {} ; // if no elements in set -> skip if ( !that.length ) return that; // if no arguments, return attributes of first in list as hash object if (!arguments.length) { Array.from( [ ...that.set ][0].attributes ).forEach( (attribute: LooseObject) => { attributes[attribute.name] = attribute.value; }); return attributes; } // if first is a string // if no value (second) is given -> return attribute value of first element in node selection // if value (second) is null -> remove attribute and return this // else set attribute values and return this if ( typeof first === 'string' ) { // return attributes if ( second === undefined ){ return [ ...that.set ][0].getAttribute( first ); } // remove attribute if ( second === null ){ [ ...that.set ].forEach( (e: HTMLElement) =>{ e.removeAttribute( first ); }); return that; } // if attribute value is an object -> convert to json string if ( typeof second === 'object' ){ second = JSON.stringify( second ); } // set attribute [ ...that.set ].forEach( (e: HTMLElement) =>{ e.setAttribute( first, second as string ); }); return that; } // if first is an object set attributes to all nodes if ( typeof first === 'object' ) { [ ...that.set ].forEach( (e: HTMLElement) =>{ Object.keys( first as LooseObject ).forEach( (key: string) => { $d(e).attr( key, ( first as LooseObject ).key ); }); }); return that; } return that; } before( selection: selection, target?: HTMLElement ): DomSelector { let that = this, before = $d( selection, target ); ; if ( !before.length ) return that; before .first() .forEach( element => { that.forEach( (e: HTMLElement) => { e.before( element ); }); }); return that; } children( selection?: selection, rootNode?: HTMLElement ): DomSelector{ let that: DomSelector = this, set: Set = new Set() ; that.#set.forEach( (e: HTMLElement) => { Array.from( e.children ).forEach( ( c ) => { set.add( c as HTMLElement ); }); }); that.#set = set; if ( selection ){ let compare = $d( selection, rootNode ); ; that.filter( ( e: HTMLElement ) => compare.has( e ) ); } return that; } descendants( selection?: selection, rootNode?: HTMLElement ): DomSelector{ let that: DomSelector = this, set: Set = new Set() ; that.#set.forEach( (e: HTMLElement) => { $d( '*', e ).forEach( ( h ) => { set.add( h as HTMLElement ); }); }); that.#set = set; if ( selection ){ let compare = $d( selection, rootNode ); ; that.filter( ( e: HTMLElement ) => compare.has( e ) ); } return that; } empty(): DomSelector { return this.filter( (e: any)=>false ); } first( selection?: selection, rootNode?: HTMLElement ): DomSelector{ let that: DomSelector = this, set: Set = new Set() ; that.#set.forEach( (e: HTMLElement) => { set.add( e.parentElement?.children[0] as HTMLElement ); }); that.#set = set; if ( selection ){ let compare = $d( selection, rootNode ); ; that.filter( ( e: HTMLElement ) => compare.has( e ) ); } return that; } has( element: HTMLElement ): boolean{ return this.#set.has( element ); } html( html?: string ): string | DomSelector{ let that = this; if ( html ){ that.forEach( (e: HTMLElement) => { !e.parentNode ? (()=>{ console.warn('Cannot access the .innerHTML of a HTMLElement that hasnt been inserted yet', e ); console.trace( e, this ); console.info( 'If you called this from inside a Web Component constructor, consider using the %c[connected] event callback!', 'color: blue;' ) })() : e.innerHTML = html; }); return that; } return that.array[0].innerHTML; } last( selection?: selection, rootNode?: HTMLElement ): DomSelector{ let that: DomSelector = this, set: Set = new Set() ; that.#set.forEach( (e: HTMLElement) => { let siblings = e.parentNode?.children as HTMLCollection ; if ( siblings ) { set.add( Array.from(siblings).at(-1) as HTMLElement ); } }); that.#set = set; if ( selection ){ let compare = $d( selection, rootNode ); ; that.filter( ( e: HTMLElement ) => compare.has( e ) ); } return that; } load(): DomSelector{ let that = this; if( !that.length ){ that.add( document.body ); } that.forEach( e => _loadRequirements( e ) ); return that; } next( selection?: selection, rootNode?: HTMLElement ): DomSelector{ let that: DomSelector = this, set: Set = new Set() ; that.#set.forEach( (e: HTMLElement) => { let siblings = e.parentNode?.children as HTMLCollection ; if ( siblings ) { let arr = Array.from(siblings), pos = arr.indexOf(e) + 1 ; if ( pos < arr.length ){ set.add( arr.at(pos) as HTMLElement ); } } }); that.#set = set; if ( selection ){ let compare = $d( selection, rootNode ); ; that.filter( ( e: HTMLElement ) => compare.has( e ) ); } return that; } normalize(): DomSelector { let that = this; that.#set.forEach( (e: HTMLElement) => { e.normalize() }); return that; } off( eventName: string, cb: Function ): DomSelector{ let that = this, eventNames = eventName.indexOf(' ') > -1 ? eventName.split(' ') : [ eventName ] ; // remove handler that.array.forEach( (e: HTMLElement) => { eventNames.forEach(function ( n: string ) { _removeEvent( e, n, cb as EventListenerOrEventListenerObject ); }); }); return that; } on( eventName: string, cb: Function, capture: boolean = false, once: boolean = false ): Function{ let that = this, eventNames = eventName.indexOf(' ') > -1 ? eventName.split(' ').filter( (s) => s !== '' ) : [ eventName ] ; function wrapper(){ wrapper.cb.apply(that, Array.from(arguments) ); }; wrapper.cb = cb; // attach handler that.forEach( (e: HTMLElement) => { eventNames.forEach(function ( n: string ) { _addEvent( e, n, (wrapper as EventListenerOrEventListenerObject), capture, once ); }); }); return wrapper; } one( eventName: string, cb: Function, capture: boolean = false ): Function{ let that = this, eventNames = eventName.indexOf(' ') > -1 ? eventName.split(' ').filter( (s) => s !== '' ) : [ eventName ] ; function wrapper(){ wrapper.cb.apply(that, Array.from(arguments) ); }; wrapper.cb = cb; // attach handler that.forEach( (e: HTMLElement) => { eventNames.forEach(function ( n: string ) { _addEvent( e, n, (wrapper as EventListenerOrEventListenerObject), capture, true ); }); }); return wrapper; } parent( selection?: selection, rootNode?: HTMLElement ): DomSelector { let that: DomSelector = this, set: Set = new Set() ; that.#set.forEach( (e: HTMLElement) => { if ( e.parentNode && (e.parentNode as HTMLElement).tagName !== 'HTML') { set.add( e.parentNode as HTMLElement ); } }); that.#set = set; if ( selection ){ let compare = $d( selection, rootNode ); ; that.filter( ( e: HTMLElement ) => compare.has( e ) ); } return that; } parents( selection?: selection, rootNode?: HTMLElement ): DomSelector { let that: DomSelector = this, set: Set = new Set() ; that.#set.forEach( (e: HTMLElement) => { let current = e ; while ( current.parentNode && (current.parentNode as HTMLElement).tagName !== 'HTML' ){ current = current.parentNode as HTMLElement; set.add( current ); } }); that.#set = set; if ( selection ){ let compare = $d( selection, rootNode ); ; that.filter( ( e: HTMLElement ) => compare.has( e ) ); } return that; } prev( selection?: selection, rootNode?: HTMLElement ): DomSelector{ let that: DomSelector = this, set: Set = new Set() ; that.#set.forEach( (e: HTMLElement) => { let siblings = e.parentNode?.children as HTMLCollection ; if ( siblings ) { let arr = Array.from(siblings), pos = arr.indexOf(e) - 1 ; if ( pos > -1 ){ set.add( arr.at(pos) as HTMLElement ); } } }); that.#set = set; if ( selection ){ let compare = $d( selection, rootNode ); ; that.filter( ( e: HTMLElement ) => compare.has( e ) ); } return that; } text( text?: string ): string | DomSelector{ let that = this; if ( text ){ that.forEach( (e: HTMLElement) => { !e.parentNode ? (()=>{ console.warn('Cannot access the .textContent of a HTMLElement that hasnt been inserted yet', e ); console.trace( e, this ); console.info( 'If you called this from inside a Web Component constructor, consider using the %c[connected] event callback!', 'color: blue;' ) })() : e.innerText = text; }); return that as DomSelector; } let t = that.array[0].innerText; return t ?? '' as string; } trigger( eventName: string, data: any = {}, bubbles: boolean = false, cancelable: boolean = false, composed: boolean = false ): DomSelector{ let that = this, ev: Event, options: LooseObject = { bubbles, cancelable, composed }, eventNames = eventName.indexOf(' ') > -1 ? eventName.split(' ') : [ eventName ] ; that.#set.forEach( (e: HTMLElement) => { eventNames.forEach(function ( n: string ) { // console.log() ev = new Event( n, options ); (ev as LooseObject ).data = data; e.dispatchEvent( ev ); }); }); return that; } /* array method mapping */ at( index: number ): DomSelector { return $d( this.array.at( index ) as HTMLElement ) as DomSelector; } concat( items: DomSelector | any[] ): DomSelector{ let that = this, arr = items instanceof DomSelector ? items.array : items as HTMLElement[] ; arr.forEach( (e: HTMLElement) => { that.#set.add( e ); }); return that; } entries(): IterableIterator<[number, any]> { return this.array.entries(); } every( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): boolean { return this.array.every( predicate, thisArg ); } filter( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): DomSelector { this.#set = new Set( this.array.filter( predicate, thisArg ) ); return this; } forEach( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): DomSelector { this.array.forEach( predicate, thisArg ); return this; } includes( item: HTMLElement ): boolean{ return this.array.indexOf( item, 0 ) !== -1; } indexOf( item: HTMLElement, start?: number ): number{ return this.array.indexOf( item, start ); } map( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): Array{ return this.array.map( predicate, thisArg ); } pop(): HTMLElement { return this.array.pop() as HTMLElement; } push( ...items: HTMLElement[] ): DomSelector { items.forEach( (item: HTMLElement) => { this.#set.add( item ); }); return this; } shift(): HTMLElement | undefined { let that = this, element = that.#set.size ? that.array[0] : undefined ; if ( element ){ that.#set.delete( element ); } return element; } slice( start: number, end?: number ): DomSelector { return $d( this.array.slice( start, end ) ); } some( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): boolean{ return this.array.some( predicate, thisArg ); } splice( start: number, deleteCount?: number | undefined ): DomSelector; splice( start: number, deleteCount: number, ...items: HTMLElement[] ): DomSelector { let that = this, arr = that.array ; arr.splice( start, deleteCount, ...items ); return $d( arr ); } values() { return this; } } class TbSelector{ #set: Set = new Set(); public get set(): Set { return this.#set; } public get array(): TbClass[] { return Array.from( this.#set ); } public set set( set: Set ) { this.#set = set; return; } public get length(): number { return this.#set.size; } public set length( value: number ){ //ignore } constructor( selection?: selection, element: HTMLElement = document.body ){ if ( !selection ) { return this; } $d( selection, element ).forEach( (e: TbTarget) => { $t( e ).forEach( (t: TbClass ) => { this.#set.add(t); }); }); } $d( selection?: selection, element: HTMLElement = document.body ): DomSelector { let result = new DomSelector(), set: Set = new Set(), compare: boolean = !!selection || false, compareSelection: Set = new Set() ; if ( compare ){ compareSelection = $d( selection, element ).set; } this.#set.forEach( (e: TbClass) => { if ( !compare || compareSelection.has( (e as any).target ) ) { set.add( (e as any).target ); } }); result.set = set; return result; } children( nameSpace?: string ): TbSelector{ let that: TbSelector = this, set: Set = new Set() ; that.#set.forEach( (e: TbClass) => { e.children( nameSpace ).forEach( ( tb: TbClass ) => { set.add( tb ); }); }); that.#set = set; return that; } descendants( nameSpace?: string ): TbSelector{ let that: TbSelector = this, set: Set = new Set() ; that.#set.forEach( (e: TbClass) => { e.descendants( nameSpace ).forEach( ( tb: TbClass ) => { set.add( tb ); }); }); that.#set = set; return that; } first( nameSpace?: string ): TbSelector{ let that: TbSelector = this, set: Set = new Set() ; that.#set.forEach( (e: TbClass) => { e .parent() .children() .$d() .first() .$t() .forEach( (tb: TbClass) => { set.add( tb ); }); }); that.#set = set; if ( nameSpace ){ that.filter( ( tb: TbClass ) => (tb as any).nameSpace = nameSpace ); } return that; } has( element: TbClass ): boolean{ return this.#set.has( element ); } last( nameSpace?: string ): TbSelector{ let that: TbSelector = this, set: Set = new Set() ; that.#set.forEach( (e: TbClass) => { e .parent() .children() .$d() .last() .$t() .forEach( (tb: TbClass) => { set.add( tb ); }); }); that.#set = set; if ( nameSpace ){ that.filter( ( e: TbClass ) => ( e as LooseObject).nameSpace = nameSpace ); } return that; } next( nameSpace?: string ): TbSelector{ let that: TbSelector = this, set: Set = new Set() ; that.#set.forEach( (e: TbClass) => { let elements: Set = new Set(), element: HTMLElement ; element = (e as any).target; while ( element.nextElementSibling !== null ){ element = element.nextElementSibling as HTMLElement; elements.add( element ); } $t([...elements]) .$d() .first() .$t() .forEach( (tb: TbClass) => { set.add( tb ); }); }); that.#set = set; if ( nameSpace ){ that.filter( ( e: TbClass ) => ( e as LooseObject).nameSpace = nameSpace ); } return that; } ns( nameSpace: string ): TbSelector{ return this.filter( (tb: TbClass) => (tb as any).nameSpace === nameSpace ); } off( name: string, cb: Function ): TbSelector{ let that = this, eventNames = name.indexOf(' ') > -1 ? name.split(' ').filter( (s) => s !== '' ) : [ name ] ; // remove handlers that.$d().array.forEach( (e: HTMLElement) => { eventNames.forEach(function ( n: string ) { // console.log('Tb Selector remove event', `[TB]${n}`, e, cb ); _removeEvent( e, `[TB]${n}`, cb as EventListenerOrEventListenerObject ); }); }); return that; } on( name: string, cb: Function, once: boolean = false ): Function{ let that = this, eventNames = name.indexOf(' ') > -1 ? name.split(' ').filter( (s) => s !== '' ) : [ name ] ; function wrapper(){ wrapper.cb.apply(that, Array.from(arguments) ); }; wrapper.cb = cb; // attach handler that.forEach( ( tb: TbClass) => { eventNames.forEach(function ( n: string ) { _addEvent( (tb as any).target, `[TB]${n}`, (wrapper as EventListenerOrEventListenerObject), false, once ); }); }); return wrapper; } one( name: string, cb: Function ): Function{ let that = this, eventNames = name.indexOf(' ') > -1 ? name.split(' ').filter( (s) => s !== '' ) : [ name ] ; function wrapper(){ wrapper.cb.apply(that, Array.from(arguments) ); }; wrapper.cb = cb; // attach handler that.forEach( ( tb: TbClass) => { eventNames.forEach(function ( n: string ) { _addEvent( (tb as any).target, `[TB]${n}`, (wrapper as EventListenerOrEventListenerObject), false, true ); }); }); return wrapper; } parent( nameSpace?: string ): TbSelector { let that: TbSelector = this, set: Set = new Set() ; that.#set.forEach( (e: TbClass) => { e.parent( nameSpace ).forEach( ( tb: TbClass ) => { set.add( tb ); }); }); that.#set = set; return that; } parents( nameSpace?: string ): TbSelector { let that: TbSelector = this, set: Set = new Set() ; that.#set.forEach( (e: TbClass) => { e.parents( nameSpace ).forEach( ( tb: TbClass ) => { set.add( tb ); }); }); that.#set = set; return that; } prev( nameSpace?: string ): TbSelector{ let that: TbSelector = this, set: Set = new Set() ; that.#set.forEach( (e: TbClass) => { let elements: Set = new Set(), element: HTMLElement ; element = (e as any).target; while ( element.previousElementSibling !== null ){ element = element.previousElementSibling as HTMLElement; elements.add( element ); } $t([...elements]) .$d() .last() .$t() .forEach( (tb: TbClass) => { set.add( tb ); }); }); that.#set = set; if ( nameSpace ){ that.filter( ( e: TbClass ) => ( e as LooseObject).nameSpace = nameSpace ); } return that; } trigger( ev: string, data: any = {}, bubble: string = 'l' ): TbSelector{ let that = this ; that.forEach( ( tb: TbClass ) => { tb.trigger( ev, data, bubble ); }); return that; } /* array method mapping */ at( index: number ): TbSelector { return $t( this.array.at( index ) ) as TbSelector; } concat( items: TbSelector ): TbSelector { let that = this ; items.forEach( ( tb: TbClass) => { that.#set.add( tb ); }); return that; } entries(): IterableIterator<[number, any]> { return this.array.entries(); } every( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): boolean { return this.array.every( predicate, thisArg ); } filter( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): TbSelector { return $t( this.array.filter( predicate, thisArg ) ); } forEach( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): TbSelector { let that = this ; that.array.forEach( predicate, thisArg ); return that; } includes( item: TbClass ): boolean{ return this.array.indexOf( item ) !== -1; } indexOf( item: TbClass, start?: number ): number{ return this.array.indexOf( item, start ); } map( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): Array{ return this.array.map( predicate, thisArg ); } pop(): TbClass { let that = this, arr = that.array, result = arr.pop() ; that.#set = new Set( arr ); return result as TbClass; } push( item: TbClass ): TbSelector { let that = this ; that.#set.add( item ); return that; } shift(): TbClass | undefined { let that = this, first = this.array.shift() as TbClass ; that.#set.delete( first ); return first; } slice( start: number, end?: number ): TbSelector { return $t( this.array.slice( start, end ) ); } some( predicate: (value: any, index: number, array: any[]) => unknown, thisArg?: any ): boolean{ return this.array.some( predicate, thisArg ); } splice( start: number, deleteCount?: number | undefined ): TbSelector; splice( start: number, deleteCount: number, ...items: never[] ): TbSelector { let that = this, arr = that.array, result = arr.splice( start, deleteCount, ...items ) ; return $t( result ); } } /* functions */ export const $d = ( selection: selection, element: any = document ): DomSelector => { let set = new Set() ; if ( selection instanceof Array ){ selection.forEach( value => { $d( value, element ).forEach( n => { set.add( n ); }); }); return new DomSelector( [...set] ); } else { return new DomSelector( selection, element ); } } export const $t = ( selection?: selection, element: any = document ): TbSelector => { return $d( selection, element ).$t(); } export const addTbInstanceToTarget = ( target: LooseObject, definitionClass: { new(...args: any[]): any; }, data: any = {} ) => { //console.log( 'createTbInstance', target, definitionClass, data ); if ( !target ){ throw new Error('no target HTML element given'); } if ( !definitionClass ){ throw new Error('no definition class given'); } const instance = new definitionClass( target, data ); target._tb = target._tb || {}; target._tb[definitionClass.name] = instance; //console.log( 'in cTb', target, data ) let nameSpace = definitionClass.name || target.tagName.toLowerCase().split('-').join('.'); Object.defineProperty( instance, 'nameSpace', { value: nameSpace, writable: false, configurable: false } ); return instance; } export const createCustomElement = ( tagName: string, definitionClass: { new(...args: any[]): any; }, observedAttributes: String[] = [], userDefinedBuiltinObject: string = 'x' ) => { //console.log( 'createCustomElement', tagName, definitionClass, observedAttributes, userDefinedBuiltinObject ); // create custon element if( customElements.get( tagName ) === undefined ){ let wantUBE = userDefinedBuiltinObject === 'x' ? false : true, extendClass: any = HTMLElement ; //console.log( 'wantUBE', wantUBE ); // parameter sanitation if ( !userDefinedBuiltinObject ){ if (tagName.indexOf('-') === -1){ throw('createCustomElement: pTagName must be a string containing at least one hyphen: "-" '); } } else { let e = document.createElement(userDefinedBuiltinObject); if ( e instanceof HTMLUnknownElement ){ //console.log(`[${userDefinedBuiltinObject}] is an unknown HTML element: default to HTMLElement`); } else { extendClass = (window as LooseObject)[ document.createElement(userDefinedBuiltinObject).constructor.name ]; } } // auto-define autonomous custom element customElements.define( tagName, class extends extendClass{ constructor(data:any){ super(); addTbInstanceToTarget( this, definitionClass, data ); } static get observedAttributes(){ return observedAttributes; } connectedCallback(){ let that: unknown = this ; $d( that as HTMLElement ).trigger( 'connected' ); } disconnectedCallback(){ let that: unknown = this ; $d( that as HTMLElement ).trigger( 'disconnected'); } adoptedCallback( ev: Event ){ let that: unknown = this ; $d( that as HTMLElement ).trigger( 'adopted'); } attributeChangedCallback( name: string, oldValue: any, newValue: any ){ let that: unknown = this ; $d( that as HTMLElement ).trigger( 'attributeChanged', { name: name, oldValue: oldValue, newValue: newValue } ); } } as any, wantUBE ? ( { extends: userDefinedBuiltinObject.toLocaleLowerCase() } as ElementDefinitionOptions ) : undefined ); } } export const getCustomElementConstructor = ( tagName: string ): CustomElementConstructor | undefined => { return customElements.get( tagName ); };