Excludes textareas outside .dom-widget (e.g. settings/form inputs) and markdown widgets, instead of blocklisting specific containers.
205 lines
6.5 KiB
JavaScript
205 lines
6.5 KiB
JavaScript
import { html } from "../highlighting/html.js";
|
|
import { SyntaxTokenizer } from "../highlighting/tokenizer.js";
|
|
import { SyntaxHighlighter } from "../highlighting/highlighter.js";
|
|
import { config } from "./state.js";
|
|
import {
|
|
originalTextareaBackgroundColors,
|
|
refreshAuthoredBackgroundColor,
|
|
setOverlayPosition,
|
|
setOverlayStyle,
|
|
} from "./styleUtils.js";
|
|
import { addTooltips } from "./tooltipManager.js";
|
|
import { api } from "../../../../scripts/api.js";
|
|
|
|
const enhancedTextareas = new WeakSet();
|
|
|
|
export function syncText(inputEl, overlayEl, tries = 1) {
|
|
const text = inputEl.value;
|
|
overlayEl.textContent = text;
|
|
|
|
const { colors, errorColor, wildcardColor, highlightType } = config;
|
|
const loraColor = colors ? colors[0] : undefined;
|
|
|
|
if (
|
|
!colors ||
|
|
!errorColor ||
|
|
!loraColor ||
|
|
!wildcardColor ||
|
|
highlightType === undefined
|
|
) {
|
|
if (tries < 5) {
|
|
setTimeout(() => syncText(inputEl, overlayEl, tries++), tries * 5);
|
|
}
|
|
return;
|
|
}
|
|
|
|
const tokenizer = new SyntaxTokenizer();
|
|
const highlighter = new SyntaxHighlighter();
|
|
|
|
const tokens = tokenizer.tokenize(text);
|
|
let highlightedText = highlighter.processTokens(tokens);
|
|
|
|
const processNode = (node) => {
|
|
if (node.nodeType === Node.TEXT_NODE) {
|
|
return node.textContent
|
|
.split(",")
|
|
.map((tag) => {
|
|
if (!tag.trim()) return tag;
|
|
return html("span", tag, {
|
|
className: "tag-span",
|
|
style: { height: "fit-content" },
|
|
attributes: { "data-tag": tag.trim() },
|
|
});
|
|
})
|
|
.join(",");
|
|
} else if (node.nodeType === Node.ELEMENT_NODE) {
|
|
const textContent = node.textContent;
|
|
if (textContent.includes(",")) {
|
|
const tags = textContent.split(",");
|
|
let result = node.outerHTML;
|
|
tags.forEach((tag, index) => {
|
|
if (tag.trim()) {
|
|
const tagRegex = new RegExp(
|
|
`(${tag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})`,
|
|
"g",
|
|
);
|
|
result = result.replace(
|
|
tagRegex,
|
|
html("span", "$1", {
|
|
className: "tag-span",
|
|
style: { height: "fit-content" },
|
|
attributes: { "data-tag": tag.trim() },
|
|
escape: false,
|
|
}),
|
|
);
|
|
}
|
|
});
|
|
return result;
|
|
} else {
|
|
const clone = node.cloneNode(true);
|
|
const trimmedText = textContent.trim();
|
|
if (trimmedText) {
|
|
return html("span", clone.outerHTML, {
|
|
className: "tag-span",
|
|
style: { height: "fit-content" },
|
|
attributes: { "data-tag": trimmedText },
|
|
escape: false,
|
|
});
|
|
}
|
|
return clone.outerHTML;
|
|
}
|
|
}
|
|
return "";
|
|
};
|
|
|
|
const tempDiv = document.createElement("div");
|
|
tempDiv.innerHTML = highlightedText;
|
|
let result = Array.from(tempDiv.childNodes)
|
|
.map((node) => processNode(node))
|
|
.join("");
|
|
overlayEl.innerHTML = result;
|
|
}
|
|
|
|
function setTextColors(inputEl, overlayEl) {
|
|
syncText(inputEl, overlayEl);
|
|
}
|
|
|
|
export function enhanceTextarea(textarea) {
|
|
if (
|
|
enhancedTextareas.has(textarea) ||
|
|
!textarea.closest(".dom-widget") ||
|
|
textarea.closest(".widget-markdown")
|
|
) {
|
|
return;
|
|
}
|
|
enhancedTextareas.add(textarea);
|
|
|
|
const overlayEl = document.createElement("div");
|
|
overlayEl.className = "input-overlay";
|
|
textarea.parentNode.insertBefore(overlayEl, textarea);
|
|
|
|
originalTextareaBackgroundColors.set(textarea, {
|
|
value: textarea.style.getPropertyValue("background-color"),
|
|
backgroundColorPriority:
|
|
textarea.style.getPropertyPriority("background-color"),
|
|
});
|
|
refreshAuthoredBackgroundColor(textarea);
|
|
|
|
textarea.style.backgroundColor = "transparent";
|
|
textarea.style.position = "relative";
|
|
textarea.style.zIndex = "1";
|
|
|
|
setOverlayPosition(textarea, overlayEl);
|
|
setOverlayStyle(textarea, overlayEl);
|
|
setTextColors(textarea, overlayEl);
|
|
addTooltips(textarea);
|
|
|
|
textarea.addEventListener("scroll", () => {
|
|
overlayEl.scrollTop = textarea.scrollTop;
|
|
overlayEl.scrollLeft = textarea.scrollLeft;
|
|
});
|
|
|
|
textarea.addEventListener("input", () => {
|
|
syncText(textarea, overlayEl);
|
|
setOverlayStyle(textarea, overlayEl);
|
|
});
|
|
|
|
textarea.addEventListener("paste", () => {
|
|
setTimeout(() => {
|
|
try {
|
|
if (overlayEl && document.contains(overlayEl)) {
|
|
syncText(textarea, overlayEl);
|
|
setOverlayStyle(textarea, overlayEl);
|
|
} else {
|
|
console.error(
|
|
"Overlay element not found during paste operation",
|
|
);
|
|
}
|
|
} catch (err) {
|
|
console.error("Error handling paste event:", err);
|
|
}
|
|
}, 10);
|
|
});
|
|
|
|
api.addEventListener("update_text_highlight", (event) => {
|
|
setTextColors(textarea, overlayEl);
|
|
setOverlayStyle(textarea, overlayEl);
|
|
syncText(textarea, overlayEl);
|
|
});
|
|
|
|
textarea.addEventListener("keydown", (event) => {
|
|
if (
|
|
event.ctrlKey &&
|
|
(event.key === "ArrowUp" || event.key === "ArrowDown")
|
|
) {
|
|
setTimeout(() => {
|
|
syncText(textarea, overlayEl);
|
|
}, 10);
|
|
}
|
|
});
|
|
|
|
const observer = new MutationObserver(() => {
|
|
refreshAuthoredBackgroundColor(textarea);
|
|
setOverlayPosition(textarea, overlayEl);
|
|
setOverlayStyle(textarea, overlayEl);
|
|
});
|
|
|
|
observer.observe(textarea, {
|
|
attributes: true,
|
|
attributeFilter: ["style"],
|
|
childList: true,
|
|
subtree: true,
|
|
characterData: true,
|
|
});
|
|
|
|
const parentObserver = new MutationObserver(() => {
|
|
if (!document.contains(textarea)) {
|
|
overlayEl.remove();
|
|
}
|
|
});
|
|
|
|
parentObserver.observe(textarea.parentNode, {
|
|
childList: true,
|
|
});
|
|
}
|