diff --git a/web/js/booruTagApi.js b/web/js/booruTagApi.js new file mode 100644 index 0000000..4bd2eb2 --- /dev/null +++ b/web/js/booruTagApi.js @@ -0,0 +1,73 @@ +export class BooruApi { + constructor() { + this.baseUrl = "https://danbooru.donmai.us"; + this.wikiUrl = `${this.baseUrl}/wiki_pages`; + this.parser = new DOMParser(); + + this.fetchCache = {}; + } + + cleanTag(tag) { + return tag.replaceAll(" ", "_").trim(); + } + + async #fetch(url, options = {}) { + if (this.fetchCache[url]) { + return this.fetchCache[url]; + } + + const { responseType = "json" } = options; + try { + const response = await fetch(url); + if (!response.ok) { + return { + success: false, + error: { + status: response.status, + message: `HTTP error! status: ${response.status}`, + url: url, + }, + }; + } + + let data; + if (responseType === "html") { + data = await response.text(); + } else { + data = await response.json(); + } + + this.fetchCache[url] = { + success: true, + data: data, + }; + return this.fetchCache[url]; + } catch (error) { + console.error(`Failed to fetch ${url}:`, error); + return { + success: false, + error: { + message: error.message, + url: url, + original: error, + }, + }; + } + } + + getElementById(html, id) { + const doc = this.parser.parseFromString(html, "text/html"); + const element = doc.querySelector(`#${id}`); + return element; + } + + async getTagDescription(tag) { + const url = `${this.wikiUrl}/${this.cleanTag(tag)}`; + const response = await this.#fetch(url, { responseType: "html" }); + if (!response.success) { + return null; + } + const body = this.getElementById(response.data, "wiki-page-body"); + return body.querySelector("p").textContent; + } +} diff --git a/web/js/textHighlight.js b/web/js/textHighlight.js index 30265a0..c00fb1c 100644 --- a/web/js/textHighlight.js +++ b/web/js/textHighlight.js @@ -1,5 +1,8 @@ import { settingsHelper, API_PREFIX } from "./settings.js"; import { hexToRgb } from "./util.js"; +import { BooruApi } from "./booruTagApi.js"; + +const booruApi = new BooruApi(); // Track all enhanced textareas const enhancedTextareas = new WeakSet(); @@ -44,7 +47,49 @@ function enhanceTextarea(textarea) { overlayEl.className = "input-overlay"; textarea.parentNode.insertBefore(overlayEl, textarea); + let currentTooltipTag = null; + let tooltipTimeout = null; textarea.style.background = "transparent"; + textarea.addEventListener("mousemove", (e) => { + // Clear existing timeout + if (tooltipTimeout) { + clearTimeout(tooltipTimeout); + } + + tooltipTimeout = setTimeout(() => { + // Temporarily hide overlay to get element below + textarea.style.pointerEvents = "none"; + const elementBelow = document.elementFromPoint( + e.clientX, + e.clientY + ); + textarea.style.pointerEvents = "auto"; + + // Check if hovering over tag span in overlay + if (elementBelow && elementBelow.closest(".tag-span")) { + const tagSpan = elementBelow.closest(".tag-span"); + const tag = tagSpan.dataset.tag; + + // Only update if different tag + if (currentTooltipTag !== tag) { + hideTagTooltip(); + showTagTooltip(tagSpan, tag); + currentTooltipTag = tag; + } + } else if (currentTooltipTag) { + hideTagTooltip(); + currentTooltipTag = null; + } + }, 50); // 50ms debounce + }); + + textarea.addEventListener("mouseout", () => { + if (tooltipTimeout) { + clearTimeout(tooltipTimeout); + } + hideTagTooltip(); + currentTooltipTag = null; + }); // Setup the textarea and overlay setOverlayPosition(textarea, overlayEl); @@ -499,9 +544,76 @@ async function syncText(inputEl, overlayEl, tries = 1) { } }); + highlightedText = highlightedText + .split(",") + .map((tag) => { + if (!tag.trim()) return tag; + return `${tag}`; + }) + .join(","); + overlayEl.innerHTML = highlightedText; } +async function showTagTooltip(element, tag) { + let tooltip = document.getElementById("tag-tooltip"); + const description = + (await booruApi.getTagDescription(tag)) ?? "No description available."; + + if (!tooltip) { + tooltip = document.createElement("div"); + tooltip.id = "tag-tooltip"; + document.body.appendChild(tooltip); + } + + tooltip.style.cssText = ` + position: fixed; + background: var(--comfy-menu-bg); + color: var(--comfy-menu-text); + padding: 12px; + border-radius: 4px; + font-size: 14px; + z-index: 1000; + pointer-events: none; + max-width: 300px; + box-shadow: 0 2px 8px rgba(0,0,0,0.15); + `; + + const rect = element.getBoundingClientRect(); + tooltip.style.left = `${rect.left}px`; + tooltip.style.top = `${rect.bottom + 5}px`; + + // Create tooltip content with title and description + const titleElement = document.createElement("div"); + titleElement.style.cssText = ` + font-weight: bold; + margin-bottom: ${description ? "8px" : "0"}; + color: var(--comfy-menu-text); + `; + titleElement.textContent = tag; + + tooltip.innerHTML = ""; + tooltip.appendChild(titleElement); + + const descElement = document.createElement("div"); + descElement.style.cssText = ` + font-size: 13px; + line-height: 1.4; + color: var(--comfy-menu-text); + opacity: 0.9; + `; + descElement.textContent = description; + tooltip.appendChild(descElement); + tooltip.style.display = "block"; +} + +function hideTagTooltip() { + const tooltips = document.querySelectorAll("#tag-tooltip"); + tooltips.forEach((tooltip) => { + tooltip.remove(); + }); +} + function setOverlayPosition(inputEl, overlayEl) { const textareaStyle = window.getComputedStyle(inputEl); overlayEl.style.left = textareaStyle.left; @@ -527,7 +639,7 @@ function setOverlayStyle(inputEl, overlayEl) { overlayEl.style.padding = textareaStyle.padding; overlayEl.style.boxSizing = textareaStyle.boxSizing; overlayEl.style.zIndex = "1"; - overlayEl.style.pointerEvents = "none"; + overlayEl.style.pointerEvents = "auto"; overlayEl.style.color = "transparent"; overlayEl.style.overflowX = textareaStyle.overflowX; overlayEl.style.overflowY = textareaStyle.overflowY;