Files
JasonHoku-ComfyUI-Ultimate-…/web/smart_json_text.js
T

240 lines
12 KiB
JavaScript

import { app } from "../../scripts/app.js";
// Register the custom widget for JSON text input with validation
app.registerExtension({
name: "SmartJSONText.Widget",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SmartJSONText") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const result = onNodeCreated?.apply(this, arguments);
const jsonWidget = this.widgets?.find(w => w.name === "json_text");
if (jsonWidget) {
const originalCallback = jsonWidget.callback;
// Validation function
const validateJSON = (value) => {
try {
JSON.parse(value);
return { valid: true, error: null, line: null, column: null };
} catch (e) {
let errorMsg = e.message;
let line = null;
let column = null;
// Extract position from error message - this is most reliable
const positionMatch = errorMsg.match(/position (\d+)/i);
if (positionMatch) {
const pos = parseInt(positionMatch[1]);
const upToError = value.substring(0, pos);
const lines = upToError.split('\n');
line = lines.length;
column = lines[lines.length - 1].length + 1;
} else {
// Try line/column directly from message
const lineMatch = errorMsg.match(/line (\d+)/i);
const columnMatch = errorMsg.match(/column (\d+)/i);
if (lineMatch) {
line = parseInt(lineMatch[1]);
}
if (columnMatch) {
column = parseInt(columnMatch[1]);
}
}
// Last resort: extract the snippet and find it in the text
if (!line) {
const snippetMatch = errorMsg.match(/"([^"]*?)"/);
if (snippetMatch) {
let snippet = snippetMatch[1];
// The snippet shows the start of the text, error is usually right after
// e.g., "[f\n[\n"" means error is at 'f'
// Find first non-whitespace, non-bracket character as likely error
const errorCharMatch = snippet.match(/[^\[\]\s\n]/);
if (errorCharMatch) {
const errorChar = errorCharMatch[0];
const errorOffset = snippet.indexOf(errorChar);
// Count newlines up to this point in snippet
const upToErrorChar = snippet.substring(0, errorOffset + 1);
const newlines = (upToErrorChar.match(/\n/g) || []).length;
line = newlines + 1;
} else {
// Just count newlines in snippet
const newlines = (snippet.match(/\n/g) || []).length;
line = newlines + 1;
}
}
}
console.log(`JSON Error - Line: ${line}, Col: ${column}, Msg: ${errorMsg}`);
return { valid: false, error: errorMsg, line, column };
}
};
jsonWidget.validationResult = { valid: true };
// Function to apply styling to the textarea
const applyTextareaStyle = () => {
if (!jsonWidget.inputEl) return;
const validation = jsonWidget.validationResult;
// Clear all styles first to prevent stacking
jsonWidget.inputEl.style.background = "";
jsonWidget.inputEl.style.backgroundColor = "";
jsonWidget.inputEl.style.backgroundAttachment = "";
jsonWidget.inputEl.style.color = "";
if (!validation.valid) {
jsonWidget.inputEl.style.color = "#ffffff";
// Create line highlighting with linear gradient if we have error line
if (validation.line) {
// Get actual computed line height from textarea
const computedStyle = window.getComputedStyle(jsonWidget.inputEl);
const lineHeightStr = computedStyle.lineHeight;
const paddingTop = parseFloat(computedStyle.paddingTop) || 0;
let lineHeight = 16; // Default fallback
if (lineHeightStr && lineHeightStr !== 'normal') {
lineHeight = parseFloat(lineHeightStr);
}
const errorLine = validation.line - 1; // 0-indexed
// Account for textarea padding
const startPx = paddingTop + (errorLine * lineHeight);
const endPx = startPx + lineHeight;
console.log(`Applying gradient: lineHeight=${lineHeight}px, paddingTop=${paddingTop}px, errorLine=${validation.line}, range=${startPx}px-${endPx}px`);
// Use gradient with bright highlighted line
jsonWidget.inputEl.style.background = `linear-gradient(to bottom, #501414 ${startPx}px, #ff4444 ${startPx}px, #ff4444 ${endPx}px, #501414 ${endPx}px)`;
jsonWidget.inputEl.style.backgroundAttachment = "local";
} else {
// Just solid dark red if no line info
console.log('No line number detected, using solid background');
jsonWidget.inputEl.style.backgroundColor = "#501414";
}
}
};
// Callback with validation
jsonWidget.callback = function (value) {
const validation = validateJSON(value);
jsonWidget.validationResult = validation;
// Update node colors
if (!validation.valid) {
this.boxColor = "#ff4444";
this.color = "#2a0a0a";
} else {
this.boxColor = null;
this.color = null;
}
// Apply textarea styling
applyTextareaStyle();
if (originalCallback) {
originalCallback.call(this, value);
}
app.graph.setDirtyCanvas(true, true);
};
// Monitor for when the textarea element is created
const checkForTextarea = () => {
if (jsonWidget.inputEl && !jsonWidget._styledTextarea) {
jsonWidget._styledTextarea = true;
applyTextareaStyle();
// Re-apply on focus/blur to ensure it sticks
jsonWidget.inputEl.addEventListener('focus', applyTextareaStyle);
jsonWidget.inputEl.addEventListener('blur', applyTextareaStyle);
}
};
// Check periodically until textarea is created
const intervalId = setInterval(() => {
checkForTextarea();
if (jsonWidget._styledTextarea) {
clearInterval(intervalId);
}
}, 100);
// Also check on serialize (when element gets created)
const originalSerializeValue = jsonWidget.serializeValue;
jsonWidget.serializeValue = function () {
checkForTextarea();
return originalSerializeValue ? originalSerializeValue.call(this) : this.value;
};
// Initial validation
jsonWidget.callback(jsonWidget.value);
// Error message display
const originalDrawForeground = this.onDrawForeground;
this.onDrawForeground = function (ctx) {
if (originalDrawForeground) {
originalDrawForeground.call(this, ctx);
}
if (jsonWidget.validationResult && !jsonWidget.validationResult.valid) {
const result = jsonWidget.validationResult;
const x = 10;
const y = this.size[1] + 5;
ctx.save();
ctx.font = "12px monospace";
ctx.fillStyle = "#ff6666";
let errorText = "⚠ JSON Error";
if (result.line) {
errorText += ` (Line ${result.line}`;
if (result.column) errorText += `, Col ${result.column}`;
errorText += ")";
}
ctx.fillText(errorText, x, y);
ctx.font = "10px monospace";
ctx.fillStyle = "#ffaaaa";
const maxWidth = 400;
const words = result.error.split(' ');
let line = '';
let lineY = y + 15;
for (let word of words) {
const testLine = line + word + ' ';
const metrics = ctx.measureText(testLine);
if (metrics.width > maxWidth && line !== '') {
ctx.fillText(line, x, lineY);
line = word + ' ';
lineY += 12;
} else {
line = testLine;
}
}
ctx.fillText(line, x, lineY);
ctx.restore();
}
};
}
return result;
};
}
}
});