hexo-theme-stellar 1.34.0 → 1.35.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_config.yml +1 -10
- package/layout/_partial/head.ejs +0 -3
- package/layout/_partial/scripts/utils.ejs +1 -44
- package/layout/layout.ejs +0 -1
- package/package.json +4 -2
- package/scripts/events/index.js +2 -1
- package/scripts/events/lib/doc_tree.js +7 -4
- package/scripts/events/lib/merge_posts.js +3 -1
- package/scripts/events/lib/notebooks.js +5 -3
- package/scripts/events/lib/path_normalize.js +22 -0
- package/scripts/generators/search.js +3 -1
- package/scripts/helpers/json_ld.js +3 -2
- package/scripts/helpers/pretty_url.js +9 -10
- package/scripts/lib/path_utils.js +24 -0
- package/source/css/_plugins/index.styl +0 -3
- package/source/js/main.js +0 -4
- package/CLAUDE.md +0 -217
- package/docs/audits/2026-08-08-stellar-analysis.md +0 -267
- package/docs/release-process.md +0 -59
- package/layout/_plugins/pjax.ejs +0 -8
- package/source/css/_plugins/pjax.styl +0 -61
- package/source/js/plugins/pjax.js +0 -646
|
@@ -1,646 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PJAX - Seamless page transitions for Stellar theme
|
|
3
|
-
* Uses pushState + AJAX for smooth navigation without full page reloads
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
(function () {
|
|
7
|
-
'use strict';
|
|
8
|
-
|
|
9
|
-
// PJAX configuration (can be overridden via window.StellarPjaxConfig)
|
|
10
|
-
const defaultConfig = {
|
|
11
|
-
selectors: ['title', '#l_cover', '.l_body'],
|
|
12
|
-
timeout: 10000,
|
|
13
|
-
cacheBust: false,
|
|
14
|
-
minLoadTime: 200 // Minimum time (ms) to show loading animation for smooth transitions
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const config = Object.assign({}, defaultConfig, window.StellarPjaxConfig || {});
|
|
18
|
-
if (!config.selectors.includes('#l_cover')) {
|
|
19
|
-
config.selectors.push('#l_cover');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// State management
|
|
23
|
-
let isLoading = false;
|
|
24
|
-
let abortController = null;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Check if a link should be handled by PJAX
|
|
28
|
-
*/
|
|
29
|
-
function shouldHandleLink(link) {
|
|
30
|
-
// Must be an anchor element with href
|
|
31
|
-
if (!link || !link.href || link.tagName !== 'A') return false;
|
|
32
|
-
|
|
33
|
-
const url = new URL(link.href);
|
|
34
|
-
const currentUrl = new URL(window.location.href);
|
|
35
|
-
|
|
36
|
-
// Skip external links
|
|
37
|
-
if (url.origin !== currentUrl.origin) return false;
|
|
38
|
-
|
|
39
|
-
// Skip hash-only links on same page
|
|
40
|
-
if (url.pathname === currentUrl.pathname && url.hash) return false;
|
|
41
|
-
|
|
42
|
-
// Skip links with target attribute (except _self)
|
|
43
|
-
if (link.target && link.target !== '_self') return false;
|
|
44
|
-
|
|
45
|
-
// Skip links with download attribute
|
|
46
|
-
if (link.hasAttribute('download')) return false;
|
|
47
|
-
|
|
48
|
-
// Skip links with data-pjax="false"
|
|
49
|
-
if (link.dataset.pjax === 'false') return false;
|
|
50
|
-
|
|
51
|
-
// Skip links to non-HTML resources
|
|
52
|
-
const ext = url.pathname.split('.').pop().toLowerCase();
|
|
53
|
-
const nonHtmlExts = ['xml', 'rss', 'pdf', 'zip', 'rar', 'exe', 'dmg', 'doc', 'xls', 'ppt', 'mp3', 'mp4', 'avi', 'mov'];
|
|
54
|
-
if (nonHtmlExts.includes(ext)) return false;
|
|
55
|
-
|
|
56
|
-
return true;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Sync all attributes from source element to target element
|
|
61
|
-
*/
|
|
62
|
-
function syncAttributes(source, target) {
|
|
63
|
-
if (!source || !target) return;
|
|
64
|
-
const oldAttrs = Array.from(target.attributes);
|
|
65
|
-
const newAttrs = Array.from(source.attributes);
|
|
66
|
-
|
|
67
|
-
// Remove attributes not in source
|
|
68
|
-
for (let attr of oldAttrs) {
|
|
69
|
-
if (!source.hasAttribute(attr.name)) target.removeAttribute(attr.name);
|
|
70
|
-
}
|
|
71
|
-
// Add or update attributes
|
|
72
|
-
for (let attr of newAttrs) {
|
|
73
|
-
if (target.getAttribute(attr.name) !== attr.value) {
|
|
74
|
-
target.setAttribute(attr.name, attr.value);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Extract content from HTML string
|
|
81
|
-
*/
|
|
82
|
-
function extractContent(html) {
|
|
83
|
-
const parser = new DOMParser();
|
|
84
|
-
const doc = parser.parseFromString(html, 'text/html');
|
|
85
|
-
return {
|
|
86
|
-
contents: {
|
|
87
|
-
title: doc.title,
|
|
88
|
-
_bodyClasses: doc.body.className,
|
|
89
|
-
_htmlAttrs: Array.from(doc.documentElement.attributes).reduce((acc, attr) => {
|
|
90
|
-
acc[attr.name] = attr.value;
|
|
91
|
-
return acc;
|
|
92
|
-
}, {})
|
|
93
|
-
},
|
|
94
|
-
doc
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Check if two widgets are identical (preserving dynamic content if API matches)
|
|
100
|
-
* 使用轻量级比较策略以提高性能
|
|
101
|
-
*/
|
|
102
|
-
function isWidgetIdentical(oldW, newW) {
|
|
103
|
-
// 1. 快速检查:如果是同一个节点,直接返回
|
|
104
|
-
if (oldW === newW) return true;
|
|
105
|
-
|
|
106
|
-
// 2. 检查 widget ID 或类型
|
|
107
|
-
const oldId = oldW.id || oldW.getAttribute('data-widget-id');
|
|
108
|
-
const newId = newW.id || newW.getAttribute('data-widget-id');
|
|
109
|
-
if (oldId && newId && oldId !== newId) return false;
|
|
110
|
-
|
|
111
|
-
// 3. 检查 data-service 标识(动态内容)
|
|
112
|
-
const oldDS = oldW.classList.contains('data-service') ? oldW : oldW.querySelector('.data-service');
|
|
113
|
-
const newDS = newW.classList.contains('data-service') ? newW : newW.querySelector('.data-service');
|
|
114
|
-
if (oldDS && newDS) {
|
|
115
|
-
const oldApi = oldDS.getAttribute('data-api');
|
|
116
|
-
const newApi = newDS.getAttribute('data-api');
|
|
117
|
-
// 如果 API 相同,保留旧内容(可能已加载数据)
|
|
118
|
-
if (oldApi && newApi && oldApi === newApi) return true;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// 4. 最后才使用 isEqualNode(比 innerHTML 更快)
|
|
122
|
-
return oldW.isEqualNode(newW);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* Replace content in the current page
|
|
127
|
-
*/
|
|
128
|
-
function replaceContent(contents, selectors, doc) {
|
|
129
|
-
// 1. Update HTML attributes (theme except data-theme, lang, etc.)
|
|
130
|
-
const newHtmlAttrs = contents._htmlAttrs;
|
|
131
|
-
if (newHtmlAttrs) {
|
|
132
|
-
// Remove data-theme from new attributes to prevent overwriting user's saved theme
|
|
133
|
-
const attrsToSync = { ...newHtmlAttrs };
|
|
134
|
-
delete attrsToSync['data-theme'];
|
|
135
|
-
|
|
136
|
-
// Sync all attributes
|
|
137
|
-
Object.keys(attrsToSync).forEach(attrName => {
|
|
138
|
-
const newValue = attrsToSync[attrName];
|
|
139
|
-
const oldValue = document.documentElement.getAttribute(attrName);
|
|
140
|
-
if (newValue !== oldValue) {
|
|
141
|
-
document.documentElement.setAttribute(attrName, newValue);
|
|
142
|
-
}
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Remove attributes that exist in current html but not in new html (except data-theme)
|
|
146
|
-
Array.from(document.documentElement.attributes).forEach(attr => {
|
|
147
|
-
if (attr.name !== 'data-theme' && !(attr.name in attrsToSync)) {
|
|
148
|
-
document.documentElement.removeAttribute(attr.name);
|
|
149
|
-
}
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (contents.title) document.title = contents.title;
|
|
154
|
-
if (contents._bodyClasses) document.body.className = contents._bodyClasses;
|
|
155
|
-
|
|
156
|
-
// 2. Replace content for each selector
|
|
157
|
-
selectors.forEach(selector => {
|
|
158
|
-
if (selector === 'title' || selector === 'html') return;
|
|
159
|
-
|
|
160
|
-
const oldEl = document.querySelector(selector);
|
|
161
|
-
const newEl = doc.querySelector(selector);
|
|
162
|
-
if (oldEl && newEl) {
|
|
163
|
-
syncAttributes(newEl, oldEl);
|
|
164
|
-
|
|
165
|
-
if (selector === '.l_body') {
|
|
166
|
-
// 1. Update main content
|
|
167
|
-
const oMain = oldEl.querySelector('.l_main');
|
|
168
|
-
const nMain = newEl.querySelector('.l_main');
|
|
169
|
-
if (oMain && nMain) {
|
|
170
|
-
oMain.replaceWith(nMain);
|
|
171
|
-
} else if (oMain) {
|
|
172
|
-
// Clear no longer existing main content
|
|
173
|
-
oMain.innerHTML = '';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// 2. Special handling for sidebars (left and right)
|
|
177
|
-
['.l_left', '.l_right'].forEach(side => {
|
|
178
|
-
const oSide = oldEl.querySelector(side);
|
|
179
|
-
const nSide = newEl.querySelector(side);
|
|
180
|
-
if (oSide && nSide) {
|
|
181
|
-
// Update Sidebar components in-place
|
|
182
|
-
['.header', '.nav-area', '.widgets', '.footer'].forEach(part => {
|
|
183
|
-
const op = oSide.querySelector(part);
|
|
184
|
-
const np = nSide.querySelector(part);
|
|
185
|
-
if (op && np) {
|
|
186
|
-
if (part === '.widgets') {
|
|
187
|
-
const savedScrollTop = op.scrollTop || 0;
|
|
188
|
-
const oldChildren = Array.from(op.children);
|
|
189
|
-
const newChildren = Array.from(np.children);
|
|
190
|
-
|
|
191
|
-
// Simple positional merger for performance and order preservation
|
|
192
|
-
const maxLength = Math.max(oldChildren.length, newChildren.length);
|
|
193
|
-
for (let i = 0; i < maxLength; i++) {
|
|
194
|
-
const oc = oldChildren[i];
|
|
195
|
-
const nc = newChildren[i];
|
|
196
|
-
if (oc && nc) {
|
|
197
|
-
if (!isWidgetIdentical(oc, nc)) {
|
|
198
|
-
oc.replaceWith(nc);
|
|
199
|
-
}
|
|
200
|
-
} else if (oc) {
|
|
201
|
-
oc.remove();
|
|
202
|
-
} else if (nc) {
|
|
203
|
-
op.appendChild(nc);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
// 恢复滚动位置(在 DOM 更新后重新获取引用)
|
|
207
|
-
const widgetsContainer = oSide.querySelector('.widgets');
|
|
208
|
-
if (widgetsContainer) {
|
|
209
|
-
widgetsContainer.style.scrollBehavior = 'auto';
|
|
210
|
-
widgetsContainer.scrollTop = savedScrollTop;
|
|
211
|
-
// 延迟恢复 scroll-behavior 以确保滚动完成
|
|
212
|
-
requestAnimationFrame(() => {
|
|
213
|
-
widgetsContainer.style.scrollBehavior = '';
|
|
214
|
-
});
|
|
215
|
-
}
|
|
216
|
-
} else {
|
|
217
|
-
// 使用 isEqualNode 而不是 innerHTML 比较
|
|
218
|
-
const isIdentical = op.isEqualNode(np);
|
|
219
|
-
if (!isIdentical) {
|
|
220
|
-
op.replaceWith(np);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
223
|
-
} else if (op) {
|
|
224
|
-
op.remove();
|
|
225
|
-
} else if (np) {
|
|
226
|
-
oSide.appendChild(np);
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
} else if (oSide && !nSide) {
|
|
230
|
-
oSide.remove();
|
|
231
|
-
} else if (!oSide && nSide) {
|
|
232
|
-
if (side === '.l_left') {
|
|
233
|
-
oldEl.prepend(nSide);
|
|
234
|
-
} else {
|
|
235
|
-
oldEl.append(nSide);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
});
|
|
239
|
-
|
|
240
|
-
// body already updated, skip general update
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// Default replacement for other selectors (like #l_cover)
|
|
245
|
-
if (!oldEl.isEqualNode(newEl)) {
|
|
246
|
-
oldEl.replaceWith(newEl);
|
|
247
|
-
}
|
|
248
|
-
} else if (oldEl) {
|
|
249
|
-
// If the selector exists in old page but not in new page, clear it
|
|
250
|
-
oldEl.innerHTML = '';
|
|
251
|
-
Array.from(oldEl.attributes).forEach(attr => {
|
|
252
|
-
if (attr.name !== 'id' && attr.name !== 'class') {
|
|
253
|
-
oldEl.removeAttribute(attr.name);
|
|
254
|
-
}
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
});
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/**
|
|
261
|
-
* Trigger custom events for other scripts to listen to
|
|
262
|
-
*/
|
|
263
|
-
function triggerEvent(name, detail = {}) {
|
|
264
|
-
const event = new CustomEvent(name, {
|
|
265
|
-
bubbles: true,
|
|
266
|
-
cancelable: true,
|
|
267
|
-
detail: detail
|
|
268
|
-
});
|
|
269
|
-
document.dispatchEvent(event);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/**
|
|
273
|
-
* Start loading indicator
|
|
274
|
-
*/
|
|
275
|
-
function startLoading() {
|
|
276
|
-
document.body.classList.add('pjax-loading');
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Stop loading indicator
|
|
281
|
-
*/
|
|
282
|
-
function stopLoading() {
|
|
283
|
-
document.body.classList.remove('pjax-loading');
|
|
284
|
-
// Trigger fade-in animation
|
|
285
|
-
document.body.classList.add('pjax-loaded');
|
|
286
|
-
|
|
287
|
-
// Remove animation class after it completes (matches CSS animation duration)
|
|
288
|
-
setTimeout(() => {
|
|
289
|
-
document.body.classList.remove('pjax-loaded');
|
|
290
|
-
}, 400);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
/**
|
|
294
|
-
* Fetch page content via AJAX
|
|
295
|
-
*/
|
|
296
|
-
async function fetchPage(url) {
|
|
297
|
-
// Abort any existing request
|
|
298
|
-
if (abortController) {
|
|
299
|
-
abortController.abort();
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
abortController = new AbortController();
|
|
303
|
-
|
|
304
|
-
const fetchUrl = config.cacheBust
|
|
305
|
-
? url + (url.includes('?') ? '&' : '?') + '_pjax=' + Date.now()
|
|
306
|
-
: url;
|
|
307
|
-
|
|
308
|
-
let timeoutId = null;
|
|
309
|
-
if (typeof config.timeout === 'number' && config.timeout > 0) {
|
|
310
|
-
timeoutId = setTimeout(function () {
|
|
311
|
-
// Abort the current request when the timeout is reached
|
|
312
|
-
if (abortController) {
|
|
313
|
-
abortController.abort();
|
|
314
|
-
}
|
|
315
|
-
}, config.timeout);
|
|
316
|
-
}
|
|
317
|
-
try {
|
|
318
|
-
const response = await fetch(fetchUrl, {
|
|
319
|
-
method: 'GET',
|
|
320
|
-
headers: {
|
|
321
|
-
'X-PJAX': 'true',
|
|
322
|
-
'X-Requested-With': 'XMLHttpRequest'
|
|
323
|
-
},
|
|
324
|
-
signal: abortController.signal
|
|
325
|
-
});
|
|
326
|
-
if (!response.ok) {
|
|
327
|
-
throw new Error(`HTTP ${response.status}`);
|
|
328
|
-
}
|
|
329
|
-
return await response.text();
|
|
330
|
-
} finally {
|
|
331
|
-
if (timeoutId !== null) {
|
|
332
|
-
clearTimeout(timeoutId);
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/**
|
|
338
|
-
* Execute comment system scripts from the new page
|
|
339
|
-
* This ensures comment init functions are registered after PJAX navigation
|
|
340
|
-
*/
|
|
341
|
-
function executeCommentScripts(doc) {
|
|
342
|
-
// Clear previous comment system init functions to avoid conflicts
|
|
343
|
-
if (window.stellar && window.stellar.initComments) {
|
|
344
|
-
window.stellar.initComments = {};
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
// Find and execute comment scripts from the new page
|
|
348
|
-
const scriptsDiv = doc.querySelector('.scripts');
|
|
349
|
-
if (!scriptsDiv) return;
|
|
350
|
-
|
|
351
|
-
// Look for comment-related script tags
|
|
352
|
-
const scripts = scriptsDiv.querySelectorAll('script');
|
|
353
|
-
scripts.forEach(oldScript => {
|
|
354
|
-
// Check if this is a comment system script by looking for initComments
|
|
355
|
-
const scriptContent = oldScript.textContent || oldScript.innerHTML;
|
|
356
|
-
if (scriptContent.includes('window.stellar.initComments')) {
|
|
357
|
-
// Create and execute a new script element
|
|
358
|
-
const newScript = document.createElement('script');
|
|
359
|
-
|
|
360
|
-
// Copy all attributes including type="module" if present
|
|
361
|
-
Array.from(oldScript.attributes).forEach(attr => {
|
|
362
|
-
newScript.setAttribute(attr.name, attr.value);
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
// For module scripts, we need to use a blob URL to preserve import statements
|
|
366
|
-
if (oldScript.type === 'module') {
|
|
367
|
-
const blob = new Blob([scriptContent], { type: 'text/javascript' });
|
|
368
|
-
const url = URL.createObjectURL(blob);
|
|
369
|
-
newScript.src = url;
|
|
370
|
-
|
|
371
|
-
// Clean up blob URL and script element after loading (or on error)
|
|
372
|
-
const cleanup = () => {
|
|
373
|
-
URL.revokeObjectURL(url);
|
|
374
|
-
newScript.remove();
|
|
375
|
-
};
|
|
376
|
-
newScript.onload = cleanup;
|
|
377
|
-
newScript.onerror = cleanup;
|
|
378
|
-
} else {
|
|
379
|
-
// For regular scripts, just copy the content
|
|
380
|
-
newScript.textContent = scriptContent;
|
|
381
|
-
// Script executes synchronously when appended, so we can remove it immediately
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
// Execute by appending to document
|
|
385
|
-
document.head.appendChild(newScript);
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
/**
|
|
391
|
-
* Navigate to a new page using PJAX
|
|
392
|
-
*/
|
|
393
|
-
async function navigate(url, options = {}) {
|
|
394
|
-
if (isLoading) return false;
|
|
395
|
-
|
|
396
|
-
const isPop = options.isPop || false;
|
|
397
|
-
const startTime = Date.now();
|
|
398
|
-
|
|
399
|
-
// Trigger before event
|
|
400
|
-
triggerEvent('pjax:before', { url });
|
|
401
|
-
|
|
402
|
-
isLoading = true;
|
|
403
|
-
startLoading();
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
// Check if we need to animate out (scroll to content start) if current page has cover
|
|
407
|
-
const currentWikiCover = document.querySelector('#l_cover .l_cover.wiki');
|
|
408
|
-
const startEl = document.getElementById('start');
|
|
409
|
-
let scrollPromise = Promise.resolve();
|
|
410
|
-
|
|
411
|
-
if (currentWikiCover && startEl && !isPop) {
|
|
412
|
-
// Scroll to #start so content slides up to top (pushing cover out of view)
|
|
413
|
-
const rect = startEl.getBoundingClientRect();
|
|
414
|
-
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
415
|
-
const targetTop = rect.top + scrollTop;
|
|
416
|
-
|
|
417
|
-
// Only animate if we are not already there
|
|
418
|
-
if (Math.abs(scrollTop - targetTop) > 5) {
|
|
419
|
-
window.scrollTo({ top: targetTop, behavior: 'smooth' });
|
|
420
|
-
// Wait for approx 400ms for scroll animation
|
|
421
|
-
scrollPromise = new Promise(resolve => setTimeout(resolve, 400));
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const [html] = await Promise.all([
|
|
426
|
-
fetchPage(url),
|
|
427
|
-
scrollPromise
|
|
428
|
-
]);
|
|
429
|
-
|
|
430
|
-
const { contents, doc } = extractContent(html, config.selectors);
|
|
431
|
-
contents._targetUrl = url;
|
|
432
|
-
|
|
433
|
-
// 确保最小加载时间以保证平滑动画
|
|
434
|
-
const elapsed = Date.now() - startTime;
|
|
435
|
-
const remainingTime = Math.max(0, config.minLoadTime - elapsed);
|
|
436
|
-
if (remainingTime > 0) {
|
|
437
|
-
await new Promise(resolve => setTimeout(resolve, remainingTime));
|
|
438
|
-
}
|
|
439
|
-
|
|
440
|
-
// Replace content
|
|
441
|
-
replaceContent(contents, config.selectors, doc);
|
|
442
|
-
|
|
443
|
-
// Execute comment scripts from the new page
|
|
444
|
-
executeCommentScripts(doc);
|
|
445
|
-
|
|
446
|
-
// Scroll to correct position
|
|
447
|
-
if (!isPop) {
|
|
448
|
-
// 如果有全屏 wiki cover,滚动到内容区域 (#start)
|
|
449
|
-
// 否则滚动到顶部
|
|
450
|
-
const wikiCover = document.querySelector('#l_cover .l_cover.wiki');
|
|
451
|
-
const newStartEl = document.getElementById('start');
|
|
452
|
-
|
|
453
|
-
if (wikiCover && newStartEl) {
|
|
454
|
-
// 立即跳转到目标位置,不使用动画(避免与导航前的滚动冲突)
|
|
455
|
-
const rect = newStartEl.getBoundingClientRect();
|
|
456
|
-
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
457
|
-
window.scrollTo({
|
|
458
|
-
top: rect.top + scrollTop,
|
|
459
|
-
behavior: 'auto'
|
|
460
|
-
});
|
|
461
|
-
} else {
|
|
462
|
-
window.scrollTo({ top: 0, behavior: 'auto' });
|
|
463
|
-
}
|
|
464
|
-
history.pushState({ pjax: true, url: url }, '', url);
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
// Re-initialize lazy loading
|
|
468
|
-
if (window.lazyLoadInstance) {
|
|
469
|
-
window.lazyLoadInstance.update();
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
// Trigger complete event for other scripts to re-initialize
|
|
473
|
-
// This is fired after DOM is updated and main content is ready
|
|
474
|
-
triggerEvent('pjax:complete', { url });
|
|
475
|
-
|
|
476
|
-
stopLoading();
|
|
477
|
-
isLoading = false;
|
|
478
|
-
|
|
479
|
-
return true;
|
|
480
|
-
} catch (error) {
|
|
481
|
-
stopLoading();
|
|
482
|
-
isLoading = false;
|
|
483
|
-
|
|
484
|
-
// If aborted, ignore
|
|
485
|
-
if (error.name === 'AbortError') {
|
|
486
|
-
return false;
|
|
487
|
-
}
|
|
488
|
-
|
|
489
|
-
console.error('PJAX Error:', error);
|
|
490
|
-
|
|
491
|
-
// Fallback to regular navigation
|
|
492
|
-
window.location.href = url;
|
|
493
|
-
return false;
|
|
494
|
-
}
|
|
495
|
-
}
|
|
496
|
-
|
|
497
|
-
/**
|
|
498
|
-
* Find the closest anchor element from event target
|
|
499
|
-
*/
|
|
500
|
-
function findAnchorElement(target) {
|
|
501
|
-
let element = target;
|
|
502
|
-
while (element && element.tagName !== 'A') {
|
|
503
|
-
element = element.parentElement;
|
|
504
|
-
}
|
|
505
|
-
return element;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
/**
|
|
509
|
-
* Check if link is a same-page hash link
|
|
510
|
-
*/
|
|
511
|
-
function isSamePageHashLink(link, currentUrl) {
|
|
512
|
-
if (!link || !link.href || link.tagName !== 'A') return false;
|
|
513
|
-
if (link.target === '_blank') return false;
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
const url = new URL(link.href);
|
|
517
|
-
return url.origin === currentUrl.origin &&
|
|
518
|
-
url.pathname === currentUrl.pathname &&
|
|
519
|
-
!!url.hash;
|
|
520
|
-
} catch (e) {
|
|
521
|
-
return false;
|
|
522
|
-
}
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
/**
|
|
526
|
-
* Decode hash ID to handle Chinese and other non-ASCII characters
|
|
527
|
-
* url.hash is URL-encoded (e.g., #%E4%B8%AD%E6%96%87), but getElementById needs decoded string
|
|
528
|
-
*/
|
|
529
|
-
function decodeHashId(hash) {
|
|
530
|
-
const rawId = hash.slice(1);
|
|
531
|
-
try {
|
|
532
|
-
return decodeURIComponent(rawId);
|
|
533
|
-
} catch (e) {
|
|
534
|
-
// If decoding fails, use the original value
|
|
535
|
-
return rawId;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
/**
|
|
540
|
-
* Scroll to element smoothly and update URL hash
|
|
541
|
-
*/
|
|
542
|
-
function scrollToElement(element, hash) {
|
|
543
|
-
const rect = element.getBoundingClientRect();
|
|
544
|
-
const scrollTop = window.pageYOffset || document.documentElement.scrollTop;
|
|
545
|
-
|
|
546
|
-
window.scrollTo({
|
|
547
|
-
top: rect.top + scrollTop,
|
|
548
|
-
behavior: 'smooth'
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
// Update URL hash without scrolling again
|
|
552
|
-
history.pushState(null, '', hash);
|
|
553
|
-
}
|
|
554
|
-
|
|
555
|
-
/**
|
|
556
|
-
* Handle same-page hash link navigation
|
|
557
|
-
* Returns true if handled, false otherwise
|
|
558
|
-
*/
|
|
559
|
-
function handleHashLink(link, currentUrl) {
|
|
560
|
-
if (!isSamePageHashLink(link, currentUrl)) return false;
|
|
561
|
-
|
|
562
|
-
try {
|
|
563
|
-
const url = new URL(link.href);
|
|
564
|
-
const targetId = decodeHashId(url.hash);
|
|
565
|
-
const target = document.getElementById(targetId);
|
|
566
|
-
|
|
567
|
-
if (target) {
|
|
568
|
-
scrollToElement(target, url.hash);
|
|
569
|
-
return true;
|
|
570
|
-
}
|
|
571
|
-
} catch (e) {
|
|
572
|
-
// Invalid URL, let it fall through
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
return false;
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
/**
|
|
579
|
-
* Handle link clicks
|
|
580
|
-
*/
|
|
581
|
-
function handleClick(event) {
|
|
582
|
-
const link = findAnchorElement(event.target);
|
|
583
|
-
|
|
584
|
-
// Handle in-page hash links FIRST (before shouldHandleLink check)
|
|
585
|
-
// This prevents page reloads when clicking table of contents in wiki mode
|
|
586
|
-
const currentUrl = new URL(window.location.href);
|
|
587
|
-
if (handleHashLink(link, currentUrl)) {
|
|
588
|
-
event.preventDefault();
|
|
589
|
-
return;
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
// Check if PJAX should handle this link
|
|
593
|
-
if (!shouldHandleLink(link)) {
|
|
594
|
-
return;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
// Check for modifier keys (new tab/window)
|
|
598
|
-
if (event.ctrlKey || event.metaKey || event.shiftKey || event.altKey) {
|
|
599
|
-
return;
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
// Prevent default and use PJAX
|
|
603
|
-
event.preventDefault();
|
|
604
|
-
navigate(link.href);
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
/**
|
|
608
|
-
* Handle browser back/forward buttons
|
|
609
|
-
*/
|
|
610
|
-
function handlePopState(event) {
|
|
611
|
-
// Only handle PJAX state or initial page
|
|
612
|
-
if (event.state?.pjax || !event.state) {
|
|
613
|
-
navigate(window.location.href, { isPop: true });
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
/**
|
|
618
|
-
* Initialize PJAX
|
|
619
|
-
*/
|
|
620
|
-
function init() {
|
|
621
|
-
// Set initial history state
|
|
622
|
-
history.replaceState({ pjax: true, url: window.location.href }, '', window.location.href);
|
|
623
|
-
|
|
624
|
-
// Listen for link clicks
|
|
625
|
-
document.addEventListener('click', handleClick, false);
|
|
626
|
-
|
|
627
|
-
// Listen for browser back/forward
|
|
628
|
-
window.addEventListener('popstate', handlePopState, false);
|
|
629
|
-
|
|
630
|
-
// Expose API
|
|
631
|
-
window.stellar = window.stellar || {};
|
|
632
|
-
window.stellar.pjax = {
|
|
633
|
-
navigate: navigate,
|
|
634
|
-
config: config
|
|
635
|
-
};
|
|
636
|
-
|
|
637
|
-
console.log('Stellar PJAX initialized');
|
|
638
|
-
}
|
|
639
|
-
|
|
640
|
-
// Initialize when DOM is ready
|
|
641
|
-
if (document.readyState === 'loading') {
|
|
642
|
-
document.addEventListener('DOMContentLoaded', init);
|
|
643
|
-
} else {
|
|
644
|
-
init();
|
|
645
|
-
}
|
|
646
|
-
})();
|