Greasy Fork

Greasy Fork is available in English.

A岛引用查看增强

让A岛网页端的引用支持嵌套查看、固定、折叠等功能

当前为 2021-03-22 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        A岛引用查看增强
// @namespace   http://tampermonkey.net/
// @version     0.1.17
// @description 让A岛网页端的引用支持嵌套查看、固定、折叠等功能
// @author      FToovvr
// @license     MIT; https://opensource.org/licenses/MIT
// @include     /^https?://(adnmb\d*.com|tnmb.org)/.*$/
// @grant       none
// ==/UserScript==

// TODO 0.2:
// TODO: 自定义配置页 https://stackoverflow.com/a/43462416
// TODO: 配置决定:点图钉是悬浮还是关闭,还是额外提供「🚫」按钮?正常载入是否还提供刷新按钮?多少秒算超时?是否自动固定存在缓存的引用内容?
// TODO 0.3:
// TODO: 持久化缓存;配置可选数量;按「最后看到的时间」、「是直接看到还是获取引用看到」决定权重
// TODO: 自动固定其他相同的引用?
// TODO ?:
// TODO: 随时有图钉按钮解除固定?
// TODO: 要不要考虑尝试在重新加载后还原先前展开/折叠的状态?
// TODO: 悬浮淡出
// TODO: 折叠时点击 mask,preventDefault?
// TODO ??:
// TODO?: 优化引用内容的空白?

// 测试场地:
// * 长内容: https://adnmb3.com/t/36053697
// * 长内容2: https://adnmb3.com/t/36048637?page=2
// * 不存在内容: https://adnmb3.com/f/值班室
// * 同一份不存在的内容两次:https://adnmb3.com/t/36081863
// * 超级嵌套: https://adnmb3.com/t/20311039?page=1641
// * 各种内容: https://adnmb3.com/t/26165309?page=3(页数是随便选的)

(function () {
    'use strict';

    // TODO: 配置决定
    // 折叠时保持的高度,低于此高度将不可折叠
    const collapsedHeight = 80;
    // 悬浮时引用内容的不透明度
    const floatingOpacity = '100%'; // '90%';
    // 悬浮淡入的时长(暂不支持淡出)
    const fadingDuration = 0; // '80ms';
    // 如为真,在固定时点击图钉按钮会直接关闭引用内容,而非转为悬浮
    const clickPinToCloseView = false;
    // 获取引用内容多少毫秒算超时
    const refFetchingTimeout = 10000; //= 10 秒
    // 在内容成功加载后是否还显示刷新按钮
    const showRefreshButtonEvenIfRefContentLoaded = false;
    // 如为真,存在缓存的引用内容会自动以折叠的形式固定
    const autoOpenRefViewIfRefContentAlreadyCached = false;
    // 如为真,展开一处引用将展开当前已知所有其他处指向相同内容的引用
    // TODO: 考虑也自动展开之后才遇到的指向相同内容的引用?
    // 尚未实现
    const autoOpenOtherRefViewsWithSameRefIdAfterOpenOne = false;

    const additionalStyleText = `

    .h-threads-content {
        word-break: break-word;
    }
    
    .fto-ref-view {
        /* 照搬自 h.desktop.css '#h-ref-view .h-threads-item-ref' */
        background: #f0e0d6;
        border: 1px solid #000;
        clear: left;
    
        position: relative;
    
        width: max-content;
        max-width: calc(100vw - var(--offset-left) - 35px);
    
        margin-left: -5px;
        margin-right: -40px;
    }
    
    .h-threads-item-ref .h-threads-content {
        margin: 5px 20px;
    }
    
    /* 修复 h.desktop.css 里 '.h-threads-item .h-threads-content' 这条选择器导致的问题 */
    .h-threads-info {
        font-size: 14px;
        line-height: 20px;
        margin: 0px;
    }
    
    .fto-ref-view[data-status="closed"] {
        /* display: none; */
        opacity: 0; display: inline-block;
        width: 0; height: 0; overflow: hidden;
        padding: 0; border: 0; margin: 0;
    
        /* transition: opacity ${fadingDuration} ease-out; */
    }
    
    .fto-ref-view[data-status="floating"] {
        position: absolute;
        z-index: 999;
    
        opacity: ${floatingOpacity};
    
        transition: opacity ${fadingDuration} ease-in;
    }
    
    .fto-ref-view[data-status="open"] {
        display: block;
    }
    .fto-ref-view[data-status="open"] + br {
        display: none;
    }
    
    .fto-ref-view[data-status="collapsed"] {
        display: block;
        max-height: ${collapsedHeight}px;
        overflow: hidden;
        text-overflow: ellipsis;
    }
    .fto-ref-view[data-status="collapsed"] + br {
        display: none;
    }
    
    /* https://stackoverflow.com/a/22809380 */
    .fto-ref-view[data-status="collapsed"]:before {
        content: '';
        position: absolute;
        top: 60px;
        height: 20px;
        width: 100%;
        background: linear-gradient(#f0e0d600, #ffeeddcc);
        z-index: 999;
    }
    
    .fto-ref-view-button {
        position: relative;
    
        font-size: smaller;
    
        cursor: pointer;
        -webkit-user-select: none;
        -moz-user-select: none;
        -ms-user-select: none;
        user-select: none;
    }
    
    .fto-ref-view-pin {
        display: inline-block;
        transform: rotate(-45deg);
    }
    /* https://codemyui.com/grayscale-emoji-using-css/ */
    .fto-ref-view[data-status="floating"]
    >.h-threads-item >.h-threads-item-ref
    >.h-threads-item-reply-main >.h-threads-info
    >.fto-ref-view-button-list > .fto-ref-view-pin,
    .fto-ref-view[data-status="floating"] >.fto-ref-view-error
    >.fto-ref-view-button-list >.fto-ref-view-pin,
    .fto-ref-view[data-status="floating"] >.fto-ref-view-loading
    >.fto-ref-view-button-list >.fto-ref-view-pin {
        transform: none;
        filter: grayscale(100%);
    }
    .fto-ref-view[data-status="collapsed"]
    >.h-threads-item >.h-threads-item-ref
    >.h-threads-item-reply-main >.h-threads-info
    >.fto-ref-view-button-list >.fto-ref-view-pin:before,
    .fto-ref-view[data-status="collapsed"] >.fto-ref-view-error
    >.fto-ref-view-button-list >.fto-ref-view-pin:before,
    .fto-ref-view[data-status="collapsed"] >.fto-ref-view-loading
    >.fto-ref-view-button-list >.fto-ref-view-pin:before  {
        content: '';
        position: absolute;
        height: 110%;
        width: 100%;
        background: linear-gradient(#f0e0d600, #f0e0d6ff);
        z-index: 999;
        transform: rotate(45deg);
    }
    
    .fto-ref-view-error {
        color: red;
    }
    
    `;

    function entry() {

        if (window.disableAdnmbReferenceViewerEnhancementUserScript) {
            console.log("「A岛引用查看增强」用户脚本被禁用(设有变量 `window.disableAdnmbReferenceViewerEnhancementUserScript`),将终止。");
            return;
        }

        const model = new Model();
        if (!model.isSupported) {
            console.log("浏览器功能不支持「A岛引用查看增强」用户脚本,将终止。");
            return;
        }

        // 销掉原先的预览方法
        document.querySelectorAll('font[color="#789922"]').forEach((elem) => {
            if (elem.textContent.startsWith('>>')) {
                const newElem = elem.cloneNode(true);
                elem.parentElement.replaceChild(newElem, elem);
            }
        });

        Controller.setupStyle();

        const controller = new Controller(model);
        controller.setupContent(document.body);
    }

    class Controller {

        /**
         * 
         * @param {Model} model 
         */
        constructor(model) {
            this.model = model;
        }

        static setupStyle() {
            const style = document.createElement('style');
            style.id = 'fto-additional-style';
            // TODO: fade out
            style.append(additionalStyleText);
            document.head.append(style);
        }

        /**
         * 
         * @param {HTMLElement} root
         */
        setupContent(root) {

            if (root === document.body) {
                root.querySelectorAll('.h-threads-item').forEach((threadItemElem) => {
                    this.setupThreadContent(threadItemElem);
                });
            } else if (ViewHelper.hasFetchingRefSucceeded(root)) {
                const repliesElem = root.closest('.h-threads-item-replys');
                let threadElem;
                if (repliesElem) { // 在串的回应中
                    threadElem = repliesElem.closest('.h-threads-item');
                } else { // 在串首中
                    threadElem = root.closest('.h-threads-item-main');
                }
                const threadID = ViewHelper.getThreadID(threadElem);
                const po = ViewHelper.getPosterID(threadElem);
                this.setupRefContent(root, threadID, po);
            } else {
                this.setupErrorRefContent(root);
                return;
            }

            root.querySelectorAll('font[color="#789922"]').forEach(linkElem => {
                if (!linkElem.textContent.startsWith('>>')) { return; }
                this.setupRefLink(linkElem);
            });
        }

        /**
         * 
         * @param {HTMLElement} threadItemElem 
         */
        setupThreadContent(threadItemElem) {
            const threadID = ViewHelper.getThreadID(threadItemElem);

            { // 将串首加入缓存
                const originalItemMainElem = threadItemElem.querySelector('.h-threads-item-main');
                const itemDiv = document.createElement('div');
                itemDiv.classList.add('h-threads-item');
                const itemRefDiv = document.createElement('div');
                itemRefDiv.classList.add('h-threads-item-reply', 'h-threads-item-ref');
                itemDiv.append(itemRefDiv);
                const itemMainDiv = originalItemMainElem.cloneNode(true);
                itemMainDiv.className = '';
                itemMainDiv.classList.add('h-threads-item-reply-main');
                itemRefDiv.append(itemMainDiv);
                const infoDiv = itemMainDiv.querySelector('.h-threads-info');
                try { // 尝试修正几个按钮的位置。以后如果A岛自己修正了这里就会抛异常
                    const messedUpDiv = infoDiv.querySelector('.h-admin-tool').closest('.h-threads-info-report-btn');
                    if (!messedUpDiv) { // 版块页面里的各个按钮没搞砸
                        infoDiv.querySelectorAll('.h-threads-info-report-btn a').forEach((aElem) => {
                            if (aElem.textContent !== "举报") {
                                aElem.closest('.h-threads-info-report-btn').remove();
                            }
                        });
                        infoDiv.querySelector('.h-threads-info-reply-btn').remove();
                    } else { // 串内容页面的各个按钮搞砸了
                        infoDiv.append(
                            '', messedUpDiv.querySelector('.h-threads-info-id'),
                            '', messedUpDiv.querySelector('.h-admin-tool'),
                        );
                        messedUpDiv.remove();
                    }
                } catch (e) {
                    console.log(e);
                }
                this.model.recordRef(threadID, itemDiv, 'global');
            }

            // 将各回应加入缓存
            threadItemElem.querySelectorAll('.h-threads-item-replys .h-threads-item-reply').forEach((originalItemElem) => {
                const div = document.createElement('div');
                div.classList.add('h-threads-item');
                const itemElem = originalItemElem.cloneNode(true);
                itemElem.classList.add('h-threads-item-ref');
                itemElem.querySelector('.h-threads-item-reply-icon').remove();
                for (const child of itemElem.querySelector('.h-threads-item-reply-main').children) {
                    if (!child.classList.contains('h-threads-info')
                        && !child.classList.contains('h-threads-content')) {
                        child.remove();
                    }
                }
                itemElem.querySelectorAll('.uk-text-primary').forEach((labelElem) => {
                    if (labelElem.textContent === "(PO主)") {
                        labelElem.remove();
                    }
                });
                div.append(itemElem);
                this.model.recordRef(ViewHelper.getPostID(itemElem), div, 'global');
            });
        }

        /**
         * 
         * @param {HTMLElement} elem 
         * @param {number} threadID
         * @param {String} po
         */
        setupRefContent(elem, threadID, po) {
            const infoElem = elem.querySelector('.h-threads-info');

            // 补标 PO
            if (ViewHelper.getPosterID(infoElem) === po) {
                const poLabel = document.createElement('span');
                poLabel.textContent = "(PO主)";
                poLabel.classList.add('uk-text-primary', 'uk-text-small', 'fto-po-label');
                const elem = infoElem.querySelector('.h-threads-info-uid');
                Utils.insertAfter(elem, poLabel);
                Utils.insertAfter(elem, document.createTextNode(' '));
            }

            // 标「外串」
            if (ViewHelper.getThreadID(infoElem) !== threadID) {
                const outerThreadLabel = document.createElement('span');
                outerThreadLabel.textContent = "(外串)";
                outerThreadLabel.classList.add('uk-text-secondary', 'uk-text-small', 'fto-outer-thread-label');
                const elem = infoElem.querySelector('.h-threads-info-id');
                elem.append(' ', outerThreadLabel);
            }

            this.setupButtons(infoElem);
        }

        /**
         * 
         * @param {Element} elem 
         */
        setupErrorRefContent(elem) {
            this.setupButtons(elem);
        }

        /**
         * 
         * @param {HTMLElement} elem
         */
        setupButtons(elem) {
            const viewDiv = elem.closest('.fto-ref-view');
            const linkElem = ViewHelper.getRefLinkByViewId(viewDiv.dataset.viewId);

            const buttonListSpan = document.createElement('span');
            buttonListSpan.classList.add('fto-ref-view-button-list');

            // 图钉📌按钮
            const pinSpan = document.createElement('span');
            pinSpan.classList.add('fto-ref-view-pin', 'fto-ref-view-button');
            pinSpan.textContent = "📌";
            pinSpan.addEventListener('click', () => {
                if (viewDiv.dataset.status === 'floating') {
                    linkElem.dataset.status = 'open';
                    viewDiv.dataset.status = 'open';
                } else {
                    linkElem.dataset.status = 'closed';
                    viewDiv.dataset.status = clickPinToCloseView ? 'closed' : 'floating';
                }
            });
            buttonListSpan.append(pinSpan);

            if (!viewDiv.isLoading &&
                (!ViewHelper.hasFetchingRefSucceeded(elem) || showRefreshButtonEvenIfRefContentLoaded)) {
                // 刷新🔄按钮
                const refreshSpan = document.createElement('span');
                refreshSpan.classList.add('fto-ref-view-refresh', 'fto-ref-view-button');
                refreshSpan.textContent = "🔄";
                refreshSpan.addEventListener('click', () => {
                    this.startLoadingViewContent(viewDiv, Number(linkElem.dataset.refId), true);
                });
                Utils.insertAfter(pinSpan, refreshSpan);
                buttonListSpan.append(refreshSpan);
            }

            elem.prepend(buttonListSpan);
        }

        /**
         * 
         * @param {HTMLElement} linkElem 
         */
        setupRefLink(linkElem) {
            linkElem.classList.add('fto-ref-link');
            // closed: 无固定显示 view; open: 有固定显示 view
            linkElem.dataset.status = 'closed';

            const r = /^>>No.(\d+)$/.exec(linkElem.textContent);
            if (!r) { return; }
            const refId = Number(r[1]);
            linkElem.dataset.refId = String(refId);

            const viewId = Utils.generateViewID();
            linkElem.dataset.viewId = viewId;

            const viewDiv = document.createElement('div');
            viewDiv.classList.add('fto-ref-view');
            // closed: 不显示; floating: 悬浮显示; open: 完整固定显示; collapsed: 折叠固定显示
            viewDiv.dataset.status = 'closed';
            viewDiv.dataset.viewId = viewId;

            viewDiv.style.setProperty('--offset-left', `${Utils.getCoords(linkElem).left}px`);

            Utils.insertAfter(linkElem, viewDiv);

            if (autoOpenRefViewIfRefContentAlreadyCached) {
                (async () => {
                    const refCache = await this.model.getRefCache(refId);
                    if (refCache) {
                        linkElem.dataset.status = 'open';
                        viewDiv.dataset.status = 'collapsed';
                        viewDiv.append(refCache);
                        this.setupContent(refCache);
                    }
                })();
            }

            // 处理悬浮
            linkElem.addEventListener('mouseenter', () => {
                if (viewDiv.dataset.status !== 'closed') {
                    viewDiv.dataset.isHovering = '1';
                    return;
                }
                viewDiv.dataset.status = 'floating';
                viewDiv.dataset.isHovering = '1';
                this.startLoadingViewContent(viewDiv, refId);
            });
            viewDiv.addEventListener('mouseenter', () => {
                viewDiv.dataset.isHovering = '1';
            });
            for (const elem of [linkElem, viewDiv]) {
                elem.addEventListener('mouseleave', () => {
                    if (viewDiv.dataset.status != 'floating') {
                        return;
                    }
                    delete viewDiv.dataset.isHovering;
                    (async () => {
                        setTimeout(() => {
                            if (!viewDiv.dataset.isHovering) {
                                viewDiv.dataset.status = 'closed';
                            }
                        }, 200);
                    })();
                });
            }

            // 处理折叠
            linkElem.addEventListener('click', () => {
                if (linkElem.dataset.status === 'closed'
                    || ['collapsed', 'floating'].includes(viewDiv.dataset.status)) {
                    linkElem.dataset.status = 'open';
                    viewDiv.dataset.status = 'open';
                } else if (viewDiv.clientHeight > collapsedHeight) {
                    viewDiv.dataset.status = 'collapsed';
                }
            });
            viewDiv.addEventListener('click', () => {
                if (viewDiv.dataset.status === 'collapsed') {
                    viewDiv.dataset.status = 'open';
                }
            });
        }

        /**
         * 
         * @param {HTMLElement} viewDiv 
         * @param {number} refId 
         * @param {boolean} force
         */
        startLoadingViewContent(viewDiv, refId, forced = false) {
            if (!forced && viewDiv.hasChildNodes()) {
                return;
            } else if (viewDiv.dataset.isLoading) { // TODO: 也可以强制从头重新加载?
                return;
            }
            this.setupLoading(viewDiv);

            this.model.subscribeForLoadingItemElement(this, refId, viewDiv.dataset.viewId, forced);
        }

        /**
         * 
         * @param {HTMLElement} viewDiv 
         */
        setupLoading(viewDiv) {
            viewDiv.dataset.isLoading = '1';

            const loadingSpan = document.createElement('span');
            loadingSpan.classList.add('fto-ref-view-loading');
            const loadingTextSpan = document.createElement('span');
            loadingTextSpan.classList.add('fto-ref-view-loading-text');
            loadingTextSpan.dataset.waitedMilliseconds = '0';
            loadingTextSpan.textContent = "加载中…";
            loadingSpan.append(loadingTextSpan);
            viewDiv.innerHTML = '';
            viewDiv.append(loadingSpan);
            this.setupButtons(loadingSpan);
        }

        /**
         * 
         * @param {String} viewId 
         */
        isLoading(viewId) {
            return !!ViewHelper.getRefViewByViewId(viewId).dataset.isLoading;
        }

        /**
         * 
         * @param {*} viewId 
         * @param {*} spentMs 
         */
        reportSpentTime(viewId, spentMs) {
            const viewDiv = ViewHelper.getRefViewByViewId(viewId);
            if (!this.isLoading(viewId)) {
                this.setupLoading(viewDiv)
            }
            viewDiv.querySelector('.fto-ref-view-loading-text')
                .textContent = `加载中… ${(spentMs / 1000.0).toFixed(2)}s`
        }

        /**
         * 
         * @param {String} viewId 
         * @param {HTMLElement} itemElement 
         */
        updateViewContent(viewId, itemElement) {
            const viewDiv = ViewHelper.getRefViewByViewId(viewId);
            delete viewDiv.dataset.isLoading;
            viewDiv.innerHTML = '';
            viewDiv.append(itemElement);
            this.setupContent(itemElement);
        }

    }

    class ViewHelper {
        /**
         * 
         * @param {HTMLElement} elem 
         */
        static getPosterID(elem) {
            if (!elem.classList.contains('.h-threads-info-uid')) {
                elem = elem.querySelector('.h-threads-info-uid');
            }
            const uid = elem.textContent;
            return /^ID:(.*)$/.exec(uid)[1];
        }

        /**
         * 
         * @param {HTMLElement} elem 
         */
        static getThreadID(elem) {
            if (!elem.classList.contains('.h-threads-info-id')) {
                elem = elem.querySelector('.h-threads-info-id');
            }
            const link = elem.getAttribute('href');
            const id = /^.*\/t\/(\d*).*$/.exec(link)[1];
            if (!id.length) {
                return null;
            }
            return Number(id);
        }

        /**
         * 
         * @param {HTMLElement} elem 
         */
        static getPostID(elem) {
            if (!elem.classList.contains('.h-threads-info-id')) {
                elem = elem.querySelector('.h-threads-info-id');
            }
            return Number(/^No.(\d+)$/.exec(elem.textContent)[1]);
        }

        /**
         * 
         * @param {HTMLElement} elem 
         */
        static hasFetchingRefSucceeded(elem) {
            return !elem.parentElement.querySelector('.fto-ref-view-error');
        }

        /**
         * 
         * @param {String} viewId 
         */
        static getRefViewByViewId(viewId) {
            return document.querySelector(`.fto-ref-view[data-view-id="${viewId}"]`);
        }
        /**
         * @param {String} viewId 
         */
        static getRefLinkByViewId(viewId) {
            return document.querySelector(`.fto-ref-link[data-view-id="${viewId}"]`);
        }

    }


    class Model {

        constructor() {
            // refId -> HTMLElement
            this.refCache = {};
            /**
             * @type {Set<number>}
            */
            this.refsInFetching = new Set();
            /**
             * @type {Map<number, Set<String>>}
             */
            this.refSubscriptions = new Map();
        }

        get isSupported() {
            if (!window.indexedDB || !window.fetch) {
                return false;
            }
            return true;
        }

        /**
         * 
         * @param {number} refId 
         * @returns {HTMLElement?}
         */
        async getRefCache(refId) {
            const elem = this.refCache[refId];
            if (!elem) { return null; }
            return elem.cloneNode(true);
        }
        /**
         * 
         * @param {number} refId 
         * @param {HTMLElement} rawItem
         * @param {'page' | 'global'} scope
         */
        async recordRef(refId, rawItem, scope = 'page') {
            this.refCache[refId] = rawItem.cloneNode(true);
        }

        /**
         * 
         * @param {Controller} controller
         * @param {number} refId 
         * @param {String} viewId
         * @param {boolean} ignoresCache
         * 
         * FIXME: 刷新没有让所有引用同步
         */
        async subscribeForLoadingItemElement(controller, refId, viewId, ignoresCache = false) {
            if (!this.refSubscriptions.has(refId)) {
                this.refSubscriptions.set(refId, new Set());
            }
            this.refSubscriptions.get(refId).add(viewId);

            const itemCache = ignoresCache ? null : await this.getRefCache(refId);
            if (itemCache) {
                const item = this.processItemElement(itemCache, refId);
                controller.updateViewContent(viewId, item);
                return;
            }

            if (this.refsInFetching.has(refId)) {
                return;
            }
            this.refsInFetching.add(refId);

            let item = await this.fetchItemElement(controller, refId, viewId);
            item = this.processItemElement(item, refId);
            this.refSubscriptions.get(refId).forEach((viewId) => {
                controller.updateViewContent(viewId, item.cloneNode(true));
            });
            this.refsInFetching.delete(refId);

        }

        /**
         * 
         * @param {Controller} controller
         * @param {number} refId 
         * @param {String} viewId
         */
        async fetchItemElement(controller, refId, viewId) {
            const itemContainer = document.createElement('div');
            const abortController = new AbortController();
            try {
                const resp = await Promise.race([
                    fetch(`/Home/Forum/ref?id=${refId}`, { signal: abortController.signal }),
                    new Promise((_, reject) => {
                        let spentMs = 0;
                        const intervalId = setInterval(() => {
                            spentMs += 20;
                            if (!controller.isLoading(viewId)) {
                                clearInterval(intervalId);
                            } else if (refFetchingTimeout && spentMs >= refFetchingTimeout) {
                                reject(new Error('Timeout'));
                                abortController.abort();
                                clearInterval(intervalId);
                            } else {
                                this.refSubscriptions.get(refId).forEach((viewId) => {
                                    controller.reportSpentTime(viewId, spentMs);
                                });
                            }
                        }, 20);
                    }),
                ]);
                itemContainer.innerHTML = await resp.text();
            } catch (e) {
                let message;
                if (e instanceof Error) {
                    if (e.message === 'Timeout') {
                        message = `获取引用内容超时!`;
                    } else {
                        message = `获取引用内容失败:${e.toString()}`;
                    }
                } else {
                    message = `获取引用内容失败:${String(e)}`;
                }
                const errorSpan = document.createElement('span');
                errorSpan.classList.add('fto-ref-view-error');
                errorSpan.textContent = message;
                return errorSpan;
            }

            const item = itemContainer.firstChild;
            this.recordRef(refId, item, 'global');
            return item;
        }

        /**
         * 
         * @param {HTMLElement} item 
         * @param {number} refId
         */
        processItemElement(item, refId) {
            if (item.querySelector('.fto-ref-view-error')) {
                return item;
            }
            if (!ViewHelper.getThreadID(item)) {
                const errorSpan = document.createElement('span');
                errorSpan.classList.add('fto-ref-view-error');
                errorSpan.textContent = `引用内容不存在!`;
                this.recordRef(refId, item, 'page');
                return errorSpan;
            }
            return item
        }

    }

    class Utils {

        // https://stackoverflow.com/a/59837035
        static generateViewID() {
            if (!Utils.currentGeneratedViewID) {
                Utils.currentGeneratedViewID = 0;
            }
            Utils.currentGeneratedViewID += 1;
            return String(Utils.currentGeneratedViewID);
        }

        /**
         * 
         * @param {Node} node 
         * @param {Node} newNode 
         */
        static insertAfter(node, newNode) {
            node.parentNode.insertBefore(newNode, node.nextSibling);
        }

        // https://stackoverflow.com/a/26230989
        static getCoords(elem) { // crossbrowser version
            var box = elem.getBoundingClientRect();

            var body = document.body;
            var docEl = document.documentElement;

            var scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop;
            var scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft;

            var clientTop = docEl.clientTop || body.clientTop || 0;
            var clientLeft = docEl.clientLeft || body.clientLeft || 0;

            var top = box.top + scrollTop - clientTop;
            var left = box.left + scrollLeft - clientLeft;

            return { top: Math.round(top), left: Math.round(left) };
        }

        // // https://stackoverflow.com/a/49857905
        // // https://stackoverflow.com/a/50101022
        // static fetchWithTimeout(url, options, timeout = 10000) {
        //     options = { ...(options || {}) };
        //     const controller = new AbortController();
        //     if (options.signal instanceof AbortSignal) {
        //         options.signal.addEventListener(function (ev) {
        //             controller.signal.dispatchEvent.call(this, ev);
        //         });
        //     }
        //     options.signal = controller.signal;
        //     return Promise.race([
        //         fetch(url, options),
        //         new Promise((_, reject) => {
        //             setTimeout(() => {
        //                 reject(new Error('Timeout'));
        //                 controller.abort();
        //             }, timeout);
        //         })
        //     ]);
        // }

    }

    entry();
})();