Files

205 lines
5.7 KiB
JavaScript

import { COLORS, EMPTY_VALUE } from "./constants.js";
import {
createEditor,
escapeHtml,
findWidget,
hideWidget,
hideWidgetInput,
hookWidget,
registerNode,
restoreCaretPosition,
saveCaretPosition,
waitForWidget,
} from "./utils.js";
// --- Constants ---
const NODE_NAME = "PromptMultiPicker";
// Alternating colors for consecutive selected lines
const SELECTED_COLORS = [COLORS.cyan, COLORS.blue];
const EMPTY_LINE_COLOR = COLORS.inactive;
// --- Helpers ---
const getNonEmptyLines = (text) =>
text
.split("\n")
.map((l) => l.trim())
.filter((l) => l && l !== EMPTY_VALUE);
const parseSelected = (raw) => {
try {
const parsed = JSON.parse(raw || "[]");
return Array.isArray(parsed) ? parsed : [];
} catch {
return [];
}
};
const serializeSelected = (selected) => JSON.stringify(selected);
// --- Highlight ---
const buildHighlightedHtml = (raw, selectedSet) => {
const lines = raw.split("\n");
let selectedColorIndex = 0;
return lines
.map((line) => {
const trimmed = line.trim();
// Empty or separator line
if (!trimmed || trimmed === EMPTY_VALUE) {
return `<span style="color:${EMPTY_LINE_COLOR}"></span>`;
}
if (selectedSet.has(trimmed)) {
const color =
SELECTED_COLORS[
selectedColorIndex % SELECTED_COLORS.length
];
selectedColorIndex++;
return `<span style="color:${color}">${escapeHtml(line)}</span>`;
}
selectedColorIndex = 0; // Reset alternation on unselected line
return `<span style="color:${COLORS.text}">${escapeHtml(line)}</span>`;
})
.join("\n");
};
// --- Line detection ---
const getLineAtY = (editor, clientY) => {
const range = document.caretRangeFromPoint?.(
editor.getBoundingClientRect().left + 4,
clientY,
);
if (!range) return null;
const preRange = document.createRange();
preRange.selectNodeContents(editor);
preRange.setEnd(range.startContainer, range.startOffset);
const offset = preRange.toString().length;
const text = editor.innerText;
const lines = text.split("\n");
let charCount = 0;
for (const line of lines) {
charCount += line.length + 1;
if (charCount > offset) return line.trim();
}
return null;
};
// --- Attach ---
const attachMultiPicker = (node) => {
const optionsWidget = findWidget(node, "options");
const selectedWidget = findWidget(node, "selected");
if (!optionsWidget || !selectedWidget) return;
hideWidget(selectedWidget, true);
hideWidgetInput(node, selectedWidget);
const textarea = optionsWidget.inputEl || optionsWidget.element;
if (!textarea?.parentNode) return;
let clearBtn = null;
// State
let selectedSet = new Set(parseSelected(selectedWidget.value));
const syncSelected = () => {
// Preserve appearance order
const lines = getNonEmptyLines(textarea.value);
const ordered = lines.filter((l) => selectedSet.has(l));
selectedWidget.value = serializeSelected(ordered);
if (clearBtn) {
clearBtn.name =
selectedSet.size > 0
? `✕ Clear All (${selectedSet.size})`
: "✕ Clear All";
}
if (node.graph) node.graph.setDirtyCanvas(true, true);
};
const renderColored = () => {
const pos = saveCaretPosition(editor);
editor.innerHTML = buildHighlightedHtml(textarea.value, selectedSet);
restoreCaretPosition(editor, pos);
};
// Clear All button
clearBtn = node.addWidget("button", "✕ Clear All", null, () => {
selectedSet.clear();
syncSelected();
renderColored();
if (node.graph) node.graph.setDirtyCanvas(true, true);
});
// Move Clear All after selected widget
const selectedIdx = node.widgets.indexOf(selectedWidget);
const currentIdx = node.widgets.indexOf(clearBtn);
if (currentIdx !== -1) {
node.widgets.splice(currentIdx, 1);
}
node.widgets.splice(selectedIdx + 1, 0, clearBtn);
// Create editor
const editor = createEditor(textarea, {
normalize: true,
onInput: () => {
// Remove selected values that no longer exist
const lines = new Set(getNonEmptyLines(textarea.value));
for (const v of selectedSet) {
if (!lines.has(v)) selectedSet.delete(v);
}
syncSelected();
renderColored();
},
});
// --- Event listener - MouseDown: Click to toggle line ---
editor.addEventListener("mousedown", (e) => {
if (!textarea.value.trim()) return;
requestAnimationFrame(() => {
const line = getLineAtY(editor, e.clientY);
if (!line || line === EMPTY_VALUE) return;
const lines = getNonEmptyLines(textarea.value);
if (!lines.includes(line)) return;
if (selectedSet.has(line)) {
selectedSet.delete(line);
} else {
selectedSet.add(line);
}
syncSelected();
renderColored();
if (node.graph) node.graph.setDirtyCanvas(true, true);
});
});
// React to external changes
hookWidget(selectedWidget, (raw) => {
selectedSet = new Set(parseSelected(raw));
renderColored();
});
// Initial render
selectedSet = new Set(parseSelected(selectedWidget.value));
renderColored();
syncSelected();
};
// --- Registration ---
registerNode(NODE_NAME, (node) =>
waitForWidget(node, "options", attachMultiPicker),
);