hexo-theme-nblog 1.0.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.
@@ -0,0 +1,415 @@
1
+ (function () {
2
+ 'use strict';
3
+
4
+ const themeKey = 'theme';
5
+ var header = document.getElementById('header');
6
+ var nav = document.getElementById('nav');
7
+ var menuBtn = document.getElementById('menuBtn');
8
+ var themeToggle = document.getElementById('themeToggle');
9
+
10
+ function initTheme() {
11
+ const savedTheme = localStorage.getItem(themeKey);
12
+ if (savedTheme === 'dark') {
13
+ document.documentElement.setAttribute('data-theme', 'dark');
14
+ } else if (savedTheme === null && window.matchMedia('(prefers-color-scheme: dark)').matches) {
15
+ document.documentElement.setAttribute('data-theme', 'dark');
16
+ }
17
+ }
18
+
19
+ function toggleTheme() {
20
+ const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
21
+ if (isDark) {
22
+ document.documentElement.removeAttribute('data-theme');
23
+ localStorage.setItem(themeKey, 'light');
24
+ } else {
25
+ document.documentElement.setAttribute('data-theme', 'dark');
26
+ localStorage.setItem(themeKey, 'dark');
27
+ }
28
+ }
29
+
30
+ function toggleMenu() {
31
+ nav.classList.toggle('header__nav--open');
32
+ }
33
+
34
+ let lastScrollY = 0;
35
+ let ticking = false;
36
+
37
+ function updateHeader() {
38
+ const scrollY = window.scrollY;
39
+
40
+ if (scrollY > 50) {
41
+ header.classList.add('header--scrolled');
42
+ } else {
43
+ header.classList.remove('header--scrolled');
44
+ }
45
+
46
+ if (scrollY > 300 && scrollY > lastScrollY) {
47
+ header.classList.add('header--hidden');
48
+ } else {
49
+ header.classList.remove('header--hidden');
50
+ }
51
+
52
+ lastScrollY = scrollY;
53
+ ticking = false;
54
+ }
55
+
56
+ function onScroll() {
57
+ if (!ticking) {
58
+ requestAnimationFrame(updateHeader);
59
+ ticking = true;
60
+ }
61
+ }
62
+
63
+ function setActiveNavLink() {
64
+ const currentPath = window.location.pathname;
65
+ const navLinks = document.querySelectorAll('.header__nav-item');
66
+
67
+ navLinks.forEach(link => {
68
+ const href = link.getAttribute('href');
69
+ if (href === currentPath || (currentPath.startsWith(href) && href !== '/')) {
70
+ link.classList.add('active');
71
+ } else {
72
+ link.classList.remove('active');
73
+ }
74
+ });
75
+ }
76
+
77
+ function loadHighlightJS(callback) {
78
+ if (window.hljs) {
79
+ callback();
80
+ return;
81
+ }
82
+
83
+ const script = document.createElement('script');
84
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js';
85
+ script.onload = function () {
86
+ const vueScript = document.createElement('script');
87
+ vueScript.src = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/vue.min.js';
88
+ vueScript.onload = callback;
89
+ document.head.appendChild(vueScript);
90
+ };
91
+ document.head.appendChild(script);
92
+ }
93
+
94
+ function applyHighlight(element, language) {
95
+ if (!window.hljs) return;
96
+
97
+ const codeElement = element.querySelector('code') || element.querySelector('.code pre') || element.querySelector('pre');
98
+ if (!codeElement) return;
99
+
100
+ let codeText = codeElement.textContent || codeElement.innerText;
101
+
102
+ element.innerHTML = '';
103
+
104
+ const pre = document.createElement('pre');
105
+ const code = document.createElement('code');
106
+ code.className = language ? `hljs language-${language}` : 'hljs';
107
+ code.textContent = codeText;
108
+ pre.appendChild(code);
109
+ element.appendChild(pre);
110
+
111
+ if (language && hljs.getLanguage(language)) {
112
+ hljs.highlightElement(code);
113
+ } else {
114
+ hljs.highlightElement(code);
115
+ }
116
+ }
117
+
118
+ function initCodeBlocks() {
119
+ const highlights = document.querySelectorAll('.post-body .highlight');
120
+
121
+ highlights.forEach(highlight => {
122
+ if (highlight.closest('.highlight-wrap')) return;
123
+
124
+ const wrapper = document.createElement('div');
125
+ wrapper.className = 'highlight-wrap';
126
+ highlight.parentNode.insertBefore(wrapper, highlight);
127
+ wrapper.appendChild(highlight);
128
+
129
+ const codeElement = highlight.querySelector('code');
130
+ let language = '';
131
+ if (codeElement) {
132
+ const classMatch = codeElement.className.match(/(?:hljs\s+)?(?:language-)?(\w+)/);
133
+ if (classMatch) {
134
+ language = classMatch[1];
135
+ }
136
+ }
137
+
138
+ const figureClass = highlight.className;
139
+ const langMatch = figureClass.match(/highlight\s+(\w+)/);
140
+ if (langMatch && langMatch[1] !== 'plaintext') {
141
+ language = langMatch[1];
142
+ }
143
+
144
+ loadHighlightJS(function () {
145
+ applyHighlight(highlight, language);
146
+ });
147
+
148
+ const copyBtn = document.createElement('button');
149
+ copyBtn.className = 'copy-btn';
150
+ copyBtn.textContent = 'Copy';
151
+ wrapper.appendChild(copyBtn);
152
+
153
+ copyBtn.addEventListener('click', async function () {
154
+ const codeEl = highlight.querySelector('code') || highlight.querySelector('.code');
155
+ let codeText = '';
156
+
157
+ if (codeEl) {
158
+ codeText = codeEl.innerText || codeEl.textContent;
159
+ } else {
160
+ codeText = highlight.innerText || highlight.textContent;
161
+ }
162
+
163
+ try {
164
+ await navigator.clipboard.writeText(codeText);
165
+ copyBtn.textContent = 'Copied';
166
+ copyBtn.classList.add('copied');
167
+
168
+ setTimeout(() => {
169
+ copyBtn.textContent = 'Copy';
170
+ copyBtn.classList.remove('copied');
171
+ }, 2000);
172
+ } catch (err) {
173
+ const textarea = document.createElement('textarea');
174
+ textarea.value = codeText;
175
+ textarea.style.position = 'fixed';
176
+ textarea.style.left = '-9999px';
177
+ document.body.appendChild(textarea);
178
+ textarea.select();
179
+ document.execCommand('copy');
180
+ document.body.removeChild(textarea);
181
+
182
+ copyBtn.textContent = 'Copied';
183
+ copyBtn.classList.add('copied');
184
+
185
+ setTimeout(() => {
186
+ copyBtn.textContent = 'Copy';
187
+ copyBtn.classList.remove('copied');
188
+ }, 2000);
189
+ }
190
+ });
191
+ });
192
+ }
193
+
194
+ function loadTocbot(callback) {
195
+ if (window.tocbot) {
196
+ callback();
197
+ return;
198
+ }
199
+
200
+ const link = document.createElement('link');
201
+ link.rel = 'stylesheet';
202
+ link.href = 'https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.28.2/tocbot.css';
203
+ document.head.appendChild(link);
204
+
205
+ const script = document.createElement('script');
206
+ script.src = 'https://cdnjs.cloudflare.com/ajax/libs/tocbot/4.28.2/tocbot.min.js';
207
+ script.onload = callback;
208
+ document.head.appendChild(script);
209
+ }
210
+
211
+ function initToc() {
212
+ const toc = document.getElementById('postToc');
213
+ const postBody = document.getElementById('postBody');
214
+ const tocContent = document.getElementById('tocContent');
215
+ const tocToggle = document.getElementById('tocToggle');
216
+
217
+ if (!toc || !postBody || !tocContent) return;
218
+
219
+ const headings = postBody.querySelectorAll('h1, h2, h3, h4, h5, h6');
220
+ if (headings.length === 0) {
221
+ toc.style.display = 'none';
222
+ return;
223
+ }
224
+
225
+ headings.forEach((heading, index) => {
226
+ if (!heading.id) {
227
+ heading.id = 'heading-' + index;
228
+ }
229
+ });
230
+
231
+ if (window.tocbot && window.tocbot.destroy) {
232
+ window.tocbot.destroy();
233
+ }
234
+
235
+ loadTocbot(function () {
236
+ tocbot.init({
237
+ tocSelector: '.js-toc',
238
+ contentSelector: '#postBody',
239
+ headingSelector: 'h1, h2, h3, h4, h5, h6',
240
+ hasInnerContainers: true,
241
+ linkClass: 'toc-link',
242
+ activeLinkClass: 'is-active-link',
243
+ listClass: 'toc-list',
244
+ listItemClass: 'toc-list-item',
245
+ activeListItemClass: 'is-active-li',
246
+ collapseDepth: 0,
247
+ scrollSmooth: true,
248
+ scrollSmoothDuration: 420,
249
+ scrollSmoothOffset: -80,
250
+ headingsOffset: 80,
251
+ throttleTimeout: 50,
252
+ disableTocScrollSync: false
253
+ });
254
+
255
+ initTocBoundaryDetection(toc, postBody);
256
+ initTocToggle(toc, tocToggle);
257
+ });
258
+ }
259
+
260
+ function initTocToggle(toc, tocToggle) {
261
+ if (!toc || !tocToggle) return;
262
+
263
+ const STORAGE_KEY = 'toc-collapsed';
264
+ const isCollapsed = localStorage.getItem(STORAGE_KEY) === 'true';
265
+
266
+ if (isCollapsed) {
267
+ toc.classList.add('post-toc--collapsed');
268
+ tocToggle.setAttribute('aria-expanded', 'false');
269
+ tocToggle.setAttribute('aria-label', '展开目录');
270
+ }
271
+
272
+ tocToggle.addEventListener('click', function () {
273
+ const currentlyCollapsed = toc.classList.toggle('post-toc--collapsed');
274
+ tocToggle.setAttribute('aria-expanded', !currentlyCollapsed);
275
+ tocToggle.setAttribute('aria-label', currentlyCollapsed ? '展开目录' : '折叠目录');
276
+ localStorage.setItem(STORAGE_KEY, currentlyCollapsed);
277
+ });
278
+
279
+ tocToggle.addEventListener('keydown', function (e) {
280
+ if (e.key === 'Enter' || e.key === ' ') {
281
+ e.preventDefault();
282
+ tocToggle.click();
283
+ }
284
+ });
285
+ }
286
+
287
+ function initTocBoundaryDetection(toc, postBody) {
288
+ const tocInner = toc.querySelector('.post-toc__inner');
289
+ const postBodyWrapper = document.getElementById('postBodyWrapper');
290
+ if (!tocInner || !postBodyWrapper) return;
291
+
292
+ const headerHeight = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--header-height')) || 64;
293
+ const tocTopOffset = headerHeight + 24;
294
+
295
+ function updateTocPosition() {
296
+ const viewportWidth = window.innerWidth;
297
+
298
+ if (viewportWidth < 1200) {
299
+ toc.classList.add('post-toc--hidden');
300
+ toc.classList.remove('post-toc--visible');
301
+ return;
302
+ }
303
+
304
+ toc.classList.remove('post-toc--hidden');
305
+
306
+ const scrollY = window.scrollY;
307
+ const wrapperRect = postBodyWrapper.getBoundingClientRect();
308
+ const tocInnerHeight = tocInner.offsetHeight;
309
+ const viewportHeight = window.innerHeight;
310
+
311
+ const wrapperTopAbsolute = wrapperRect.top + scrollY;
312
+ const wrapperBottomAbsolute = wrapperRect.bottom + scrollY;
313
+
314
+ const maxTocHeight = viewportHeight - tocTopOffset - 24;
315
+ if (tocInnerHeight > maxTocHeight) {
316
+ tocInner.style.maxHeight = maxTocHeight + 'px';
317
+ }
318
+
319
+ const tocStartY = wrapperTopAbsolute - tocTopOffset;
320
+ const tocEndY = wrapperBottomAbsolute - tocInnerHeight - tocTopOffset - 24;
321
+
322
+ if (scrollY < tocStartY || scrollY > tocEndY) {
323
+ toc.classList.remove('post-toc--visible');
324
+ } else {
325
+ toc.classList.add('post-toc--visible');
326
+ }
327
+ }
328
+
329
+ function onScroll() {
330
+ requestAnimationFrame(updateTocPosition);
331
+ }
332
+
333
+ window.addEventListener('scroll', onScroll, { passive: true });
334
+ window.addEventListener('resize', onScroll, { passive: true });
335
+
336
+ updateTocPosition();
337
+ setTimeout(updateTocPosition, 100);
338
+ setTimeout(updateTocPosition, 500);
339
+ }
340
+
341
+ function initPage() {
342
+ header = document.getElementById('header');
343
+ nav = document.getElementById('nav');
344
+ menuBtn = document.getElementById('menuBtn');
345
+ themeToggle = document.getElementById('themeToggle');
346
+
347
+ if (themeToggle) {
348
+ themeToggle.removeEventListener('click', toggleTheme);
349
+ themeToggle.addEventListener('click', toggleTheme);
350
+ }
351
+
352
+ if (menuBtn) {
353
+ menuBtn.removeEventListener('click', toggleMenu);
354
+ menuBtn.addEventListener('click', toggleMenu);
355
+ }
356
+
357
+ setActiveNavLink();
358
+ initCodeBlocks();
359
+ initToc();
360
+
361
+ window.scrollTo(0, 0);
362
+ }
363
+
364
+ window.initPage = initPage;
365
+
366
+ function init() {
367
+ initTheme();
368
+ setActiveNavLink();
369
+
370
+ if (themeToggle) {
371
+ themeToggle.removeEventListener('click', toggleTheme);
372
+ themeToggle.addEventListener('click', toggleTheme);
373
+ }
374
+
375
+ if (menuBtn) {
376
+ menuBtn.removeEventListener('click', toggleMenu);
377
+ menuBtn.addEventListener('click', toggleMenu);
378
+ }
379
+
380
+ window.removeEventListener('scroll', onScroll);
381
+ window.addEventListener('scroll', onScroll, { passive: true });
382
+
383
+ document.removeEventListener('click', handleOutsideClick);
384
+ document.addEventListener('click', handleOutsideClick);
385
+
386
+ const navLinks = document.querySelectorAll('.header__nav-item');
387
+ navLinks.forEach(link => {
388
+ link.removeEventListener('click', handleNavLinkClick);
389
+ link.addEventListener('click', handleNavLinkClick);
390
+ });
391
+
392
+ initCodeBlocks();
393
+ initToc();
394
+ }
395
+
396
+ function handleOutsideClick(e) {
397
+ if (nav && nav.classList.contains('header__nav--open')) {
398
+ if (!nav.contains(e.target) && !menuBtn.contains(e.target)) {
399
+ toggleMenu();
400
+ }
401
+ }
402
+ }
403
+
404
+ function handleNavLinkClick() {
405
+ if (nav && nav.classList.contains('header__nav--open')) {
406
+ toggleMenu();
407
+ }
408
+ }
409
+
410
+ if (document.readyState === 'loading') {
411
+ document.addEventListener('DOMContentLoaded', init);
412
+ } else {
413
+ init();
414
+ }
415
+ })();