Files
rspade_system/app/RSpade/Core/Js/Rsx.js
root f6ac36c632 Enhance refactor commands with controller-aware Route() updates and fix code quality violations
Add semantic token highlighting for 'that' variable and comment file references in VS Code extension
Add Phone_Text_Input and Currency_Input components with formatting utilities
Implement client widgets, form standardization, and soft delete functionality
Add modal scroll lock and update documentation
Implement comprehensive modal system with form integration and validation
Fix modal component instantiation using jQuery plugin API
Implement modal system with responsive sizing, queuing, and validation support
Implement form submission with validation, error handling, and loading states
Implement country/state selectors with dynamic data loading and Bootstrap styling
Revert Rsx::Route() highlighting in Blade/PHP files
Target specific PHP scopes for Rsx::Route() highlighting in Blade
Expand injection selector for Rsx::Route() highlighting
Add custom syntax highlighting for Rsx::Route() and Rsx.Route() calls
Update jqhtml packages to v2.2.165
Add bundle path validation for common mistakes (development mode only)
Create Ajax_Select_Input widget and Rsx_Reference_Data controller
Create Country_Select_Input widget with default country support
Initialize Tom Select on Select_Input widgets
Add Tom Select bundle for enhanced select dropdowns
Implement ISO 3166 geographic data system for country/region selection
Implement widget-based form system with disabled state support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-30 06:21:56 +00:00

544 lines
19 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');
}
// 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 = {};
/**
* 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];
}
}
}
/**
* Generate URL for a controller route
*
* This method generates URLs for controller actions by looking up route patterns
* and replacing parameters. It handles both regular 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
* // Simple route without parameters (defaults to 'index' action)
* const url = Rsx.Route('Frontend_Index_Controller');
* // Returns: /dashboard
*
* // Route with explicit action
* const url = Rsx.Route('Frontend_Index_Controller', 'index');
* // Returns: /dashboard
*
* // Route with integer parameter (sets 'id')
* const url = Rsx.Route('Frontend_Client_View_Controller', 'view', 123);
* // Returns: /clients/view/123
*
* // Route with named parameters (object)
* const url = Rsx.Route('Frontend_Client_View_Controller', 'view', {id: 'C001'});
* // Returns: /clients/view/C001
*
* // Route with required and query parameters
* const url = Rsx.Route('Frontend_Client_View_Controller', 'view', {
* id: 'C001',
* tab: 'history'
* });
* // Returns: /clients/view/C001?tab=history
*
* // Route not found - uses default pattern
* const url = Rsx.Route('Unimplemented_Controller', 'some_action', {foo: 'bar'});
* // Returns: /_/Unimplemented_Controller/some_action?foo=bar
*
* // Placeholder route
* const url = Rsx.Route('Future_Controller', '#index');
* // Returns: #
* ```
*
* @param {string} class_name The controller class name (e.g., 'User_Controller')
* @param {string} [action_name='index'] The action/method name (defaults to 'index'). Use '#action' for placeholders.
* @param {number|Object} [params=null] Route parameters. Integer sets 'id', object provides named params.
* @returns {string} The generated URL
*/
static Route(class_name, action_name = 'index', params = null) {
// 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 definitions
let pattern;
if (Rsx._routes[class_name] && Rsx._routes[class_name][action_name]) {
pattern = Rsx._routes[class_name][action_name];
} else {
// Route not found - use default pattern /_/{controller}/{action}
pattern = `/_/${class_name}/${action_name}`;
}
// Generate URL from pattern
return Rsx._generate_url_from_pattern(pattern, params_obj);
}
/**
* 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;
}
/**
* 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;
// 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 page state from URL hash
*
* Usage:
* ```javascript
* const state = Rsx.get_all_page_state();
* // Returns: {dg_page: '2', dg_sort: 'name'}
* ```
*
* @returns {Object} All hash parameters as key-value pairs
*/
static get_all_page_state() {
return Rsx._parse_hash();
}
/**
* Get single value from URL hash state
*
* Usage:
* ```javascript
* const page = Rsx.get_page_state('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 get_page_state(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.set_page_state('dg_page', 2);
* // URL becomes: http://example.com/page#dg_page=2
*
* Rsx.set_page_state('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 set_page_state(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.set_all_page_state({dg_page: 2, dg_sort: 'name'});
* // URL becomes: http://example.com/page#dg_page=2&dg_sort=name
* ```
*
* @param {Object} new_state Object with key-value pairs to set
*/
static set_all_page_state(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);
}
}