/** Datei: plugins/rest/assets.OLD/js/core/prototype.js **/ Object.defineProperty( DOMTokenList.prototype, 'removeRexEx', { value: function( regex ) { if( !( regex instanceof RegExp ) ) { throw new TypeError('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 }); /** Datei: plugins/rest/assets.OLD/js/core/template.js **/ /** * generator/template.js * * Dynamisches Modul für die Verwaltung und das Laden von Generatoren. * * @author Sparky * @copyright 2025 ARUNOX * @version 1.3.0 * * @since 1.0.0 Datei erstellt * @since 1.1.0 Dynamischer Import von Generatoren basierend auf Verzeichnisstruktur * @since 1.2.0 Nutzung von debugLog() statt console.log() * @since 1.3.0 Implementierung von exists() zur Generatorprüfung vor Import */ import { router } from 'plugins/rest/core/router'; class Template { #loaded = {}; #known = {}; #current = null; constructor() {} extend( name, templateFunction ) { this.#loaded[ name ] = templateFunction; } getUUID( config, data, name ) { this.route = router.getDataRoute( config ?? this.route, data ?? this.data ); data ??= this.data; name ??= this.name return btoa( `${name}:${data.hashid ?? data.id ?? 0}` ); return btoa( `${name}:${this.route}:${data.hashid ?? data.id ?? 0}` ); } async exists( name ) { try { await this.#load( name ); return true; } catch( error ) { return false; } } async generate( name, config, response ) { if( this.#current === name ) return; if( await this.exists( name ) ) { const templateFunction = await this.#load( name ); const templateWrapper = document.createElement('div'); document.body.innerHTML = ''; document.body.append( templateWrapper ); templateWrapper.classList.add( 'template__wrapper' ); this.#current = name; if( templateFunction ) { return templateFunction( config, response, templateWrapper ); } } return this.generate( 'template/default', config, response ); } async #isLoaded( name ) { return !!this.#loaded[ name ]; } async #load( name ) { if( await this.#isLoaded( name ) ) { return this.#loaded[ name ]; } await import( `template/${name}` ); return this.#loaded[ name ] ?? null; } } export const template = new Template(); /** Datei: plugins/rest/assets.OLD/js/core/helpers.js **/ /** * plugins/rest/core/helpers.js * * Enthält allgemeine Hilfsfunktionen, darunter eine Debugging-Funktion. * * @author Sparky * @copyright 2025 ARUNOX * @version 1.4.1 * * @since 1.0.0 File is created * @since 1.1.0 Debugging-Funktionen hinzugefügt * @since 1.1.1 Sicherstellung, dass bestehende Funktionen erhalten bleiben * @since 1.1.2 Fehlende Funktionen wiederhergestellt * @since 1.4.0 Neue globale Funktion `handleClickAction()` zur Verarbeitung von Klicks. * - Unterstützt API-Requests mit `fetch()`. * - Kann Routen wechseln (`loadRoute()` oder `_blank`). * - Konfigurierbar mit `method`, `api`, `route` und `target`. * @since 1.4.1 Fix: `handleClickAction()` übergibt jetzt das `main`-Element an `loadRoute()`. * - Falls `main` nicht existiert, wird eine Fehlermeldung ausgegeben. * - `handleClickAction()` unterstützt jetzt korrekt das Routing über `loadRoute()`. */ import { router } from 'plugins/rest/core/router'; import { updater } from 'plugins/rest/core/updater'; import { template } from 'plugins/rest/core/template'; import { generator } from 'plugins/rest/core/generator'; export async function handleAction( config, sendData = {}, parseData = {}, mode = 'data' ) { if( !config ) { console.warn("⚠️ Ungültige Konfiguration für handleAction()."); return; } if( !config.resolved ) { config = router.parseRoute( config, parseData, true, mode ); } // console.log( config ); let response = { click : [], route : [], dialog : [], updater : [], template : [], generator : [], }; for( const item of await router.fetch( config, sendData, {}, mode ) ) { response[ item.type ].push( item ); } // console.log( config, response, ARXTheme ); handleRoute = 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 handleAction( item, {}, {}, 'rest' ); } handleRoute = config.history; } if( response.template.length && !response.route.length ) { for( const item of response.template ) { console.log( ARXTheme.template, RESTTheme.template ); // ARXTheme.template( item.template, config, item ); // await template.generate( item.template, config, item ); } } if( response.generator.length && !response.route.length ) { for( const item of response.generator ) { // await generator.generate( item.generator, config, item ); } } if( response.updater.length && !response.route.length ) { for( const item of response.updater ) { // await updater.update( item.updater, item.data, item ); } } if( response.click.length ) { for( const item of response.click ) { const element = document.querySelector(`[data-uuid="${item.parent}"] ${item.selector ?? ''}` ); element.click(); } } handleRoute = config.history; } window.handleRoute = null; window.handleAction = handleAction; window.handleRestAction = ( url ) => handleAction( url, {}, {}, 'rest' ); export async function generateMenus() { const menuElements = document.querySelectorAll('[data-rest-menu]' ); const arxElements = document.querySelectorAll('[data-arx-menu]' ); if( !menuElements.length && !arxElements.length ) return; const routes = await router.routes(); for( const menuElement of menuElements ) { menuElement.innerHTML = ''; await generator.generate( 'layouts/menu', { name : menuElement.dataset.restMenu, path : window.location.pathname, }, routes, menuElement ); } const arxRoutes = routes.map( item => Object.assign( {}, item, { menu : item.menu ? ( Array.isArray( item.menu ) ? item.menu : [ item.menu ] ) : [] } )) for( const arxElement of arxElements ) { await arxElement.REST.addMenus( arxRoutes ); } } export function getObject( config, additional = {}, columnName = 'content' ) { if( typeof config !== 'object' ) { config = { [columnName]: config }; } return Object.assign( {}, config, additional ); } export const capitalize = (str) => { if (!str) return ''; return str.charAt(0).toUpperCase() + str.slice(1); }; export const loadScript = (src) => { return new Promise((resolve, reject) => { if (document.querySelector(`script[src="${src}"]`)) { resolve(); return; } const script = document.createElement('script'); script.src = src; script.onload = () => resolve(); script.onerror = () => reject(new Error(`Fehler beim Laden des Skripts: ${src}`)); document.head.appendChild(script); }); }; /** Datei: plugins/rest/assets.OLD/js/core/generator.js **/ /** * generator/generator.js * * Dynamisches Modul für die Verwaltung und das Laden von Generatoren. * * @author Sparky * @copyright 2025 ARUNOX * @version 1.3.0 * * @since 1.0.0 Datei erstellt * @since 1.1.0 Dynamischer Import von Generatoren basierend auf Verzeichnisstruktur * @since 1.2.0 Nutzung von debugLog() statt console.log() * @since 1.3.0 Implementierung von exists() zur Generatorprüfung vor Import */ import { router } from 'plugins/rest/core/router'; class Generator { #loaded = {}; #known = {}; constructor() {} extend( name, generatorFunction ) { this.#loaded[ name ] = generatorFunction; } getUUID( config, data, name ) { this.route = router.getDataRoute( config ?? this.route, data ?? this.data, false ); data ??= this.data; name ??= this.name return btoa( `${name}:${data.hashid ?? data.id ?? 0}` ); return btoa( `${name}:${this.route}:${data.hashid ?? data.id ?? 0}` ); } async exists( name ) { try { await this.#load( name ); return true; } catch( error ) { return false; } } async generate( name, config, data, container, ...additional ) { if( await this.exists( name ) ) { const generatorFunction = await this.#load( name ); if( generatorFunction ) { const context = new Generator(); context.parent = this.hasOwnProperty('name') ? this : null; context.#loaded = this.#loaded; context.#known = this.#known; context.route = config.api ?? null; context.name = name; context.data = data; return generatorFunction( config, data, container, ...additional, context ); } } return false; } async #isLoaded( name ) { return !!this.#loaded[ name ]; } async #load( name ) { if( await this.#isLoaded( name ) ) { return this.#loaded[ name ]; } await import( `generator/${name}` ); return this.#loaded[ name ] ?? null; } } export const generator = new Generator(); /** Datei: plugins/rest/assets.OLD/js/core/updater.js **/ /** * generator/updater.js * * Dynamisches Modul für die Verwaltung und das Laden von Generatoren. * * @author Sparky * @copyright 2025 ARUNOX * @version 1.3.0 * * @since 1.0.0 Datei erstellt * @since 1.1.0 Dynamischer Import von Generatoren basierend auf Verzeichnisstruktur * @since 1.2.0 Nutzung von debugLog() statt console.log() * @since 1.3.0 Implementierung von exists() zur Generatorprüfung vor Import */ import { router } from 'plugins/rest/core/router'; class Updater { #loaded = {}; #known = {}; constructor() {} extend( name, updaterFunction ) { this.#loaded[ name ] = updaterFunction; } getUUID( config, data, name ) { data ??= this.data; config ??= this.route name ??= this.name return btoa( `${name}:${data.hashid ?? data.id ?? 0}` ); return btoa( `${name}:${router.getDataRoute( config, data )}:${data.hashid ?? data.id ?? 0}` ); } async exists( name ) { try { await this.#load( name ); return true; } catch( error ) { return false; } } async update( uuid, data ) { const name = atob( uuid ).split(':')[0]; if( await this.exists( name ) ) { const updaterFunction = await this.#load( name ); if( updaterFunction ) { const context = Object.create(this); const elements = document.querySelectorAll(`[data-uuid="${uuid}"]` ); for( const element of elements ) { updaterFunction( element, element?.RESTconfig ?? {}, data, context ); } } } return false; } async #isLoaded( name ) { return !!this.#loaded[ name ]; } async #load( name ) { if( await this.#isLoaded( name ) ) { return this.#loaded[ name ]; } await import( `updater/${name}` ); return this.#loaded[ name ] ?? null; } } export const updater = new Updater(); /** Datei: plugins/rest/assets.OLD/js/core/validator.js **/ /** * generator/validator.js * * Dynamisches Modul für die Verwaltung und das Laden von Validatoren. * * @author Sparky * @copyright 2025 ARUNOX * @version 1.0.0 * * @since 1.0.0 Datei erstellt * @since 1.1.0 Erste Validator-Implementierung mit WeakMap * @since 1.2.0 `add()` erweitert zur Speicherung mehrerer Validatoren * @since 1.3.0 `validateAll()` hinzugefügt für sequentielle Validierung */ import { router } from 'plugins/rest/core/router'; import { generator } from 'plugins/rest/core/generator'; class Validator { #loaded = {}; #known = {}; #elements = new WeakMap(); constructor() {} extend( name, validatorFunction ) { this.#loaded[ name ] = validatorFunction; } async exists( name ) { try { await this.#load( name ); return true; } catch( error ) { return false; } } async add( element, validator ) { if( !this.#elements.has( element ) ) { this.#elements.set( element , new Set() ); } this.#elements.get( element ).add( validator ); } on( event, element, config, data, container, ...additional ) { if( Array.isArray( element ) ) { for( const elementItem of element ) { elementItem.addEventListener( event, async () => this.validateAll( element, config, data, container, ...additional ) ); } return; } element.addEventListener( event, async () => this.validateAll( element, config, data, container, ...additional ) ); } async validate( element, name, config, data, container, ...additional ) { if( await this.exists( name ) ) { const validatorFunction = await this.#load( name ); if( validatorFunction ) { return validatorFunction( element, config, data, container, ...additional ); } } return true; } async validateAll( element, config, data, container, ...additional ) { if( !this.#elements.has( element ) ) return true; let isValid = true; for( const name of this.#elements.get( element ) ) { // console.log( name, element ); let result = await this.validate( element, name, config, data, container, ...additional ); if( !element.validity.valid ) { element.validate.browser = element.validationMessage; result = 'browser'; } if( result !== true ) { await generator.generate('data/form/additional/feedback', config, result, false, container ); isValid = false; } } if( isValid ) { await generator.generate('data/form/additional/feedback', config, 'valid', true, container ); } if( !Array.isArray( element ) ) { element = [ element ]; } for( const item of element ) { item.dispatchEvent( new Event('validationComplete', { bubbles: true }) ); } return isValid; } async #isLoaded( name ) { return !!this.#loaded[ name ]; } async #load( name ) { if( await this.#isLoaded( name ) ) { return this.#loaded[ name ]; } await import( `validator/${name}` ); return this.#loaded[ name ] ?? null; } } export const validator = new Validator(); /** Datei: plugins/rest/assets.OLD/js/core/router.js **/ /** * plugins/rest/core/router.js * * Verwaltet die Client-seitige Routensteuerung und API-Anfragen. * * @author Sparky * @copyright 2025 ARUNOX * @version 1.3.2 * * @since 1.0.0 Erstellt als separate Datei * @since 1.2.0 Erweiterung der Routenlogik für dynamische Platzhalter * @since 1.2.5 Fehlerhandling für API-Requests verbessert * @since 1.3.0 Neue `fetch()` Logik für GET & POST Requests implementiert * @since 1.3.2 Query-Parameter für `GET` korrekt an URL angehängt */ import { API_PREFIX } from "../app.js"; class Router { #routes = {}; #cache = new Map(); #lastCacheKey = null; #isReloaded = false; #CACHE_TIMEOUT = 500; constructor() {} async routes() { 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 ); // if( !response.ok ) { // throw new Error(`❌ API-Fehler ${response.status}: ${response.statusText}`); // } const data = await response.json(); const fullcache = data.caching || false; delete data.caching; // 🗂️ Response im Cache speichern 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; // Aus Cache liefern } 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); // Entfernt leere Segmente let breadcrumbs = []; let currentPath = ""; if( !segments.length ) { segments = ['']; } // console.log( segments, this.#routes ); 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 ) ); // console.log( segment, currentPath, route, group ); 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 resolved = null; let cachekey = null; 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", }; } 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: ${prefixedUrl}` ); return null; } return resolvedUrl; } #resolveApiUrl( route, params = {}, prefix = true, hash = true ) { 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_PREFIX ) { if( route.startsWith( api_prefix ) ) { return route.replace( api_prefix, '' ); } return route; } async #isLoaded( name ) { return !!Object.entries( this.#routes ).length; } async #loadList() { if( !this.#isLoaded() ) return true; this.#routes = await this.fetchList("/rest/routes"); } } export const router = new Router();