123 lines
3.8 KiB
JavaScript
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
|
|
}
|
|
})();
|