您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
知乎长文目录,方便阅读。
// ==UserScript== // @name My知乎目录 // @namespace https://github.com/kongkongye/zh-category // @version 1.1.0 // @description 知乎长文目录,方便阅读。 // @author 空空叶 // @match https://www.zhihu.com/* // @license Apache // @require http://cdn.staticfile.org/jquery/3.4.1/jquery.min.js // ==/UserScript== (function() { 'use strict'; /** * 文章列表 */ let articles = [] let articlesMap = {} /** * 当前文章,可以没有 */ let currentArticle /** * 标题条 */ let titleBar /** * 最顶端那条的高度 */ let appHeaderHeight = 0; (() => { Array.prototype.removeIf = function(callback) { let i = 0; while (i < this.length) { if (callback(this[i], i)) { this.splice(i, 1); } else { ++i; } } }; })() /** * 常量 */ let Consts = { textColorSel: '#76839b',//文字选中颜色 textColorUnSel: '#8590a6',//文字未选中颜色 } /** * 实用类 */ let Utils = { // Generate four random hex digits. S4: () => (((1+Math.random())*0x10000)|0).toString(16).substring(1), // Generate a pseudo-GUID by concatenating random hexadecimal. guid: () => (Utils.S4()+Utils.S4()+"-"+Utils.S4()+"-"+Utils.S4()+"-"+Utils.S4()+"-"+Utils.S4()+Utils.S4()+Utils.S4()), //获取属性 getAttr: (ele, key) => $(ele).attr(key), //设置属性 setAttr: (ele, key, value) => $(ele).attr(key, value), //滚动 goToByScroll: ele => { $('html,body').animate({ scrollTop: $(ele).offset().top - appHeaderHeight }, 'slow'); }, goToEndByScroll: ele => { $('html,body').animate({ scrollTop: $(ele).offset().top + $(ele).height() - $(window).height() }, 'slow'); }, }; /** * 不同页面适配器 */ let Wrappers = { /** * 得到打开的问题列表 */ getArticleRoots: () => { let result let path = window.location.pathname if (path === '/' || path === '/follow') {//首页&关注 result = $('.TopstoryItem') }else if (path === '/search') {//搜索页 result = $('.SearchResult-Card') }else if (path.startsWith("/question")) { if (path.indexOf("answer") !== -1) {//问题的指定回答 //指定回答&更多回答 result = $('.AnswerCard,.List-item') }else {//问题的所有回答 result = $('.List-item') } }else if (path === '/topic' || path === '/explore') {//话题 result = $('.feed-item').filter((index, ele) => { let RichText = $('.zm-item-rich-text', ele) if (RichText && RichText.length > 0) { let children = RichText.children() if (children && children.length > 0 && children[0].nodeName.toLowerCase() === 'div' && $(children[0]).css('display') !== 'none') { return true } } return false }) return result }else { console.debug('[此页面无法解析]', path) } if (result) { result = result.filter((index, ele) => { let RichContent = $('.RichContent', ele) return RichContent && RichContent.length > 0 && !RichContent.hasClass('is-collapsed') }) } return result }, /** * 尝试解析文章数据 * @param $root 文章根元素 * @return Article,解析不出返回null */ parseArticle: $root => { let id = Utils.guid() let title //文章标题 //解析 let path = window.location.pathname if (path === '/' || path === '/follow') {//首页&关注 let $ContentItem = $('.ContentItem', $root) let data = JSON.parse($ContentItem.attr('data-zop')) title = data.title }else if (path === '/search') {//搜索页 let $ContentItemTitle = $('.ContentItem-title', $root) let $nameMeta = $('meta[itemprop="name"]', $ContentItemTitle) title = $nameMeta.attr('content') }else if (path.startsWith("/question")) { //不需要显示标题 }else if (path === '/topic' || path === '/explore') {//话题&发现(旧) let $questionLink = $('.question_link', $root) title = $questionLink.text() }else { console.debug('[此页面无法解析]', path) return null } return new Article({ id, title, $root, }) }, /** * 获取头部条 */ getHeader: () => { let path = window.location.pathname if (path === '/topic' || path === '/explore') {//话题&发现(旧) return $('.zu-top') }else {//其他(新) return $('.AppHeader') } }, /** * 获取侧边栏 */ getSideBar: () => { let path = window.location.pathname if (path === '/' || path === '/follow') {//首页&关注 return $('.GlobalSideBar') }else if (path === '/search') {//搜索页 return $('.SearchSideBar') }else if (path.startsWith("/question")) { return $('.Question-sideColumn') }else if (path === '/topic' || path === '/explore') {//话题&发现(旧) return $('.zu-main-sidebar') }else { console.debug('[此页面无法解析]', path) } }, /** * 获取文本容器 */ getTextWrapper: $root => { let path = window.location.pathname if (path === '/topic' || path === '/explore') {//话题&发现(旧) return $('.zm-item-rich-text', $root) }else {//其他(新) return $('.RichText', $root) } } } /** * 文章类 */ let Article = function({id, title, $root}) { this.id = id this.title = title //可以为null this.$root = $root this.titles = [] this.titlesMap = {} $root.on('mousemove', () => toggleCurrentArticle(this)) let $TextWrapper = Wrappers.getTextWrapper($root) $('h1,h2,h3,h4,h5,h6', $TextWrapper).each((index, $title) => { $title = $($title) let id = Utils.guid() let titleEle = { id: id, ele: $title, content: $title.text() } this.titles.push(titleEle) this.titlesMap[id] = titleEle }) console.debug(this.titles) } /** * 切换当前的文章 * @param article 可为null */ let toggleCurrentArticle = article => { if (currentArticle === article) { return } console.debug('[切换当前文章]', article) currentArticle = article refreshTitleBar() } /** * 刷新标题条(根据当前文章) */ let refreshTitleBar = () => { //当前没有文章 if (!currentArticle) { titleBar.hide() return } //有标题 let titleBarContent = $('<div style="padding: 5px 10px;display: flex;flex-direction: column;"></div>') //标题 if (currentArticle.title) { let titleLine = $('<div style="align-self: center;text-align: center;">'+currentArticle.title+'</div>') titleLine.css('color', Consts.textColorSel) titleBarContent.append(titleLine) } //到顶部 let toTop = $('<a href="javascript: void(0)" style="align-self: center;margin-top: 5px;color: '+Consts.textColorUnSel+';">↑到顶部↑</a>') toTop.on('click', () => Utils.goToByScroll(currentArticle.$root)) toTop.hover(() => toTop.css('color', Consts.textColorSel), () => toTop.css('color', Consts.textColorUnSel)) titleBarContent.append(toTop) //中间标题 currentArticle.titles.forEach(item => { let titleEle = $('<a href="javascript: void(0)" ' + 'style="border-radius: 5px;border: solid 1px lightgrey;padding: 5px 10px;margin-top: 5px;color: '+Consts.textColorUnSel+';"' + '>'+item.content+'</a>') titleEle.hover(() => { titleEle.css('border-style', 'inset') titleEle.css('color', Consts.textColorSel) }, () => { titleEle.css('border-style', 'solid') titleEle.css('color', Consts.textColorUnSel) }) titleEle.on('click', () => Utils.goToByScroll(item.ele)) titleBarContent.append(titleEle) }) //到底部 let toBottom = $('<a href="javascript: void(0)" style="align-self: center;margin-top: 5px;margin-bottom: 5px;color: '+Consts.textColorUnSel+';">↓到底部↓</a>') toBottom.on('click', () => Utils.goToEndByScroll(currentArticle.$root)) toBottom.hover(() => toBottom.css('color', Consts.textColorSel), () => toBottom.css('color', Consts.textColorUnSel)) titleBarContent.append(toBottom) //更新标题内容 titleBar.children().remove() titleBar.append(titleBarContent) titleBar.show() } /** * 刷新 */ let refresh = () => { console.debug('[刷新]') //得到打开的问题列表 let openedArticleRoots = Wrappers.getArticleRoots() //全部初始化 let activeIds = {} if (openedArticleRoots) { openedArticleRoots.each((index, ele) => { let id = init($(ele)) if (id) { activeIds[id] = true } }) } articles.removeIf(e => { if (!activeIds[e.id]) { //注销事件 e.$root.off() return true } }) articlesMap = {} articles.forEach(article => articlesMap[article.id] = article) //检测刷新当前文章 if (currentArticle && !articlesMap[currentArticle.id]) { toggleCurrentArticle(null) } //日志 console.debug('[打开的文章]', articles) } /** * 初始化 * @param $root 打开的项 * @return id */ let init = $root => { let id = $root.attr('id') //已经有id了 if (id) { if (articlesMap[id]) {//缓存有效 console.debug('[已经有缓存了]', id) return id }else { //缓存失效了 } } let article = Wrappers.parseArticle($root) if (article) { let id = article.id //设置id $root.attr('id', id) //添加缓存 articles.push(article) articlesMap[id] = article //日志 console.info('[初始化]', id, article, $root) return id } } /** * 获取有指定类名的父类 * @param element 当前元素 * @param className 类名 * @return 未找到返回null(如果本身就有此类名,则返回本身) */ let _getParent = (element, className) => { //元素为null if (!element || $(element).length === 0) { return null } //本身 if ($(element).hasClass(className)) { return $(element) } //父 return _getParent($(element).parent(), className) } $(document).click(({target}) => { console.debug('[点击]', target) setTimeout(refresh) }) //ready时运行 $(() => { //计算头部高度 appHeaderHeight = Wrappers.getHeader().height() //初始化标题条 let $SideBar = Wrappers.getSideBar() if ($SideBar) { let topBottomMargin = appHeaderHeight+30; titleBar = $('<div id="tagsTitleBar" style="position: fixed;top: '+topBottomMargin+'px;bottom: '+topBottomMargin+'px;background-color: rgba(255, 255, 255, 0.95);z-index: 180;opacity: 0.5;border-radius: 5px;overflow-y: auto;box-shadow: rgba(152, 152, 152, 0.5) 0px 1px 3px;word-break: break-word;"></div>') titleBar.css('color', Consts.textColorUnSel) titleBar.width($SideBar.width()) titleBar.hover(() => { titleBar.css('opacity', '1') }, () => { titleBar.css('opacity', '0.5') }) titleBar.hide() $SideBar.append(titleBar) } //初始刷新 refresh() //定时刷新 setInterval(refresh, 3000) }) })();