MediaWiki:Common.js:修订间差异
外观
(未显示1个用户的20个中间版本) | |||
第1行: | 第1行: | ||
const rootStyles = getComputedStyle(document.documentElement); | |||
const hexColor = rootStyles.getPropertyValue('--background-color-base').trim(); | |||
function hexToRgb(hex) { | |||
hex = hex.replace('#', ''); | |||
if (hex.length === 3) { | |||
hex = hex.split('').map(c => c + c).join(''); | |||
} | |||
const r = parseInt(hex.substring(0, 2), 16); | |||
const g = parseInt(hex.substring(2, 4), 16); | |||
const b = parseInt(hex.substring(4, 6), 16); | |||
return `${r}, ${g}, ${b}`; | |||
} | |||
const rgbColor = hexToRgb(hexColor); | |||
document.documentElement.style.setProperty( | |||
'--background-color-rgb', | |||
rgbColor | |||
); | |||
console.log('转换结果:', rgbColor); | |||
//上传重定向 | |||
mw.loader.using(['mediawiki.util', 'oojs-ui', 'mediawiki.storage'], function () { | |||
if (mw.config.get('wgCanonicalSpecialPageName') === 'Upload') { | |||
const STORAGE_KEY = 'uploadPreference'; | |||
const savedChoice = mw.storage.get(STORAGE_KEY); | |||
// 用户之前选择了上传向导,自动跳转 | |||
if (savedChoice === 'wizard') { | |||
window.location.href = mw.util.getUrl('Special:UploadWizard'); | |||
return; | |||
} | |||
// 用户选择了继续使用传统上传,不再提示 | |||
if (savedChoice === 'classic') { | |||
return; | |||
} | |||
// 否则弹出选择窗口 | |||
var windowManager = new OO.ui.WindowManager(); | |||
$(document.body).append(windowManager.$element); | |||
function UploadDialog(config) { | |||
UploadDialog.super.call(this, config); | |||
} | |||
OO.inheritClass(UploadDialog, OO.ui.ProcessDialog); | |||
UploadDialog.static.name = 'UploadDialog'; | |||
UploadDialog.static.title = '请选择上传方式'; | |||
UploadDialog.static.actions = [ | |||
{ | |||
action: 'classic', | |||
label: '❌ 使用传统上传方式(更快)', | |||
flags: ['safe'] | |||
} | |||
]; | |||
UploadDialog.prototype.initialize = function () { | |||
UploadDialog.super.prototype.initialize.call(this); | |||
var panel = new OO.ui.PanelLayout({ padded: true, expanded: false }); | |||
// 创建“记住我的选择”复选框 | |||
var rememberCheckbox = new OO.ui.CheckboxInputWidget({ selected: false }); | |||
this._rememberCheckbox = rememberCheckbox; | |||
var rememberField = new OO.ui.FieldLayout(rememberCheckbox, { | |||
label: '记住我的选择,下次不再提示', | |||
align: 'inline' | |||
}); | |||
// 创建“上传向导”按钮(跳转链接形式) | |||
var wizardButton = new OO.ui.ButtonWidget({ | |||
label: '✅ 使用上传向导(推荐)', | |||
flags: ['primary', 'progressive'], | |||
href: mw.util.getUrl('Special:UploadWizard'), | |||
target: '_self', | |||
framed: true | |||
}); | |||
// 给按钮添加“记住选择”逻辑 | |||
wizardButton.on('click', () => { | |||
if (rememberCheckbox.isSelected()) { | |||
mw.storage.set(STORAGE_KEY, 'wizard'); | |||
} | |||
}); | |||
// 拼接内容 | |||
panel.$element.append( | |||
$('<p>').text('请选择你要使用的上传方式:'), | |||
$('<div>').css({ | |||
'margin': '12px 0', | |||
'background': '#f8f9fa', | |||
'padding': '10px', | |||
'border': '1px solid #ccc', | |||
'border-radius': '5px' | |||
}).append( | |||
$('<strong>').text('✅ 上传向导(推荐)'), | |||
$('<ul>').append( | |||
$('<li>').text('支持多文件批量上传'), | |||
$('<li>').text('显示上传进度条'), | |||
$('<li>').text('信息填写更完整') | |||
), | |||
$('<div>').css('margin-top', '8px').append(wizardButton.$element) | |||
), | |||
$('<div>').css({ | |||
'margin-top': '15px', | |||
'background': '#ffffff', | |||
'padding': '10px', | |||
'border': '1px dashed #bbb', | |||
'border-radius': '5px' | |||
}).append( | |||
$('<strong>').text('⚡️ 传统上传方式'), | |||
$('<ul>').append( | |||
$('<li>').text('界面更简单'), | |||
$('<li>').text('加载速度快'), | |||
$('<li>').text('适合上传单个文件') | |||
) | |||
), | |||
$('<div>').css('margin-top', '15px').append(rememberField.$element) | |||
); | |||
this.$body.append(panel.$element); | |||
}; | |||
UploadDialog.prototype.getActionProcess = function (action) { | |||
if (action === 'classic') { | |||
if (this._rememberCheckbox.isSelected()) { | |||
mw.storage.set(STORAGE_KEY, 'classic'); | |||
} | |||
return new OO.ui.Process(() => this.close()); | |||
} | |||
return UploadDialog.super.prototype.getActionProcess.call(this, action); | |||
}; | |||
var dialog = new UploadDialog(); | |||
windowManager.addWindows([dialog]); | |||
windowManager.openWindow(dialog); | |||
} | |||
}); | |||
//--- | |||
// IIFE | |||
(function() { | |||
var loadingImgUrl = 'https://wiki.ottohub.cn/images/0/02/Loading.png'; | |||
var loadingDiv = null; // Will be created on demand | |||
var removalTimeoutId = null; | |||
var isShowingLoading = false; // Track if loading is currently managed by this script | |||
// --- Function to CREATE and SHOW loading indicator --- | |||
function showLoading(reason) { | |||
if (isShowingLoading && document.getElementById('loadingIndicator')) { | |||
// console.log('[Loading] Already showing, triggered by:', reason); | |||
return; // Already showing or element exists | |||
} | |||
isShowingLoading = true; | |||
console.log('[Loading] Showing. Reason:', reason); | |||
// Create if it doesn't exist | |||
if (!document.getElementById('loadingIndicator')) { | |||
loadingDiv = document.createElement('div'); | |||
loadingDiv.id = 'loadingIndicator'; | |||
loadingDiv.style.position = 'fixed'; | |||
loadingDiv.style.bottom = '20px'; | |||
loadingDiv.style.right = '20px'; | |||
loadingDiv.style.width = '256px'; | |||
loadingDiv.style.height = '256px'; | |||
loadingDiv.style.zIndex = '99999'; | |||
loadingDiv.style.pointerEvents = 'none'; | |||
loadingDiv.style.opacity = '0'; // Start transparent for fadeIn | |||
var img = document.createElement('img'); | |||
img.src = loadingImgUrl; | |||
img.alt = 'Loading...'; | |||
img.style.width = '100%'; | |||
img.style.height = '100%'; | |||
img.style.objectFit = 'contain'; | |||
loadingDiv.appendChild(img); | |||
if (document.body) { | |||
document.body.appendChild(loadingDiv); | |||
} else { | |||
// Fallback if body isn't ready (e.g. script in head without defer) | |||
document.addEventListener('DOMContentLoaded', function onReadyAppend() { | |||
if (document.body && !document.getElementById('loadingIndicator')) { | |||
document.body.appendChild(loadingDiv); | |||
fadeInLoading(); | |||
} | |||
document.removeEventListener('DOMContentLoaded', onReadyAppend); | |||
}); | |||
return; // fadeIn will be called in DOMContentLoaded | |||
} | |||
} else { | |||
loadingDiv = document.getElementById('loadingIndicator'); // Get existing | |||
} | |||
fadeInLoading(); | |||
// Reset timeout for removal (if any previous one was set for page load) | |||
if (removalTimeoutId) { | |||
clearTimeout(removalTimeoutId); | |||
} | |||
// Set a timeout for link clicks as well, in case something goes wrong | |||
// You might want a different timeout duration for link clicks vs initial load | |||
removalTimeoutId = setTimeout(function() { | |||
hideLoading('timeout fallback after click (15s)'); | |||
}, 15000); // e.g., 15 seconds for navigation | |||
} | |||
function fadeInLoading() { | |||
if (loadingDiv) { | |||
loadingDiv.style.display = 'block'; // Make sure it's visible if previously hidden | |||
requestAnimationFrame(function() { // Ensure display:block is applied before transition | |||
loadingDiv.style.transition = 'opacity 0.2s ease-in'; | |||
loadingDiv.style.opacity = '1'; | |||
}); | |||
} | |||
} | |||
// --- Function to HIDE and REMOVE loading indicator --- | |||
function hideLoading(reason) { | |||
if (!isShowingLoading && !document.getElementById('loadingIndicator')) { | |||
// console.log('[Loading] Already hidden or not found, trigger for hide:', reason); | |||
return; | |||
} | |||
console.log('[Loading] Hiding. Reason:', reason); | |||
var el = document.getElementById('loadingIndicator'); | |||
if (el) { | |||
el.style.transition = 'opacity 0.3s ease-out'; | |||
el.style.opacity = '0'; | |||
setTimeout(function() { | |||
if (el.parentNode) { | |||
el.parentNode.removeChild(el); | |||
} | |||
// console.log('[Loading] Removed from DOM. Reason:', reason); | |||
loadingDiv = null; // Clear reference | |||
}, 300); | |||
} | |||
isShowingLoading = false; | |||
if (removalTimeoutId) { | |||
clearTimeout(removalTimeoutId); | |||
removalTimeoutId = null; | |||
} | |||
// Clean up general page load listeners if they were for the initial load | |||
// For link clicks, these might not be relevant or might need to be re-added if it's SPA | |||
window.removeEventListener('load', onWindowLoad); | |||
document.removeEventListener('DOMContentLoaded', onDOMContentLoaded); | |||
} | |||
// --- Initial Page Load Logic --- | |||
// (This part is similar to before, but uses show/hide functions) | |||
// If script runs very early, body might not be available. | |||
// `showLoading` handles appending to body when it's ready. | |||
// Check initial state | |||
if (document.readyState === 'loading') { // Document is still loading | |||
showLoading('initial page load - document loading'); | |||
} else if (document.readyState === 'interactive' || document.readyState === 'complete') { | |||
// Document already interactive or complete (e.g. from cache, or script deferred) | |||
// We might still want to show it briefly if it's 'interactive' but not 'complete' | |||
// or if it's 'complete' but the script just ran. | |||
// For 'complete' from cache, this might flash, so careful. | |||
if (document.readyState === 'complete') { | |||
// If truly complete, maybe don't show, or show and hide very quickly | |||
// This addresses the "cache loaded too fast" problem. | |||
// For now, let's assume we don't show if already fully complete on script exec. | |||
// console.log('[Loading] Initial page load - document already complete. Skipping initial loading indicator.'); | |||
isShowingLoading = false; // Ensure it's not marked as showing for initial load | |||
} else { // interactive | |||
showLoading('initial page load - document interactive'); | |||
} | |||
} | |||
function onWindowLoad() { | |||
hideLoading('window.load'); | |||
} | |||
function onDOMContentLoaded() { | |||
if (document.readyState === 'interactive' || document.readyState === 'complete') { | |||
hideLoading('DOMContentLoaded (interactive/complete)'); | |||
} | |||
} | |||
// Only add these listeners if we decided to show loading initially | |||
if (isShowingLoading || document.readyState === 'loading') { // Add if showing or if we expect it to show | |||
window.addEventListener('load', onWindowLoad); | |||
document.addEventListener('DOMContentLoaded', onDOMContentLoaded); | |||
// Fallback timeout for initial load | |||
if (removalTimeoutId) clearTimeout(removalTimeoutId); // Clear any from showLoading | |||
removalTimeoutId = setTimeout(function() { | |||
hideLoading('timeout fallback (initial 5s)'); | |||
}, 5000); | |||
} | |||
// --- Logic for Link Clicks --- | |||
document.addEventListener('click', function(event) { | |||
// Find the closest <a> tag ancestor | |||
var targetElement = event.target.closest('a'); | |||
if (targetElement && targetElement.href) { | |||
// Exclude specific links if needed: | |||
// if (targetElement.classList.contains('no-loading-indicator')) return; | |||
// if (targetElement.getAttribute('target') === '_blank') return; // Don't show for new tabs | |||
// if (targetElement.href.startsWith('javascript:')) return; | |||
// if (targetElement.href.startsWith('#')) return; // Typically same-page anchors | |||
var href = targetElement.getAttribute('href'); | |||
var targetAttr = targetElement.getAttribute('target'); | |||
// Basic checks to avoid showing loading for non-navigation links | |||
if (href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:')) { | |||
return; | |||
} | |||
if (targetAttr === '_blank') { // Opening in new tab, current page isn't "loading" | |||
return; | |||
} | |||
// Check if it's an internal link that might be handled by SPA router | |||
// This is a very basic check. SPA routers often have their own events. | |||
var isLikelyInternalSPALink = false; // Set to true if you detect SPA behavior | |||
if (typeof window.MyAppRouter !== 'undefined' && window.MyAppRouter.isHandling(href)) { | |||
isLikelyInternalSPALink = true; | |||
} | |||
// For regular page navigations: | |||
if (!isLikelyInternalSPALink) { | |||
showLoading('link click - page navigation'); | |||
// The `beforeunload` event is also an option here, but click is more direct | |||
// `beforeunload` fires when the page is *about* to unload. | |||
} | |||
// For SPA/AJAX, you'd typically call showLoading() *before* your fetch/XHR call | |||
// and hideLoading() in the .then()/.catch()/.finally() or equivalent callback. | |||
} | |||
}, true); // Use capture phase to catch clicks early | |||
// --- Handling page unload for regular navigations --- | |||
// This ensures the loading indicator is shown if a navigation is initiated | |||
// by something other than a direct click (e.g. form submission, window.location change) | |||
// However, `beforeunload` can be tricky and sometimes doesn't allow much time for UI updates. | |||
// Also, if the user cancels the navigation, the loading indicator might stay. | |||
/* | |||
window.addEventListener('beforeunload', function() { | |||
// Only show if not already triggered by a click and it's a real navigation | |||
// This is harder to get right because 'beforeunload' doesn't tell you *if* navigation will proceed. | |||
// For simplicity, relying on link click + timeout might be better. | |||
// If you do use this, make sure `isShowingLoading` is checked. | |||
if (!isShowingLoading) { | |||
// showLoading('beforeunload - page navigating'); | |||
} | |||
}); | |||
*/ | |||
// --- For SPAs or AJAX-driven content --- | |||
// You need to manually call showLoading() and hideLoading() around your async operations. | |||
// Example: | |||
/* | |||
function fetchDataAndUpdatePage(url) { | |||
showLoading('AJAX request started'); | |||
fetch(url) | |||
.then(response => response.json()) | |||
.then(data => { | |||
// update page content | |||
hideLoading('AJAX request complete'); | |||
}) | |||
.catch(error => { | |||
console.error('AJAX error:', error); | |||
hideLoading('AJAX request failed'); | |||
}); | |||
} | |||
// When a link for SPA navigation is clicked: | |||
// document.getElementById('mySpaLink').addEventListener('click', function(e) { | |||
// e.preventDefault(); | |||
// fetchDataAndUpdatePage(this.href); | |||
// }); | |||
*/ | |||
// Expose functions if needed for manual control (e.g., from SPA router) | |||
window.globalLoadingIndicator = { | |||
show: showLoading, | |||
hide: hideLoading | |||
}; | |||
})(); |
2025年5月17日 (六) 12:22的最新版本
const rootStyles = getComputedStyle(document.documentElement); const hexColor = rootStyles.getPropertyValue('--background-color-base').trim(); function hexToRgb(hex) { hex = hex.replace('#', ''); if (hex.length === 3) { hex = hex.split('').map(c => c + c).join(''); } const r = parseInt(hex.substring(0, 2), 16); const g = parseInt(hex.substring(2, 4), 16); const b = parseInt(hex.substring(4, 6), 16); return `${r}, ${g}, ${b}`; } const rgbColor = hexToRgb(hexColor); document.documentElement.style.setProperty( '--background-color-rgb', rgbColor ); console.log('转换结果:', rgbColor); //上传重定向 mw.loader.using(['mediawiki.util', 'oojs-ui', 'mediawiki.storage'], function () { if (mw.config.get('wgCanonicalSpecialPageName') === 'Upload') { const STORAGE_KEY = 'uploadPreference'; const savedChoice = mw.storage.get(STORAGE_KEY); // 用户之前选择了上传向导,自动跳转 if (savedChoice === 'wizard') { window.location.href = mw.util.getUrl('Special:UploadWizard'); return; } // 用户选择了继续使用传统上传,不再提示 if (savedChoice === 'classic') { return; } // 否则弹出选择窗口 var windowManager = new OO.ui.WindowManager(); $(document.body).append(windowManager.$element); function UploadDialog(config) { UploadDialog.super.call(this, config); } OO.inheritClass(UploadDialog, OO.ui.ProcessDialog); UploadDialog.static.name = 'UploadDialog'; UploadDialog.static.title = '请选择上传方式'; UploadDialog.static.actions = [ { action: 'classic', label: '❌ 使用传统上传方式(更快)', flags: ['safe'] } ]; UploadDialog.prototype.initialize = function () { UploadDialog.super.prototype.initialize.call(this); var panel = new OO.ui.PanelLayout({ padded: true, expanded: false }); // 创建“记住我的选择”复选框 var rememberCheckbox = new OO.ui.CheckboxInputWidget({ selected: false }); this._rememberCheckbox = rememberCheckbox; var rememberField = new OO.ui.FieldLayout(rememberCheckbox, { label: '记住我的选择,下次不再提示', align: 'inline' }); // 创建“上传向导”按钮(跳转链接形式) var wizardButton = new OO.ui.ButtonWidget({ label: '✅ 使用上传向导(推荐)', flags: ['primary', 'progressive'], href: mw.util.getUrl('Special:UploadWizard'), target: '_self', framed: true }); // 给按钮添加“记住选择”逻辑 wizardButton.on('click', () => { if (rememberCheckbox.isSelected()) { mw.storage.set(STORAGE_KEY, 'wizard'); } }); // 拼接内容 panel.$element.append( $('<p>').text('请选择你要使用的上传方式:'), $('<div>').css({ 'margin': '12px 0', 'background': '#f8f9fa', 'padding': '10px', 'border': '1px solid #ccc', 'border-radius': '5px' }).append( $('<strong>').text('✅ 上传向导(推荐)'), $('<ul>').append( $('<li>').text('支持多文件批量上传'), $('<li>').text('显示上传进度条'), $('<li>').text('信息填写更完整') ), $('<div>').css('margin-top', '8px').append(wizardButton.$element) ), $('<div>').css({ 'margin-top': '15px', 'background': '#ffffff', 'padding': '10px', 'border': '1px dashed #bbb', 'border-radius': '5px' }).append( $('<strong>').text('⚡️ 传统上传方式'), $('<ul>').append( $('<li>').text('界面更简单'), $('<li>').text('加载速度快'), $('<li>').text('适合上传单个文件') ) ), $('<div>').css('margin-top', '15px').append(rememberField.$element) ); this.$body.append(panel.$element); }; UploadDialog.prototype.getActionProcess = function (action) { if (action === 'classic') { if (this._rememberCheckbox.isSelected()) { mw.storage.set(STORAGE_KEY, 'classic'); } return new OO.ui.Process(() => this.close()); } return UploadDialog.super.prototype.getActionProcess.call(this, action); }; var dialog = new UploadDialog(); windowManager.addWindows([dialog]); windowManager.openWindow(dialog); } }); //--- // IIFE (function() { var loadingImgUrl = 'https://wiki.ottohub.cn/images/0/02/Loading.png'; var loadingDiv = null; // Will be created on demand var removalTimeoutId = null; var isShowingLoading = false; // Track if loading is currently managed by this script // --- Function to CREATE and SHOW loading indicator --- function showLoading(reason) { if (isShowingLoading && document.getElementById('loadingIndicator')) { // console.log('[Loading] Already showing, triggered by:', reason); return; // Already showing or element exists } isShowingLoading = true; console.log('[Loading] Showing. Reason:', reason); // Create if it doesn't exist if (!document.getElementById('loadingIndicator')) { loadingDiv = document.createElement('div'); loadingDiv.id = 'loadingIndicator'; loadingDiv.style.position = 'fixed'; loadingDiv.style.bottom = '20px'; loadingDiv.style.right = '20px'; loadingDiv.style.width = '256px'; loadingDiv.style.height = '256px'; loadingDiv.style.zIndex = '99999'; loadingDiv.style.pointerEvents = 'none'; loadingDiv.style.opacity = '0'; // Start transparent for fadeIn var img = document.createElement('img'); img.src = loadingImgUrl; img.alt = 'Loading...'; img.style.width = '100%'; img.style.height = '100%'; img.style.objectFit = 'contain'; loadingDiv.appendChild(img); if (document.body) { document.body.appendChild(loadingDiv); } else { // Fallback if body isn't ready (e.g. script in head without defer) document.addEventListener('DOMContentLoaded', function onReadyAppend() { if (document.body && !document.getElementById('loadingIndicator')) { document.body.appendChild(loadingDiv); fadeInLoading(); } document.removeEventListener('DOMContentLoaded', onReadyAppend); }); return; // fadeIn will be called in DOMContentLoaded } } else { loadingDiv = document.getElementById('loadingIndicator'); // Get existing } fadeInLoading(); // Reset timeout for removal (if any previous one was set for page load) if (removalTimeoutId) { clearTimeout(removalTimeoutId); } // Set a timeout for link clicks as well, in case something goes wrong // You might want a different timeout duration for link clicks vs initial load removalTimeoutId = setTimeout(function() { hideLoading('timeout fallback after click (15s)'); }, 15000); // e.g., 15 seconds for navigation } function fadeInLoading() { if (loadingDiv) { loadingDiv.style.display = 'block'; // Make sure it's visible if previously hidden requestAnimationFrame(function() { // Ensure display:block is applied before transition loadingDiv.style.transition = 'opacity 0.2s ease-in'; loadingDiv.style.opacity = '1'; }); } } // --- Function to HIDE and REMOVE loading indicator --- function hideLoading(reason) { if (!isShowingLoading && !document.getElementById('loadingIndicator')) { // console.log('[Loading] Already hidden or not found, trigger for hide:', reason); return; } console.log('[Loading] Hiding. Reason:', reason); var el = document.getElementById('loadingIndicator'); if (el) { el.style.transition = 'opacity 0.3s ease-out'; el.style.opacity = '0'; setTimeout(function() { if (el.parentNode) { el.parentNode.removeChild(el); } // console.log('[Loading] Removed from DOM. Reason:', reason); loadingDiv = null; // Clear reference }, 300); } isShowingLoading = false; if (removalTimeoutId) { clearTimeout(removalTimeoutId); removalTimeoutId = null; } // Clean up general page load listeners if they were for the initial load // For link clicks, these might not be relevant or might need to be re-added if it's SPA window.removeEventListener('load', onWindowLoad); document.removeEventListener('DOMContentLoaded', onDOMContentLoaded); } // --- Initial Page Load Logic --- // (This part is similar to before, but uses show/hide functions) // If script runs very early, body might not be available. // `showLoading` handles appending to body when it's ready. // Check initial state if (document.readyState === 'loading') { // Document is still loading showLoading('initial page load - document loading'); } else if (document.readyState === 'interactive' || document.readyState === 'complete') { // Document already interactive or complete (e.g. from cache, or script deferred) // We might still want to show it briefly if it's 'interactive' but not 'complete' // or if it's 'complete' but the script just ran. // For 'complete' from cache, this might flash, so careful. if (document.readyState === 'complete') { // If truly complete, maybe don't show, or show and hide very quickly // This addresses the "cache loaded too fast" problem. // For now, let's assume we don't show if already fully complete on script exec. // console.log('[Loading] Initial page load - document already complete. Skipping initial loading indicator.'); isShowingLoading = false; // Ensure it's not marked as showing for initial load } else { // interactive showLoading('initial page load - document interactive'); } } function onWindowLoad() { hideLoading('window.load'); } function onDOMContentLoaded() { if (document.readyState === 'interactive' || document.readyState === 'complete') { hideLoading('DOMContentLoaded (interactive/complete)'); } } // Only add these listeners if we decided to show loading initially if (isShowingLoading || document.readyState === 'loading') { // Add if showing or if we expect it to show window.addEventListener('load', onWindowLoad); document.addEventListener('DOMContentLoaded', onDOMContentLoaded); // Fallback timeout for initial load if (removalTimeoutId) clearTimeout(removalTimeoutId); // Clear any from showLoading removalTimeoutId = setTimeout(function() { hideLoading('timeout fallback (initial 5s)'); }, 5000); } // --- Logic for Link Clicks --- document.addEventListener('click', function(event) { // Find the closest <a> tag ancestor var targetElement = event.target.closest('a'); if (targetElement && targetElement.href) { // Exclude specific links if needed: // if (targetElement.classList.contains('no-loading-indicator')) return; // if (targetElement.getAttribute('target') === '_blank') return; // Don't show for new tabs // if (targetElement.href.startsWith('javascript:')) return; // if (targetElement.href.startsWith('#')) return; // Typically same-page anchors var href = targetElement.getAttribute('href'); var targetAttr = targetElement.getAttribute('target'); // Basic checks to avoid showing loading for non-navigation links if (href.startsWith('#') || href.startsWith('javascript:') || href.startsWith('mailto:') || href.startsWith('tel:')) { return; } if (targetAttr === '_blank') { // Opening in new tab, current page isn't "loading" return; } // Check if it's an internal link that might be handled by SPA router // This is a very basic check. SPA routers often have their own events. var isLikelyInternalSPALink = false; // Set to true if you detect SPA behavior if (typeof window.MyAppRouter !== 'undefined' && window.MyAppRouter.isHandling(href)) { isLikelyInternalSPALink = true; } // For regular page navigations: if (!isLikelyInternalSPALink) { showLoading('link click - page navigation'); // The `beforeunload` event is also an option here, but click is more direct // `beforeunload` fires when the page is *about* to unload. } // For SPA/AJAX, you'd typically call showLoading() *before* your fetch/XHR call // and hideLoading() in the .then()/.catch()/.finally() or equivalent callback. } }, true); // Use capture phase to catch clicks early // --- Handling page unload for regular navigations --- // This ensures the loading indicator is shown if a navigation is initiated // by something other than a direct click (e.g. form submission, window.location change) // However, `beforeunload` can be tricky and sometimes doesn't allow much time for UI updates. // Also, if the user cancels the navigation, the loading indicator might stay. /* window.addEventListener('beforeunload', function() { // Only show if not already triggered by a click and it's a real navigation // This is harder to get right because 'beforeunload' doesn't tell you *if* navigation will proceed. // For simplicity, relying on link click + timeout might be better. // If you do use this, make sure `isShowingLoading` is checked. if (!isShowingLoading) { // showLoading('beforeunload - page navigating'); } }); */ // --- For SPAs or AJAX-driven content --- // You need to manually call showLoading() and hideLoading() around your async operations. // Example: /* function fetchDataAndUpdatePage(url) { showLoading('AJAX request started'); fetch(url) .then(response => response.json()) .then(data => { // update page content hideLoading('AJAX request complete'); }) .catch(error => { console.error('AJAX error:', error); hideLoading('AJAX request failed'); }); } // When a link for SPA navigation is clicked: // document.getElementById('mySpaLink').addEventListener('click', function(e) { // e.preventDefault(); // fetchDataAndUpdatePage(this.href); // }); */ // Expose functions if needed for manual control (e.g., from SPA router) window.globalLoadingIndicator = { show: showLoading, hide: hideLoading }; })();