Add "watched session" flag feature to ARM DevSummit site.
// ==UserScript== // @name ARM DevSummit Watched Session Tracker // @namespace https://greasyfork.org/users/382804 // @version 0.5 // @description Add "watched session" flag feature to ARM DevSummit site. // @author [email protected] // @match https://devsummit.arm.com/* // @icon https://www.google.com/s2/favicons?sz=64&domain=arm.com // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/js.cookie.min.js // @grant GM_addStyle // @run-at document-idle // @license MIT // ==/UserScript== // date 2020-11-04 // log Replaced checkbox with unicode symbols and added css styles. // Checkbox click behavior inside <a> is too inconsistent across browsers. // date 2020-11-03 // log Fix clicking on session number span contents. // date 2022-11-02 // log Fix MutationObserver usage. // No more need for context-menu trigger. // date 2022-10-31 // log Initial revision. "use strict"; var use_local_storeage = false; // otherwise uses cookies var watchedSessions = new Set(); // Set default cookie properties. var cookiesApi = Cookies.withAttributes({ expires: 1000, // days from creation domain: "devsummit.arm.com", path: "/", secure: true }); // Load the watched sessions from browser-side storage. function loadWatchedSessions() { var watched; if (use_local_storeage) { watched = window.localStorage.getItem("watchedSessions"); } else { // cookies watched = cookiesApi.get("watchedSessions"); } watchedSessions = new Set(JSON.parse(watched ? watched : "[]")); } // Save the watched sessions to browser-side storage. function storeWatchedSessions() { var watchedSessions_json = JSON.stringify(Array.from(watchedSessions)); if (use_local_storeage) { window.localStorage.setItem("watchedSessions", watchedSessions_json); } else { // cookies cookiesApi.set("watchedSessions", watchedSessions_json); } } // Returns all text nodes of the document. function getTextNodes(rootNode) { if (rootNode.nodeType == 3) { // We're already at a text node - has no children, can't be tree-walked. return [rootNode]; } var walker = document.createTreeWalker( rootNode, NodeFilter.SHOW_TEXT, { acceptNode(node) { var parentTag = node.parentNode.tagName; if (parentTag && parentTag.toUpperCase() == "SCRIPT") { return NodeFilter.FILTER_REJECT; } else { return NodeFilter.FILTER_ACCEPT; } } } ); var node; var textNodes = []; while(node = walker.nextNode()) { textNodes.push(node); } return textNodes; } function handleSessionClick(event) { // Toggle "watched" state. var watched = ! (this.getAttribute("watched").toLowerCase() === 'true'); this.setAttribute("watched", watched); // Prevent outer <a> element (if present) from firing. event.preventDefault(); //event.stopPropagation(); // Update saved state. var sessionNumber = this.getAttribute("session-number"); if (watched) { watchedSessions.add(sessionNumber); } else { watchedSessions.delete(sessionNumber); } storeWatchedSessions(); } // Add styling of watched/unwatched sessions. function installWatchedSessionStyle() { GM_addStyle("span.session-number { cursor: pointer !important; display: inline !important; }"); GM_addStyle("span.session-number[watched=true] { color: green; }"); GM_addStyle("span.session-number[watched=true]::after { content: '✅'; }"); GM_addStyle("span.session-number[watched=false] { color: red; }"); GM_addStyle("span.session-number[watched=false]::after { content: '🔲'; }"); } function addWatchedSessionTracker(rootNode) { // console.log("running addWatchSessiontracker on ", rootNode.nodeName); // console.log(rootNode); // We want to handle session number patterns of the form "[123]". // Find all session number patterns in all text nodes and convert them // to separate spans that we can handle separately in the DOM. for (var node of getTextNodes(rootNode)) { // Each while iteration handles one occurrence of a session. // We need the while loop since there can be more than one session in a text node. while(node) { // If we've already handled this node in a previous call to addWatchedSessionTracker, skip it. if (node.parentNode.hasAttribute("session-number")) { break; } var start = node.nodeValue.search(/\[\d+\]/); // session number pattern if (start < 0) { // No session left in this node. break; } if (start > 0) { // There is text before the session number pattern. // Split it out into a separate text node that precedes the node // starting with the session number pattern. node = node.splitText(start); } // Invariant: node starts with session number pattern. // Find the end of the pattern. var end = node.nodeValue.search("]"); if (end < 0) { console.log("Error: could not find session pattern end ] in '" + node.nodeValue + "'"); break; } // Break out the session number and remove it from the text node. var sessionNumber = node.nodeValue.substring(1, end); // console.log("found session " + sessionNumber + " in: " + node.nodeValue.substr(0, 20)); node.nodeValue = node.nodeValue.substring(end + 1); // Replace the session number in the text node with a new <span> containing // the session number and a checkbox. var sessionSpan = document.createElement("span"); sessionSpan.setAttribute("class", "session-number"); sessionSpan.setAttribute("session-number", sessionNumber); sessionSpan.setAttribute("watched", watchedSessions.has(sessionNumber)); node.parentNode.insertBefore(sessionSpan, node); var sessionText = document.createTextNode("[" + sessionNumber + "] "); sessionSpan.appendChild(sessionText); // React to button clicks on the session number span. sessionSpan.addEventListener("click", handleSessionClick, false); } } } (function() { // Load watched sessions from browser-side storage. loadWatchedSessions(); console.log("watched sessions:", watchedSessions); function handleMutations(mutations) { for (var mutation of mutations) { if (mutation.type == "childList") { for (var node of mutation.addedNodes) { // console.log("running addWachedSessionTracker from mutation watcher"); addWatchedSessionTracker(node); } } } } var observer = new MutationObserver(handleMutations); // Run a pass over modified DOM sections. observer.observe(document.body, { childList: true, subtree: true }); // Perform a first pass over the whole document body to catch everything // that happened before we started watching for mutations. addWatchedSessionTracker(document.body); installWatchedSessionStyle(); })();