ultimate-jekyll-manager
Version:
Ultimate Jekyll dependency manager
493 lines (405 loc) • 15.7 kB
JavaScript
// Libraries
let webManager = null;
// Module
export default (Manager) => {
return new Promise(async function (resolve) {
// Shortcuts
webManager = Manager.webManager;
// Initialize when DOM is ready
await webManager.dom().ready();
try {
// Initialize iframe resizing
initializeIframeResize();
// Initialize admin navigation
await initializeAdminNavigation();
} catch (error) {
webManager.sentry().captureException(new Error('Failed to initialize admin panel', { cause: error }));
}
// Resolve after initialization
return resolve();
});
};
// Global variables
let $iframe = null;
let $sidebar = null;
let lastClickedExternalUrl = null;
let isNavigatingFromHash = false;
// Initialize iframe dynamic resizing
function initializeIframeResize() {
function resizeIframe() {
const $container = document.querySelector('.admin-iframe-container');
const $pageHeader = document.querySelector('.page-header');
const $mainContent = document.querySelector('#main-content');
if (!$container) {
return;
}
// Get the actual position of the iframe container
const containerRect = $container.getBoundingClientRect();
const topPosition = containerRect.top + window.scrollY;
// Account for current position from top and main content padding
const mainPadding = $mainContent ? parseInt(window.getComputedStyle($mainContent).paddingBottom) : 0;
// Calculate available height
const availableHeight = window.innerHeight - topPosition - mainPadding;
// Set the iframe container height and as important to override any inline styles
// $container.style.height = availableHeight + 'px';
$container.style.setProperty('height', availableHeight + 'px', 'important');
}
// Initial resize after a small delay to ensure layout is complete
setTimeout(resizeIframe, 50);
// Resize on window resize
window.addEventListener('resize', resizeIframe);
}
// Initialize admin navigation system
async function initializeAdminNavigation() {
// Get DOM elements
$iframe = document.getElementById('admin-content-frame');
$sidebar = document.querySelector('.sidebar');
if (!$iframe) {
console.warn('[Admin] No iframe found');
return;
}
// Set up iframe load listener
$iframe.addEventListener('load', handleIframeLoad);
// Set up sidebar link handlers
setupSidebarLinkHandlers();
// Check for hash on page load and navigate if present
navigateToHash();
// Listen for hash changes
window.addEventListener('hashchange', function() {
console.log('[Admin] Hash changed to:', window.location.hash);
navigateToHash();
});
// Listen for messages from iframe
window.addEventListener('message', handleIframeMessage);
// Initial active state
updateSidebarActive($iframe.src);
}
// Handle iframe load events
function handleIframeLoad() {
console.log('[Admin] Iframe loaded, checking URL...');
try {
// Try to get the actual URL (will fail for cross-origin)
const iframeUrl = $iframe.contentWindow.location.href;
updateSidebarActive(iframeUrl);
} catch (e) {
// Cross-origin restriction - external site or different domain
handleCrossOriginIframe();
}
}
// Handle cross-origin iframe scenarios
function handleCrossOriginIframe() {
// Update header for external site
const $breadcrumbEl = document.getElementById('admin-page-breadcrumb');
const $titleEl = document.getElementById('admin-page-title');
if (lastClickedExternalUrl) {
// Try to extract a reasonable name from the URL
try {
const url = new URL(lastClickedExternalUrl);
const siteName = url.hostname.replace('www.', '').split('.')[0];
const formattedName = siteName.charAt(0).toUpperCase() + siteName.slice(1);
if ($breadcrumbEl) $breadcrumbEl.textContent = formattedName;
if ($titleEl) $titleEl.textContent = formattedName;
} catch (e) {
if ($breadcrumbEl) $breadcrumbEl.textContent = 'External';
if ($titleEl) $titleEl.textContent = 'External';
}
}
// Update sidebar active state for external URL
const urlToMatch = lastClickedExternalUrl || $iframe.src;
if (!$sidebar) return;
// Clear all active states first
const $allLinks = $sidebar.querySelectorAll('a[href]');
$allLinks.forEach(link => {
link.classList.remove('active');
});
// Find and activate the matching external link
if (urlToMatch) {
$allLinks.forEach(link => {
const linkHref = link.getAttribute('href');
// Match against the tracked external URL
if (linkHref && linkHref === urlToMatch) {
link.classList.add('active');
// Also activate parent elements if nested
const $parentNavItem = link.closest('.nav-item');
if ($parentNavItem) {
$parentNavItem.classList.add('active');
}
// If it's in a dropdown, activate the parent toggle
const $parentDropdown = link.closest('.dropdown');
if ($parentDropdown) {
const $dropdownToggle = $parentDropdown.querySelector('.nav-link');
if ($dropdownToggle) {
$dropdownToggle.classList.add('active');
}
}
}
});
}
}
// Update sidebar active state based on iframe URL
function updateSidebarActive(iframeUrl) {
if (!$sidebar) {
return;
}
// Parse the URL to get the path
let iframePath;
try {
const url = new URL(iframeUrl);
iframePath = url.pathname;
} catch (e) {
return;
}
// Find ALL links in the sidebar
const $allLinks = $sidebar.querySelectorAll('a[href]');
// Remove active from all links
$allLinks.forEach(link => {
link.classList.remove('active');
});
// Also remove active from all nav-items
const $allNavItems = $sidebar.querySelectorAll('.nav-item');
$allNavItems.forEach(item => {
item.classList.remove('active');
});
// Find and activate matching link
$allLinks.forEach(link => {
const linkHref = link.getAttribute('href');
if (!linkHref) {
return;
}
// Normalize paths for comparison (remove trailing slashes)
const normalizedLinkPath = linkHref.replace(/\/$/, '');
const normalizedIframePath = iframePath.replace(/\/$/, '');
// Check for exact match
if (normalizedIframePath === normalizedLinkPath) {
link.classList.add('active');
// Also activate parent nav-item if it's a nested link
const $parentNavItem = link.closest('.nav-item');
if ($parentNavItem) {
$parentNavItem.classList.add('active');
}
// If link is inside a collapsible section, expand it
const $parentCollapse = link.closest('.collapse');
if ($parentCollapse) {
// Add show class to expand the collapse
$parentCollapse.classList.add('show');
// Find and update the toggle button
const collapseId = $parentCollapse.id;
const $collapseToggle = $sidebar ? $sidebar.querySelector(`[data-bs-target="#${collapseId}"]`) : null;
if ($collapseToggle) {
$collapseToggle.classList.remove('collapsed');
$collapseToggle.setAttribute('aria-expanded', 'true');
}
}
// If link is inside a dropdown menu, activate and expand parent dropdown
const $parentDropdownMenu = link.closest('.dropdown-menu');
if ($parentDropdownMenu) {
// Show the dropdown menu
$parentDropdownMenu.classList.add('show');
// Find the parent dropdown container
const $parentDropdown = $parentDropdownMenu.closest('.dropdown');
if ($parentDropdown) {
// Find and activate the dropdown toggle (direct child only)
const $dropdownToggle = $parentDropdown.querySelector(':scope > .nav-link[data-bs-toggle="dropdown"]');
if ($dropdownToggle) {
$dropdownToggle.classList.add('active');
$dropdownToggle.setAttribute('aria-expanded', 'true');
}
}
}
}
});
}
// Handle sidebar link clicks
function handleSidebarLink(e) {
const $link = e.currentTarget;
const href = $link.getAttribute('href');
const target = $link.getAttribute('target');
if (!href) return;
// Analyze link type
const isExternalLink = href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//');
const isAdminLink = !isExternalLink && (href.includes('/admin') || href.includes('admin'));
console.log('[Admin] Link clicked:', {
href: href,
isExternal: isExternalLink,
isAdmin: isAdminLink,
target: target
});
// Determine action based on link type and target
let action = 'parent'; // default
if (isAdminLink) {
action = 'iframe';
} else if (isExternalLink) {
if (target === '_self') {
action = 'iframe';
} else if (target === '_blank') {
action = 'new-tab';
} else if (target === '_parent' || !target) {
action = 'parent';
}
} else {
// Internal non-admin links
action = 'parent';
}
// Execute action
switch (action) {
case 'iframe':
e.preventDefault();
e.stopPropagation();
console.log('[Admin] Loading in iframe:', href);
// Track if this is an external URL
if (isExternalLink) {
lastClickedExternalUrl = href;
} else {
lastClickedExternalUrl = null;
}
$iframe.src = href;
// Update URL hash for admin links (but not if we're navigating from hash)
if (isAdminLink && !isNavigatingFromHash) {
// Extract the path after /admin/
const adminPath = href.replace(/^.*\/admin\//, '').replace(/\/$/, '');
if (adminPath && adminPath !== 'admin') {
window.location.hash = adminPath;
console.log('[Admin] Updated URL hash to:', adminPath);
} else {
window.location.hash = '';
}
}
// Update active state
updateSidebarActive(href);
// Don't close dropdowns/collapses if the clicked link is inside one
// This keeps them open for nested navigation
const $clickedDropdownMenu = $link.closest('.dropdown-menu');
const $clickedCollapse = $link.closest('.collapse');
if (!$clickedDropdownMenu) {
// Close any open dropdowns only if we're not clicking inside one
const $openDropdowns = $sidebar.querySelectorAll('.dropdown-menu.show');
$openDropdowns.forEach(dropdown => {
dropdown.classList.remove('show');
const $toggle = dropdown.previousElementSibling;
if ($toggle) $toggle.setAttribute('aria-expanded', 'false');
});
}
// Note: We intentionally don't close collapse sections automatically
// as they typically stay open for navigation purposes
// Collapse mobile sidebar if open (but only if it's the mobile menu, not a regular collapse)
if (window.innerWidth < 768) {
const $mobileSidebar = $sidebar ? $sidebar.querySelector('.navbar-collapse.show') : null;
if ($mobileSidebar) {
$mobileSidebar.classList.remove('show');
}
}
break;
case 'new-tab':
// Let default behavior handle it
console.log('[Admin] Opening in new tab:', href);
break;
case 'parent':
// Let default behavior handle it
console.log('[Admin] Navigating parent window to:', href);
break;
}
}
// Set up sidebar link handlers
function setupSidebarLinkHandlers() {
if (!$sidebar) return;
const $sidebarLinks = $sidebar.querySelectorAll('a[href]');
$sidebarLinks.forEach(link => {
// Remove any existing click handlers by cloning
const $newLink = link.cloneNode(true);
link.parentNode.replaceChild($newLink, link);
// Add our handler
$newLink.addEventListener('click', handleSidebarLink, true);
});
}
// Navigate to hash location
function navigateToHash() {
const hash = window.location.hash.slice(1); // Remove the #
if (hash) {
console.log('[Admin] Hash detected:', hash);
// Build the full admin URL
const adminPath = '/admin/' + hash;
// Get base URL from current page
const baseUrl = window.location.pathname.replace(/\/admin.*$/, '');
const fullPath = baseUrl + adminPath;
// Find the matching sidebar link
const $sidebarLinks = $sidebar ? $sidebar.querySelectorAll('a[href]') : [];
let $targetLink = null;
$sidebarLinks.forEach(link => {
const linkHref = link.getAttribute('href');
if (linkHref && linkHref.replace(/\/$/, '') === fullPath.replace(/\/$/, '')) {
$targetLink = link;
}
});
if ($targetLink) {
console.log('[Admin] Found matching link for hash, triggering click:', fullPath);
// Check if the link is inside a collapsible section
const $parentCollapse = $targetLink.closest('.collapse');
if ($parentCollapse) {
// Find the toggle button for this collapse
const collapseId = $parentCollapse.id;
const $collapseToggle = $sidebar ? $sidebar.querySelector(`[data-bs-target="#${collapseId}"]`) : null;
if ($collapseToggle && !$parentCollapse.classList.contains('show')) {
console.log('[Admin] Opening collapse section before clicking nested link');
// Click the toggle to expand the collapse
$collapseToggle.click();
}
}
// Click the link immediately
isNavigatingFromHash = true;
$targetLink.click();
isNavigatingFromHash = false;
} else {
// No matching link found, just load the URL directly
console.log('[Admin] No matching sidebar link found, loading URL directly:', fullPath);
$iframe.src = fullPath;
updateSidebarActive(fullPath);
}
} else {
// No hash, default to dashboard
updateSidebarActive($iframe.src);
}
}
// Handle messages from iframe
function handleIframeMessage(event) {
console.log('[Admin] Received message:', event?.data?.command, event.data);
// Ignore messages without our prefix
if (!event.data || typeof event.data.command !== 'string' || !event.data.command.startsWith('uj:admin:')) {
return;
}
// Handle iframe navigation messages
if (event.data.command === 'uj:admin:iframe-navigation' && event.data.payload) {
// Update sidebar active state
updateSidebarActive(event.data.payload.url);
// Update URL hash based on iframe navigation
try {
const iframeUrl = new URL(event.data.payload.url);
const iframePath = iframeUrl.pathname;
// If it's an admin path, update the hash
if (iframePath.includes('/admin/')) {
const adminPath = iframePath.replace(/^.*\/admin\//, '').replace(/\/$/, '');
if (adminPath && adminPath !== 'admin') {
window.location.hash = adminPath;
console.log('[Admin] Updated URL hash from iframe navigation to:', adminPath);
} else if (iframePath.endsWith('/admin') || iframePath.endsWith('/admin/')) {
window.location.hash = '';
}
}
} catch (e) {
console.error('[Admin] Error parsing iframe navigation URL:', e);
}
// Update page title and breadcrumb if meta info is provided
if (event.data.payload.meta) {
const meta = event.data.payload.meta;
// Update the breadcrumb text
const $breadcrumbEl = document.getElementById('admin-page-breadcrumb');
if ($breadcrumbEl && meta.breadcrumb) {
$breadcrumbEl.textContent = meta.breadcrumb;
}
// Optionally also update the title
const $titleEl = document.getElementById('admin-page-title');
if ($titleEl && meta.breadcrumb) {
$titleEl.textContent = meta.breadcrumb;
}
}
}
}