让A岛网页端的引用支持嵌套查看、固定、折叠等功能
当前为
// ==UserScript==
// @name A岛引用查看增强
// @namespace http://tampermonkey.net/
// @version 0.1.6
// @description 让A岛网页端的引用支持嵌套查看、固定、折叠等功能
// @author FToovvr
// @license MIT; https://opensource.org/licenses/MIT
// @include /^https?://(adnmb\d*.com|tnmb.org)/.*$/
// @grant none
// ==/UserScript==
// TODO: 把一看到的纳入缓存
// TODO: 持久化缓存
// TODO: 刷新按钮
// TODO: 自定义配置页 https://stackoverflow.com/a/43462416
// TODO: 20秒超时/异常处理
// TODO: 更好的「加载中…」?;计时器?
// TODO: 悬浮淡入、淡出
// TODO?: 减少一下引用里的内容的空白?;右边不需要留空白
// TODO: 保留折叠状态
// TODO: 高度过低拒绝折叠?
// TODO: 折叠时图钉的图标应该也有变化(渐变?)
// TODO: cache 先占个位,减小重复请求可能性
// 人的手不可能在添加 dict 项这么短的时间内触发两次事件
// TODO: 随时有图钉按钮解除固定?
// TODO: 标记外串引用
(function () {
'use strict';
function entry() {
const model = new Model();
if (!model.isSupported) {
console.log("浏览器功能不支持「A岛引用查看增强」脚本。");
return;
}
// 销掉原先的预览方法
document.querySelectorAll('font[color="#789922"]').forEach((elem) => {
const newElem = elem.cloneNode(true);
elem.parentNode.replaceChild(newElem, elem);
});
ViewHelper.setupStyle();
ViewHelper.setupContent(model, document.body);
}
class ViewHelper {
static setupStyle() {
const style = document.createElement('style');
// TODO: fade out
style.appendChild(document.createTextNode(`
.ref-view {
/* 照搬自 h.desktop.css */
background: #f0e0d6;
border: 1px solid #000;
position: relative;
width: fit-content;
margin-left: -5px;
margin-right: -40px;
}
.ref-view .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;
}
.ref-view[data-status="closed"] {
display: none;
}
.ref-view[data-status="floating"] {
position: absolute;
z-index: 999;
transition: opacity 100ms ease-in;
}
.ref-view[data-status="open"] {
display: block;
}
.ref-view[data-status="open"] + br {
display: none;
}
.ref-view[data-status="collapsed"] {
display: block;
max-height: 80px;
overflow: hidden;
text-overflow: ellipsis;
}
.ref-view[data-status="collapsed"] + br {
display: none;
}
/* https://stackoverflow.com/a/22809380 */
.ref-view[data-status="collapsed"]:before {
content: '';
position: absolute;
top: 60px;
height: 20px;
width: 100%;
background: linear-gradient(#f0e0d600, #ffeeddcc);
z-index: 999;
}
.ref-view-button {
cursor: pointer;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.ref-view-pin {
display: inline-block;
transform: rotate(-45deg);
}
/* https://codemyui.com/grayscale-emoji-using-css/ */
.ref-view[data-status="floating"]
>.ref-view-item-container
>.h-threads-item
>.h-threads-item-ref
>.h-threads-item-reply-main
>.h-threads-info
>.ref-view-pin {
transform: none;
filter: grayscale(100%);
}
`));
document.getElementsByTagName('head')[0].appendChild(style);
}
/**
*
* @param {Model} model
* @param {HTMLElement} root
*/
static setupContent(model, root) {
const po = ViewHelper.po;
if (root !== document.body) {
// 补标 PO
root.querySelectorAll('.h-threads-info-uid').forEach((elem) => {
if (ViewHelper.getPosterID(elem.parentNode) === po) {
const poLabel = document.createElement('span');
poLabel.textContent = "(PO主)";
poLabel.classList.add('uk-text-primary', 'uk-text-small');
Utils.insertAfter(elem, poLabel);
Utils.insertAfter(elem, document.createTextNode(' '));
}
});
// 图钉📌按钮和刷新🔄按钮
root.querySelectorAll('.h-threads-info').forEach((parentElem) => {
const pinSpan = document.createElement('span');
pinSpan.classList.add('ref-view-pin', 'ref-view-button');
pinSpan.textContent = "📌";
pinSpan.addEventListener('click', (el) => {
const viewDiv = pinSpan.closest('.ref-view');
const linkElem = viewDiv.parentNode.querySelector('.ref-link');
if (viewDiv.dataset.status === 'floating') {
linkElem.dataset.status = 'open';
viewDiv.dataset.status = 'open';
} else {
linkElem.dataset.status = 'closed';
viewDiv.dataset.status = 'floating';
}
});
// const refreshSpan = document.createElement('span');
// refreshSpan.classList.add('ref-view-refresh', 'ref-view-button');
// refreshSpan.textContent = "🔄";
parentElem.prepend(pinSpan, /*refreshSpan*/);
});
}
root.querySelectorAll('font[color="#789922"]').forEach(linkElem => {
linkElem.classList.add('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.generateRandomID();
linkElem.dataset.viewId = viewId;
const viewDiv = document.createElement('div');
viewDiv.classList.add('ref-view');
// closed: 不显示; floating: 悬浮显示; open: 完整固定显示; collapsed: 折叠固定显示
viewDiv.dataset.status = 'closed';
viewDiv.dataset.viewId = viewId;
const itemContainer = document.createElement('div');
itemContainer.classList.add('ref-view-item-container');
viewDiv.appendChild(itemContainer);
Utils.insertAfter(linkElem, viewDiv);
// 处理悬浮
linkElem.addEventListener('mouseenter', (ev) => {
if (viewDiv.dataset.status !== 'closed') {
viewDiv.dataset.isHovering = '1';
return;
}
viewDiv.dataset.status = 'floating';
viewDiv.dataset.isHovering = '1';
this.doLoadViewContent(model, 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'
|| viewDiv.dataset.status === 'collapsed') {
linkElem.dataset.status = 'open';
viewDiv.dataset.status = 'open';
} else {
viewDiv.dataset.status = 'collapsed';
}
});
viewDiv.addEventListener('click', () => {
if (viewDiv.dataset.status === 'collapsed') {
viewDiv.dataset.status = 'open';
}
});
});
}
/**
*
* @param {Model} model
* @param {HTMLElement} viewDiv
* @param {number} refId
*/
static doLoadViewContent(model, viewDiv, refId) {
const viewId = viewDiv.dataset.viewId;
// TODO: 更好的「加载中」
viewDiv.classList.add('ref-view-loading');
const itemContainer = viewDiv.getElementsByClassName('ref-view-item-container')[0];
itemContainer.textContent = "加载中…";
(async (model) => {
const itemElement = await model.loadItemElement(refId, viewId);
viewDiv.classList.remove('ref-view-loading');
itemContainer.innerHTML = '';
itemContainer.appendChild(itemElement);
})(model);
}
static get po() {
return ViewHelper.getPosterID(document.querySelector('.h-threads-item-main'));
}
/**
*
* @param {HTMLElement} elem
*/
static getPosterID(elem) {
const uid = elem.querySelector('.h-threads-info-uid').textContent;
return /^ID:(.*)$/.exec(uid)[1];
}
}
class Model {
constructor() {
this.viewCache = {};
this.refCache = {};
}
get isSupported() {
if (!window.indexedDB || !window.fetch) {
return false;
}
return true;
}
// TODO: indexedDB 持久化数据
/**
*
* @param {String} viewId
* @returns {HTMLElement?}
*/
async getViewCache(viewId) {
return this.viewCache[viewId];
}
/**
*
* @param {String} viewId
* @param {HTMLElement} item
*/
async recordView(viewId, item) {
this.viewCache[viewId] = item;
}
/**
*
* @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
*/
async recordRef(refId, rawItem) {
this.refCache[refId] = rawItem.cloneNode(true);
}
/**
*
* @param {number} refId
* @param {String} viewId
*/
async loadItemElement(refId, viewId) {
{
const viewItemCache = await this.getViewCache(viewId);
if (viewItemCache) {
return viewItemCache;
}
}
const itemContainer = document.createElement('div');
const itemCache = await this.getRefCache(refId);
if (itemCache) {
itemContainer.appendChild(itemCache);
} else {
// TODO: timeout 20s
try {
const resp = await fetch(`/Home/Forum/ref?id=${refId}`);
itemContainer.innerHTML = await resp.text();
} catch (e) {
// TODO: 异常处理
console.log(e);
itemContainer.innerHTML = "<span>获取引用内容失败</span>";
return itemContainer.firstChild;
}
}
const item = itemContainer.firstChild;
this.recordRef(refId, item);
ViewHelper.setupContent(this, item);
this.recordView(item);
return item;
}
}
class Utils {
// https://stackoverflow.com/a/59837035
static generateRandomID() {
return Math.random().toString(36).replace('0.', '');
}
/**
*
* @param {Node} node
* @param {Node} newNode
*/
static insertAfter(node, newNode) {
node.parentNode.insertBefore(newNode, node.nextSibling);
}
}
entry();
})();