Files
BarangaySystem/resources/js/composables/Core/useNavigate.js
2026-06-06 18:43:00 +08:00

228 lines
8.0 KiB
JavaScript

// resources/js/composables/Core/useNavigate.js
import { getCurrentInstance, h } from 'vue'
import { encodeHash, encodePayload } from '../useUrlEncoder'
import { useUIStore } from '../../stores/ui'
import { useModal } from './useModal'
// Page module map — populated once from app.js for prefetching
let _pageModules = null;
export function setPageModules(modules) { _pageModules = modules; }
// Track which pages have already been prefetched to avoid duplicate calls
const _prefetchedPages = new Set();
export function useNavigate({ currentPage, currentProps, loading } = {}) {
const instance = getCurrentInstance()
const uiStore = useUIStore()
const modal = useModal()
// Try to get the global navigate helper first from window.$navigateHelper,
// then fall back to instance?.proxy?.$navigate
const globalNavigate = (window.$navigateHelper || instance?.proxy?.$navigate)
/**
* Prefetch a page component chunk by name.
* Call on hover/focus to start loading the JS bundle before navigation.
*/
const prefetch = (pageName) => {
if (!pageName || !_pageModules || _prefetchedPages.has(pageName)) return;
const loader = _pageModules[pageName];
if (loader) {
_prefetchedPages.add(pageName);
loader().catch(() => { /* chunk may not exist, ignore */ });
}
}
const navigate = async (arg1, arg2 = {}) => {
// Universal navigate handler that accepts:
// 1. navigate({ page: 'Home', props: {} })
// 2. navigate('Home', { target: '...' })
let page, props, delay, fromPopState;
if (typeof arg1 === 'string') {
page = arg1;
props = arg2.props || arg2 || {};
delay = arg2.delay || 0;
fromPopState = arg2.fromPopState || false;
} else {
({ page, props = {}, delay = 0, fromPopState = false } = arg1);
}
if (!page) {
console.error('[useNavigate] Navigation failed: No page specified.', { arg1, arg2 });
return;
}
// Global Page Disabled Check — fire-and-forget refresh if stale (don't block navigation)
const cacheThreshold = 30 * 1000; // 30 seconds
const isStale = !uiStore.lastSynced || (new Date() - new Date(uiStore.lastSynced) > cacheThreshold);
if (isStale) {
// Non-blocking: refresh in background, don't await
uiStore.refreshSettings().catch(e => {
console.error('[useNavigate] Failed to refresh settings during navigation:', e);
});
}
const pageLower = page.toLowerCase();
const isDisabled = uiStore.disabledPages && uiStore.disabledPages.some(p => p.toLowerCase() === pageLower);
if (isDisabled) {
console.warn(`[useNavigate] Page ${page} is globally disabled.`);
modal.open({
title: 'Access Restricted',
body: h('div', { class: 'text-center p-4' }, [
h('div', { class: 'icon-shape bg-soft-danger text-danger rounded-circle mx-auto mb-4 p-3 shadow-sm' }, [
h('i', { class: 'fas fa-lock fa-3x' })
]),
h('h4', { class: 'fw-black mb-3 text-dark' }, 'Page Offline'),
h('p', { class: 'text-muted mb-4 px-3' }, `The requested page (${page}) has been temporarily disabled by the system administrator.`),
h('button', {
class: 'btn btn-primary rounded-pill px-5 py-2 fw-bold shadow-primary-sm',
onClick: () => {
modal.close();
if (pageLower !== 'home') navigate({ page: 'Home' });
}
}, 'Back to Dashboard')
]),
footer: null
});
if (loading) loading.value = false;
return;
}
// If local refs are provided (root app usage), update them directly
if (currentPage || currentProps || loading) {
if (loading) loading.value = true
if (delay) await new Promise(r => setTimeout(r, delay))
if (currentPage) currentPage.value = page
if (currentProps) currentProps.value = props
// Update browser URL (only if NOT coming from a popstate event)
if (!fromPopState) {
let url;
if (page === 'Home') {
url = '/';
} else if (page === 'Auth.Login') {
url = '/login';
} else {
// Check for special props that should be encoded into URL
const hashkeyProp = props?.target || props?.target_user || props?.hashkey || props?.id;
const payloadProp = props?.payload;
let basePageUrl = `/${page
.replace(/\./g, '/')
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.toLowerCase()}`;
if (hashkeyProp) {
// Encode the hashkey into URL as --h:ENCODED
const encodedHash = encodeHash(hashkeyProp);
url = `${basePageUrl}--${encodedHash}`;
} else if (payloadProp) {
// Encode the payload into URL as --e:ENCODED_PAYLOAD
const encodedPayload = encodePayload(payloadProp);
url = `${basePageUrl}--${encodedPayload}`;
} else {
url = basePageUrl;
}
}
window.history.pushState({ page, props }, '', url);
}
if (loading) loading.value = false
return
}
// In child components, fall back to the global $navigate helper
if (globalNavigate) {
globalNavigate({ page, props, delay, fromPopState })
} else {
console.warn('[useNavigate] No local refs and global navigate helper not found.')
}
}
const reloadPage = () => {
if (currentPage) {
navigate({ page: currentPage.value, props: currentProps?.value, delay: 0 })
} else if (globalNavigate) {
// In children, we can't easily get 'currentPage' unless we pass it or use a store
// but usually reloadPage is used in the context that knows the current state.
console.warn('[useNavigate] reloadPage called in child without refs.')
}
}
return { navigate, reloadPage, prefetch }
}
/**
* Helper to derive component name from a URL path in a generic way.
* Matches the logic in VueRouteMap.php.
*/
const getPageFromUrl = (path) => {
let cleanPath = path.trim().replace(/^\/+|\/+$/g, '');
if (!cleanPath) return 'Home';
// Handle explicit common mappings
if (cleanPath === 'login') return 'Auth.Login';
if (cleanPath === 'app') return 'Home';
// Strip hash/payload suffix (e.g., "view-store-market--h:ABC" -> "view-store-market")
const slug = cleanPath.split('--h:')[0].split('--e:')[0];
// Convert kebab-case/path to PascalCase/dots (e.g., "market/list-products" -> "Market.ListProducts")
return slug.split('/').map(part => {
return part.split('-').map(subPart => {
if (!subPart) return '';
return subPart.charAt(0).toUpperCase() + subPart.slice(1);
}).join('');
}).join('.');
}
// Global popstate listener to handle browser back button
if (typeof window !== 'undefined') {
window.addEventListener('popstate', (event) => {
if (event.state && event.state.page) {
if (window.$navigateHelper) {
window.$navigateHelper({
page: event.state.page,
props: event.state.props,
fromPopState: true,
delay: 0
})
} else {
// Fallback if app not yet initialized
window.location.reload()
}
} else {
// If no state (initial load entry point), attempt to restore from initial DOM payload
const el = document.getElementById('main-body');
if (el && el.dataset.page) {
try {
const initialPage = JSON.parse(el.dataset.page);
if (window.$navigateHelper) {
window.$navigateHelper({
page: initialPage.component,
props: initialPage.props,
fromPopState: true,
delay: 0
});
return;
}
} catch (e) {
console.error('[useNavigate] Failed to parse initial page from DOM:', e);
}
}
// Ultimate Fallback: Derive the page from the URL (without props, as we can't reliably decode them here)
const page = getPageFromUrl(window.location.pathname);
if (window.$navigateHelper) {
window.$navigateHelper({ page, fromPopState: true, delay: 0 })
}
}
})
}