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>
70 lines
1.7 KiB
JavaScript
70 lines
1.7 KiB
JavaScript
/**
|
|
* Auto-dismisses flash notices after their data-flash-timeout expires.
|
|
*/
|
|
import { resolveHost } from '../core/app-dom.js';
|
|
|
|
export function initFlashAutoDismiss(root = document, options = {}) {
|
|
const {
|
|
selector = '.flash-stack .notice[data-flash-timeout]',
|
|
defaultTimeout = 0,
|
|
} = options;
|
|
const host = resolveHost(root);
|
|
const notices = Array.from(host.querySelectorAll(selector));
|
|
if (!notices.length) {
|
|
return { destroy: () => {} };
|
|
}
|
|
|
|
const timers = [];
|
|
let destroyed = false;
|
|
|
|
const postForm = async (form) => {
|
|
const action = form.getAttribute('action');
|
|
if (!action) {
|
|
return null;
|
|
}
|
|
const body = new URLSearchParams(new FormData(form));
|
|
return fetch(action, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/x-www-form-urlencoded',
|
|
'X-Requested-With': 'fetch',
|
|
},
|
|
body,
|
|
});
|
|
};
|
|
|
|
notices.forEach((notice) => {
|
|
const timeout = Number.parseInt(
|
|
notice.dataset.flashTimeout || `${defaultTimeout}`,
|
|
10
|
|
);
|
|
if (!timeout || timeout <= 0) {
|
|
return;
|
|
}
|
|
notice.style.setProperty('--flash-timeout', `${timeout}ms`);
|
|
notice.classList.add('flash-timed');
|
|
|
|
const timer = window.setTimeout(async () => {
|
|
if (destroyed) {
|
|
return;
|
|
}
|
|
const form = notice.querySelector('form');
|
|
if (form) {
|
|
const response = await postForm(form);
|
|
if (!response || !response.ok) {
|
|
return;
|
|
}
|
|
}
|
|
notice.remove();
|
|
}, timeout);
|
|
timers.push(timer);
|
|
});
|
|
|
|
const destroy = () => {
|
|
destroyed = true;
|
|
timers.forEach((timer) => window.clearTimeout(timer));
|
|
};
|
|
|
|
return { destroy };
|
|
}
|