// ==UserScript==
// @name 文章列表导航
// @namespace dawsonenjoy_article_list_nav
// @version 0.0.1
// @description 简书、腾讯课堂、网易云课堂内容列表导航
// @author dawsonenjoy
// @homepageURL https://github.com/dawsonenjoy/tampermonkey_script
// @match https://www.jianshu.com/nb/*
// @match https://www.jianshu.com/u/*
// @match https://www.jianshu.com/
// @match https://study.163.com/course/*
// @match https://ke.qq.com/course/*
// @grant none
// ==/UserScript==
(function () {
"use strict";
// Your code here...
// --------------------------------------------------------------
// 使用说明:
// ·根据页面文章列表自动生成并更新导航,如未更新,请点击标题,即可实现手动刷新
// ·单击对应文章时,将会跳转到指定位置,并且有高亮提示,可自配置相关处理回调以及相关样式
// ·双击对应文章时,相当于点击对应文章,一般是页面跳转,可自配置相关处理回调
// ·在标题栏处按住鼠标可自由拖拽导航栏
// ·点击选择框可切换主题(明/暗)
// ·点击标题栏左边小三角,可以进行显示/隐藏控制
// --------------------------------------------------------------
const config = {
// 页面相关配置,可自定义
pages: {
// 相关配置参数
// nodes: 文章列表节点选择器,
// watcherNode: 监听节点选择器,
// watcherConfig: 监听配置,
// urlpre: 跳转url前缀,
// pageStyle: 对应页面样式,
// backgroundColor: 点击提示时的背景色,
// checked: 初始化样式主题,true为白色主题,否则为黑色主题,
// theme: 样式主题,
// offset: 节点跳转位置控制,
// getUrl: 节点url链接获取,
// onclick: 节点单击回调,
// ondblclick: 节点双击回调,
// ----------------------------------------------------------
// 简书
jianshu: {
nodes: "[data-note-id] .content > .title",
watcherNode: ".note-list",
watcherConfig: { childList: true },
getUrl: node => node.getAttribute("href"),
pageStyle: `
.directory-root {
border-width: 0px;
background: #525252;
}`,
backgroundColor: "rgba(0, 0, 0, .3)",
theme: {
dark: {
background: "#525252",
borderWidth: "0px",
color: "#c8c8c8"
},
lighten: {
background: "white",
borderWidth: "1px",
color: "black"
}
}
},
// 腾讯课堂
qq: {
nodes: ".task-tt-text",
watcherNode: "#js_dir_tab",
watcherConfig: { attributes: true },
checked: true,
getUrl: node => node.parentElement.parentElement.getAttribute("href")
},
// 网易云课堂
"study.163": {
nodes: ".ksname",
watcherNode: "#j-chapter-list",
watcherConfig: { childList: true },
checked: true,
getOndblclick(ele) {
let index = ele.getAttribute("index");
if (!index) return;
utils.getNode(index).click();
}
}
// ----------------------------------------------------------
},
// 默认配置
defaultConfig: {
nodes: "",
watcherNode: "",
watcherConfig: {},
urlpre: "",
pageStyle: ``,
backgroundColor: "rgba(255, 255, 0, .3)",
checked: false,
theme: {
dark: {
background: "black",
borderWidth: "0px",
color: "white"
},
lighten: {
background: "white",
borderWidth: "1px",
color: "black"
}
},
offset: [0, -100],
getUrl: node => node.getAttribute("href"),
getOnclick: () => {},
getOndblclick: () => {}
},
// 获取页面配置
get pageConfig() {
if (this._pageConfig !== undefined) return this._pageConfig;
for (let page in this.pages) {
if (location.href.includes(page)) {
this._pageConfig = this.pages[page];
break;
}
}
return this._pageConfig || this.defaultConfig;
},
// 获取配置属性
getConfig(attr) {
return this.pageConfig[attr] || this.defaultConfig[attr];
},
// 文章列表
get nodes() {
let nodes = this.getConfig("nodes");
if (!nodes) return [];
return Array.from(document.querySelectorAll(nodes));
},
get root() {
if (this._root !== undefined) return this._root;
this._root = document.querySelector(".directory-root");
return this._root;
},
// 主题选中状态
get isChecked() {
return (this.checkbox && this.checkbox.checked) || false;
},
get checkbox() {
return document.querySelector(".directory-theme > input");
},
get body() {
return document.querySelector(".directory-body");
},
get backgroundColor() {
return this.getConfig("backgroundColor");
},
get checked() {
return this.getConfig("checked");
},
get watcherNode() {
let watcherNode = this.getConfig("watcherNode");
if (!watcherNode) return "";
return document.querySelector(watcherNode);
},
get watcherConfig() {
return this.getConfig("watcherConfig");
},
get urlpre() {
return this.getConfig("urlpre");
},
get pageStyle() {
return this.getConfig("pageStyle");
},
get theme() {
return this.getConfig("theme");
},
get offset() {
return this.getConfig("offset");
},
getNode(index) {
return this.nodes[index];
},
getUrl(node) {
return this.getConfig("getUrl")(node);
},
getOnclick(node) {
return this.getConfig("getOnclick")(node);
},
getOndblclick(node) {
return this.getConfig("getOndblclick")(node);
},
// 拖拽行为使用,允许拖拽
drag: false,
// 隐藏行为使用,保存位置
left: null,
// 通用样式
commonStyle: `
.directory-root {
height: 540px;
width: 300px;
position: fixed;
right: 0px;
top: 15%;
box-sizing: border-box;
border-radius: 5px;
border-width: 0px;
border-style: solid;
border-color: black;
background: black;
color: white;
z-index: 100000;
}
.directory-head {
width: 100%;
height: 40px;
position: relative;
border-bottom: 1px solid #3f3f3f;
text-align: center;
font-size: 20px;
font-weight: bold;
line-height: 40px;
cursor: pointer;
user-select: none;
}
.directory-title {
display: block;
width: 100%;
}
.directory-nav {
width: 0px;
height: 0px;
position: absolute;
left: -13px;
top: 8px;
transform: rotate(-45deg);
border-top: 25px solid #191919;
border-right: 25px solid transparent;
}
.directory-theme {
display: flex;
height: 100%;
right: 0;
top: 0px;
position: absolute;
align-items: center;
}
.directory-theme > input {
width: 30px;
height: 30px;
margin: 0;
line-height: 30px;
vertical-align: middle;
outline: none;
}
.directory-body {
height: 500px;
overflow: auto;
}
.directory-li {
padding: 7px;
border-bottom: 1px solid #3f3f3f;
line-height: 20px;
cursor: pointer;
user-select: none;
}
.directory-head > *:not(input):hover, .directory-li:hover {
opacity: 0.7;
}
.directory-body::-webkit-scrollbar-thumb {
background: #2b2b2b;
border-radius: 10px;
}
.directory-body::-webkit-scrollbar {
width: 5px;
height: 8px;
}
`
};
const utils = {
// 获取指定文章
getNode(index) {
return config.getNode(index);
},
// 获取文章跳转链接
getUrl(node) {
return config.getUrl(node);
},
// 获取文章点击事件
getOnclick(node) {
return config.getOnclick(node);
},
// 获取文章双击事件
getOndblclick(node) {
return config.getOndblclick(node);
},
// 更新文章列表时,记录对应的滚轮位置
getScrollTop() {
let body = config.body;
return body ? body.scrollTop : 0;
},
setScrollTop(top = 0) {
let body = config.body;
if (!body) return;
config.body.scrollTop = top;
},
// 主题样式设置
setThemeStyle(node, themeType) {
Object.entries(config.theme[themeType]).map(themeStyle => {
let styleName = themeStyle[0];
let styleVal = themeStyle[1];
node.style[styleName] = styleVal;
});
},
setDarkTheme(root) {
this.setThemeStyle(root, "dark");
},
setLightenTheme(root) {
this.setThemeStyle(root, "lighten");
},
// 主题切换
toggleTheme() {
let root = config.root;
if (config.isChecked) return this.setLightenTheme(root);
this.setDarkTheme(root);
},
// 显示/隐藏
toggleRoot() {
let root = config.root;
if (parseInt(root.style.right) >= 0 || root.style.right === "") {
// 隐藏
config.left = window.getComputedStyle(root).left;
root.style.left = "unset";
root.style.right = "-300px";
return;
}
// 显示
config.left && (root.style.left = config.left);
root.style.right = 0;
},
// 移动到指定节点位置
scrollTo(node) {
node.scrollIntoView();
window.scrollBy(...config.offset);
},
// 跳转位置高亮
hightlight(node) {
let nodeStyle = node.style;
let tmpBgc = nodeStyle.background;
nodeStyle.background = config.backgroundColor;
setTimeout(() => {
nodeStyle.background = tmpBgc;
}, 800);
},
moveDirection(node, distance, direction) {
node.style[direction] =
parseInt(window.getComputedStyle(node)[direction]) + distance + "px";
},
// 移动节点
move(node, e) {
this.moveDirection(node, e.movementX, "left");
this.moveDirection(node, e.movementY, "top");
},
// 移动root
moveRoot(e) {
let root = config.root;
this.move(root, e);
}
};
const Dom = {
setStyle() {
let style = document.createElement("style");
style.innerHTML = config.commonStyle;
style.innerHTML += config.pageStyle;
document.head.appendChild(style);
},
createRoot() {
let root = document.createElement("div");
root.className = "directory-root";
let head = this.createHead();
root.appendChild(head);
this.updateUl(root);
return root;
},
createHead() {
let head = document.createElement("div");
head.className = "directory-head";
let title = this.createTitle();
head.appendChild(title);
let nav = this.createNav();
head.appendChild(nav);
let theme = this.createTheme();
head.appendChild(theme);
return head;
},
createTitle() {
let title = document.createElement("span");
title.className = "directory-title";
title.setAttribute("title", "点击刷新");
title.innerText = "文章列表";
return title;
},
createNav() {
let nav = document.createElement("div");
nav.className = "directory-nav";
return nav;
},
createTheme() {
let theme = document.createElement("div");
theme.className = "directory-theme";
theme.innerHTML = `<input type="checkbox" name="theme" ${
config.checked ? "checked" : ""
}>`;
return theme;
},
updateUl(root) {
let top = utils.getScrollTop();
config.body && config.body.remove();
let ul = this.createUl();
root.appendChild(ul);
utils.setScrollTop(top);
},
createUl() {
let ul = document.createElement("ul");
ul.className = "directory-body";
config.nodes.map((node, index) =>
ul.appendChild(this.createLi(node, index))
);
return ul;
},
createLi(node, index) {
let li = document.createElement("li");
li.className = "directory-li";
li.innerText = node.innerText;
li.setAttribute("index", index);
li.setAttribute("title", `${index + 1}.${node.innerText}`);
li.setAttribute("href", utils.getUrl(node) || "");
return li;
},
// 监听文章数量变化,更新文章列表
watcher() {
let node = config.watcherNode;
let watcherConfig = config.watcherConfig;
if (!node || Object.keys(watcherConfig).length < 1) return;
let MutationObserver =
window.MutationObserver ||
window.WebKitMutationObserver ||
window.MozMutationObserver;
let observer = new MutationObserver((mutationsList, observer) =>
Dom.updateUl(config.root)
);
observer.observe(node, watcherConfig);
},
bindEvent() {
document.body.onclick = e => {
let target = e.target;
// 单击回调
utils.getOnclick instanceof Function && utils.getOnclick(target);
// 点击标题刷新
if (target.className === "directory-title")
return this.updateUl(config.root);
// 点击导航按钮(黑色三角形)隐藏/显示
if (target.className === "directory-nav") return utils.toggleRoot();
// 单击菜单内容到达页面对应位置,并进行颜色提示
if (target.className === "directory-li")
return (window.toPosTimeout = setTimeout(() => {
let index = target.getAttribute("index");
let node = utils.getNode(index);
utils.scrollTo(node);
utils.hightlight(node);
}, 0));
// 单击选择框切换主题
if (target.getAttribute("name") === "theme") return utils.toggleTheme();
};
// 双击菜单内容跳转页面
document.body.ondblclick = e => {
let target = e.target;
// 双击回调
utils.getOndblclick instanceof Function && utils.getOndblclick(target);
// href跳转
if (target.className !== "directory-li") return;
window.toPosTimeout && clearTimeout(window.toPosTimeout);
target.getAttribute("href") &&
window.open(config.urlpre + target.getAttribute("href"));
};
// 鼠标按下标题允许拖拽
document.body.onmousedown = e => {
let target = e.target;
if (target.className !== "directory-title") return;
config.drag = true;
};
// 鼠标按下标题允许拖拽
document.body.onmouseup = e => {
config.drag = false;
};
// 鼠标按下标题时移动拖拽框
document.body.onmousemove = e => {
if (!config.drag) return;
utils.moveRoot(e);
};
// 监听并自动更新文章列表
this.watcher();
},
initDom() {
let root = this.createRoot();
document.body.appendChild(root);
},
init() {
this.initDom();
this.setStyle();
utils.toggleTheme();
this.bindEvent();
}
};
Dom.init();
})();