diff --git a/static/picker.js b/static/picker.js new file mode 100644 index 0000000..c74e292 --- /dev/null +++ b/static/picker.js @@ -0,0 +1,324 @@ +// Element picker for ublproxy +// Loaded by the bootstrap script when the user presses Alt+Shift+B. +// Receives UBLPROXY_PORTAL, UBLPROXY_TOKEN, and UBLPROXY_HOST as function arguments. + +// --- Selector Generation --- + +function generateSelector(el) { + if (el.id) return "#" + CSS.escape(el.id); + + var tag = el.tagName.toLowerCase(); + + // Try classes first - most specific and common for ad elements + if (el.classList.length > 0) { + var classSelector = "." + Array.from(el.classList).map(CSS.escape).join("."); + // Check uniqueness on page + if (document.querySelectorAll(classSelector).length === 1) return classSelector; + // With tag + var tagClass = tag + classSelector; + if (document.querySelectorAll(tagClass).length === 1) return tagClass; + // Return class selector even if not unique (still useful as element hiding) + return classSelector; + } + + // Try common identifying attributes + var attrs = ["name", "data-ad", "data-ad-slot", "data-testid", "role"]; + for (var i = 0; i < attrs.length; i++) { + var val = el.getAttribute(attrs[i]); + if (val) { + var attrSel = tag + "[" + attrs[i] + '="' + CSS.escape(val) + '"]'; + if (document.querySelectorAll(attrSel).length <= 3) return attrSel; + } + } + + // Fallback: tag with nth-of-type (nth-child counts all siblings + // regardless of tag, nth-of-type counts only same-tag siblings) + var parent = el.parentElement; + if (parent) { + var siblings = Array.from(parent.children).filter(function(c) { + return c.tagName === el.tagName; + }); + if (siblings.length === 1) { + return generateSelector(parent) + " > " + tag; + } + var idx = siblings.indexOf(el) + 1; + return generateSelector(parent) + " > " + tag + ":nth-of-type(" + idx + ")"; + } + + return tag; +} + +// --- Panel UI (Shadow DOM for full isolation) --- + +var panelHost = document.createElement("div"); +panelHost.id = "__ublproxy_picker__"; +panelHost.style.cssText = "position:fixed;top:16px;right:16px;z-index:2147483647;"; +document.body.appendChild(panelHost); + +var shadow = panelHost.attachShadow({ mode: "closed" }); + +shadow.innerHTML = '' + + '
' + + '
' + + '
ublproxy picker
' + + '
Hover over an element and click to select it
' + + ' ' + + ' ' + + ' ' + + '
' + + '
'; + +var highlight = shadow.getElementById("highlight"); +var panel = shadow.getElementById("panel"); +var selectorEl = shadow.getElementById("selector"); +var domainEl = shadow.getElementById("domain"); +var hintEl = shadow.getElementById("hint"); +var buttonsEl = shadow.getElementById("buttons"); +var blockBtn = shadow.getElementById("block"); +var cancelBtn = shadow.getElementById("cancel"); +var closeBtn = shadow.getElementById("close"); +var statusEl = shadow.getElementById("status"); + +// --- Highlight Overlay --- + +var currentTarget = null; + +function updateHighlight(el) { + if (!el || el === document.body || el === document.documentElement) { + highlight.style.display = "none"; + currentTarget = null; + return; + } + var rect = el.getBoundingClientRect(); + highlight.style.display = "block"; + highlight.style.top = rect.top + "px"; + highlight.style.left = rect.left + "px"; + highlight.style.width = rect.width + "px"; + highlight.style.height = rect.height + "px"; + currentTarget = el; +} + +// --- Make panel draggable --- + +var dragging = false, dragX = 0, dragY = 0; + +function onDragMove(e) { + if (!dragging) return; + panelHost.style.left = (e.clientX - dragX) + "px"; + panelHost.style.right = "auto"; + panelHost.style.top = (e.clientY - dragY) + "px"; +} + +function onDragUp() { dragging = false; } + +panel.addEventListener("mousedown", function(e) { + if (e.target.tagName === "BUTTON") return; + dragging = true; + dragX = e.clientX - panelHost.getBoundingClientRect().left; + dragY = e.clientY - panelHost.getBoundingClientRect().top; + e.preventDefault(); +}); +document.addEventListener("mousemove", onDragMove); +document.addEventListener("mouseup", onDragUp); + +// --- State --- + +var picking = true; +var selectedElement = null; +var selectedSelector = ""; + +// The actual hostname of the page being viewed, passed from the bootstrap +// script. Falls back to location.hostname if not provided. +var pageHost = (typeof UBLPROXY_HOST !== "undefined" && UBLPROXY_HOST) ? UBLPROXY_HOST : location.hostname; + +// --- Event handlers --- + +function isPickerElement(el) { + while (el) { + if (el === panelHost) return true; + el = el.parentElement; + } + return false; +} + +function onMouseMove(e) { + if (!picking || dragging) return; + var el = document.elementFromPoint(e.clientX, e.clientY); + if (isPickerElement(el)) { + highlight.style.display = "none"; + return; + } + updateHighlight(el); +} + +function onMouseDown(e) { + if (!picking) return; + if (isPickerElement(e.target)) return; + + e.preventDefault(); + e.stopPropagation(); + + if (!currentTarget) return; + + selectedElement = currentTarget; + selectedSelector = generateSelector(selectedElement); + picking = false; + + // Update panel UI + hintEl.style.display = "none"; + selectorEl.textContent = selectedSelector; + selectorEl.style.display = "block"; + domainEl.textContent = pageHost; + domainEl.style.display = "block"; + buttonsEl.style.display = "flex"; + statusEl.style.display = "none"; +} + +function resetPicker() { + picking = true; + selectedElement = null; + selectedSelector = ""; + highlight.style.display = "none"; + hintEl.style.display = "block"; + selectorEl.style.display = "none"; + domainEl.style.display = "none"; + buttonsEl.style.display = "none"; + statusEl.style.display = "none"; +} + +function closePicker() { + picking = false; + highlight.style.display = "none"; + panelHost.style.display = "none"; + + // Remove all event listeners to allow clean re-open + document.removeEventListener("mousemove", onMouseMove, true); + document.removeEventListener("mousedown", onMouseDown, true); + document.removeEventListener("keydown", onKeyDown, true); + document.removeEventListener("mousemove", onDragMove); + document.removeEventListener("mouseup", onDragUp); +} + +function openPicker() { + panelHost.style.display = ""; + picking = true; + resetPicker(); + + // Re-register all event listeners + document.addEventListener("mousemove", onMouseMove, true); + document.addEventListener("mousedown", onMouseDown, true); + document.addEventListener("keydown", onKeyDown, true); + document.addEventListener("mousemove", onDragMove); + document.addEventListener("mouseup", onDragUp); +} + +// Expose openPicker for the bootstrap script to call on re-toggle +panelHost.__ublproxyOpen = openPicker; + +function onKeyDown(e) { + if (e.key === "Escape") { + if (selectedElement) { + resetPicker(); + } else { + closePicker(); + } + e.preventDefault(); + e.stopPropagation(); + } +} + +function showStatus(msg, isError) { + statusEl.textContent = msg; + statusEl.className = "status" + (isError ? " error" : ""); + statusEl.style.display = "block"; +} + +// --- Block action --- + +blockBtn.addEventListener("click", function() { + if (!selectedSelector) return; + + var rule = pageHost + "##" + selectedSelector; + blockBtn.disabled = true; + blockBtn.textContent = "Saving..."; + + fetch(UBLPROXY_PORTAL + "/api/rules", { + method: "POST", + headers: { + "Content-Type": "application/json", + "Authorization": "Bearer " + UBLPROXY_TOKEN + }, + body: JSON.stringify({ + rule: rule, + domain: pageHost + }) + }) + .then(function(r) { + if (!r.ok) throw new Error("HTTP " + r.status); + return r.json(); + }) + .then(function() { + // Immediately hide the element on the current page + if (selectedElement) { + selectedElement.style.setProperty("display", "none", "important"); + } + // Also inject a style rule for immediate effect on dynamic content + var style = document.createElement("style"); + style.textContent = selectedSelector + " { display: none !important; }"; + document.head.appendChild(style); + + showStatus("Blocked! Rule will apply on next page load.", false); + highlight.style.display = "none"; + blockBtn.textContent = "Block element"; + blockBtn.disabled = false; + buttonsEl.style.display = "none"; + + // Reset after a short delay + setTimeout(resetPicker, 2000); + }) + .catch(function(err) { + showStatus("Failed to save: " + err.message, true); + blockBtn.textContent = "Block element"; + blockBtn.disabled = false; + }); +}); + +cancelBtn.addEventListener("click", resetPicker); +closeBtn.addEventListener("click", closePicker); + +// --- Start picking --- + +document.addEventListener("mousemove", onMouseMove, true); +document.addEventListener("mousedown", onMouseDown, true); +document.addEventListener("keydown", onKeyDown, true);