MediaWiki:Timeless.js

来自「荏苒之境」
Sicusa留言 | 贡献2025年8月27日 (三) 17:10的版本

注意:在发布之后,您可能需要清除浏览器缓存才能看到所作出的更改的影响。

  • Firefox或Safari:按住Shift的同时单击刷新,或按Ctrl-F5Ctrl-R(Mac为⌘-R
  • Google Chrome:Ctrl-Shift-R(Mac为⌘-Shift-R
  • Edge:按住Ctrl的同时单击刷新,或按Ctrl-F5
/* 将为Timeless皮肤的用户加载此处的所有JavaScript  */

class HeadingTracker {
    constructor(rootElement) {
    	this.rootElement = rootElement
        this.headings = [];
        this.cacheExpired = true;
        this.init();
    }

    init() {
        this.collectHeadings();
        window.addEventListener('resize', () => this.cacheExpired = true);
        window.addEventListener('DOMContentLoaded', () => this.cacheExpired = true);
    }

    collectHeadings() {
        const rawHeadings = Array.from(this.rootElement.querySelectorAll('h1, h2, h3, h4, h5, h6'));
        this.headings = rawHeadings
            .map(heading => ({
                element: heading,
                top: heading.getBoundingClientRect().top + window.scrollY
            }))
            .sort((a, b) => a.top - b.top);
        this.cacheExpired = false;
    }

    getNearestHeadingAboveViewport() {
        if (this.cacheExpired) this.collectHeadings();

        const scrollY = window.scrollY + 60;
        let left = 0, right = this.headings.length - 1;
        let result = null;

        while (left <= right) {
            const mid = Math.floor((left + right) / 2);
            if (this.headings[mid].top <= scrollY) {
                result = this.headings[mid].element;
                left = mid + 1;
            } else {
                right = mid - 1;
            }
        }
        return result;
    }
}

class FloatingToC {
	constructor(toc) {
		this.element = toc;
		this.ignoreScrollUpdate = false;
	    
	    let tocTextList = Array.from(toc.querySelectorAll(".toctext"));
	    this.tocTexts = new Map(tocTextList.map(t => {
	    	let textContent = t.textContent
	    	t.parentElement.onclick = () => {
	    		this.ignoreScrollUpdate = true;
	    		this.setCurrentHeading(textContent);
	    	};
	    	return [textContent, t];
	    }));

		this.tracker = new HeadingTracker(
			document.getElementById('mw-content-text'));
		
		this.scrollHandler = this.onScroll.bind(this);
		window.addEventListener('scroll', this.scrollHandler);
	}
	
	remove() {
		window.removeEventListener('scroll', this.scrollHandler);
		this.element.remove();
	}
	
    setCurrentHeading(heading) {
    	const lastHeading = this.lastHeading;
    	if (heading == lastHeading) {
    		return;
    	}
    	if (lastHeading) {
    		let text = this.tocTexts.get(lastHeading)
    		if (text) text.classList.remove('current-heading');
    	}
    	this.lastHeading = heading;
    	if (heading) {
    		let text = this.tocTexts.get(heading)
    		if (text) text.classList.add('current-heading');
    	}
    };
	
	onScroll() {
		if (window.innerWidth < 1100) {
			return;
		}
		if (this.ignoreScrollUpdate) {
			this.ignoreScrollUpdate = false;
			return;
		}
	    let heading = this.tracker.getNearestHeadingAboveViewport();
	    this.setCurrentHeading(heading ? heading.textContent : null);
	}
}

let siteNavigation = document.getElementById('mw-site-navigation');
let floatingToC;
let toc = document.getElementById('toc');

const userAgent = navigator.userAgent;
const isSafari = userAgent.includes("Safari");

mw.hook('ve.newTarget').add(target => {
	if (target.constructor.static.name !== 'article') {
		return;
	}
	target.on('teardown', () => {
		var event = new CustomEvent("pageupdate");
		document.dispatchEvent(event);
	});
});

const updateTocParent = () => {
	let width = window.innerWidth;
	if (isSafari && width > 1355 || !isSafari && width > 1339) {
		if (toc.parentNode != siteNavigation) {
			toc.parentNode.removeChild(toc);
			siteNavigation.appendChild(toc);
		}
	}
	else {
		if (toc.parentNode == siteNavigation) {
			toc.parentNode.removeChild(toc);
			siteNavigation.parentNode.lastChild.before(toc);
		}
	}
}

window.addEventListener("resize", () => {
	if (floatingToC == null) {
		return;
	}
	updateTocParent();
});

document.addEventListener('pageupdate', () => {
	let new_toc = document.querySelectorAll('.mw-parser-output > #toc')[0];
	if (new_toc == null) {
		return;
	}
	if (floatingToC != null) {
		floatingToC.remove();
	}
	toc = new_toc;
	updateTocParent();
    floatingToC = new FloatingToC(toc);
});

if (toc) {
    floatingToC = new FloatingToC(toc);
    updateTocParent();
}