/** Datei: plugins/rest/assets/js/core/templates.js **/ /** * /arunox/core/templates.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX ??= {}; ARUNOX.Templates = { _type : 'templates', _componentData : { content : undefined, config : undefined, data : undefined, htmlTag : 'div', template : null, container : null, // DOM-Object root : null, // DOM-Object parent : null, // ARX-Object children : [], // ARX-Object nodes : () => Object.create(null), hooks : () => Object.create(null), keepOnReplace : () => ({ classes : [], // CSS-Klassen die erhalten bleiben sollen attributes : [], // Attribute die erhalten bleiben sollen datasets : [], // Data-Attribute die erhalten bleiben sollen structure : null, // DOM-Struktur die erhalten werden soll }), takeOnReplace : () => ({ classes : [], // CSS-Klassen die übernommen werden sollen attributes : [], // Attribute die übernommen werden sollen datasets : [], // Data-Attribute die übernommen werden sollen }), mounted : false, destroyed : false, }, _componentProto : { apply : function( hook, ...args ) { ARUNOX.Hooks.apply.call( this, hook, ...args ); if( this.extends ) ARUNOX.Hooks.apply.call( this, `${this.extends}->${hook}`, ...args ); ARUNOX.Hooks.apply.call( this, `${this.template}->${hook}`, ...args ); // if( this.nodeName ) console.log( 'apply', `${this.template}->${hook}`, this.nodeName ); }, filter : function( hook, value, ...args ) { value = ARUNOX.Hooks.filter.call( this, hook, value, ...args ); if( this.extends ) value = ARUNOX.Hooks.filter.call( this, `${this.extends}->${hook}`, value, ...args ); // if( this.nodeName ) console.log( 'filter', `${this.template}->${hook}`, this.nodeName ); return ARUNOX.Hooks.filter.call( this, `${this.template}->${hook}`, value, ...args ); }, render : function( templateName, config, data, container ) { return ARUNOX.Templates.render( templateName, config, data, container || this.root, this ); }, replace : function( template, config, data ) { const newCtx = typeof template === 'object' ? template : ARUNOX.Templates.render( template, config, data, null, this.parent ); const newRoot = newCtx && newCtx.root instanceof HTMLElement ? newCtx.root : null; if( !newRoot ) return this.root; const preserveData = this._collectPreservables( this, newCtx ); if( this.mounted && this.root instanceof HTMLElement ) { this.root.replaceWith( newRoot ); } this._applyPreserved( newRoot, preserveData ); this.root = newRoot; this.nodes = newCtx.nodes || Object.create(null); newCtx.mounted = true; return this; }, node : function( nodeName, nodeConfig = {}, placement = 'append', refElement = null ) { if( !this.nodes ) this.nodes = {}; if( !this.nodes[ nodeName ] ) { nodeConfig = typeof nodeConfig === 'object' ? nodeConfig : { textNode: nodeConfig }; nodeConfig = Object.assign( { htmlTag : 'div', datasets : { arx : { node : nodeName, }, }, }, nodeConfig ); nodeConfig = this.filter(`node`, nodeConfig ); nodeConfig = this.filter(`node${nodeName.toUpperCaseFirst()}`, nodeConfig ); refElement = refElement instanceof HTMLElement ? refElement : refElement?.root || null; const nodeCtx = ARUNOX.Templates._createContext( nodeName, nodeConfig, this, this.root, { htmlTag: nodeConfig.htmlTag }); const ctxRender = nodeCtx.render; const ctxReplace = nodeCtx.replace; nodeCtx.render = function( template, config, data, container ) { if( !this.root ) { const tmp = ARUNOX.Templates.render( this.config.htmlTag, this.config, {}, this.parent?.root, this.parent ); this.root = tmp.root; this.nodes = tmp.nodes; this.nodeName = nodeName; this.root.ARX = this; if( placement === 'prepend' ) this.parent.root.prepend( this.root ); else if( placement === 'append' ) this.parent.root.append( this.root ); else if( placement === 'before' && refElement ) refElement.before( this.root ); else if( placement === 'after' && refElement ) refElement.after( this.root ); this.mounted = true; } return ctxRender.call( this, template, config, data, container || this.root ) || this; }; nodeCtx.replace = function( template, config, data ) { if( !this.root ) this.render(); return ctxReplace.call( this, template, config, data ); } nodeCtx.nodeName = nodeName; // console.log(nodeCtx); this.nodes[ nodeName ] = nodeCtx; } return this.nodes[ nodeName ]; }, closest : function( template ) { if( this.template == template || this.extends == template ) { return this; } return this.parent.closest( template ); }, dataset : function( dataset, namespace = 'arx' ) { dataset = typeof dataset === 'string' ? { [dataset] : true } : dataset; for( const [ key, value ] of Object.entries( dataset ) ) { this.root.setAttribute( `data-${namespace}-${key}`, Array.isArray( value ) ? value.join(',') : value ); } }, setUUID : function( hashid, context = '') { this.dataset({ 'uuid': btoa( [ this.template, hashid, context ].filter( Boolean ).join(':') ).replace( /[^a-zA-Z0-9]/g, '' ) }); }, _collectPreservables : function( oldCtx, newCtx ) { const preserveData = { classes : new Set(), attributes : new Map(), datasets : new Map(), structure : null }; if( oldCtx.keepOnReplace ) this._collectPreserveData( oldCtx.root, oldCtx.keepOnReplace, preserveData ); if( newCtx.takeOnReplace ) this._collectPreserveData( oldCtx.root, newCtx.takeOnReplace, preserveData ); return preserveData; }, _collectPreserveData : function( element, rules, preserveData ) { if( !element ) return; // CSS Classes if( rules.classes ) { rules.classes.forEach( pattern => { const matchingClasses = this._matchClasses( element, pattern ); matchingClasses.forEach( cls => preserveData.classes.add( cls ) ); }); } // Attributes if( rules.attributes ) { rules.attributes.forEach( pattern => { const matchingAttrs = this._matchAttributes( element, pattern ); matchingAttrs.forEach( ([name, value]) => { preserveData.attributes.set( name, value ); }); }); } // Datasets if( rules.datasets ) { rules.datasets.forEach( pattern => { const matchingData = this._matchDatasets( element, pattern ); matchingData.forEach( ([name, value]) => { preserveData.datasets.set( name, value ); }); }); } // Structure (komplexerer Fall - erstmal placeholder) if( rules.structure ) { preserveData.structure = rules.structure; } }, _applyPreserved : function( newRoot, preserveData ) { preserveData.classes.forEach( cls => { newRoot.classList.add( cls ); }); preserveData.attributes.forEach( (value, name) => { newRoot.setAttribute( name, value ); }); preserveData.datasets.forEach( (value, name) => { newRoot.setAttribute( `data-${name}`, value ); }); // Structure handling würde hier kommen (komplexer) if( preserveData.structure ) { // TODO: Implementierung für Structure-Preservation console.log( 'Structure preservation not yet implemented:', preserveData.structure ); } }, _matchClasses : function( element, pattern ) { const classes = Array.from( element.classList ); if( pattern.includes('*') ) { // Wildcard matching: "lcars-*" matches "lcars-panel", "lcars-frame", etc. const regex = new RegExp( '^' + pattern.replace(/\*/g, '.*') + '$' ); return classes.filter( cls => regex.test( cls ) ); } else { // Exact match return classes.includes( pattern ) ? [pattern] : []; } }, _matchAttributes : function( element, pattern ) { const attributes = Array.from( element.attributes ); const matching = []; attributes.forEach( attr => { if( pattern.includes('*') ) { const regex = new RegExp( '^' + pattern.replace(/\*/g, '.*') + '$' ); if( regex.test( attr.name ) ) { matching.push( [attr.name, attr.value] ); } } else if( attr.name === pattern ) { matching.push( [attr.name, attr.value] ); } }); return matching; }, _matchDatasets : function( element, pattern ) { const matching = []; // Durchsuche alle data-* Attribute Array.from( element.attributes ).forEach( attr => { if( attr.name.startsWith('data-') ) { const dataName = attr.name.substring(5); // Remove 'data-' prefix if( pattern.includes('*') ) { const regex = new RegExp( '^' + pattern.replace(/\*/g, '.*') + '$' ); if( regex.test( dataName ) ) { matching.push( [dataName, attr.value] ); } } else if( dataName === pattern ) { matching.push( [dataName, attr.value] ); } } }); return matching; }, }, register : function( template, callback, priority ) { return ARUNOX.Registry.register( this._type, template, callback, priority ); }, once : function( template, callback, priority = 10 ) { return ARUNOX.Registry.once( this._type, template, callback, priority ); }, remove : function( template, callback ) { return ARUNOX.Registry.remove( this._type , template, callback ); }, resolve : function( template ) { const mergeHooks = (baseHooks={}, childHooks={}) => { const out = {}; for( const [ key, fn ] of Object.entries( baseHooks ) ) { out[ key ] = Array.isArray( fn ) ? fn : [ fn ]; } for( const [ key, fn ] of Object.entries( childHooks ) ) { const hooks = Array.isArray( fn ) ? fn : [ fn ]; out[ key ] = out[ key ] ? [ ...out[ key ], ...hooks ] : hooks; } return out; }; const extendsSupport = function( childName ) { const base = ARUNOX.Templates.resolve( this.extends ); const merged = { ...base, ...this }; merged.hooks = mergeHooks( base.hooks, this.hooks ); // merged.extends = base; // delete merged.extends; return merged; }; let def = ARUNOX.Registry.resolve( this._type, template, extendsSupport ); if( !def ) { const source = ARUNOX.Loader.findSource(template); if( source ) { if( ARUNOX.Loader.loadSync( source.path ) ) { // Hooks laden (nur registrierte) if( source.source.hooks.has( template ) ) { ARUNOX.Loader.loadSync( source.source.hooks.get( template ) ); } // Plugin-Hooks for( const [ name, plugin ] of ARUNOX.Loader._sources.plugins ) { if( plugin.hooks.has( template ) ) { ARUNOX.Loader.loadSync( plugin.hooks.get( template ) ); } } // Theme-Hooks if( ARUNOX.Loader._sources.theme.hooks.has( template ) ) { ARUNOX.Loader.loadSync( ARUNOX.Loader._sources.theme.hooks.get( template ) ); } def = ARUNOX.Registry.resolve( this._type, template, extendsSupport ); } } } if( !def ) { if( template.match(/\//) ) return false; if( !( document.createElement( template ) instanceof HTMLUnknownElement ) ) { return { htmlTag: template }; } return false } if( typeof def === 'function' ) return { builder: def }; if( typeof def === 'object') { if( !def.htmlTag ) { if( !( document.createElement( template ) instanceof HTMLUnknownElement ) ) { return { ...def, htmlTag: template }; } throw new Error(`[ARUNOX.Templates] Invalid definition for: ${template}`); } if( def.hooks ) { // Normalisiere alle hooks zu Arrays for (const [key, fn] of Object.entries(def.hooks)) { if (!Array.isArray(fn)) { def.hooks[key] = [fn]; } } } return def; } throw new Error(`[Templates] Invalid definition for: ${template}`); }, exists : function( template ) { return ARUNOX.Registry.exists( this._type, template ); }, all : function( template ) { return [ ...ARUNOX.Registry.all( this._type, template ) ].reverse(); }, config : function( template, config, data ) { return { template, config, data }; }, render : function( template, config, data, container, parentCtx ) { if( !template ) return; const definition = this.resolve( template ); if( !definition ) return; config = Object.assign( {}, config ); const ctx = this._createContext( template, config, parentCtx || null, container, definition ); this._registerHooks( template, ctx ); config = ctx.config = ctx.filter( 'config', config, data ); data = ctx.data = ctx.filter( 'data', data, config ); const doRebuild = ctx.filter( 'shouldRebuild', null, config, data ); const doRender = ctx.filter( 'shouldRender', true, config, data ); if( doRebuild ) { const rebuildCtx = doRebuild?.ARX ? doRebuild.ARX : doRebuild; if( rebuildCtx ) { rebuildCtx.apply( 'beforeRebuild', config, data ); this._applyConfig( rebuildCtx, config ); // Config kann sich geändert haben this._renderContent( rebuildCtx, config, data ); // Neuer Content queueMicrotask( ()=> { this._collectNodes( rebuildCtx ); rebuildCtx.apply( 'afterRebuild', config, data ); }); } return rebuildCtx; } if( doRender ) { ctx.apply( 'beforeRender', config, data ); this._buildRoot( definition, ctx, config, data ); this._collectNodes( ctx ); this._renderContent( ctx, config, data ); this._applyConfig( ctx, config ); ctx.root = ctx.filter( 'wrapper', ctx.root ); this._mount( ctx, container ); queueMicrotask( ()=>{ this._collectNodes( ctx ); ctx.apply( 'afterRender', config, data ); }); return ctx; } return ctx; }, _registerHooks : function( template, ctx ) { for( const [ hookKey, hookDef ] of Object.entries( ctx.hooks ) ) { const hookArray = Array.isArray( hookDef ) ? hookDef : [ hookDef ]; hookArray.forEach( def => { // Parse Hook-Definition const hooks = this._parseHookDefinition( hookKey, def, template ); // Registriere alle geparsten Hooks hooks.forEach( hookConfig => { const originalCallback = hookConfig.callback; const wrappedCallback = function( ...args ) { this.owner = ctx; return originalCallback.call( this, ...args ); }; wrappedCallback._originalCallback = originalCallback; if( !ARUNOX.Hooks.exists( `${hookConfig.template}->${hookConfig.hook}`, originalCallback ) ) { if( hookConfig.once ) { ARUNOX.Hooks.once( `${hookConfig.template}->${hookConfig.hook}`, wrappedCallback, hookConfig.priority ); } else { ARUNOX.Hooks.register( `${hookConfig.template}->${hookConfig.hook}`, wrappedCallback, hookConfig.priority ); } } }); }); } }, _parseHookDefinition : function( hookKey, hookDef, currentTemplate ) { // Function → Standard Hook für eigenes Template if( typeof hookDef === 'function' ) { return [{ template : currentTemplate, hook : hookKey, callback : hookDef, priority : 9, once : false }]; } // Object → Vollständige Definition let templates, hookName; // Templates ermitteln if( hookDef.templates ) { templates = hookDef.templates; } else if( hookDef.template ) { templates = [ hookDef.template ]; } else if( hookKey.includes('->') ) { templates = [ hookKey.split('->')[0] ]; } else { templates = [ currentTemplate ]; } // Hook-Name ermitteln if( hookDef.hook ) { hookName = hookDef.hook; } else if( hookKey.includes('->') ) { hookName = hookKey.split('->')[1]; } else { hookName = hookKey; } // Für jedes Template eine Hook-Config erstellen return templates.map( templateName => ({ template : templateName, hook : hookName, callback : hookDef.callback, priority : hookDef.priority || 9, once : hookDef.once || false })); }, _createContext : function( template, config, parent = null, container = null, definition ) { const prototype = Object.create( this._componentProto ); const context = Object.assign( prototype, definition ); for( const key in this._componentData ) { if( typeof context[ key ] !== 'undefined' ) continue; const val = this._componentData[ key ]; context[ key ] = typeof val === 'function' ? val() : val; } if( definition.keepOnReplace || config.keepOnReplace ) { context.keepOnReplace = this._mergeReplaceRules( definition.keepOnReplace || {}, config.keepOnReplace || {} ); } if( definition.takeOnReplace || config.takeOnReplace ) { context.takeOnReplace = this._mergeReplaceRules( definition.takeOnReplace || {}, config.takeOnReplace || {} ); } context.extends = definition.extends; context.template = template; context.parent = parent; context.container = container || null; context.config = config || {}; // console.log( definition ); // console.log( template, context ); return context; }, _mergeReplaceRules : function( templateRules, configRules ) { return { classes: [ ...(templateRules.classes || []), ...(configRules.classes || []) ], attributes: [ ...(templateRules.attributes || []), ...(configRules.attributes || []) ], datasets: [ ...(templateRules.datasets || []), ...(configRules.datasets || []) ], structure: configRules.structure || templateRules.structure || null }; }, _createElement : function( tagResolver, config, data, ctx ) { // Tag ermitteln (String oder Function(config, data, ctx) -> String) const tag = ( typeof tagResolver === 'function' ) ? tagResolver( config, data, ctx ) : ( tagResolver || 'div' ); return document.createElement( String(tag || 'div') ); }, _buildRoot : function( definition, ctx, config, data ) { ctx.apply( 'buildRootBefore' ); // 1) Root vorab erstellen (Definition darf optional `tag` liefern: string | fn(cfg,data,ctx) -> string) if( !ctx.root ) { ctx.root = this._createElement( definition.htmlTag, config, data, ctx ); } // 2) Definition ausführen – nur wenn render existiert (permissive) if( typeof definition.builder === 'function' ) { const out = definition.builder.call( ctx, config, data ); if( out instanceof HTMLElement ) { ctx.root = out; } else if( typeof out === 'string' ) { const tpl = document.createElement('template'); tpl.innerHTML = out.trim(); ctx.root = tpl.content.firstElementChild || ctx.root; } } // 3) Inputs persistieren ctx.config = config; ctx.data = data; ctx.root.ARX = ctx; ctx.apply( 'buildRootAfter' ); return ctx.root; }, _collectNodes : function( ctx ) { ctx.apply( 'collectNodesBefore' ); if( !ctx.root ) return; // console.log( ctx.root.querySelectorAll('[data-arx-node]') ); ctx.root.querySelectorAll('[data-arx-node]').forEach( element => { const nodeName = element.getAttribute('data-arx-node'); if( nodeName && element.ARX && !ctx.nodes[ nodeName ] ) { element.ARX.nodeName = nodeName; ctx.nodes[ nodeName ] = element.ARX; } }); ctx.apply( 'collectNodesAfter' ); }, _applyConfig : function( ctx, config ) { if( !ctx.root || !config ) return; ctx.apply( 'applyConfigBefore', ctx.root, config ); if( config.classes ) { if( typeof config.classes === 'string' ) ctx.root.classList.add( ...config.classes.split(/\s+/).filter( Boolean ) ); else if( Array.isArray( config.classes ) ) ctx.root.classList.add( ...config.classes ); else if( typeof config.classes === 'object' ) { const { add = [], remove = [] } = config.classes; if( add.length ) ctx.root.classList.add( ...add ); if( remove.length ) ctx.root.classList.remove( ...remove ); } } if( typeof config === 'object' ) { for( const key of Object.keys( config ) ) { const match = key.match( /^on([a-z].*)/i ); if( !match ) continue; config.events ??= {}; config.events[ match[1].toLowerCase() ] = config[ key ]; } } if( typeof config.events === 'object' ) { for( const [ name, callback ] of Object.entries( config.events ) ) ctx.root.addEventListener( name, callback ); } if( typeof config.styles === 'object' ) { for (const [k, v] of Object.entries( config.styles )) ctx.root.style[k] = v; } if( typeof config.nodeName === 'string' ) { config.datasets.arx.node = config.nodeName; if( ctx.parent ) { ctx.parent.nodes[ config.nodeName ] = ctx; } } if( typeof config.datasets === 'object' ) { const applyDataset = ( obj, prefix = '' ) => { for( const [ k, v ] of Object.entries(obj)) { const key = prefix ? `${prefix}-${k}` : k; if( v && typeof v === 'object' && !Array.isArray( v ) ) { applyDataset( v, key ); // rekursiv } else { ctx.root.setAttribute( `data-${key}`, String( v ) ); } } }; applyDataset( config.datasets ); } if( typeof config.attributes === 'object' ) { for (const [k, v] of Object.entries( config.attributes )) { if (v === false || v == null) ctx.root.removeAttribute(k); else ctx.root.setAttribute(k, String(v)); } } if( typeof config.apply === 'object' ) ctx.apply( ...config.apply ); if( typeof config.innerHTML === 'string' ) ctx.root.innerHTML = config.innerHTML; if( typeof config.innerText === 'string' ) ctx.root.innerText = config.innerText; ctx.apply( 'applyConfigAfter', ctx.root, config ); }, _renderContent : function( ctx, config, data ) { const content = ctx.filter( 'content', config?.content || [], config, data ); ctx.apply( 'beforeContent', config, data ); if( typeof content === 'string' ) { ctx.root.innerText = content; } else if( Array.isArray( content ) ) { content.filter( Boolean ).forEach( item => { if( item.history ) { if( location.pathname != item.history ) { window.history.pushState({}, "", item.history ); } } ctx.render( item.template || item.generator, item.config, item.data ); }); } else if( typeof content === 'object' ) { if( content.history ) { if( location.pathname != content.history ) { window.history.pushState({}, "", content.history ); } } ctx.render( content.template || content.generator, content.config, content.data ); } ctx.apply( 'afterContent', config, data ); }, _mount : function( ctx, container ) { if( !ctx || !ctx.root ) return ctx; ctx.apply( 'mount' ); // Ziel ermitteln: explizites container-Arg → sonst ctx.container let target = ( container !== undefined ? container : ctx.container ); if( !target ) return ctx; // Selector-String erlauben if( typeof target === 'string' ) target = document.querySelector( target ); // ARX als Container erlauben else if( target && typeof target === 'object' && target.root instanceof HTMLElement ) target = target.root; if( !( target instanceof HTMLElement ) ) return ctx; target.appendChild( ctx.root ); ctx.container = target; ctx.mounted = true; ctx.apply( 'mounted' ); return ctx; }, }; /** Datei: plugins/rest/assets/js/core/utilities.js **/ /** * /arunox/core/utilities.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX ??= {}; ARUNOX.Utils = { device: { get isRotatable() { return window.orientation > -1; }, get isMobile() { return this.isRotatable || screen.availHeight <= 800 || screen.availWidth <= 800; }, get screenSize() { return { width: screen.availWidth, height: screen.availHeight }; } }, // Weitere Utils können hier hinzugefügt werden dom: { ready(callback) { if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', callback); } else { callback(); } } } }; /** Datei: plugins/rest/assets/js/core/prototypes.js **/ /** * /arunox/core/prototypes.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ Object.defineProperty( DOMTokenList.prototype, 'removeRexEx', { value: function( regex ) { if( !( regex instanceof RegExp ) ) { throw new TypeError('DOMTokenList.removeRegex erwartet ein RegExp-Objekt.'); } const toRemove = [ ...this ].filter( cls => regex.test( cls ) ); if( toRemove.length ) this.remove( ...toRemove ); }, enumerable : false, writable : true, configurable: true }); Object.defineProperty( DOMTokenList.prototype, 'match', { value: function( regex, returnMatchIndex = 1 ) { if( !( regex instanceof RegExp ) ) { throw new TypeError('DOMTokenList.match erwartet ein RegExp-Objekt.'); } return [ ...this ] .map( cls => regex.exec( cls ) ) .filter( Boolean ) .map( m => m[ returnMatchIndex ] ); }, enumerable : false, writable : true, configurable: true }); Object.defineProperty( Object.prototype, 'assignRecursive', { value: function( target, ...sources ) { for( const source of sources ) { for( const [ key, item ] of Object.entries( source ) ) { if( item === null ) { delete target[ key ]; continue; } if( Array.isArray( item ) ) { target[ key ] = [].concat( item, target[ key ] || [] ); continue; } if( typeof item === 'object' && item !== null ) { if( typeof target[ key ] !== 'object' || target[ key ] === null ) { target[ key ] = target[ key ] === undefined ? {} : { [key] : target[ key ] }; } target[ key ] = Object.assignRecursive( target[ key ] || {}, item ); continue; } target[ key ] = item; } } return target; }, enumerable : false, writable : true, configurable: true }); Object.defineProperty( String.prototype, 'toUpperCaseFirst', { value: function() { return String( this ).charAt(0).toUpperCase() + String( this ).slice(1); }, enumerable : false, writable : true, configurable: true }); /** Datei: plugins/rest/assets/js/core/loader.js **/ /** * /arunox/core/loader.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX ??= {}; ARUNOX.Loader = { _loaded : new Set(), _loading : new Map(), _sources : { core : { basePath: '/plugins/rest/assets/js', templates: new Map(), hooks: new Map() }, theme : { basePath: '/themes/lcars/assets/js', templates: new Map(), hooks: new Map() }, plugins : new Map() }, load : async function( path, options = {} ) { const { force = false, cache = true, type = 'module' } = options; if( !force && this._loaded.has( path ) ) return Promise.resolve(); // Already loaded? if( this._loading.has( path ) ) return this._loading.get( path ); // Currently loading? const loadPromise = this._loadModule( path, type, cache ); this._loading.set( path, loadPromise ); try { await loadPromise; this._loaded.add( path ); } catch( error ) { console.error( `Failed to load module: ${path}`, error ); throw error; } finally { this._loading.delete( path ); } return loadPromise; }, loadAll : async function( modules ) { const promises = modules.map( module => { if( typeof module === 'string' ) { return this.load( module ); } return this.load( module.path, module.options || {} ); }); return Promise.all( promises ); }, loadIf : async function( condition, path, options = {} ) { if( typeof condition === 'function' ) { condition = condition(); } if( condition ) return this.load( path, options ); return Promise.resolve(); }, loadSync : function( path ) { if( this._loaded.has( path ) || this._loaded.has(path + ':404' ) ) return this._loaded.has( path ); try { const xhr = new XMLHttpRequest(); xhr.open('GET', path, false); // SYNCHRON! xhr.send(); if( xhr.status === 200 ) { // Extrahiere und entferne import Statements const importRegex = /import\s+['"](.*?)['"]\s*;?/g; const imports = []; let code = xhr.responseText; let match; while ((match = importRegex.exec(code)) !== null) { imports.push(match[1]); } // Entferne imports aus dem Code code = code.replace(importRegex, ''); // Lade alle imports zuerst (rekursiv) imports.forEach(importPath => { this.loadSync(importPath); }); const script = document.createElement('script'); // script.type = 'module'; script.textContent = code; document.head.appendChild( script ); document.head.removeChild( script ); this._loaded.add( path ); return true; } if( xhr.status === 404 ) { // 404 merken, damit wir es nicht nochmal versuchen this._loaded.add( path + ':404' ); return false; } return false; } catch( e ) { return false; } }, isLoaded : function( path ) { return this._loaded.has( path ); }, unload : function( path ) { this._loaded.delete( path ); }, registerSource : function( type, name, config ) { if( type === 'core' ) { this._sources.core.basePath = config.basePath || this._sources.core.basePath; this._sources.core.templates = this._parseTemplates( config.templates, this._sources.core.basePath ); this._sources.core.hooks = this._parseHooks( config.hooks, this._sources.core.basePath ); } else if( type === 'theme' ) { this._sources.theme.basePath = config.basePath || this._sources.theme.basePath; this._sources.theme.templates = this._parseTemplates( config.templates, this._sources.theme.basePath ); this._sources.theme.hooks = this._parseHooks( config.hooks, this._sources.theme.basePath ); } else if( type === 'plugin' ) { this._sources.plugins.set( name, { basePath : config.basePath, templates : this._parseTemplates( config.templates, config.basePath ), hooks : this._parseHooks( config.hooks, config.basePath ) }); } }, findSource : function( template ) { // Theme (höchste Priorität) if( this._sources.theme.templates.has( template ) ) { return { type : 'theme', path : this._sources.theme.templates.get( template ), source : this._sources.theme }; } // Plugins for( const [ name, plugin ] of this._sources.plugins ) { if( plugin.templates.has( template ) ) { return { type : 'plugin', name, path : plugin.templates.get( template ), source : plugin }; } } // Core (Fallback) if( this._sources.core.templates.has( template ) ) { return { type : 'core', path : this._sources.core.templates.get( template), source : this._sources.core }; } return null; }, _loadModule : function( path, type, cache ) { return new Promise(( resolve, reject ) => { const script = document.createElement('script'); script.onload = () => { document.head.removeChild( script ); resolve(); }; script.onerror = () => { document.head.removeChild( script ); reject( new Error( `Failed to load script: ${path}` ) ); }; if( type === 'module' ) { script.type = 'module'; } if( !cache ) { const url = new URL( path, window.location.origin ); url.searchParams.set( '_t', Date.now() ); script.src = url.toString(); } else { script.src = path; } document.head.appendChild( script ); }); }, _parseTemplates : function( templates, basePath ) { if( Array.isArray( templates ) ) { // Einfache Liste → nutzt basePath const map = new Map(); templates.forEach( t => map.set( t, `${basePath}/components/${t}.js` ) ); return map; } else if( typeof templates === 'object' ) { // Explizite Pfade return new Map( Object.entries( templates ) ); } return new Map(); }, _parseHooks : function( hooks, basePath ) { if( Array.isArray( hooks ) ) { const map = new Map(); hooks.forEach( h => map.set( h, `${basePath}/hooks/${h}.js` ) ); return map; } else if( typeof hooks === 'object' ) { return new Map( Object.entries( hooks ) ); } return new Map(); }, }; /** Datei: plugins/rest/assets/js/core/hooks.js **/ /** * /arunox/core/hooks.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX = window.ARUNOX || {}; ARUNOX.Hooks = { _type : 'hooks', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority ); }, once : function( name, callback, priority = 10 ) { return ARUNOX.Registry.once( this._type, name, callback, priority ); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type , name, callback ); }, exists : function( name, callback ) { return ARUNOX.Registry.exists( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, all : function( name ) { return [ ...ARUNOX.Registry.all( this._type, name ) ].reverse(); }, filter : function( name, value, ...args ) { let hookNames = Array.isArray( name ) ? name : name.split(' '); let callbacks = hookNames.map( n => ARUNOX.Hooks.all( n ) ).flat(2).filter(Boolean); let stopped = false; let context = { stop : function() { stopped = true; }, instance : this }; // console.log( hookNames ); if( this.hooks?.hooks ) { callbacks = callbacks.concat( hookNames.map( n => this.hooks.hooks[ n ] ).filter(Boolean).flat() ); } // if( callbacks.length ) console.log( callbacks, context.instance ); for( const fn of callbacks ) { if( stopped ) break; if( fn.ARX?.once && fn.ARX?.done ) continue; value = fn.call( context, value, ...args ); if( fn.ARX?.once ) fn.ARX.done = true; } return value; }, apply : function( name, ...args ) { let hookNames = Array.isArray( name ) ? name : name.split(' '); let callbacks = hookNames.map( n => ARUNOX.Hooks.all( n ) ).flat(2).filter(Boolean); let stopped = false; let context = { stop : function() { stopped = true; }, instance : this }; // console.log( hookNames ); if( this.hooks?.hooks ) { callbacks = callbacks.concat( hookNames.map( n => this.hooks.hooks[ n ] ).filter(Boolean).flat() ); } for( const fn of callbacks ) { if( stopped ) break; if( fn.ARX?.once && fn.ARX?.done ) continue; fn.call( context, ...args ); if( fn.ARX?.once ) fn.ARX.done = true; } }, Instance : class { constructor( owner, hooks ) { this.owner = owner; this.hooks = hooks || {}; } _hooks( name ) { return [...new Set([ name, this.owner.constructor?.extends ? `${this.owner.constructor.extends.type}->${name}` : null, `${this.owner.type}->${name}`, this.owner.constructor?.extends ? `${this.owner.constructor.extends.hook}->${name}` : null, `${this.owner.hook}->${name}`, ].filter(Boolean))]; } register( name, callback ) { this.hooks[ name ] ??= []; this.hooks[ name ].push( callback ); return this; } exists( name ) { return ARUNOX.Hooks.exists( name ); } filter( name, value, ...args ) { return ARUNOX.Hooks.filter.call( this.owner, this._hooks( name ), value, ...args ); } apply( name, ...args ) { ARUNOX.Hooks.apply.call( this.owner, this._hooks( name ), ...args ); } } }; /** Datei: plugins/rest/assets/js/core/router.js **/ /** * /arunox/core/router.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX = ARUNOX || {}; ARUNOX.Router = new ( class { constructor() { this._routes = {}; this._cache = new Map(); this._lastCacheKey = null; this._isReloaded = false; this._CACHE_TIMEOUT = 500; } async handle( config, sendData = {}, parseData = {}, mode = 'data' ) { const debug = false; if( !config ) { console.warn("⚠️ Ungültige Konfiguration für handleAction()."); return; } if( !config.resolved ) { config = this.parseRoute( config, parseData, true, mode ); } if( debug ) console.groupCollapsed( config.resolved ); // console.log( config ); let response = { click : [], route : [], dialog : [], updater : [], wrapper : [], template : [], generator : [], javascript : [], }; for( const item of await this.fetch( config, sendData, {}, mode ) ) { response[ item.type ].push( item ); } // console.log( config, response ); ARUNOX.Router.history = config.history; if( response.dialog ) { for( const item of response.dialog ) { let dialog = null; if( item.name === '#ALL' ) { document.querySelectorAll('dialog').forEach( dialog => dialog.closeDialog() ); continue; } if( dialog = document.querySelector(`dialog[data-id="${item.name}"]`) ) { dialog.closeDialog(); continue; } if( dialog = document.querySelector('dialog') ) { dialog.closeDialog(); } } } if( response.route.length ) { for( const item of response.route ) { await ARUNOX.Router.handle( item, {}, {}, 'rest' ); } ARUNOX.Router.history = config.history; } if( response.template.length && !response.route.length ) { document.body.innerHTML = ''; ARUNOX.Router.template = ARUNOX.Templates.render( response.template[0].template, response.template[0], Object.assign( {}, config ), document.body ); } // if( response.template.length && !response.route.length ) { // document.body.innerHTML = ''; // template = ARUNOX.Theme.template.render( response.template[0].template, Object.assign( {}, config ), response.template[0], document.body ); // } if( response.wrapper.length && !response.route.length ) { for( const item of response.wrapper ) { ARUNOX.Router.template.nodes.content.render( item.generator, item.config, item.list || item.data || {} ); if( item.history ) { if( location.pathname != item.history ) { window.history.pushState({}, "", item.history ); } } } } // if( response.generator.length && !response.route.length ) { // for( const item of response.generator ) { // ARUNOX.Theme.generator.render( item.generator, config, item, template ); // } // } // if( response.updater.length && !response.route.length ) { for( const item of response.updater ) { document.querySelectorAll(`[data-arx-uuid="${item.updater}"]`).forEach( element => element.ARX.apply( 'update', item.data, item.status ) ); } } // // if( response.click.length ) { // for( const item of response.click ) { // const element = document.querySelector(`[data-uuid="${item.parent}"] ${item.selector ?? ''}` ); // // element.click(); // } // } if( response.javascript.length ) { for( const item of response.javascript ) { eval( item.script ); } } if( debug ) console.groupEnd(); ARUNOX.Router.history = config.history; } async routes() { if( !this._isLoaded() ) { await this._loadList(); } return this._routes; } async fetch(config, params = {}, options = {}, mode = 'data') { try { let cached = this.getCache(!config, {}, mode); if (cached !== false) return cached; cached = this.getCache(config, params, mode); if (cached !== false) return cached; if (!config.resolved) config = this.parseRoute(config, params, true, mode); if (!config.resolved) throw new Error(`❌ Fehlerhafte API-URL: ${config.resloved.data}`); const cacheKey = `${config.method}:${config.cachekey}`; const defaultOptions = { method: config.method, headers: {}, ...options }; if (config.method === "GET" || config.method === "DELETE") { const queryString = new URLSearchParams(params).toString(); config.resolved += queryString ? `?${queryString}` : ""; } else { if (params instanceof FormData) { defaultOptions.body = params; } else { defaultOptions.body = JSON.stringify(params); defaultOptions.headers["Content-Type"] = "application/json"; } } const response = await fetch(config.resolved, defaultOptions); const data = await response.json(); const fullcache = data.caching || false; delete data.caching; this._cache.set(cacheKey, { response: data, fullcache: false, timestamp: Date.now() }); this._lastCacheKey = cacheKey; return data; } catch (error) { console.error('❌ Fehler beim Abrufen der API-Daten:', error); return null; } } async fetchData(config, params = {}, options = {}) { const response = await this.fetch(config, params, options); return response.data; } async fetchList(config, params = {}, options = {}) { const response = await this.fetch(config, params, options); return response.list; } async fetchConfig(config, params = {}, options = {}) { const response = await this.fetch(config, params, options); return response.config; } async fetchGenerator(config, params = {}, options = {}) { const response = await this.fetch(config, params, options); return response.generator; } async fetchStream(config, params = {}, options = {}, onChunk) { try { if (!config.resolved) config = this.parseRoute(config, params, true); if (!config.resolved) throw new Error(`❌ Fehlerhafte API-URL: ${config.resloved.data}`); const defaultOptions = { method: config.method, headers: {}, ...options }; const response = await fetch(config.resolved, defaultOptions); if (!response.body) return; const decoder = new TextDecoder(); const reader = response.body.getReader(); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; if (onChunk) { buffer += decoder.decode(value, { stream: true }); let lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { if (!line.trim()) continue; try { onChunk(JSON.parse(line)); } catch { onChunk(line); } } } } if (onChunk && buffer.trim()) { try { onChunk(JSON.parse(buffer)); } catch { onChunk(buffer); } } } catch (error) { console.error('❌ Fehler beim Abrufen des API-Stream:', error); return null; } } async find(path, includeHidden = false) { await this._loadList(); return this._routes.find(route => { if (!includeHidden && route.visibility === "hidden") return false; const routePattern = new RegExp(`^${route.path.replace(/{\w+}/g, "([^/]+)")}$`); return routePattern.test(path); }); } getCache(config, params = {}, mode = 'data') { if (config === true && this._lastCacheKey) { const cached = this._cache.get(this._lastCacheKey); if (cached && Date.now() - cached.timestamp < this._CACHE_TIMEOUT) { cached.timestamp = Date.now(); return cached.response; } return null; } if (typeof config === 'object' || typeof config === 'string') { if (!config.resolved) config = this.parseRoute(config, params, {}, mode); const cacheKey = `${config.method}:${config.cachekey}`; const cached = this._cache.get(cacheKey); if (cached && Date.now() - cached.timestamp < this._CACHE_TIMEOUT) { this._lastCacheKey = cacheKey; return cached.response; } if (cached && cached.fullcache && location.pathname == config.cachekey) { return cached.response; } } return false; } getBreadcrumbSegments(path) { let segments = path.split('/').filter(Boolean); let breadcrumbs = []; let currentPath = ""; if (!segments.length) { segments = ['']; } segments.forEach(segment => { currentPath += `/${segment}`; const route = this._routes.find(r => r.path && new RegExp(`^${r.path.replace(/{\w+}/g, "([^/]+)")}$`).test(currentPath)); const group = this._routes.find(r => r.name && (new RegExp(`^${r.name.replace(/{\w+}/g, "([^/]+)")}$`).test(currentPath) || r.name == segment)); if (route) { breadcrumbs.push({ label: route.breadcrumb || route.label || segment, path: currentPath, }); } else if (group) { breadcrumbs.push({ label: group.breadcrumb || group.label || segment, path: false, }); } }); return breadcrumbs; } getDataRoute(config, params = {}, queryIncludes = true) { config = this.parseRoute(config ?? location.pathname, params, queryIncludes, 'data'); return config.resolved; } parseRoute( config, params = {}, queryIncludes = true, mode = 'data' ) { let route = typeof config === "string" ? config : config.route; let query = new URLSearchParams( config?.query || [] ); let connector = /\?/.test( route ) ? '&' : '?'; let resolved = null; let cachekey = null; if( query.size ) route += connector + query; if( !queryIncludes ) route = route.replace(/\?.*/, ''); switch (mode) { case 'rest': resolved = this._resolveApiUrl(route, params, '/rest'); cachekey = this._resolveApiUrl(route, params, '/rest', false); break; case 'data': resolved = this._resolveApiUrl(route, params, true); cachekey = this._resolveApiUrl(route, params, false, false); break; } return { route : route, resolved: resolved, cachekey: cachekey, history : this._resolveApiUrl(route, params, false), method : config?.method || "GET", query : config?.query || {}, }; } parseUrl(route, params) { const missingKeys = []; const resolvedUrl = route.replace(/{(\w+)}/g, (_, key) => { if (params[key] === 'undefined') { missingKeys.push(key); return `{${key}}`; } return params[key]; }).replace(/\/$/, ''); if (missingKeys.length > 0) { console.error(`Nicht ersetzte Platzhalter: ${missingKeys.join(', ')}`, `URL: ${route}`); return null; } return resolvedUrl; } _isLoaded() { return !!Object.entries( this._routes ).length; } async _loadList() { this._routes = await this.fetchList("/rest/routes"); } _resolveApiUrl(route, params = {}, prefix = true, hash = true) { const API_PREFIX = '/api/v1'; if (typeof prefix === 'string') { route = this._stripApiPrefix(route, API_PREFIX); route = route.startsWith(prefix) ? route : `${prefix}${route}`; } const mergedParams = params; const prefixedUrl = route.startsWith(API_PREFIX) ? route : `${API_PREFIX}${route}`; const resolvedUrl = this.parseUrl(prefixedUrl, params); if (!hash) { const withoutHash = resolvedUrl.replace(/#.*/, ''); return prefix ? withoutHash : this._stripApiPrefix(withoutHash, API_PREFIX); } return prefix ? resolvedUrl : this._stripApiPrefix(resolvedUrl, API_PREFIX); } _stripApiPrefix(route, api_prefix = '/api/v1') { if (route.startsWith(api_prefix)) { return route.replace(api_prefix, ''); } return route; } })(); ARUNOX.Router.history = null; /** Datei: plugins/rest/assets/js/core/registry.js **/ /** * /arunox/core/regestry.js * * Some Javascript codes without further description * * @author Sparky * @copyright 2025 ARUNOX * @version 2.0.0 * @since 2.0.0 Creation of new ctx methods */ window.ARUNOX = window.ARUNOX || {}; ARUNOX.Registry = { _store : {}, register : function( type, name, callback, priority = 10, once = false ) { if( !this._store[ type ] ) this._store[ type ] = {}; if( !this._store[ type ][ name ]) this._store[ type ][ name ] = []; // callback.name = name; callback.once = once; callback.priority = priority; this._store[ type ][ name ].unshift( callback ); this._store[ type ][ name ].sort( ( a, b ) => b.priority - a.priority ); }, once : function( type, name, callback, priority = 10 ) { return this.register( type, name, callback, priority, true ); }, remove : function( type, name, callback ) { const arr = this._store[ type ]?.[ name ]; if (!arr) return; this._store[type][name] = arr.filter(e => e !== callback); }, resolve : function( type, name, extendsSupportCallback ) { const resolved = this._store[ type ]?.[ name ]?.[0]; if( !resolved ) return null; if( resolved?.extends && typeof resolved?.extends === 'string' ) { return extendsSupportCallback.call( resolved, name ); } return resolved; }, exists : function( type, name, callback, originCallback ) { if( this._store[ type ]?.[ name ] ) { return !!( this._store[ type ]?.[ name ]?.filter( item => item == callback || item._originalCallback == callback ).length ); } return false; }, all : function( type, name ) { if( !type ) return this._store; if( !name ) return Object.keys(this._store[ type ] || {} ); return this._store[ type ]?.[ name ] || []; }, template : { _type : 'template', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, exists : function( name ) { return ARUNOX.Registry.exists( this._type, name ); }, all : function( name ) { return ARUNOX.Registry.all( this._type, name ); }, }, wrapper : { _type : 'wrapper', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, exists : function( name ) { return ARUNOX.Registry.exists( this._type, name ); }, all : function( name ) { return ARUNOX.Registry.all( this._type, name ); }, }, generator : { _type : 'generator', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, exists : function( name ) { return ARUNOX.Registry.exists( this._type, name ); }, all : function( name ) { return ARUNOX.Registry.all( this._type, name ); }, }, htmlTag : { _type : 'htmltag', register : function( name, callback, priority ) { return ARUNOX.Registry.register( this._type, name, callback, priority); }, remove : function( name, callback ) { return ARUNOX.Registry.remove( this._type, name, callback ); }, resolve : function( name ) { return ARUNOX.Registry.resolve( this._type, name ); }, exists : function( name ) { return ARUNOX.Registry.exists( this._type, name ); }, all : function( name ) { return ARUNOX.Registry.all( this._type, name ); }, }, _mergeHooks : function( baseHooks = {}, childHooks = {} ) { const merged = {...baseHooks}; for (const [name, hookFn] of Object.entries(childHooks)) { if (name.startsWith('!')) { // explizites Überschreiben: "!config" const realName = name.slice(1); merged[realName] = hookFn; } else { // beide behalten → Liste draus machen if (merged[name]) { merged[name] = [].concat(merged[name], hookFn); } else { merged[name] = hookFn; } } } return merged; } };