Tampermonkey is een Chrome-extensie waarmee je eigen JavaScript-scripts kunt uitvoeren op websites. Binnen Flooris gebruiken we dit om extra knoppen toe te voegen aan ClickUp-taken en GitHub Pull Requests, zodat veelgebruikte informatie snel te kopiëren is.
// ==UserScript==// @name ClickUp Copy buttons// @namespace https://flooris.nl/// @version 4.0// @description Betere ClickUp omschrijving kopieren// @author Arthur// @match https://app.clickup.com/*// @icon https://www.google.com/s2/favicons?sz=64&domain=clickup.com// @grant GM_setClipboard// @grant GM_addElement// @updateURL https://flooris.nl/assets/clickup.js// @downloadURL https://flooris.nl/assets/clickup.js// ==/UserScript==(function(){"use strict";// ─── State ───────────────────────────────────────────────────────────────────letlastKnownTaskId=null;letcurrentTaskId=null;// ─── DOM Queries ─────────────────────────────────────────────────────────────functiongetTaskIdElement(){returndocument.querySelector("[data-test='task-view-task-label__taskid-button']");}functiongetTaskIdButtonLocation(){returndocument.querySelector("cu-task-view-task-label");}functiongetButton(dataInfo){returndocument.querySelector(`[data_info='${dataInfo}']`);}// ─── Task Data ───────────────────────────────────────────────────────────────functiongetTaskId(){constel=getTaskIdElement();if(!el)returnnull;consttext=el.innerText?.trim();if(text&&text!=="Copied!"){lastKnownTaskId=text;}returnlastKnownTaskId;}functiongetTaskName(){returndocument.querySelector(".cu-task-title__title")?.value??null;}functiongetTaskUrl(){returnwindow.location.href;}// ─── Copy Text Formatters ────────────────────────────────────────────────────functiontextIdName(){return`${getTaskId()}${getTaskName()}`;}functiontextIdNameUrl(){return`${getTaskId()}${getTaskName()} (${getTaskUrl()})`;}functiontextMarkdown(){return`[${getTaskId()}](${getTaskUrl()}) ${getTaskName()}`;}// ─── Angular ngContent Helper ────────────────────────────────────────────────functiongetNgContentAttrName(element){if(!element)returnnull;for(constattrofelement.attributes){if(attr.name.startsWith("_ngcontent")){returnattr.name;}}returnnull;}functiongetTaskIdNgAttrName(){returngetNgContentAttrName(getTaskIdElement());}// ─── Button Factory ───────────────────────────────────────────────────────────//// Buttons are created once. On task change, only the event listeners are// rebound. A `data-listener-bound` guard prevents listener stacking.functioncreateButton(dataInfo,label){constlocation=getTaskIdButtonLocation();if(!location)return;constngAttr=getTaskIdNgAttrName();constbtnAttrs={data_info:dataInfo,class:"cu-task-id-button ng-star-inserted",};if(ngAttr)btnAttrs[ngAttr]=null;GM_addElement(location,"button",btnAttrs);constbtn=getButton(dataInfo);if(!btn)return;constspanAttrs={textContent:label,class:"cu-task-view-task-label__button-label",};if(ngAttr)spanAttrs[ngAttr]=null;GM_addElement(btn,"span",spanAttrs);}functionensureButtons(){if(!getButton("id-task")){createButton("id-task","ID - Name");}if(!getButton("id-task-url")){createButton("id-task-url","ID - Name (url)");}if(!getButton("context-md")){createButton("context-md","MarkDown");}}// ─── Listener Binding ────────────────────────────────────────────────────────//// Uses a `data-listener-bound` attribute as a guard so listeners are never// stacked. On task change the attribute is cleared and listeners are rebound.functionbindListener(dataInfo,textFn){constbtn=getButton(dataInfo);if(!btn)return;if(btn.dataset.listenerBound)return;btn.dataset.listenerBound="1";btn.addEventListener("mouseup",()=>{consttext=textFn();if(text)GM_setClipboard(text,"text");});}functionbindAllListeners(){bindListener("id-task",textIdName);bindListener("id-task-url",textIdNameUrl);bindListener("context-md",textMarkdown);}functionrebindAllListeners(){["id-task","id-task-url","context-md"].forEach((dataInfo)=>{constbtn=getButton(dataInfo);if(btn)deletebtn.dataset.listenerBound;});bindAllListeners();}// ─── Intervals ───────────────────────────────────────────────────────────────// Ensures buttons exist and listeners are bound. Runs every second.setInterval(()=>{if(!getTaskId())return;ensureButtons();bindAllListeners();},1000);// Detects task navigation and rebinds listeners when the task ID changes.setInterval(()=>{constid=getTaskId();if(!id)return;if(id!==currentTaskId){currentTaskId=id;rebindAllListeners();}},500);})();