import { Router } from "./Router.js";
import { StateManager } from "./StateManager.js";
import { View } from "./View.js";
import { I18n } from "./I18n.js";
import { CustomEvents } from "./CustomEvents.js";
import { Helper } from "../util/Helper.js";
import { PathObject } from "../util/PathObject.js";
import { DefaultIndexState } from "../base/DefaultIndexState.js";
const VERSION = '1.0.1';
const DEFAULT_CONFIG = {
"app" : {
"id" : null,
"title" : null,
"sayHello" : true
},
"l18n" : {
"defaultLanguage" : "en"
},
"router" : {
"isEnabled" : true,
"mode" : "url",
"basePath" : null
},
"stateManager" : {
"notFoundState" : DefaultIndexState
}
};
/**
* App
* The App class is the logical core unit of every InfrontJS application.
* It serves as a dependency injection container and lifecycle manager for the framework's core subsystems:
* Router, StateManager, View, and I18n.
*
* @class
* @extends CustomEvents
*
* @example
* // Basic app setup
* const container = document.querySelector('#app');
* const app = new IF.App(container, {
* app: { title: 'My Application' },
* router: { mode: 'url' },
* l18n: { defaultLanguage: 'en' }
* });
*
* // Register states
* app.stateManager.add(HomeState, AboutState, UserState);
*
* // Start the application
* await app.run();
*
* @example
* // Multiple app instances
* const app1 = new IF.App(container1, { app: { id: 'main-app' } });
* const app2 = new IF.App(container2, { app: { id: 'widget-app' } });
*
* // Access by ID
* const mainApp = IF.App.get('main-app');
*
* @example
* // Cleanup when done
* await app.destroy(); // Properly cleans up all resources
*/
class App extends CustomEvents
{
/**
* Global pool of all app instances
* @static
* @type {Object.<string, App>}
*/
static POOL = {};
/**
* Get an app instance from the global pool by UID
*
* @static
* @param {string|null} [uid=null] - Unique identifier of the app. If null, returns the first app in the pool.
* @returns {(App|null)} The app instance or null if not found
*
* @example
* // Get specific app
* const app = IF.App.get('my-app-id');
*
* @example
* // Get first app (useful for single-app setups)
* const app = IF.App.get();
*/
static get( uid = null )
{
if ( uid && App.POOL.hasOwnProperty( uid ) )
{
return App.POOL[ uid ];
}
else if ( null === uid && Object.keys( App.POOL ).length > 0 )
{
return App.POOL[ Object.keys( App.POOL )[ 0 ] ];
}
else
{
return null;
}
}
/**
* Create an app instance
*
* @constructor
* @param {HTMLElement|null} [container=null] - The root container of the application. If null, creates a new <section> element in document.body
* @param {object} [config={}] - Application configuration object
* @param {object} [config.app] - App-level configuration
* @param {string|null} [config.app.id=null] - Unique ID of app instance. Auto-generated if not provided
* @param {string|null} [config.app.title=null] - App's title. If set, updates document.title
* @param {boolean} [config.app.sayHello=true] - Whether to log InfrontJS version to console
* @param {object} [config.l18n] - Internationalization configuration
* @param {string} [config.l18n.defaultLanguage='en'] - Default language code
* @param {object} [config.router] - Router configuration
* @param {boolean} [config.router.isEnabled=true] - Enable/disable routing
* @param {string} [config.router.mode='url'] - Router mode: 'url' (pushState) or 'hash'
* @param {string|null} [config.router.basePath=null] - Base path for subdirectory deployments
* @param {object} [config.stateManager] - State manager configuration
* @param {Function} [config.stateManager.notFoundState] - State class to use for 404 errors
*
* @throws {Error} If not running in browser environment
* @throws {Error} If container is not an HTMLElement
*
* @example
* // Minimal setup with auto-generated container
* const app = new IF.App();
*
* @example
* // Full configuration
* const app = new IF.App(document.querySelector('#app'), {
* app: {
* id: 'my-app',
* title: 'My Application',
* sayHello: false
* },
* router: {
* mode: 'url',
* basePath: '/myapp'
* },
* l18n: {
* defaultLanguage: 'de'
* },
* stateManager: {
* notFoundState: Custom404State
* }
* });
*
* @example
* // Using existing container
* const container = document.createElement('div');
* container.id = 'app-root';
* document.body.appendChild(container);
* const app = new IF.App(container);
*/
constructor( container = null, config = {} )
{
super();
this.container = container;
this.config = new PathObject( Helper.deepMerge( DEFAULT_CONFIG, config ) );
if ( null === this.config.get( 'app.id', null ) )
{
this.config.set( 'app.id', Helper.createUid() );
}
if ( typeof window === 'undefined' )
{
throw new Error( 'InfrontJS works only in browser mode.' );
}
// If container property is a string, check if it is a querySelector
if ( this.container !== null && !(this.container instanceof HTMLElement) )
{
throw new Error( 'Invalid app container.' );
}
else if ( this.container === null )
{
const body = document.querySelector( 'body' );
const customContainer = document.createElement( 'section' );
body.appendChild( customContainer );
this.container = customContainer;
}
this.container.setAttribute( 'data-ifjs-app-id', this.config.get( 'app.id') );
// Init core components
this.initRouter();
this.initL18n();
this.initStates();
this.initView();
// Add app to global app pool
App.POOL[ this.config.get('app.id') ] = this;
if ( true === this.config.get( 'app.sayHello' ) && console )
{
console && console.log( "%c»InfrontJS« Version " + VERSION, "font-family: monospace sans-serif; background-color: black; color: white;" );
}
this.dispatchEvent(new CustomEvent( CustomEvents.TYPE.READY ));
}
/**
* Initialize internationalization subsystem
* @private
*/
initL18n()
{
this.l18n = new I18n( this );
}
/**
* Initialize state management subsystem
* @private
*/
initStates()
{
this.stateManager = new StateManager( this );
}
/**
* Initialize routing subsystem
* @private
*/
initRouter()
{
this.router = new Router( this );
}
/**
* Initialize view/template rendering subsystem
* @private
*/
initView()
{
this.view = new View( this );
}
/**
* Get InfrontJS version
*
* @returns {string} Version string (e.g., '1.0.0-rc9')
*
* @example
* const version = app.getVersion();
* console.log('Running InfrontJS', version);
*/
getVersion()
{
return VERSION;
}
/**
* Run application logic and activate routing
* This is an asynchronous function that starts the application lifecycle.
* It enables the router and processes the initial route.
*
* @async
* @param {string|null} [route=null] - Initial route to navigate to. If null, uses current URL
* @returns {Promise<void>}
*
* @example
* // Start with current URL
* await app.run();
*
* @example
* // Start at specific route
* await app.run('/dashboard');
*
* @example
* // Complete startup flow
* const app = new IF.App(container, config);
* app.stateManager.add(HomeState, AboutState);
*
* // Load translations before starting
* await app.l18n.loadTranslations('en', '/locales/en.json');
*
* // Start application
* await app.run('/home');
*/
async run( route = null )
{
if ( this.config.get( 'app.title' ) )
{
this.view.setWindowTitle( this.config.get( 'app.title' ) );
}
this.router.enable();
if ( route )
{
// @todo Fix this for "url" router mode
this.router.redirect( route, ( this.router.resolveRoute( route ) === this.router.resolveRoute( location.hash ) ) );
}
else
{
this.router.process();
}
}
/**
* Destroys InfrontJS application instance and cleans up all resources
* This method properly disposes of all subsystems, removes event listeners,
* cleans up the DOM, and removes the app from the global pool.
*
* @async
* @returns {Promise<void>}
*
* @example
* // Cleanup when app is no longer needed
* await app.destroy();
*
* @example
* // Cleanup before page navigation
* window.addEventListener('beforeunload', async () => {
* await app.destroy();
* });
*
* @example
* // Switch between apps
* await oldApp.destroy();
* const newApp = new IF.App(container, newConfig);
* await newApp.run();
*/
async destroy()
{
// Disable router to stop processing events
if (this.router) {
this.router.disable();
}
// Exit current state and clean up state manager
if (this.stateManager && this.stateManager.currentState) {
try {
await this.stateManager.currentState.exit();
await this.stateManager.currentState.dispose();
} catch (error) {
console.warn('Error during state cleanup:', error);
}
this.stateManager.currentState = null;
}
// Clear container content
if (this.container) {
this.container.innerHTML = '';
this.container.removeAttribute('data-ifjs-app-id');
}
// Remove from global app pool
if (App.POOL[this.config.get('app.id')]) {
delete App.POOL[this.config.get('app.id')];
}
// Clear references to prevent memory leaks
this.router = null;
this.stateManager = null;
this.view = null;
this.l18n = null;
this.container = null;
this.config = null;
}
}
export { App };