ARM DevSummit Watched Session Tracker

Add "watched session" flag feature to ARM DevSummit site.

As of 02/11/2022. See the latest version.

// ==UserScript==
// @name         ARM DevSummit Watched Session Tracker
// @namespace    https://greasyfork.org/users/382804
// @version      0.3
// @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
// @run-at       document-idle
// @license      MIT
// ==/UserScript==
// date          2022-11-02
// log           Mix 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) {
        // Already a text node - has no children.
        return [rootNode];
    }

    var walker = document.createTreeWalker(
        rootNode,
        NodeFilter.SHOW_TEXT,
        { acceptNode(node) {
            var parentTag = node.parentNode.tagName;
            if (parentTag && parentTag.toLowerCase() == "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) {
    event.preventDefault(); // Prevent outer <a> element (if present) from firing.
    // event.stopPropagation();
    var checkbox = this.lastChild;
    // FIXME: For some reason, clicking on the checkbox itself doesn't work.
    //        You must click on the session number span instead.
    // toggle "watched" checkbox
    checkbox.checked = ! checkbox.checked;
    var sessionNumber = this.getAttribute("session_number");
    if (checkbox.checked) {
        watchedSessions.add(sessionNumber);
    } else {
        watchedSessions.delete(sessionNumber);
    }
    styleSessionSpan(this);
    storeWatchedSessions();
}

function styleSessionSpan(sessionSpan) {
    var checkbox = sessionSpan.lastChild;
    if (checkbox.checked) {
        sessionSpan.style.color = "red";
    } else {
        sessionSpan.style.color = "green";
    }
}

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("style", "display:inline; cursor: pointer");
            node.parentNode.insertBefore(sessionSpan, node);

            var sessionText = document.createTextNode("[" + sessionNumber + "] ");
            sessionSpan.appendChild(sessionText);

            var sessionWatchedCheckbox = document.createElement("input");
            sessionWatchedCheckbox.setAttribute("type", "checkbox");
            sessionWatchedCheckbox.setAttribute("style", "cursor: pointer");
            sessionWatchedCheckbox.checked = watchedSessions.has(sessionNumber);
            sessionSpan.appendChild(sessionWatchedCheckbox);

            // Must have added checkbox before call to styling!
            styleSessionSpan(sessionSpan);

            // 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 (let mutation of mutations) {
            if (mutation.type === "childList") {
                for (var node of mutation.addedNodes) {
                    // console.log("running addWachedSessionTracker from mutation watcher");
                    addWatchedSessionTracker(node);
                }
            }
        }
    }

    let observer = new MutationObserver(handleMutations);

    observer.observe(document.body, {
        childList: true,
        subtree: true
    });

    addWatchedSessionTracker(document.body);

})();