newechoes/src/components/swup-init.js

542 lines
17 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// 统一初始化Swup和所有插件
import Swup from 'swup';
import SwupFragmentPlugin from '@swup/fragment-plugin';
// 添加Head插件解决CSS丢失问题
import SwupHeadPlugin from '@swup/head-plugin';
// 添加预加载插件 - 优化导航体验
import SwupPreloadPlugin from '@swup/preload-plugin';
// 添加Scripts插件 - 确保页面转场后脚本能重新执行
import SwupScriptsPlugin from '@swup/scripts-plugin';
// 创建加载动画元素
function createLoadingSpinner() {
// 检查是否已存在加载动画元素
const existingSpinner = document.getElementById('swup-loading-spinner');
if (existingSpinner) {
return existingSpinner;
}
// 创建加载动画容器
const spinner = document.createElement('div');
spinner.id = 'swup-loading-spinner';
spinner.className = 'loading-spinner-container';
// 创建内部旋转元素
const spinnerInner = document.createElement('div');
spinnerInner.className = 'loading-spinner';
// 添加到页面
spinner.appendChild(spinnerInner);
// 默认隐藏
spinner.style.display = 'none';
return spinner;
}
// 将加载动画添加到body并固定在内容区域的中心
function addSpinnerToBody(spinner) {
if (!spinner) return;
try {
// 先从DOM中移除(如果已存在)
if (spinner.parentNode) {
spinner.parentNode.removeChild(spinner);
}
// 获取当前活跃元素
const activeElement = getActiveElement();
// 添加到body而不是活跃容器避免内容替换时被移除
document.body.appendChild(spinner);
// 如果有活跃元素,根据其位置调整加载动画的位置
if (activeElement) {
// 获取活跃元素的位置信息
const rect = activeElement.getBoundingClientRect();
// 计算中心点相对于视口的位置
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
// 设置加载动画位置
spinner.style.position = 'fixed';
spinner.style.top = centerY + 'px';
spinner.style.left = centerX + 'px';
spinner.style.transform = 'translate(-50%, -50%)';
spinner.style.zIndex = '9999'; // 确保在最顶层
} else {
// 如果没有活跃元素,则居中显示
spinner.style.position = 'fixed';
spinner.style.top = '50%';
spinner.style.left = '50%';
spinner.style.transform = 'translate(-50%, -50%)';
spinner.style.zIndex = '9999'; // 确保在最顶层
}
} catch (error) {
console.error('添加加载动画时出错:', error);
}
}
// 显示加载动画
function showLoadingSpinner(spinner, forceNew = false) {
if (!spinner) return;
// 确保加载动画已添加到body
addSpinnerToBody(spinner);
// 检查加载动画是否已在显示
if (spinner.classList.contains('is-active') && !forceNew) {
return;
}
spinner.style.display = 'flex';
spinner.classList.add('is-active');
}
// 隐藏加载动画
function hideLoadingSpinner(spinner) {
if (!spinner || !document.body.contains(spinner) || !spinner.classList.contains('is-active')) {
return;
}
spinner.classList.remove('is-active');
// 添加淡出效果后移除
setTimeout(() => {
if (spinner && document.body.contains(spinner)) {
spinner.style.display = 'none';
}
}, 300);
}
// 检查是否是文章相关页面
function isArticlePage() {
const path = window.location.pathname;
return path.includes('/articles') || path.includes('/filtered');
}
// 检查DOM中是否存在指定的容器
function containerExists(selector) {
return document.querySelector(selector) !== null;
}
// 为元素设置过渡状态
function setElementTransition(element) {
if (!element) return;
// 添加data-swup属性标记
element.setAttribute('data-swup-transition', 'true');
}
// 设置元素淡入/淡出效果
function setElementOpacity(element, opacity) {
if (!element) return;
element.style.opacity = opacity.toString();
}
// 应用过渡效果到相关元素
function applyTransitions() {
// 应用到主容器 - 只在非文章页面
const mainElement = document.querySelector('main');
if (mainElement) {
mainElement.classList.add('transition-fade');
// 只有在非文章页面时才为main添加必要的过渡标记
if (!isArticlePage()) {
setElementTransition(mainElement);
}
}
// 应用到文章内容 - 只在文章页面
const articleContent = document.querySelector('#article-content');
if (articleContent) {
articleContent.classList.add('swup-transition-article');
setElementTransition(articleContent);
}
}
// 获取当前页面的活跃元素(用于动画)
function getActiveElement() {
if (isArticlePage()) {
return document.querySelector('#article-content');
} else {
return document.querySelector('main');
}
}
// 在DOM加载完成后初始化
document.addEventListener('DOMContentLoaded', () => {
// 应用过渡效果
applyTransitions();
// 创建加载动画元素
const spinner = createLoadingSpinner();
// 页面状态跟踪
let animationInProgress = false;
// 根据当前页面动态确定容器配置
const containers = ['main']; // 主容器始终存在
// 只有当文章内容容器存在时才添加
if (containerExists('#article-content')) {
containers.push('#article-content');
}
// 创建Swup实例
const swup = new Swup({
// Swup的基本配置
animationSelector: '[class*="transition-"], .swup-transition-article, #article-content',
cache: true,
containers: containers, // 使用动态容器配置
animationScope: 'html', // 确保动画状态类添加到html元素
linkSelector: 'a[href^="/"]:not([data-no-swup]), a[href^="' + window.location.origin + '"]:not([data-no-swup])',
// 使用默认的skipPopStateHandling设置只处理由swup创建的历史记录
skipPopStateHandling: (event) => event.state?.source !== 'swup',
// 修复resolveUrl实现确保返回URL字符串而不是对象
resolveUrl: function(url) {
// 直接返回URL字符串
return url;
},
// 增加自定义容器解析,解决容器不匹配的问题
resolveContainers: async function(visit) {
// 根据URL路径动态决定要使用哪些容器
const isFromArticlePage = visit?.from?.url.includes('/articles') || visit?.from?.url.includes('/filtered');
const isToArticlePage = visit?.to?.url.includes('/articles') || visit?.to?.url.includes('/filtered');
// 当从文章页到非文章页,或从非文章页到文章页时
if (isFromArticlePage !== isToArticlePage) {
return ['main'];
}
// 对于文章页面之间的导航,使用两个容器
if (isFromArticlePage && isToArticlePage) {
return ['main', '#article-content'];
}
// 默认情况使用main容器
return ['main'];
},
plugins: [] // 手动添加插件以控制顺序
});
// 发送页面转换事件 - 自定义全局事件
function sendPageTransitionEvent() {
// 创建自定义事件并触发
const event = new CustomEvent('page-transition', {
bubbles: true,
cancelable: false,
detail: { timestamp: Date.now() }
});
document.dispatchEvent(event);
}
// 添加预加载插件 - 代替原有的预加载功能
const preloadPlugin = new SwupPreloadPlugin({
// 最多同时预加载5个链接
throttle: 5,
// 开启鼠标悬停预加载
preloadHoveredLinks: true,
// 开启视口内链接预加载,自定义配置
preloadVisibleLinks: {
// 链接可见面积达到30%时预加载
threshold: 0.3,
// 链接可见500毫秒后开始预加载
delay: 500,
// 在哪些容器内寻找链接
containers: ['body'],
// 忽略带有data-no-preload属性的链接
ignore: (el) => el.hasAttribute('data-no-preload')
},
// 预加载初始页面,以便"后退"导航更快
preloadInitialPage: true
});
swup.use(preloadPlugin);
// 创建并注册Head插件用于解决CSS丢失问题
const headPlugin = new SwupHeadPlugin({
persistTags: 'link[rel="stylesheet"], style, meta', // 保留所有样式表和相关标签
persistAssets: true, // 保留已加载的资源
keepScrollOnReload: true, // 保持滚动位置
awaitAssets: true // 等待资源加载完成再显示页面
});
swup.use(headPlugin);
// 添加Scripts插件 - 确保页面转场后脚本能重新执行
const scriptsPlugin = new SwupScriptsPlugin({
// 以下选项确定哪些脚本会被重新执行
head: true, // 重新执行head中的脚本
body: true, // 重新执行body中的脚本
optin: false, // 是否只执行带有[data-swup-reload-script]属性的脚本
oprout: false, // 是否排除带有[data-no-swup]属性的脚本
once: true // 是否每个脚本只执行一次
});
swup.use(scriptsPlugin);
// 创建Fragment插件 - 只在需要的页面使用
const fragmentPlugin = new SwupFragmentPlugin({
debug: false, // 关闭调试模式
// 修改规则,增加更细致的配置
rules: [
{
name: 'article-pages',
from: ['/articles', '/filtered'],
to: ['/articles', '/filtered'],
containers: ['#article-content']
}
],
// 默认情况下忽略URL片段只使用路径部分
considerFragment: false
});
// 修改Fragment插件的加载逻辑 - 始终加载,但根据页面类型动态启用/禁用
swup.use(fragmentPlugin);
// 初始化后手动扫描并预加载带有data-swup-preload属性的链接
const preloadLinks = document.querySelectorAll('[data-swup-preload]');
if (preloadLinks.length > 0) {
preloadLinks.forEach(link => {
// 检查链接是否符合预加载条件
if (link.tagName.toLowerCase() === 'a' && link.href) {
// 调用预加载插件的方法
preloadPlugin.preloadPage(link.href);
}
});
}
// 重新设置过渡元素
function setupTransition() {
// 应用过渡效果
applyTransitions();
// 确保初始状态正确
setTimeout(() => {
// 获取并设置当前活跃元素的不透明度
const activeElement = getActiveElement();
if (activeElement) {
activeElement.style.opacity = '1';
}
}, 0);
}
// 初始化时设置
setupTransition();
// ===== 生命周期钩子 =====
// 1. 访问开始 - 显示加载动画,准备页面退出
swup.hooks.on('visit:start', (visit) => {
isLoading = true;
contentReady = false;
animationInProgress = true;
// 发送页面切换事件
sendPageTransitionEvent();
// 显示加载动画
showLoadingSpinner(spinner);
// 检查目标URL是否为文章相关页面
const isTargetArticlePage = visit.to.url.includes('/articles') || visit.to.url.includes('/filtered');
const isCurrentArticlePage = isArticlePage();
// 如果当前是文章页面但目标不是恢复main动画
if (isCurrentArticlePage && !isTargetArticlePage) {
const mainElement = document.querySelector('main');
if (mainElement) {
setElementOpacity(mainElement, 0);
}
}
// 如果当前不是文章页面但目标是准备article-content动画
else if (!isCurrentArticlePage && isTargetArticlePage) {
const mainElement = document.querySelector('main');
if (mainElement) {
// 移除main的过渡效果
mainElement.style.transition = '';
mainElement.style.opacity = '1';
}
}
});
// 2. 内容已加载但尚未替换 - 设置内容状态
swup.hooks.on('page:load', (visit) => {
contentReady = true;
// 如果是载入文章页面但Fragment插件未加载则加载它
if ((visit.to.url.includes('/articles') || visit.to.url.includes('/filtered')) &&
!swup.findPlugin('fragment')) {
swup.use(fragmentPlugin);
}
// 如果快速加载,先检查动画是否完成
if (!animationInProgress) {
// 如果动画已经完成,允许加载动画淡出
setTimeout(() => {
hideLoadingSpinner(spinner);
}, 50);
}
});
// 3. 页面退出动画开始 - 添加动画逻辑
swup.hooks.on('animation:out:start', () => {
animationInProgress = true;
// 获取并淡出当前活跃元素
const activeElement = getActiveElement();
setElementOpacity(activeElement, 0);
});
swup.hooks.on('content:replace', () => {
// 重新设置过渡样式,但不要立即隐藏加载动画
setTimeout(() => {
setupTransition();
}, 10);
});
// 5. 页面进入动画开始 - 控制新内容的显示
swup.hooks.on('animation:in:start', () => {
setTimeout(() => {
// 获取并淡入当前活跃元素
const activeElement = getActiveElement();
setElementOpacity(activeElement, 1);
hideLoadingSpinner(spinner);
}, 50);
});
// 7. 页面进入动画结束 - 完成所有过渡
swup.hooks.on('animation:in:end', () => {
animationInProgress = false;
isLoading = false;
// 确保隐藏加载动画
hideLoadingSpinner(spinner);
});
// 8. 页面完全加载完成
swup.hooks.on('page:view', () => {
isLoading = false;
contentReady = false;
animationInProgress = false;
// 最终确保隐藏加载动画
hideLoadingSpinner(spinner);
});
// 加载失败处理
swup.hooks.on('fetch:error', (error) => {
isLoading = false;
contentReady = false;
animationInProgress = false;
hideLoadingSpinner(spinner);
console.error('Fetch error:', error);
// 在严重错误时回退到页面刷新
try {
const targetUrl = error?.visit?.to?.url || window.location.pathname;
window.location.href = targetUrl;
} catch (e) {
// 如果获取目标URL失败刷新当前页面
window.location.reload();
}
});
// 处理容器不匹配错误
const originalErrorHandler = window.console.error;
window.console.error = function(...args) {
// 调用原始错误处理器
originalErrorHandler.apply(this, args);
// 检查是否是容器不匹配错误
if (
args.length > 0 &&
typeof args[0] === 'string' &&
(args[0].includes('Container missing') || args[0].includes('Container mismatch'))
) {
// 尝试恢复
try {
// 隐藏加载动画
hideLoadingSpinner(spinner);
// 重置状态
isLoading = false;
contentReady = false;
animationInProgress = false;
// 检查是否可以使用备用容器
const mainContainer = document.querySelector('main');
if (mainContainer) {
// 强制使用main容器
swup.options.containers = ['main'];
// 手动为main容器添加过渡状态
mainContainer.classList.add('transition-fade');
setElementTransition(mainContainer);
setElementOpacity(mainContainer, 1);
}
// 发送页面转换事件
sendPageTransitionEvent();
} catch (e) {
console.error('Recovery failed:', e);
}
}
};
// 在页面内容替换后确保新内容动画正确显示
document.addEventListener('swup:contentReplaced', () => {
// 获取活跃元素
const activeElement = getActiveElement();
if (!activeElement) return;
// 先设置透明
setElementOpacity(activeElement, 0);
// 重新应用适当的类
if (isArticlePage() && activeElement.id === 'article-content') {
activeElement.classList.add('swup-transition-article');
setElementTransition(activeElement);
} else if (!isArticlePage() && activeElement.tagName.toLowerCase() === 'main') {
activeElement.classList.add('transition-fade');
setElementTransition(activeElement);
}
// 延迟后淡入 - 但不要立刻隐藏加载动画
setTimeout(() => {
setElementOpacity(activeElement, 1);
}, 50);
});
// 监听Fragment插件是否成功应用
document.addEventListener('swup:fragmentReplaced', () => {
// 确保新内容有正确的过渡样式
setTimeout(() => {
setupTransition();
hideLoadingSpinner(spinner);
}, 10);
});
// 在页面卸载和Astro视图转换时清理资源
const cleanup = () => {
// 发送页面切换事件
sendPageTransitionEvent();
if (swup) {
// 移除所有已使用的插件
if (swup.findPlugin('fragment')) {
swup.unuse(fragmentPlugin);
}
swup.unuse(headPlugin);
swup.unuse(preloadPlugin);
swup.unuse(scriptsPlugin);
swup.destroy();
}
};
// 注册清理事件
window.addEventListener('beforeunload', cleanup, { once: true });
document.addEventListener('astro:before-swap', cleanup, { once: true });
});