fix: DockerSecretsResolver - don't normalize absolute paths like /var/www/html/...
Some checks failed
Deploy Application / deploy (push) Has been cancelled
Some checks failed
Deploy Application / deploy (push) Has been cancelled
This commit is contained in:
347
resources/js/modules/livecomponent/ProgressiveEnhancement.js
Normal file
347
resources/js/modules/livecomponent/ProgressiveEnhancement.js
Normal file
@@ -0,0 +1,347 @@
|
||||
/**
|
||||
* Progressive Enhancement for LiveComponents
|
||||
*
|
||||
* Provides automatic AJAX handling for links and forms within data-lc-boost containers.
|
||||
* Falls back gracefully to normal navigation/submit when JavaScript is disabled.
|
||||
*
|
||||
* Features:
|
||||
* - Automatic AJAX for links and forms
|
||||
* - Graceful degradation (works without JS)
|
||||
* - Integration with LiveComponents system
|
||||
* - SPA Router coordination (if available)
|
||||
*/
|
||||
|
||||
export class ProgressiveEnhancement {
|
||||
constructor(liveComponentManager) {
|
||||
this.liveComponentManager = liveComponentManager;
|
||||
this.boostContainers = new Set();
|
||||
this.initialized = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize progressive enhancement
|
||||
*
|
||||
* Sets up event listeners for boost containers and their links/forms.
|
||||
*/
|
||||
init() {
|
||||
if (this.initialized) {
|
||||
console.warn('[ProgressiveEnhancement] Already initialized');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find all boost containers
|
||||
this.findBoostContainers();
|
||||
|
||||
// Setup mutation observer to handle dynamically added boost containers
|
||||
this.setupMutationObserver();
|
||||
|
||||
// Setup global click handler for links
|
||||
document.addEventListener('click', (e) => this.handleLinkClick(e), true);
|
||||
|
||||
// Setup global submit handler for forms
|
||||
document.addEventListener('submit', (e) => this.handleFormSubmit(e), true);
|
||||
|
||||
this.initialized = true;
|
||||
console.log('[ProgressiveEnhancement] Initialized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all boost containers and setup handlers
|
||||
*/
|
||||
findBoostContainers() {
|
||||
const containers = document.querySelectorAll('[data-lc-boost="true"]');
|
||||
containers.forEach(container => {
|
||||
this.setupBoostContainer(container);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup boost container
|
||||
*
|
||||
* @param {HTMLElement} container - Boost container element
|
||||
*/
|
||||
setupBoostContainer(container) {
|
||||
if (this.boostContainers.has(container)) {
|
||||
return; // Already setup
|
||||
}
|
||||
|
||||
this.boostContainers.add(container);
|
||||
|
||||
// Mark links and forms as boosted (for identification)
|
||||
container.querySelectorAll('a[href], form[action]').forEach(element => {
|
||||
if (!element.hasAttribute('data-lc-boost')) {
|
||||
element.setAttribute('data-lc-boost', 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Setup mutation observer to detect new boost containers
|
||||
*/
|
||||
setupMutationObserver() {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
mutation.addedNodes.forEach((node) => {
|
||||
if (node.nodeType === Node.ELEMENT_NODE) {
|
||||
// Check if node itself is a boost container
|
||||
if (node.hasAttribute && node.hasAttribute('data-lc-boost') &&
|
||||
node.getAttribute('data-lc-boost') === 'true') {
|
||||
this.setupBoostContainer(node);
|
||||
}
|
||||
|
||||
// Check for boost containers within added node
|
||||
const boostContainers = node.querySelectorAll?.('[data-lc-boost="true"]');
|
||||
if (boostContainers) {
|
||||
boostContainers.forEach(container => {
|
||||
this.setupBoostContainer(container);
|
||||
});
|
||||
}
|
||||
|
||||
// Check for links/forms within boost containers
|
||||
const links = node.querySelectorAll?.('a[href], form[action]');
|
||||
if (links) {
|
||||
links.forEach(element => {
|
||||
const boostContainer = element.closest('[data-lc-boost="true"]');
|
||||
if (boostContainer && !element.hasAttribute('data-lc-boost')) {
|
||||
element.setAttribute('data-lc-boost', 'true');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle link clicks
|
||||
*
|
||||
* @param {Event} event - Click event
|
||||
*/
|
||||
handleLinkClick(event) {
|
||||
const link = event.target.closest('a[href]');
|
||||
if (!link) return;
|
||||
|
||||
// Check if link is in a boost container
|
||||
const boostContainer = link.closest('[data-lc-boost="true"]');
|
||||
if (!boostContainer) return;
|
||||
|
||||
// Check if link explicitly opts out
|
||||
if (link.hasAttribute('data-lc-boost') && link.getAttribute('data-lc-boost') === 'false') {
|
||||
return; // Let normal navigation happen
|
||||
}
|
||||
|
||||
// Check for special link types that should not be boosted
|
||||
const href = link.getAttribute('href');
|
||||
if (!href || href === '#' || href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:')) {
|
||||
return; // Let normal behavior happen
|
||||
}
|
||||
|
||||
// Check if target is _blank (new window)
|
||||
if (link.getAttribute('target') === '_blank') {
|
||||
return; // Let normal behavior happen
|
||||
}
|
||||
|
||||
// Prevent default navigation
|
||||
event.preventDefault();
|
||||
|
||||
// Try to use SPA Router if available
|
||||
if (window.SPARouter && typeof window.SPARouter.navigate === 'function') {
|
||||
window.SPARouter.navigate(href);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: Use fetch to load content
|
||||
this.loadContentViaAjax(href, link);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle form submissions
|
||||
*
|
||||
* @param {Event} event - Submit event
|
||||
*/
|
||||
handleFormSubmit(event) {
|
||||
const form = event.target;
|
||||
if (!form || form.tagName !== 'FORM') return;
|
||||
|
||||
// Check if form is in a boost container
|
||||
const boostContainer = form.closest('[data-lc-boost="true"]');
|
||||
if (!boostContainer) return;
|
||||
|
||||
// Check if form explicitly opts out
|
||||
if (form.hasAttribute('data-lc-boost') && form.getAttribute('data-lc-boost') === 'false') {
|
||||
return; // Let normal submit happen
|
||||
}
|
||||
|
||||
// Check if form has data-live-action (handled by LiveComponents)
|
||||
if (form.hasAttribute('data-live-action')) {
|
||||
return; // Let LiveComponents handle it
|
||||
}
|
||||
|
||||
// Prevent default submission
|
||||
event.preventDefault();
|
||||
|
||||
// Submit form via AJAX
|
||||
this.submitFormViaAjax(form);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load content via AJAX
|
||||
*
|
||||
* @param {string} url - URL to load
|
||||
* @param {HTMLElement} link - Link element that triggered the load
|
||||
*/
|
||||
async loadContentViaAjax(url, link) {
|
||||
try {
|
||||
// Show loading state
|
||||
link.setAttribute('aria-busy', 'true');
|
||||
link.classList.add('lc-loading');
|
||||
|
||||
// Fetch content
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'text/html'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
// Try to extract main content (look for <main> tag)
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const mainContent = doc.querySelector('main') || doc.body;
|
||||
|
||||
// Update page content
|
||||
const currentMain = document.querySelector('main') || document.body;
|
||||
if (currentMain) {
|
||||
currentMain.innerHTML = mainContent.innerHTML;
|
||||
} else {
|
||||
document.body.innerHTML = mainContent.innerHTML;
|
||||
}
|
||||
|
||||
// Update URL without reload
|
||||
window.history.pushState({}, '', url);
|
||||
|
||||
// Reinitialize LiveComponents
|
||||
if (this.liveComponentManager) {
|
||||
const components = document.querySelectorAll('[data-live-component]');
|
||||
components.forEach(component => {
|
||||
this.liveComponentManager.init(component);
|
||||
});
|
||||
}
|
||||
|
||||
// Dispatch custom event
|
||||
window.dispatchEvent(new CustomEvent('lc:boost:navigated', {
|
||||
detail: { url, link }
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ProgressiveEnhancement] Failed to load content:', error);
|
||||
|
||||
// Fallback to normal navigation on error
|
||||
window.location.href = url;
|
||||
} finally {
|
||||
// Remove loading state
|
||||
link.removeAttribute('aria-busy');
|
||||
link.classList.remove('lc-loading');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit form via AJAX
|
||||
*
|
||||
* @param {HTMLFormElement} form - Form element
|
||||
*/
|
||||
async submitFormViaAjax(form) {
|
||||
try {
|
||||
// Show loading state
|
||||
form.setAttribute('aria-busy', 'true');
|
||||
form.classList.add('lc-loading');
|
||||
|
||||
const formData = new FormData(form);
|
||||
const method = form.method.toUpperCase() || 'POST';
|
||||
const action = form.action || window.location.href;
|
||||
|
||||
// Fetch response
|
||||
const response = await fetch(action, {
|
||||
method: method,
|
||||
body: formData,
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
'Accept': 'text/html'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const html = await response.text();
|
||||
|
||||
// Check if response is a redirect
|
||||
if (response.redirected) {
|
||||
// Follow redirect
|
||||
await this.loadContentViaAjax(response.url, form);
|
||||
return;
|
||||
}
|
||||
|
||||
// Try to extract main content
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(html, 'text/html');
|
||||
const mainContent = doc.querySelector('main') || doc.body;
|
||||
|
||||
// Update page content
|
||||
const currentMain = document.querySelector('main') || document.body;
|
||||
if (currentMain) {
|
||||
currentMain.innerHTML = mainContent.innerHTML;
|
||||
} else {
|
||||
document.body.innerHTML = mainContent.innerHTML;
|
||||
}
|
||||
|
||||
// Update URL if form has action
|
||||
if (form.action && form.action !== window.location.href) {
|
||||
window.history.pushState({}, '', form.action);
|
||||
}
|
||||
|
||||
// Reinitialize LiveComponents
|
||||
if (this.liveComponentManager) {
|
||||
const components = document.querySelectorAll('[data-live-component]');
|
||||
components.forEach(component => {
|
||||
this.liveComponentManager.init(component);
|
||||
});
|
||||
}
|
||||
|
||||
// Dispatch custom event
|
||||
window.dispatchEvent(new CustomEvent('lc:boost:submitted', {
|
||||
detail: { form, action }
|
||||
}));
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ProgressiveEnhancement] Failed to submit form:', error);
|
||||
|
||||
// Fallback to normal submit on error
|
||||
form.submit();
|
||||
} finally {
|
||||
// Remove loading state
|
||||
form.removeAttribute('aria-busy');
|
||||
form.classList.remove('lc-loading');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default ProgressiveEnhancement;
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user