Remove unused blade settings pages not linked from UI Convert remaining frontend pages to SPA actions Convert settings user_settings and general to SPA actions Convert settings profile pages to SPA actions Convert contacts and projects add/edit pages to SPA actions Convert clients add/edit page to SPA action with loading pattern Refactor component scoped IDs from $id to $sid Fix jqhtml comment syntax and implement universal error component system Update all application code to use new unified error system Remove all backwards compatibility - unified error system complete Phase 5: Remove old response classes Phase 3-4: Ajax response handler sends new format, old helpers deprecated Phase 2: Add client-side unified error foundation Phase 1: Add server-side unified error foundation Add unified Ajax error response system with constants 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
919 lines
33 KiB
JavaScript
Executable File
919 lines
33 KiB
JavaScript
Executable File
// @ROUTE-EXISTS-01-EXCEPTION - This file contains documentation examples with fictional route names
|
|
|
|
/**
|
|
* Rsx - Core JavaScript Runtime System
|
|
*
|
|
* The Rsx class is the central hub for the RSX JavaScript runtime, providing essential
|
|
* system-level utilities that all other framework components depend on. It serves as the
|
|
* foundation for the client-side framework, handling core operations that must be globally
|
|
* accessible and consistently available.
|
|
*
|
|
* Core Responsibilities:
|
|
* - Event System: Application-wide event bus for framework lifecycle and custom events
|
|
* - Environment Detection: Runtime environment identification (dev/production)
|
|
* - Route Management: Type-safe route generation and URL building
|
|
* - Unique ID Generation: Client-side unique identifier generation
|
|
* - Framework Bootstrap: Multi-phase initialization orchestration
|
|
* - Logging: Centralized logging interface (delegates to console_debug)
|
|
*
|
|
* The Rsx class deliberately keeps its scope limited to core utilities. Advanced features
|
|
* are delegated to specialized classes:
|
|
* - Manifest operations → Manifest class
|
|
* - Caching → Rsx_Cache class
|
|
* - AJAX/API calls → Ajax_* classes
|
|
* - Route proxies → Rsx_Route_Proxy class
|
|
* - Behaviors → Rsx_Behaviors class
|
|
*
|
|
* All methods are static - Rsx is never instantiated. It's available globally from the
|
|
* moment bundles load and remains constant throughout the application lifecycle.
|
|
*
|
|
* Usage Examples:
|
|
* ```javascript
|
|
* // Event system
|
|
* Rsx.on('app_ready', () => console.log('App initialized'));
|
|
* Rsx.trigger('custom_event', {data: 'value'});
|
|
*
|
|
* // Environment detection
|
|
* if (Rsx.is_dev()) { console.log('Development mode'); }
|
|
*
|
|
* // Route generation
|
|
* const url = Rsx.Route('Controller::action').url();
|
|
*
|
|
* // Unique IDs
|
|
* const uniqueId = Rsx.uid(); // e.g., "rsx_1234567890_1"
|
|
* ```
|
|
*
|
|
* @static
|
|
* @global
|
|
*/
|
|
class Rsx {
|
|
// Gets set to true to interupt startup sequence
|
|
static __stopped = false;
|
|
|
|
// Initialize event handlers storage
|
|
static _init_events() {
|
|
if (typeof Rsx._event_handlers === 'undefined') {
|
|
Rsx._event_handlers = {};
|
|
}
|
|
if (typeof Rsx._triggered_events === 'undefined') {
|
|
Rsx._triggered_events = {};
|
|
}
|
|
}
|
|
|
|
// Register an event handler
|
|
static on(event, callback) {
|
|
Rsx._init_events();
|
|
|
|
if (typeof callback !== 'function') {
|
|
throw new Error('Callback must be a function');
|
|
}
|
|
|
|
if (!Rsx._event_handlers[event]) {
|
|
Rsx._event_handlers[event] = [];
|
|
}
|
|
|
|
Rsx._event_handlers[event].push(callback);
|
|
|
|
// If this event was already triggered, call the callback immediately
|
|
if (Rsx._triggered_events[event]) {
|
|
console_debug('RSX_INIT', 'Triggering ' + event + ' for late registered callback');
|
|
callback(Rsx._triggered_events[event]);
|
|
}
|
|
}
|
|
|
|
// Trigger an event with optional data
|
|
static trigger(event, data = {}) {
|
|
Rsx._init_events();
|
|
|
|
// Record that this event was triggered
|
|
Rsx._triggered_events[event] = data;
|
|
|
|
if (!Rsx._event_handlers[event]) {
|
|
return;
|
|
}
|
|
|
|
console_debug('RSX_INIT', 'Triggering ' + event + ' for ' + Rsx._event_handlers[event].length + ' callbacks');
|
|
|
|
// Call all registered handlers for this event in order
|
|
for (const callback of Rsx._event_handlers[event]) {
|
|
callback(data);
|
|
}
|
|
}
|
|
|
|
// Alias for trigger.refresh(''), should be called after major UI updates to apply such effects as
|
|
// underlining links to unimplemented # routes
|
|
static trigger_refresh() {
|
|
// Use Rsx.on('refresh', callback); to register a callback for refresh
|
|
this.trigger('refresh');
|
|
}
|
|
|
|
/**
|
|
* Setup global unhandled exception handlers
|
|
* Must be called before framework initialization begins
|
|
*
|
|
* Exception Event Contract:
|
|
* -------------------------
|
|
* When exceptions occur, they are broadcast via:
|
|
* Rsx.trigger('unhandled_exception', { exception, meta })
|
|
*
|
|
* Event payload structure:
|
|
* {
|
|
* exception: Error|string, // The exception (Error object or string)
|
|
* meta: {
|
|
* source: 'window_error'|'unhandled_rejection'|undefined
|
|
* // source = undefined means manually triggered (already logged by catcher)
|
|
* // source = 'window_error' or 'unhandled_rejection' means needs logging
|
|
* }
|
|
* }
|
|
*
|
|
* The exception can be:
|
|
* 1. An Error object (preferred) - Has .message, .stack, .filename, .lineno properties
|
|
* 2. A string - Plain error message text
|
|
*
|
|
* Consumers should handle both formats:
|
|
* ```javascript
|
|
* Rsx.on('unhandled_exception', function(payload) {
|
|
* const exception = payload.exception;
|
|
* const meta = payload.meta || {};
|
|
*
|
|
* let message;
|
|
* if (exception instanceof Error) {
|
|
* message = exception.message;
|
|
* // Can also access: exception.stack, exception.filename, exception.lineno
|
|
* } else if (typeof exception === 'string') {
|
|
* message = exception;
|
|
* } else {
|
|
* message = String(exception);
|
|
* }
|
|
*
|
|
* // Only log if from global handler (not already logged)
|
|
* if (meta.source === 'window_error' || meta.source === 'unhandled_rejection') {
|
|
* console.error('[Handler]', exception);
|
|
* }
|
|
* });
|
|
* ```
|
|
*
|
|
* Exception Flow:
|
|
* - window error/unhandledrejection → _handle_unhandled_exception(exception, {source})
|
|
* - Triggers 'unhandled_exception' event with exception and metadata
|
|
* - Debugger.js: Logs to server
|
|
* - Exception_Handler: Logs to console (if from global handler) and displays
|
|
* - Disables SPA navigation
|
|
*/
|
|
static _setup_exception_handlers() {
|
|
// Handle uncaught JavaScript errors
|
|
window.addEventListener('error', function (event) {
|
|
// Pass the Error object directly if available, otherwise create one
|
|
const exception = event.error || new Error(event.message);
|
|
// Attach additional metadata if not already present
|
|
if (!exception.filename) exception.filename = event.filename;
|
|
if (!exception.lineno) exception.lineno = event.lineno;
|
|
if (!exception.colno) exception.colno = event.colno;
|
|
|
|
Rsx._handle_unhandled_exception(exception, { source: 'window_error' });
|
|
});
|
|
|
|
// Handle unhandled promise rejections
|
|
window.addEventListener('unhandledrejection', function (event) {
|
|
// event.reason can be Error, string, or any value
|
|
const exception = event.reason instanceof Error
|
|
? event.reason
|
|
: new Error(event.reason ? String(event.reason) : 'Unhandled promise rejection');
|
|
|
|
Rsx._handle_unhandled_exception(exception, { source: 'unhandled_rejection' });
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Internal handler for unhandled exceptions
|
|
* Triggers event and disables SPA
|
|
* Display and logging handled by Exception_Handler listening to the event
|
|
*
|
|
* @param {Error|string|Object} exception - Exception object, string, or object with message
|
|
* @param {Object} meta - Metadata about exception source
|
|
* @param {string} meta.source - 'window_error', 'unhandled_rejection', or undefined for manual triggers
|
|
*/
|
|
static _handle_unhandled_exception(exception, meta = {}) {
|
|
// Trigger event for listeners:
|
|
// - Debugger.js: Logs to server
|
|
// - Exception_Handler: Logs to console and displays error (layout or flash alert)
|
|
// Pass exception and metadata (source info for logging decisions)
|
|
Rsx.trigger('unhandled_exception', { exception, meta });
|
|
|
|
// Disable SPA navigation if in SPA mode
|
|
// This allows user to navigate away from broken page using normal browser navigation
|
|
if (typeof Spa !== 'undefined' && window.rsxapp && window.rsxapp.is_spa) {
|
|
Spa.disable();
|
|
}
|
|
}
|
|
|
|
// Log to server that an event happened
|
|
static log(type, message = 'notice') {
|
|
Core_Log.log(type, message);
|
|
}
|
|
|
|
// Returns true if the app is being run in dev mode
|
|
// This should affect caching and some debug checks
|
|
static is_dev() {
|
|
return window.rsxapp.debug;
|
|
}
|
|
|
|
static is_prod() {
|
|
return !window.rsxapp.debug;
|
|
}
|
|
|
|
// Generates a unique number for the application instance
|
|
static uid() {
|
|
if (typeof Rsx._uid == undef) {
|
|
Rsx._uid = 0;
|
|
}
|
|
return Rsx._uid++;
|
|
}
|
|
|
|
// Storage for route definitions loaded from bundles
|
|
static _routes = {};
|
|
|
|
/**
|
|
* Calculate scope key from current environment.
|
|
*
|
|
* Scope key is a hash key which includes the current value of the session, user, site, and build keys.
|
|
* Data hashed with this key will be scoped to the current logged in user, and will be invalidated if
|
|
* the user logs out or the application source code is updated / redeployed / etc.
|
|
*
|
|
* @returns {string}
|
|
* @private
|
|
*/
|
|
static scope_key() {
|
|
const parts = [];
|
|
|
|
// Get session hash (hashed on server for non-reversible scoping)
|
|
if (window.rsxapp?.session_hash) {
|
|
parts.push(window.rsxapp.session_hash);
|
|
}
|
|
|
|
// Get user ID
|
|
if (window.rsxapp?.user?.id) {
|
|
parts.push(window.rsxapp.user.id);
|
|
}
|
|
|
|
// Get site ID
|
|
if (window.rsxapp?.site?.id) {
|
|
parts.push(window.rsxapp.site.id);
|
|
}
|
|
|
|
// Get build key
|
|
if (window.rsxapp?.build_key) {
|
|
parts.push(window.rsxapp.build_key);
|
|
}
|
|
|
|
return parts.join('_');
|
|
}
|
|
|
|
/**
|
|
* Generate URL for a controller route or SPA action
|
|
*
|
|
* This method generates URLs by looking up route patterns and replacing parameters.
|
|
* It handles controller routes, SPA action routes, and Ajax endpoints.
|
|
*
|
|
* If the route is not found in the route definitions, a default pattern is used:
|
|
* `/_/{controller}/{action}` with all parameters appended as query strings.
|
|
*
|
|
* Usage examples:
|
|
* ```javascript
|
|
* // Controller route (defaults to 'index' method)
|
|
* const url = Rsx.Route('Frontend_Index_Controller');
|
|
* // Returns: /dashboard
|
|
*
|
|
* // Controller route with explicit method
|
|
* const url = Rsx.Route('Frontend_Client_View_Controller::view', 123);
|
|
* // Returns: /clients/view/123
|
|
*
|
|
* // SPA action route
|
|
* const url = Rsx.Route('Contacts_Index_Action');
|
|
* // Returns: /contacts
|
|
*
|
|
* // Route with integer parameter (sets 'id')
|
|
* const url = Rsx.Route('Contacts_View_Action', 123);
|
|
* // Returns: /contacts/123
|
|
*
|
|
* // Route with named parameters (object)
|
|
* const url = Rsx.Route('Contacts_View_Action', {id: 'C001'});
|
|
* // Returns: /contacts/C001
|
|
*
|
|
* // Route with required and query parameters
|
|
* const url = Rsx.Route('Contacts_View_Action', {
|
|
* id: 'C001',
|
|
* tab: 'history'
|
|
* });
|
|
* // Returns: /contacts/C001?tab=history
|
|
*
|
|
* // Placeholder route
|
|
* const url = Rsx.Route('Future_Controller::#index');
|
|
* // Returns: #
|
|
* ```
|
|
*
|
|
* @param {string} action Controller class, SPA action, or "Class::method". Defaults to 'index' method if not specified.
|
|
* @param {number|Object} [params=null] Route parameters. Integer sets 'id', object provides named params.
|
|
* @returns {string} The generated URL
|
|
*/
|
|
static Route(action, params = null) {
|
|
// Parse action into class_name and action_name
|
|
// Format: "Controller_Name" or "Controller_Name::method_name" or "Spa_Action_Name"
|
|
let class_name, action_name;
|
|
if (action.includes('::')) {
|
|
[class_name, action_name] = action.split('::', 2);
|
|
} else {
|
|
class_name = action;
|
|
action_name = 'index';
|
|
}
|
|
|
|
// Normalize params to object
|
|
let params_obj = {};
|
|
if (typeof params === 'number') {
|
|
params_obj = { id: params };
|
|
} else if (params && typeof params === 'object') {
|
|
params_obj = params;
|
|
} else if (params !== null && params !== undefined) {
|
|
throw new Error('Params must be number, object, or null');
|
|
}
|
|
|
|
// Placeholder route: action starts with # means unimplemented/scaffolding
|
|
if (action_name.startsWith('#')) {
|
|
return '#';
|
|
}
|
|
|
|
// Check if route exists in PHP controller definitions
|
|
let pattern;
|
|
if (Rsx._routes[class_name] && Rsx._routes[class_name][action_name]) {
|
|
const route_patterns = Rsx._routes[class_name][action_name];
|
|
|
|
// Route patterns are always arrays (even for single routes)
|
|
pattern = Rsx._select_best_route_pattern(route_patterns, params_obj);
|
|
|
|
if (!pattern) {
|
|
// Route exists but no pattern satisfies the provided parameters
|
|
const route_list = route_patterns.join(', ');
|
|
throw new Error(
|
|
`No suitable route found for ${class_name}::${action_name} with provided parameters. ` +
|
|
`Available routes: ${route_list}`
|
|
);
|
|
}
|
|
} else {
|
|
// Not found in PHP routes - check if it's a SPA action
|
|
pattern = Rsx._try_spa_action_route(class_name, params_obj);
|
|
|
|
if (!pattern) {
|
|
// Route not found - use default pattern /_/{controller}/{action}
|
|
// For SPA actions, action_name defaults to 'index'
|
|
pattern = `/_/${class_name}/${action_name}`;
|
|
}
|
|
}
|
|
|
|
// Generate URL from pattern
|
|
return Rsx._generate_url_from_pattern(pattern, params_obj);
|
|
}
|
|
|
|
/**
|
|
* Select the best matching route pattern from available patterns based on provided parameters
|
|
*
|
|
* Selection algorithm:
|
|
* 1. Filter patterns where all required parameters can be satisfied by provided params
|
|
* 2. Among satisfiable patterns, prioritize those with MORE parameters (more specific)
|
|
* 3. If tie, any pattern works (deterministic by using first match)
|
|
*
|
|
* @param {Array<string>} patterns Array of route patterns
|
|
* @param {Object} params_obj Provided parameters
|
|
* @returns {string|null} Selected pattern or null if none match
|
|
*/
|
|
static _select_best_route_pattern(patterns, params_obj) {
|
|
const satisfiable = [];
|
|
|
|
for (const pattern of patterns) {
|
|
// Extract required parameters from pattern
|
|
const required_params = [];
|
|
const matches = pattern.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
|
|
if (matches) {
|
|
// Remove the : prefix from each match
|
|
for (const match of matches) {
|
|
required_params.push(match.substring(1));
|
|
}
|
|
}
|
|
|
|
// Check if all required parameters are provided
|
|
let can_satisfy = true;
|
|
for (const required of required_params) {
|
|
if (!(required in params_obj)) {
|
|
can_satisfy = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (can_satisfy) {
|
|
satisfiable.push({
|
|
pattern: pattern,
|
|
param_count: required_params.length
|
|
});
|
|
}
|
|
}
|
|
|
|
if (satisfiable.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
// Sort by parameter count descending (most parameters first)
|
|
satisfiable.sort((a, b) => b.param_count - a.param_count);
|
|
|
|
// Return the pattern with the most parameters
|
|
return satisfiable[0].pattern;
|
|
}
|
|
|
|
/**
|
|
* Generate URL from route pattern by replacing parameters
|
|
*
|
|
* @param {string} pattern The route pattern (e.g., '/users/:id/view')
|
|
* @param {Object} params Parameters to fill into the route
|
|
* @returns {string} The generated URL
|
|
*/
|
|
static _generate_url_from_pattern(pattern, params) {
|
|
// Extract required parameters from the pattern
|
|
const required_params = [];
|
|
const matches = pattern.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g);
|
|
if (matches) {
|
|
// Remove the : prefix from each match
|
|
for (const match of matches) {
|
|
required_params.push(match.substring(1));
|
|
}
|
|
}
|
|
|
|
// Check for required parameters
|
|
const missing = [];
|
|
for (const required of required_params) {
|
|
if (!(required in params)) {
|
|
missing.push(required);
|
|
}
|
|
}
|
|
|
|
if (missing.length > 0) {
|
|
throw new Error(`Required parameters [${missing.join(', ')}] are missing for route ${pattern}`);
|
|
}
|
|
|
|
// Build the URL by replacing parameters
|
|
let url = pattern;
|
|
const used_params = {};
|
|
|
|
for (const param_name of required_params) {
|
|
const value = params[param_name];
|
|
// URL encode the value
|
|
const encoded_value = encodeURIComponent(value);
|
|
url = url.replace(':' + param_name, encoded_value);
|
|
used_params[param_name] = true;
|
|
}
|
|
|
|
// Collect any extra parameters for query string
|
|
const query_params = {};
|
|
for (const key in params) {
|
|
if (!used_params[key]) {
|
|
query_params[key] = params[key];
|
|
}
|
|
}
|
|
|
|
// Append query string if there are extra parameters
|
|
if (Object.keys(query_params).length > 0) {
|
|
const query_string = Object.entries(query_params)
|
|
.map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
|
|
.join('&');
|
|
url += '?' + query_string;
|
|
}
|
|
|
|
return url;
|
|
}
|
|
|
|
/**
|
|
* Try to find a route pattern for a SPA action class
|
|
* Returns the route pattern or null if not found
|
|
*
|
|
* @param {string} class_name The action class name
|
|
* @param {Object} params_obj The parameters for route selection
|
|
* @returns {string|null} The route pattern or null
|
|
*/
|
|
static _try_spa_action_route(class_name, params_obj) {
|
|
// Get all classes from manifest
|
|
const all_classes = Manifest.get_all_classes();
|
|
|
|
// Find the class by name
|
|
for (const class_info of all_classes) {
|
|
if (class_info.class_name === class_name) {
|
|
const class_object = class_info.class_object;
|
|
|
|
// Check if it's a SPA action (has Spa_Action in prototype chain)
|
|
if (typeof Spa_Action !== 'undefined' &&
|
|
class_object.prototype instanceof Spa_Action) {
|
|
|
|
// Get route patterns from decorator metadata
|
|
const routes = class_object._spa_routes || [];
|
|
|
|
if (routes.length > 0) {
|
|
// Select best matching route based on parameters
|
|
const selected = Rsx._select_best_route_pattern(routes, params_obj);
|
|
|
|
if (!selected) {
|
|
// Routes exist but none are satisfiable
|
|
throw new Error(
|
|
`No suitable route found for SPA action ${class_name} with provided parameters. ` +
|
|
`Available routes: ${routes.join(', ')}`
|
|
);
|
|
}
|
|
|
|
return selected;
|
|
}
|
|
}
|
|
|
|
// Found the class but it's not a SPA action or has no routes
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Class not found
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Define routes from bundled data
|
|
* Called by generated JavaScript in bundles
|
|
*/
|
|
static _define_routes(routes) {
|
|
// Merge routes into the global route storage
|
|
for (const class_name in routes) {
|
|
if (!Rsx._routes[class_name]) {
|
|
Rsx._routes[class_name] = {};
|
|
}
|
|
for (const method_name in routes[class_name]) {
|
|
Rsx._routes[class_name][method_name] = routes[class_name][method_name];
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal: Call a specific method on all classes that have it
|
|
* Collects promises from return values and waits for all to resolve
|
|
* @param {string} method_name The method name to call on all classes
|
|
* @returns {Promise} Promise that resolves when all method calls complete
|
|
*/
|
|
static async _rsx_call_all_classes(method_name) {
|
|
const all_classes = Manifest.get_all_classes();
|
|
const classes_with_method = [];
|
|
const promise_pile = [];
|
|
|
|
for (const class_info of all_classes) {
|
|
const class_object = class_info.class_object;
|
|
const class_name = class_info.class_name;
|
|
|
|
// Check if this class has the method (static methods are on the class itself)
|
|
if (typeof class_object[method_name] === 'function') {
|
|
classes_with_method.push(class_name);
|
|
const return_value = await class_object[method_name]();
|
|
|
|
// Collect promises from return value
|
|
if (return_value instanceof Promise) {
|
|
promise_pile.push(return_value);
|
|
} else if (Array.isArray(return_value)) {
|
|
for (const item of return_value) {
|
|
if (item instanceof Promise) {
|
|
promise_pile.push(item);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Rsx.__stopped) {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (classes_with_method.length > 0) {
|
|
console_debug('RSX_INIT', `${method_name}: ${classes_with_method.length} classes`);
|
|
}
|
|
|
|
// Await all promises before returning
|
|
if (promise_pile.length > 0) {
|
|
console_debug('RSX_INIT', `${method_name}: Awaiting ${promise_pile.length} promises`);
|
|
await Promise.all(promise_pile);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal: Execute multi-phase initialization for all registered classes
|
|
* This runs various initialization phases in order to properly set up the application
|
|
* @returns {Promise} Promise that resolves when all initialization phases complete
|
|
*/
|
|
static async _rsx_core_boot() {
|
|
if (Rsx.__booted) {
|
|
console.error('Rsx._rsx_core_boot called more than once');
|
|
return;
|
|
}
|
|
|
|
Rsx.__booted = true;
|
|
|
|
// Setup exception handlers first, before any initialization phases
|
|
Rsx._setup_exception_handlers();
|
|
|
|
// Get all registered classes from the manifest
|
|
const all_classes = Manifest.get_all_classes();
|
|
|
|
console_debug('RSX_INIT', `Starting _rsx_core_boot with ${all_classes.length} classes`);
|
|
|
|
if (!all_classes || all_classes.length === 0) {
|
|
// No classes to initialize
|
|
shouldnt_happen('No classes registered in js - there should be at least the core framework classes');
|
|
return;
|
|
}
|
|
|
|
// Define initialization phases in order
|
|
const phases = [
|
|
{ event: 'framework_core_define', method: '_on_framework_core_define' },
|
|
{ event: 'framework_modules_define', method: '_on_framework_modules_define' },
|
|
{ event: 'framework_core_init', method: '_on_framework_core_init' },
|
|
{ event: 'app_modules_define', method: 'on_app_modules_define' },
|
|
{ event: 'app_define', method: 'on_app_define' },
|
|
{ event: 'framework_modules_init', method: '_on_framework_modules_init' },
|
|
{ event: 'app_modules_init', method: 'on_app_modules_init' },
|
|
{ event: 'app_init', method: 'on_app_init' },
|
|
{ event: 'app_ready', method: 'on_app_ready' },
|
|
];
|
|
|
|
// Execute each phase in order
|
|
for (const phase of phases) {
|
|
await Rsx._rsx_call_all_classes(phase.method);
|
|
|
|
if (Rsx.__stopped) {
|
|
return;
|
|
}
|
|
|
|
Rsx.trigger(phase.event);
|
|
}
|
|
|
|
// Ui refresh callbacks
|
|
Rsx.trigger_refresh();
|
|
|
|
// All phases complete
|
|
console_debug('RSX_INIT', 'Initialization complete');
|
|
|
|
// TODO: Find a good wait to wait for all jqhtml components to load, then trigger on_ready and on('ready') emulating the top level last syntax that jqhtml components operateas, but as a standard js class (such as a page class). The biggest question is, how do we efficiently choose only the top level jqhtml components. do we only consider components cretaed directly on blade templates? that seams reasonable...
|
|
|
|
// Trigger _debug_ready event - this is ONLY for tooling like rsx:debug
|
|
// DO NOT use this in application code - use on_app_ready() phase instead
|
|
// This event exists solely for debugging tools that need to run after full initialization
|
|
Rsx.trigger('_debug_ready');
|
|
}
|
|
|
|
/* Calling this stops the boot process. */
|
|
static async _rsx_core_boot_stop(reason) {
|
|
console.error(reason);
|
|
Rsx.__stopped = true;
|
|
}
|
|
|
|
/**
|
|
* Parse URL hash into key-value object
|
|
* Handles format: #key=value&key2=value2
|
|
*
|
|
* @returns {Object} Parsed hash parameters
|
|
*/
|
|
static _parse_hash() {
|
|
const hash = window.location.hash;
|
|
if (!hash || hash === '#') {
|
|
return {};
|
|
}
|
|
|
|
// Remove leading # and parse as query string
|
|
const hash_string = hash.substring(1);
|
|
const params = {};
|
|
|
|
const pairs = hash_string.split('&');
|
|
for (const pair of pairs) {
|
|
const [key, value] = pair.split('=');
|
|
if (key) {
|
|
params[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '';
|
|
}
|
|
}
|
|
|
|
return params;
|
|
}
|
|
|
|
/**
|
|
* Serialize object into URL hash format
|
|
* Produces format: #key=value&key2=value2
|
|
*
|
|
* @param {Object} params Key-value pairs to encode
|
|
* @returns {string} Encoded hash string (with leading #, or empty string)
|
|
*/
|
|
static _serialize_hash(params) {
|
|
const pairs = [];
|
|
for (const key in params) {
|
|
const value = params[key];
|
|
if (value !== null && value !== undefined && value !== '') {
|
|
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(value)}`);
|
|
}
|
|
}
|
|
|
|
return pairs.length > 0 ? '#' + pairs.join('&') : '';
|
|
}
|
|
|
|
/**
|
|
* Get all hash state from URL hash
|
|
*
|
|
* Usage:
|
|
* ```javascript
|
|
* const state = Rsx.url_hash_get_all();
|
|
* // Returns: {dg_page: '2', dg_sort: 'name'}
|
|
* ```
|
|
*
|
|
* @returns {Object} All hash parameters as key-value pairs
|
|
*/
|
|
static url_hash_get_all() {
|
|
return Rsx._parse_hash();
|
|
}
|
|
|
|
/**
|
|
* Get single value from URL hash state
|
|
*
|
|
* Usage:
|
|
* ```javascript
|
|
* const page = Rsx.url_hash_get('dg_page');
|
|
* // Returns: '2' or null if not set
|
|
* ```
|
|
*
|
|
* @param {string} key The key to retrieve
|
|
* @returns {string|null} The value or null if not found
|
|
*/
|
|
static url_hash_get(key) {
|
|
const state = Rsx._parse_hash();
|
|
return state[key] ?? null;
|
|
}
|
|
|
|
/**
|
|
* Set single value in URL hash state (replaces history, doesn't add)
|
|
*
|
|
* Usage:
|
|
* ```javascript
|
|
* Rsx.url_hash_set_single('dg_page', 2);
|
|
* // URL becomes: http://example.com/page#dg_page=2
|
|
*
|
|
* Rsx.url_hash_set_single('dg_page', null); // Remove key
|
|
* ```
|
|
*
|
|
* @param {string} key The key to set
|
|
* @param {string|number|null} value The value (null/empty removes the key)
|
|
*/
|
|
static url_hash_set_single(key, value) {
|
|
const state = Rsx._parse_hash();
|
|
|
|
// Update or remove the key
|
|
if (value === null || value === undefined || value === '') {
|
|
delete state[key];
|
|
} else {
|
|
state[key] = String(value);
|
|
}
|
|
|
|
// Update URL without adding history
|
|
const new_hash = Rsx._serialize_hash(state);
|
|
const url = window.location.pathname + window.location.search + new_hash;
|
|
history.replaceState(null, '', url);
|
|
}
|
|
|
|
/**
|
|
* Set multiple values in URL hash state at once
|
|
*
|
|
* Usage:
|
|
* ```javascript
|
|
* Rsx.url_hash_set({dg_page: 2, dg_sort: 'name'});
|
|
* // URL becomes: http://example.com/page#dg_page=2&dg_sort=name
|
|
*
|
|
* Rsx.url_hash_set({dg_page: null}); // Remove key from hash
|
|
* ```
|
|
*
|
|
* @param {Object} new_state Object with key-value pairs to set (null removes key)
|
|
*/
|
|
static url_hash_set(new_state) {
|
|
const state = Rsx._parse_hash();
|
|
|
|
// Merge new state
|
|
for (const key in new_state) {
|
|
const value = new_state[key];
|
|
if (value === null || value === undefined || value === '') {
|
|
delete state[key];
|
|
} else {
|
|
state[key] = String(value);
|
|
}
|
|
}
|
|
|
|
// Update URL without adding history
|
|
const new_hash = Rsx._serialize_hash(state);
|
|
const url = window.location.pathname + window.location.search + new_hash;
|
|
history.replaceState(null, '', url);
|
|
}
|
|
|
|
/**
|
|
* Render an error in a DOM element
|
|
*
|
|
* Displays errors from Ajax calls in a standardized format. Handles different
|
|
* error types (fatal, validation, auth, generic) with appropriate formatting.
|
|
*
|
|
* Usage:
|
|
* ```javascript
|
|
* try {
|
|
* const result = await Controller.method();
|
|
* } catch (error) {
|
|
* Rsx.render_error(error, '#error_container');
|
|
* }
|
|
* ```
|
|
*
|
|
* @param {Error|Object} error - Error object from Ajax call
|
|
* @param {jQuery|string} container - jQuery element or selector for error display
|
|
*/
|
|
static render_error(error, container) {
|
|
const $container = $(container);
|
|
|
|
if (!$container.exists()) {
|
|
console.error('Rsx.render_error: Container not found', container);
|
|
return;
|
|
}
|
|
|
|
// Clear existing content
|
|
$container.empty();
|
|
|
|
let html = '';
|
|
|
|
// Handle different error types
|
|
if (error.type === 'fatal' && error.details) {
|
|
// Fatal PHP error with file/line/error
|
|
const details = error.details;
|
|
const file = details.file || 'Unknown file';
|
|
const line = details.line || '?';
|
|
const message = details.error || error.message || 'Fatal error occurred';
|
|
|
|
html = `
|
|
<div class="alert alert-danger" role="alert">
|
|
<h5>Uncaught Fatal Error in ${file}:${line}:</h5>
|
|
<p class="mb-0">${Rsx._escape_html(message)}</p>
|
|
</div>
|
|
`;
|
|
} else if (error.type === 'form_error' && error.details) {
|
|
// Validation errors - show unmatched errors only
|
|
// (matched errors should be handled by Form_Utils.apply_form_errors)
|
|
const errors = error.details;
|
|
const error_list = [];
|
|
|
|
for (const field in errors) {
|
|
error_list.push(errors[field]);
|
|
}
|
|
|
|
if (error_list.length > 0) {
|
|
html = `
|
|
<div class="alert alert-warning" role="alert">
|
|
<h5>Validation Errors:</h5>
|
|
<ul class="mb-0">
|
|
${error_list.map((err) => `<li>${Rsx._escape_html(err)}</li>`).join('')}
|
|
</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
} else if (error.type === 'auth_required' || error.type === 'unauthorized') {
|
|
// Authentication/authorization errors
|
|
const message = error.message || 'Authentication required';
|
|
html = `
|
|
<div class="alert alert-warning" role="alert">
|
|
<p class="mb-0">${Rsx._escape_html(message)}</p>
|
|
</div>
|
|
`;
|
|
} else if (error.type === 'network') {
|
|
// Network errors
|
|
const message = error.message || 'Unable to reach server. Please check your connection.';
|
|
html = `
|
|
<div class="alert alert-danger" role="alert">
|
|
<p class="mb-0">${Rsx._escape_html(message)}</p>
|
|
</div>
|
|
`;
|
|
} else {
|
|
// Generic/unknown error
|
|
const message = error.message || error.toString() || 'An unknown error occurred';
|
|
html = `
|
|
<div class="alert alert-danger" role="alert">
|
|
<p class="mb-0">${Rsx._escape_html(message)}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
$container.html(html);
|
|
}
|
|
|
|
/**
|
|
* Escape HTML to prevent XSS in error messages
|
|
* @private
|
|
*/
|
|
static _escape_html(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|