// ==UserScript== // @name Caret Read Progress // @namespace http://tampermonkey.net/ // @version 1.5 // @description Moves caret with arrow keys, highlights read text, works across paragraphs and divs // @author You // @match *://*/* // @grant none // ==/UserScript== (function () { 'use strict'; let highlightDiv = document.createElement("div"); highlightDiv.style.position = "absolute"; highlightDiv.style.background = "rgba(255, 255, 0, 0.3)"; // Yellow highlight highlightDiv.style.height = "1.5em"; highlightDiv.style.pointerEvents = "none"; highlightDiv.style.zIndex = "9999"; highlightDiv.style.width = "0px"; // Start with no highlight document.body.appendChild(highlightDiv); document.addEventListener("keydown", function (event) { if (event.key !== "ArrowRight" && event.key !== "ArrowLeft") return; let sel = window.getSelection(); if (!sel.rangeCount) return; let range = sel.getRangeAt(0); let moved = moveCaret(event.key === "ArrowRight" ? 1 : -1); if (moved) updateHighlight(); }); function moveCaret(direction) { let sel = window.getSelection(); if (!sel.rangeCount) return false; let range = sel.getRangeAt(0); let node = range.startContainer; let offset = range.startOffset; // Move within the same text node if (direction === 1 && offset < node.length) { offset++; } else if (direction === -1 && offset > 0) { offset--; } // If at the end of a node, move to the next text node else if (direction === 1) { let nextNode = findNextTextNode(node); if (nextNode) { node = nextNode; offset = 0; } else { return false; } } // If at the start of a node, move to the previous text node else if (direction === -1) { let prevNode = findPreviousTextNode(node); if (prevNode) { node = prevNode; offset = prevNode.length; } else { return false; } } // Update selection range let newRange = document.createRange(); newRange.setStart(node, offset); newRange.collapse(true); sel.removeAllRanges(); sel.addRange(newRange); return true; } function findNextTextNode(node) { while (node) { if (node.nextSibling) { node = node.nextSibling; while (node && node.nodeType !== Node.TEXT_NODE) { node = node.firstChild || node.nextSibling; } return node; } node = node.parentNode; } return null; } function findPreviousTextNode(node) { while (node) { if (node.previousSibling) { node = node.previousSibling; while (node && node.nodeType !== Node.TEXT_NODE) { node = node.lastChild || node.previousSibling; } return node; } node = node.parentNode; } return null; } function updateHighlight() { let sel = window.getSelection(); if (!sel.rangeCount) return; let range = sel.getRangeAt(0); let rect = range.getBoundingClientRect(); if (rect.width === 0 && rect.height === 0) { return; // Ignore empty selections } highlightDiv.style.top = `${rect.top + window.scrollY}px`; highlightDiv.style.left = `0px`; // Always start from the left highlightDiv.style.width = `${rect.right}px`; // Expand highlight only to where you've read } })();