/*! * The MIT License * * Copyright 2021 FToovvr * * Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ // ==UserScript== // @name A岛引用查看增强 // @description 让A岛网页端的引用支持嵌套查看、固定、折叠等功能 // @namespace http://tampermonkey.net/ // @include /^https?://(adnmb\d*.com|tnmb.org)/.*$/ // @homepageURL https://github.com/FToovvr/adnmb-reference-enhancement.user.js // @require https://openuserjs.org/src/libs/sizzle/GM_config.js // @author FToovvr // @license MIT; https://opensource.org/licenses/MIT // @version 0.3.1 // @grant GM_registerMenuCommand // @downloadURL none // ==/UserScript== (function () { 'use strict'; /*! ***************************************************************************** Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. ***************************************************************************** */ function __awaiter(thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); } class ViewHelper { 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]; } 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); } 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]); } static hasFetchingRefSucceeded(elem) { return !elem.parentElement.querySelector('.fto-ref-view-error'); } static getRefViewByViewId(viewId) { return document.querySelector(`.fto-ref-view[data-view-id="${viewId}"]`); } static getRefLinkByViewId(viewId) { return document.querySelector(`.fto-ref-link[data-view-id="${viewId}"]`); } static *getAncestorRefViews(currentView, refId = null) { if (!currentView.classList.contains('fto-ref-view')) { currentView = currentView.closest('.fto-ref-view'); } const next = (v) => v.parentElement.closest('.fto-ref-view' + (refId ? `[data-ref-id="${refId}"]` : '')); for (let ancestorView = next(currentView); ancestorView; ancestorView = next(currentView)) { yield ancestorView; } } } var configWindowStyle = "body{width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.config_var{display:flex;justify-content:space-between}.config_var>label{min-width:-webkit-fit-content;min-width:-moz-fit-content;min-width:fit-content}.config_var>input[type=text]{flex-grow:1;text-align:right}.config_var :nth-child(2){text-align:right}.config_var :nth-child(2) input[type=checkbox]~:before,.config_var :nth-child(2) input[type=radio]~:before{content:\"\\A\";white-space:pre}"; /// function canConfigurate() { return typeof GM_configStruct !== 'undefined'; } class Configurations { constructor() { this.onConfigurationChangeCallbacks = []; this.defaults = { // 折叠时保持的高度,低于此高度将不可折叠 collapsedHeight: 80, // 悬浮时引用内容的不透明度 floatingOpacity: 100, // 悬浮淡入的时长(暂不支持淡出) fadingDuration: 0, // hoverRefLinkToFloatRefView: true, // 如为真,在固定时点击图钉按钮会直接关闭引用内容,而非转为悬浮 clickPinToCloseView: false, // 获取引用内容多少毫秒算超时 refFetchingTimeout: 10000, // 如为真,存在缓存的引用内容会自动以折叠的形式固定 autoOpenRefViewIfRefContentAlreadyCached: false, // // 如为真,展开一处引用将展开当前已知所有其他处指向相同内容的引用 // // TODO: 考虑也自动展开之后才遇到的指向相同内容的引用? // // 尚未实现 // autoOpenOtherRefViewsWithSameRefIdAfterOpenOne: false, // 在内容成功加载后是否还显示刷新按钮 showRefreshButtonEvenIfRefContentLoaded: false, }; if (!canConfigurate()) { return; } this.gmc = new GM_configStruct({ id: "adnmb-reference-enhancement", title: "「A岛引用查看增强」 用户脚本 设置", fields: { collapsedHeight: { section: ["引用视图", "外观表现"], label: "折叠时高度(px)", labelPos: 'left', type: 'float', title: "引用视图被折叠后保持的高度。" + "低于此高度的引用内容不可折叠。", default: 80, }, floatingOpacity: { label: "悬浮不透明度(%)", labelPos: 'left', type: 'float', title: "悬浮时引用视图的不透明度。", default: 100, }, fadingDuration: { label: "悬浮淡入时长(毫秒)", labelPos: 'left', type: 'float', title: "为什么只有淡入?因为淡出的代码不能一步到位,摸了 (ゝ∀・)", default: 0, }, onHoverOnRefLink: { section: [null, "行为"], label: "当鼠标位于引用链接上时", labelPos: 'left', type: 'radio', options: ["无行为", "悬浮展现引用内容"], default: "悬浮展现引用内容", }, onClickPinOnOpenRefView: { label: "在引用视图固定时点击「📌」", labelPos: 'left', type: 'radio', options: ["悬浮引用视图", "关闭引用视图"], default: "悬浮引用视图", }, refFetchingTimeout: { section: "引用内容加载", label: "超时时限(毫秒)", labelPos: 'left', type: 'float', title: "获取引用内容多久算超时。" + "如为「0」则代表无超时时限。", default: 10000, }, showRefreshButtonEvenIfRefContentLoaded: { label: "总是显示刷新按钮", labelPos: 'left', type: 'checkbox', title: "即使引用内容成功加载,也显示刷新按钮。" + "无论选定与否,目前都不会在加载途中显示刷新按钮。", default: false, } }, events: { open: () => { const frame = this.gmc.frame; frame.setAttribute('style', ` position: fixed; z-index: 9999; left: 50%; top: 50%; transform: translate(-50%, -50%); width: fit-content; height: 500px; max-height: 80%; border: 1px solid black; `); }, save: () => { for (const fn of this.onConfigurationChangeCallbacks) { fn(); } }, }, css: configWindowStyle, }); } openConfigurationWindow() { this.gmc.open(); } onConfigurationChange(fn) { this.onConfigurationChangeCallbacks.push(fn); } getValue(name) { return canConfigurate() ? this.gmc.get(name) : null; } get collapsedHeight() { var _a; return (_a = this.getValue('collapsedHeight')) !== null && _a !== void 0 ? _a : this.defaults.collapsedHeight; } get floatingOpacity() { var _a; return (_a = this.getValue('floatingOpacity')) !== null && _a !== void 0 ? _a : this.defaults.floatingOpacity; } get fadingDuration() { var _a; return (_a = this.getValue('fadingDuration')) !== null && _a !== void 0 ? _a : this.defaults.fadingDuration; } get hoverRefLinkToFloatRefView() { var _a; return (_a = (this.getValue('onHoverOnRefLink') === "悬浮展现引用内容")) !== null && _a !== void 0 ? _a : this.defaults.hoverRefLinkToFloatRefView; } get clickPinToCloseView() { var _a; return (_a = (this.getValue('onClickPinOnOpenRefView') === "关闭引用视图")) !== null && _a !== void 0 ? _a : this.defaults.clickPinToCloseView; } get refFetchingTimeout() { var _a; return (_a = this.getValue('refFetchingTimeout')) !== null && _a !== void 0 ? _a : this.defaults.refFetchingTimeout; } get autoOpenRefViewIfRefContentAlreadyCached() { return this.defaults.autoOpenRefViewIfRefContentAlreadyCached; } get showRefreshButtonEvenIfRefContentLoaded() { var _a; return (_a = this.getValue('showRefreshButtonEvenIfRefContentLoaded')) !== null && _a !== void 0 ? _a : this.defaults.showRefreshButtonEvenIfRefContentLoaded; } } var configurations = new Configurations(); class Model { constructor() { this.refCache = {}; this.refsInFetching = new Set(); this.refSubscriptions = new Map(); } get isSupported() { if (!window.indexedDB) { return false; } return true; } getRefCache(refId) { return __awaiter(this, void 0, void 0, function* () { const elem = this.refCache[refId]; if (!elem) { return null; } return elem.cloneNode(true); }); } recordRef(refId, rawItem, scope = 'page') { return __awaiter(this, void 0, void 0, function* () { this.refCache[refId] = rawItem.cloneNode(true); }); } subscribeForLoadingItemElement(controller, refId, viewId, ignoresCache = false) { return __awaiter(this, void 0, void 0, function* () { if (!this.refSubscriptions.has(refId)) { this.refSubscriptions.set(refId, new Set()); } this.refSubscriptions.get(refId).add(viewId); const itemCache = ignoresCache ? null : yield this.getRefCache(refId); if (itemCache) { const item = this.processItemElement(itemCache, refId); controller.updateViewContent(viewId, item); } else if (!this.refsInFetching.has(refId)) { this.refsInFetching.add(refId); let item = yield this.fetchItemElement(controller, refId, viewId); item = this.processItemElement(item, refId); this.refSubscriptions.get(refId).forEach((subscriptedViewId) => { controller.updateViewContent(subscriptedViewId, item.cloneNode(true)); }); this.refsInFetching.delete(refId); } }); } fetchItemElement(controller, refId, viewId) { return __awaiter(this, void 0, void 0, function* () { const itemContainer = document.createElement('div'); const abortController = new AbortController(); try { const resp = yield 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 (configurations.refFetchingTimeout && spentMs >= configurations.refFetchingTimeout) { reject(new Error('Timeout')); abortController.abort(); clearInterval(intervalId); } else { this.refSubscriptions.get(refId).forEach((viewIdToReport) => { controller.reportSpentTime(viewIdToReport, spentMs); }); } }, 20); }), ]); itemContainer.innerHTML = yield 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.firstElementChild; this.recordRef(refId, item, 'global'); return item; }); } 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() { Utils.currentGeneratedViewID += 1; return String(Utils.currentGeneratedViewID); } static insertAfter(node, newNode) { node.parentNode.insertBefore(newNode, node.nextSibling); } // https://stackoverflow.com/a/26230989 static getCoords(elem) { const box = elem.getBoundingClientRect(); const body = document.body; const docEl = document.documentElement; const scrollTop = window.pageYOffset || docEl.scrollTop || body.scrollTop; const scrollLeft = window.pageXOffset || docEl.scrollLeft || body.scrollLeft; const clientTop = docEl.clientTop || body.clientTop || 0; const clientLeft = docEl.clientLeft || body.clientLeft || 0; const top = box.top + scrollTop - clientTop; const left = box.left + scrollLeft - clientLeft; return { top: Math.round(top), left: Math.round(left) }; } } Utils.currentGeneratedViewID = 0; var additionalStyleText = ".h-threads-content{word-break:break-word}.h-threads-item-ref .h-threads-content{margin:5px 20px}.h-threads-info{font-size:14px;line-height:20px;margin:0}.fto-ref-view[data-status=collapsed],.fto-ref-view[data-status=open]{display:block}.fto-ref-view[data-status=collapsed]+br,.fto-ref-view[data-status=open]+br{display:none}.fto-ref-view{background:#f0e0d6;border:1px solid #000;clear:left;position:relative;width:-webkit-max-content;width:-moz-max-content;width:max-content;max-width:calc(100vw - var(--offset-left) - 35px);margin-left:-5px;margin-right:-40px}.fto-ref-view[data-status=closed]{opacity:0;display:inline-block;width:0;height:0;overflow:hidden;padding:0;border:0;margin:0}.fto-ref-view[data-status=floating]{position:absolute;z-index:999}.fto-ref-view[data-status=collapsed]{overflow:hidden;text-overflow:ellipsis}.fto-ref-view[data-status=collapsed]:before{content:\"\";position:absolute;top:60px;height:20px;width:100%;background:linear-gradient(rgba(240,224,214,0),rgba(255,238,221,.8));z-index:999}.fto-ref-view .fto-ref-view-error{color:red}.fto-ref-view .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 .fto-ref-view-pin{display:inline-block;transform:rotate(-45deg)}.fto-ref-view[data-is-loading] .fto-ref-view-refresh{display:none}.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,.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{transform:none;filter:grayscale(100%)}.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,.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{content:\"\";position:absolute;height:110%;width:100%;background:linear-gradient(rgba(240,224,214,0),#f0e0d6);z-index:999;transform:rotate(45deg)}"; class Controller { constructor(model) { this.model = model; } static makeAdditionalVariableStyleText() { let styleText = ` .fto-ref-view[data-status="floating"] { opacity: ${configurations.floatingOpacity}%; transition: opacity ${configurations.fadingDuration}ms ease-in; } .fto-ref-view[data-status="collapsed"] { max-height: ${configurations.collapsedHeight}px; } .fto-ref-view[data-status="closed"] { /* transition: opacity ${configurations.fadingDuration}ms ease-out; */ } `; if (!configurations.showRefreshButtonEvenIfRefContentLoaded) { styleText += ` .fto-ref-view-refresh { display: none; } .fto-ref-view-error .fto-ref-view-refresh { display: inline; } `; } return styleText; } static setupStyle() { for (const [styleText, id] of [ [additionalStyleText, 'fto-style-additional-fixed'], [this.makeAdditionalVariableStyleText(), 'fto-style-additional-variable'], ]) { const style = document.createElement('style'); style.id = id; style.classList.add('fto-style'); // TODO: fade out style.append(styleText); document.head.append(style); } configurations.onConfigurationChange(() => { const style = document.querySelector('#fto-style-additional-variable'); style.innerHTML = ''; style.append(this.makeAdditionalVariableStyleText()); }); } setupContent(root, parentAutoOpenPromiseResolve = null) { 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; } const linkElems = root.querySelectorAll('font[color="#789922"]'); if (linkElems.length === 0) { parentAutoOpenPromiseResolve === null || parentAutoOpenPromiseResolve === void 0 ? void 0 : parentAutoOpenPromiseResolve(); return; } let unfinished = linkElems.length; linkElems.forEach(linkElem => { if (!linkElem.textContent.startsWith('>>')) { return; } (() => __awaiter(this, void 0, void 0, function* () { this.setupRefLink(linkElem, () => { unfinished--; if (unfinished === 0) { parentAutoOpenPromiseResolve === null || parentAutoOpenPromiseResolve === void 0 ? void 0 : parentAutoOpenPromiseResolve(); } }); }))(); }); } 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(); itemElem.querySelectorAll('.uk-text-primary').forEach((labelElem) => { if (labelElem.textContent === "(PO主)") { labelElem.remove(); } }); div.append(itemElem); this.model.recordRef(ViewHelper.getPostID(itemElem), div, 'global'); }); } 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 uidElem = infoElem.querySelector('.h-threads-info-uid'); Utils.insertAfter(uidElem, poLabel); Utils.insertAfter(uidElem, 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 idElem = infoElem.querySelector('.h-threads-info-id'); idElem.append(' ', outerThreadLabel); } this.setupButtons(infoElem); } setupErrorRefContent(elem) { this.setupButtons(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') { this.changeViewStatus(viewDiv, 'open'); } else { this.changeViewStatus(viewDiv, configurations.clickPinToCloseView ? 'closed' : 'floating'); } }); buttonListSpan.append(pinSpan); // 刷新🔄按钮 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); } setupRefLink(linkElem, parentAutoOpenPromiseResolve) { 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'); viewDiv.dataset.refId = String(refId); viewDiv.dataset.viewId = viewId; // closed: 不显示; floating: 悬浮显示; open: 完整固定显示; collapsed: 折叠固定显示 this.changeViewStatus(viewDiv, 'closed'); viewDiv.style.setProperty('--offset-left', `${Utils.getCoords(linkElem).left}px`); Utils.insertAfter(linkElem, viewDiv); if (configurations.autoOpenRefViewIfRefContentAlreadyCached && !this.isInsideReference(viewDiv, refId)) { (() => __awaiter(this, void 0, void 0, function* () { const refCache = yield this.model.getRefCache(refId); if (refCache) { yield new Promise((resolve) => __awaiter(this, void 0, void 0, function* () { this.changeViewStatus(viewDiv, 'open'); viewDiv.append(refCache); this.setupContent(refCache, resolve); })); this.changeViewStatus(viewDiv, 'collapsed'); } parentAutoOpenPromiseResolve === null || parentAutoOpenPromiseResolve === void 0 ? void 0 : parentAutoOpenPromiseResolve(); }))(); } else { parentAutoOpenPromiseResolve === null || parentAutoOpenPromiseResolve === void 0 ? void 0 : parentAutoOpenPromiseResolve(); } // 处理悬浮 linkElem.addEventListener('mouseenter', () => { if (viewDiv.dataset.status !== 'closed') { viewDiv.dataset.isHovering = '1'; return; } else if (configurations.hoverRefLinkToFloatRefView) { this.changeViewStatus(viewDiv, '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; (() => __awaiter(this, void 0, void 0, function* () { setTimeout(() => { if (!viewDiv.dataset.isHovering) { this.changeViewStatus(viewDiv, 'closed'); } }, 200); }))(); }); } // 处理折叠 linkElem.addEventListener('click', () => { if (viewDiv.dataset.status === 'open') { this.changeViewStatus(viewDiv, 'collapsed'); } else { this.changeViewStatus(viewDiv, 'open'); this.startLoadingViewContent(viewDiv, refId); } }); viewDiv.addEventListener('click', () => { if (viewDiv.dataset.status === 'collapsed') { this.changeViewStatus(viewDiv, 'open'); } }); } 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); } 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); } isLoading(viewId) { return !!ViewHelper.getRefViewByViewId(viewId).dataset.isLoading; } 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`; } updateViewContent(viewId, itemElement) { const viewDiv = ViewHelper.getRefViewByViewId(viewId); delete viewDiv.dataset.isLoading; viewDiv.innerHTML = ''; viewDiv.append(itemElement); this.setupContent(itemElement); } changeViewStatus(viewDiv, status) { switch (status) { case 'closed': case 'floating': case 'open': viewDiv.dataset.status = status; break; case 'collapsed': if (viewDiv.clientHeight > configurations.collapsedHeight) { viewDiv.dataset.status = 'collapsed'; } else { viewDiv.dataset.status = 'open'; } break; } const linkElem = ViewHelper.getRefLinkByViewId(viewDiv.dataset.viewId); switch (viewDiv.dataset.status) { case 'closed': case 'floating': linkElem.dataset.status = 'closed'; break; case 'open': case 'collapsed': linkElem.dataset.status = 'open'; } } isInsideReference(viewDiv, refId) { for (const _ of ViewHelper.getAncestorRefViews(viewDiv, refId)) { return true; } return false; } } function entry() { if (window.disableAdnmbReferenceViewerEnhancementUserScript) { console.log("「A岛引用查看增强」用户脚本被禁用(设有变量 `window.disableAdnmbReferenceViewerEnhancementUserScript`),将终止。"); return; } const model = new Model(); if (!model.isSupported) { console.log("浏览器功能不支持「A岛引用查看增强」用户脚本,将终止。"); return; } if (canConfigurate() && typeof GM_registerMenuCommand !== 'undefined') { GM_registerMenuCommand("打开配置窗口", () => { configurations.openConfigurationWindow(); }, 'c'); } // 销掉原先的预览方法 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); } entry(); }());