diff --git a/TogglLibrary.js b/TogglLibrary.js index 48fc72b9c1960fbe9d927e6e29859f429e90378b..cb7ab94e8e49ad77064feec78a3240aba672dc60 100644 --- a/TogglLibrary.js +++ b/TogglLibrary.js @@ -9,6 +9,10 @@ */ function TogglButtonGM(selector, renderer) { + + function debug() { + console.debug.apply(console, prepend("TogglLibrary:", arguments)); + } var $activeApiUrl = null, @@ -168,26 +172,86 @@ function TogglButtonGM(selector, renderer) { } } - function getIdFromEntry(entry) { - return Number.parseInt(entry.querySelector("input[type=\"checkbox\"]").getAttribute("data-id"), 10); + function getIdFromEntry(entryNode) { + return Number.parseInt(entryNode.querySelector("input[type=\"checkbox\"]").getAttribute("data-id"), 10); } - this.tryDecorateTimer = () => { - if (!window.location.href.startsWith("https://toggl.com/app/timer")) - return; - const timerEntries = document.querySelectorAll("#time-entry-list ul li.entry"); - console.log("Timer entries: " + timerEntries.length); - for (var entry of timerEntries) { - let id = getIdFromEntry(entry); - let originUrl = GM_getValue("_url["+id+"]", null); + function decorateEntry(entry) { + debug("Decorating entry", entry); + // Additional column for all entries, to keep column alignment + let renderTarget = entry.querySelector("div.paragon-decoration"); + if (!renderTarget) { + renderTarget = document.createElement("div"); + renderTarget.classList.add("paragon-decoration"); + const duration = entry.querySelector("div.duration-container"); + entry.insertBefore(renderTarget, duration); + } + + const id = getIdFromEntry(entry); + // URL will only be found within script that has it in its database + // Multiple scripts are not supposed to have URL's for same entries, so they won't be in conflict while rendering decoration + const originUrl = GM_getValue("_url["+id+"]", null); if (originUrl) { - console.log("Origin for entry: " + id + ": " + originUrl); - let backReferenceColumn = document.createElement("div"); - renderer.renderOriginLink(backReferenceColumn, originUrl); - entry.appendChild(backReferenceColumn); + debug("Origin for entry: " + id + ": " + originUrl); + renderer.renderOriginLink(renderTarget, originUrl); } + } + + function forEachNode(nodeList, consumer) { + for (var entry of nodeList) { + consumer(entry); } + } + + // Decorates node if is an entry or its nested entries + function decorateAllEntriesIn(node) { + debug("Decorating all in:", node); + if (node.nodeName == "LI" && node.classList.contains("entry")) { + decorateEntry(node); + } else if (node.querySelectorAll) { + forEachNode(node.querySelectorAll("li.entry"), decorateEntry); + } + } + + function prepend(value, array) { + var newArray = Array.prototype.slice.call(array); + newArray.unshift(value); + return newArray; + } + + function waitForElement(element, query) { + return new Promise((resolve, reject) => { + const observer = new MutationObserver(function(mutations) { + const result = element.querySelector(query); + if (result) { + debug("Matched query:", query, result); + resolve(result); + observer.disconnect(); + } + }); + observer.observe(element, {childList: true, subtree:true}); + }); + } + + + this.tryDecorateTimer = () => { + if (!window.location.href.startsWith("https://toggl.com/app/timer")) + return; + GM_addStyle("li.entry div.paragon-decoration { width:3em; padding:16px; }"); + waitForElement(document, "#time-entry-list").then(timeEntryList => { + const observer = new MutationObserver(mutations => { + mutations.forEach(mutation => { + debug("Mutation:", mutation.type, " target:", mutation.target, "added:", mutation.addedNodes, "removed:", mutation.removedNodes); + forEachNode(mutation.addedNodes,decorateAllEntriesIn); + if (mutation.removedNodes.length > 0) // Entry editing removes decoration + decorateAllEntriesIn(mutation.target); + }) + }); + observer.observe(timeEntryList, { subtree:true, childList: true }); + decorateAllEntriesIn(timeEntryList); + }); }; + this.clickLinks = function() { for (i in $instances) { $instances[i].clickLink();