tampermonkey-scripts/caret-read-progress.js

123 lines
3.8 KiB
JavaScript

// ==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
}
})();