Completes the generic module platform that enables modules to contribute
UI elements, runtime JS components, and search resources without any
core hardcoding.
New generic UI slot types:
- topbar.right_item: module-contributed topbar buttons
- layout.body_end_template: module-contributed dialog/overlay templates
- layout.head_style: module-contributed global CSS
- runtime.component: declarative JS component registration with phase ordering
New infrastructure:
- ModuleAutoloader: PSR-4 autoloading for module-local PHP classes
- ModuleRuntimePageBuilder: symlinks module pages into runtime directory
- ModuleRuntimeAssetPublisher: publishes module CSS/JS to web/modules/
- ModulePermissionSynchronizer: syncs module permissions to DB
- CLI scripts: module-runtime-sync, module-build, module-migrate,
module-permissions-sync, module-assets-sync
- {{userId}} placeholder in SearchDataService for user-scoped search queries
- Component runtime with phased initialization (early/default/late)
- AppContainer.protectExistingBindings() to prevent module→core overwrites
- Architecture tests: ModuleStructureContractTest, CoreTemplateIsolationTest,
FrontendComponentRuntimeContractTest, AppContainerIsolationContractTest
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
94 lines
3.0 KiB
JavaScript
94 lines
3.0 KiB
JavaScript
import './core/app-telemetry.js';
|
|
import { createComponentRuntime } from './core/app-component-runtime.js';
|
|
import { resolveHost } from './core/app-dom.js';
|
|
import { initFlashAutoDismiss } from './components/app-flash-auto-dismiss.js';
|
|
import { initPasswordHints } from './components/app-password-hints.js';
|
|
import { initPasswordToggles } from './components/app-password-toggle.js';
|
|
|
|
const initLoginErrorFocus = (root = document, config = {}) => {
|
|
const host = resolveHost(root);
|
|
const selector = String(config.selector || '.notice[data-variant="error"][role="alert"][tabindex="-1"]').trim()
|
|
|| '.notice[data-variant="error"][role="alert"][tabindex="-1"]';
|
|
const errorNotice = host.querySelector(selector);
|
|
if (errorNotice instanceof HTMLElement) {
|
|
errorNotice.focus();
|
|
}
|
|
return { destroy: () => {} };
|
|
};
|
|
|
|
const initLoginSubmitLock = (root = document, config = {}) => {
|
|
const host = resolveHost(root);
|
|
const selector = String(config.selector || 'form[data-login-submit-lock="1"]').trim() || 'form[data-login-submit-lock="1"]';
|
|
const forms = Array.from(host.querySelectorAll(selector)).filter(
|
|
(form) => form instanceof HTMLFormElement
|
|
);
|
|
if (!forms.length) {
|
|
return { destroy: () => {} };
|
|
}
|
|
|
|
const cleanupFns = [];
|
|
forms.forEach((form) => {
|
|
if (form.dataset.loginSubmitLockBound === '1') {
|
|
return;
|
|
}
|
|
form.dataset.loginSubmitLockBound = '1';
|
|
|
|
const onSubmit = () => {
|
|
if (form.dataset.submitted === '1') {
|
|
return;
|
|
}
|
|
form.dataset.submitted = '1';
|
|
form.setAttribute('aria-busy', 'true');
|
|
const submitButton = form.querySelector('button[type="submit"]');
|
|
if (!(submitButton instanceof HTMLButtonElement)) {
|
|
return;
|
|
}
|
|
submitButton.disabled = true;
|
|
submitButton.setAttribute('aria-busy', 'true');
|
|
submitButton.dataset.submitState = 'loading';
|
|
};
|
|
form.addEventListener('submit', onSubmit);
|
|
cleanupFns.push(() => {
|
|
form.removeEventListener('submit', onSubmit);
|
|
delete form.dataset.loginSubmitLockBound;
|
|
});
|
|
});
|
|
|
|
return {
|
|
destroy: () => {
|
|
cleanupFns.forEach((cleanup) => cleanup());
|
|
},
|
|
};
|
|
};
|
|
|
|
const runtime = createComponentRuntime({
|
|
root: document,
|
|
configId: 'login-components',
|
|
});
|
|
|
|
runtime.register('flash-auto-dismiss', initFlashAutoDismiss, {
|
|
selector: '[data-app-component="flash-auto-dismiss"]',
|
|
configPath: 'components.flashAutoDismiss',
|
|
defaultConfig: {
|
|
selector: '.notice[data-flash-timeout]',
|
|
},
|
|
});
|
|
runtime.register('password-hints', initPasswordHints, {
|
|
scope: 'global',
|
|
configPath: 'components.passwordHints',
|
|
});
|
|
runtime.register('password-toggle', initPasswordToggles, {
|
|
scope: 'global',
|
|
configPath: 'components.passwordToggle',
|
|
});
|
|
runtime.register('login-error-focus', initLoginErrorFocus, {
|
|
scope: 'global',
|
|
configPath: 'components.loginErrorFocus',
|
|
});
|
|
runtime.register('login-submit-lock', initLoginSubmitLock, {
|
|
scope: 'global',
|
|
configPath: 'components.loginSubmitLock',
|
|
});
|
|
|
|
runtime.mountAll();
|