// ==UserScript== // @name rZhihu // @author nonoroazoro // @description 为知乎首页添加类似 Google Reader 的快捷键。 // @description:en Adding Google-like keyboard shortcuts for Zhihu homepage. // @homepageURL https://github.com/nonoroazoro/firefox/tree/master/greasemonkey/rZhihu // @namespace https://greasyfork.org/zh-CN/scripts/30036-rzhihu // @grant none // @version 1.2.0 // @run-at document-end // @include https://www.zhihu.com/ // @include https://www.zhihu.com/#* // @downloadURL none // ==/UserScript== let currentIndex = -1; let inProgress = false; let maxIndex = -1; let currentTopic = null; let currentTopicStrip = null; let stories = null; let storyContainer = null; const ignoreList = [ { nodeName: "DIV", className: "public-DraftEditor-content" }, { nodeName: "INPUT" }, { nodeName: "TEXTAREA" } ]; function start() { initCurrentTopicStrip(); storyContainer = document.getElementById("TopstoryContent"); observe(storyContainer, update); storyContainer.addEventListener("click", _handleClick); document.body.addEventListener("keydown", _handleKeydown); } function initCurrentTopicStrip() { currentTopicStrip = document.createElement("div"); currentTopicStrip.style.position = "absolute"; currentTopicStrip.style.right = "0"; currentTopicStrip.style.top = "0"; currentTopicStrip.style.bottom = "0"; currentTopicStrip.style.width = "2px"; currentTopicStrip.style.backgroundColor = "#6cb8ff"; } function _handleClick(e) { // highlight the clicked story. const index = _getIndexOfStory(_getAncestor(e.target, "TopstoryItem")); if (index !== -1) { _flip(index, false); } } function _handleKeydown(e) { if (!stories || _isIgnored(e.target) || e.altKey || e.shiftKey || e.ctrlKey || e.metaKey) { return; } if (e.keyCode === 74) { // press "j" _next(); } else if (e.keyCode === 75) { // press "k" _prev(); } else if (e.keyCode === 79 || e.keyCode === 13) { // press "o/enter" _toggle(); } else if (e.keyCode === 67) { // press "c" _toggleComment(); } else if (e.keyCode === 85) { // press "u" _unlike(); } else if (e.keyCode === 86) { // press "v" _openInNewTab(); } } /** * check if key press should be ignored. */ function _isIgnored(target) { let ignored = false; const nodeName = target.nodeName; const className = target.className; for (const item of ignoreList) { ignored = nodeName === item.nodeName; if (item.className) { ignored = ignored && (className.indexOf(item.className) !== -1); } if (ignored) { break; } } return ignored; } /** * Flip to next story. */ function _next() { _flip(currentIndex + 1); } /** * Flip to previous story. */ function _prev() { _flip(currentIndex - 1); } /** * Flip to story. * * @param {number} index * @param {boolean} [ensureVisible=true] */ function _flip(index, ensureVisible = true) { if (!inProgress) { inProgress = true; let targetIndex = index; if (targetIndex < 0) { targetIndex = 0; } if (targetIndex > maxIndex) { targetIndex = maxIndex; } const target = stories[targetIndex]; if (target) { currentIndex = targetIndex; // Show strip. stories[currentIndex].appendChild(currentTopicStrip); if (ensureVisible) { window.scrollTo({ left: 0, top: target.offsetTop - target.offsetHeight, behavior: "smooth" }); } } inProgress = false; } } /** * Toggle story expand/collapse. */ function _toggle() { const expand = _query(".is-collapsed .RichContent-inner"); if (expand) { expand.click(); } else { const collapse = _query(".ContentItem-actions > button:last-child"); if (collapse) { collapse.click(); } } } /** * Toggle comment expand/collapse. */ function _toggleComment() { const close = document.querySelector(".Modal.Modal--fullPage > button"); if (close) { // should close the dialog when comment dialog is shown. close.click(); } else { // otherwise expand/collapse the comment. const comment = _query(".ContentItem-actions > button:nth-child(2)"); if (comment) { comment.click(); // resume state _flip(currentIndex); } } } /** * Toggle story unlike. */ function _unlike() { const undo = _query("button:first-child"); if (undo && undo.textContent === "撤销") { undo.click(); } else { const element = _query(".ContentItem-actions > .Popover > button:last-child"); if (element) { element.click(); Promise.resolve().then(() => { const unlike = document.querySelector(".Menu.AnswerItem-selfMenu button:last-child"); if (unlike && unlike.textContent === "不感兴趣") { unlike.click(); // auto-next _next(); } }); } } } /** * Open current story in a new tab. */ function _openInNewTab() { // 1. answer; // 2. empty answer; // 3. advertisement. const element = _query(".ContentItem-title a, .QuestionItem-title > a, .Advert--card > a"); if (element) { element.click(); } } /** * Finds the specified element in current story. * * @param {string} selector * @returns {Element} */ function _query(selector) { if (selector) { const story = stories[currentIndex]; if (story) { return story.querySelector(selector); } } return null; } /** * Gets the ancestor of the element with specified class name. * * @param {Element} element * @param {string} className * @returns {Element} */ function _getAncestor(element, className) { if (element && className) { let ancestor = element; while (ancestor) { if (ancestor.className.trim().split(/\W+/).indexOf(className) !== -1) { return ancestor; } else { ancestor = ancestor.parentNode; } } } return null; } /** * Gets the index of the story. * * @param {Element} story * @returns {number} */ function _getIndexOfStory(story) { if (story && stories) { return Array.prototype.indexOf.call(stories, story); } return -1; } /** * Updates after the story list is loaded. */ function update(mutations) { if (mutations.length > 0) { const topicContainer = storyContainer.children[0]; if (topicContainer) { if (topicContainer.className !== currentTopic) { // Switch topic and reset index. currentIndex = -1; currentTopic = topicContainer.className; } const subContainer = topicContainer.children[0]; if (subContainer) { // Ignore adverts. stories = subContainer.querySelectorAll(".Card.TopstoryItem:not(.TopstoryItem--advertCard)"); maxIndex = stories.length - 1; return; } } // Reset. currentIndex = -1; maxIndex = -1; stories = []; } } function observe(element, callback) { if (element && typeof callback === "function") { (new window.MutationObserver(callback)).observe(element, { childList: true, subtree: true }); } } function debounce(callback, delay = 500) { let timer = null; return (...args) => { const context = this; window.clearTimeout(timer); timer = window.setTimeout(() => callback.apply(context, args), delay); }; } /** * Check if run-at is working correctly. */ function isRunAtAvailabe() { let available = false; if (typeof GM_info === "undefined") { available = true; } else { available = GM_info.scriptHandler === "Tampermonkey"; } return available; } if (isRunAtAvailabe()) { start(); } else { document.addEventListener("DOMContentLoaded", () => start()); }