import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; app.registerExtension({ name: "RaykoStylesLoader", async beforeRegisterNodeDef(nodeType, nodeData, app) { if (nodeData.name === "Rayko_Styles_CSV_Loader") { console.log("[Rayko] RS Styles Loader JS loaded"); const onNodeCreated = nodeType.prototype.onNodeCreated; nodeType.prototype.onNodeCreated = function() { const result = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; this.data = { active_csv_file: "", styles: [], csv_list: [] }; this.rowHeight = 28; this.padding = 10; this.labelWidth = 110; this.targetWidth = 340; this.clickZones = []; this.menuPosition = null; this.menuScrollPosition = 0; this.hiddenWidget = this.widgets.find(w => w.name === "node_data"); if (this.hiddenWidget) { this.hiddenWidget.hidden = true; this.hiddenWidget.tooltip = ""; // ← Полностью отключаем нативное поведение виджета this.hiddenWidget.type = "hidden"; if (this.hiddenWidget.element) { this.hiddenWidget.element.style.display = "none"; this.hiddenWidget.element.style.pointerEvents = "none"; } this.hiddenWidget.serializeValue = () => { this.syncData(); return this.hiddenWidget.value; }; try { const saved = JSON.parse(this.hiddenWidget.value); if (saved && typeof saved === 'object') { this.data = { ...this.data, ...saved }; } } catch (e) { console.error("[Rayko] Error loading saved data", e); } } if (this.widgets) { this.widgets.forEach(w => w.hidden = true); } this.setSize([this.targetWidth, 300]); const self = this; this.loadCSVList = async function() { try { const response = await api.fetchApi("/rayko_get_csv_files"); if (response.ok) { const files = await response.json(); self.data.csv_list = files.length > 0 ? files : ["No CSV files"]; if (!self.data.active_csv_file || !self.data.csv_list.includes(self.data.active_csv_file)) { self.data.active_csv_file = self.data.csv_list[0]; } self.syncData(); self.graph.setDirtyCanvas(true, true); } } catch (e) { console.error("[Rayko] Error loading CSV list:", e); self.data.csv_list = ["No CSV files"]; } }; this.drawSeparator = function(ctx, text, x, y, w, h) { ctx.fillStyle = "#444"; ctx.fillRect(x, y + 8, w, 1); ctx.fillStyle = "#888"; ctx.font = "10px sans-serif"; ctx.textAlign = "center"; ctx.fillText(text, x + w/2, y + 22); }; this.drawLabel = function(ctx, text, x, y, w, h) { ctx.fillStyle = "#aaa"; ctx.font = "11px sans-serif"; ctx.textAlign = "left"; ctx.fillText(text, x, y + h/2 + 4); }; this.drawComboField = function(ctx, value, x, y, w, h) { ctx.fillStyle = "#222"; ctx.fillRect(x, y, w, h); ctx.strokeStyle = "#444"; ctx.strokeRect(x, y, w, h); ctx.fillStyle = "#fff"; ctx.font = "11px sans-serif"; ctx.textAlign = "center"; let displayValue = value || "No CSV files"; if (displayValue.length > 25) { displayValue = displayValue.substring(0, 22) + "..."; } ctx.fillText(displayValue, x + w/2, y + h/2 + 4); ctx.fillStyle = "#666"; ctx.beginPath(); ctx.moveTo(x + w - 12, y + h/2 - 3); ctx.lineTo(x + w - 6, y + h/2 - 3); ctx.lineTo(x + w - 9, y + h/2 + 3); ctx.fill(); }; this.drawButton = function(ctx, text, x, y, w, h, iconColor = "#fff") { ctx.fillStyle = "#242427"; ctx.fillRect(x, y, w, h); ctx.strokeStyle = "#3a3a3c"; ctx.strokeRect(x, y, w, h); ctx.font = "bold 11px sans-serif"; ctx.textAlign = "center"; ctx.textBaseline = "middle"; const parts = text.split(" "); const icon = parts[0]; const label = parts.slice(1).join(" "); ctx.fillStyle = iconColor; ctx.fillText(icon, x + w/2 - (label.length * 3.5), y + h/2); ctx.fillStyle = "#fff"; ctx.fillText(label, x + w/2 + 8, y + h/2); }; this.onDrawForeground = function(ctx, visibleRect) { this.clickZones = []; const startY = 80; const rowH = this.rowHeight; const pad = 10; let y = startY; this.drawLabel(ctx, "SELECT CSV FILE", pad, y, this.labelWidth, rowH); this.drawComboField(ctx, this.data.active_csv_file, pad + this.labelWidth, y, this.size[0] - pad*2 - this.labelWidth, rowH); this.clickZones.push({ type: "combo", field: "active_csv_file", x: pad + this.labelWidth, y: y, w: this.size[0] - pad*2 - this.labelWidth, h: rowH }); y += rowH + 10; this.drawButton(ctx, "📂 UPLOAD NEW CSV FILE", pad, y, this.size[0] - pad*2, rowH, "#FF9800"); this.clickZones.push({ type: "upload", x: pad, y: y, w: this.size[0] - pad*2, h: rowH }); y += rowH + 10; this.drawButton(ctx, "➕ ADD STYLE", pad, y, this.size[0] - pad*2, rowH, "#9C27B0"); this.clickZones.push({ type: "add_style", x: pad, y: y, w: this.size[0] - pad*2, h: rowH }); y += rowH + 15; this.drawSeparator(ctx, "ACTIVE STYLES", pad, y, this.size[0] - pad*2, rowH); y += rowH + 10; const rightPanelWidth = 40; const toggleStartX = 20; const toggleRadius = 7; const nameStartX = 42; for (let i = 0; i < this.data.styles.length; i++) { const row = this.data.styles[i]; const styleY = y + (i * rowH); const h = rowH - 2; const centerY = styleY + h/2; ctx.fillStyle = i % 2 === 0 ? "rgba(0,0,0,0.3)" : "rgba(0,0,0,0.15)"; ctx.fillRect(pad, styleY, this.size[0] - (pad * 2), h); ctx.fillStyle = row.enabled ? "#4CAF50" : "#555"; ctx.beginPath(); ctx.arc(toggleStartX, centerY, toggleRadius, 0, Math.PI * 2); ctx.fill(); this.clickZones.push({ type: "toggle", index: i, x: toggleStartX - toggleRadius - 5, y: styleY, w: toggleRadius * 2 + 10, h: h }); const nameW = this.size[0] - nameStartX - rightPanelWidth - pad - 10; ctx.fillStyle = row.enabled ? "#fff" : "#777"; ctx.font = "12px sans-serif"; ctx.textAlign = "left"; ctx.textBaseline = "middle"; let displayName = row.display_name || row.name; if (displayName.includes("|")) { const parts = displayName.split("|"); displayName = parts[parts.length - 1].trim(); } if (ctx.measureText(displayName).width > nameW) { while (ctx.measureText(displayName + "...").width > nameW && displayName.length > 0) { displayName = displayName.slice(0, -1); } displayName = displayName + "..."; } ctx.fillText(displayName, nameStartX, centerY); this.clickZones.push({ type: "name", index: i, x: nameStartX, y: styleY, w: nameW, h: h }); const delX = this.size[0] - rightPanelWidth; ctx.fillStyle = "#f44336"; ctx.fillText("❌", delX, centerY); this.clickZones.push({ type: "delete", index: i, x: delX, y: styleY, w: 30, h: h }); } const totalH = y + (this.data.styles.length * rowH) + 20; if (this.size[1] < totalH) { this.setSize([this.targetWidth, totalH]); } }; this.onMouseDown = function(e, pos, canvas) { if (!this.clickZones.length) return false; for (const zone of this.clickZones) { const inX = pos[0] >= zone.x && pos[0] <= zone.x + zone.w; const inY = pos[1] >= zone.y && pos[1] <= zone.y + zone.h; if (inX && inY) { if (zone.type === "combo" && zone.field === "active_csv_file") { self.showCSVSelector(e); return true; } if (zone.type === "upload") { self.uploadCSVFile(); return true; } if (zone.type === "add_style") { self.showStyleSelector(e); return true; } if (zone.type === "toggle") { self.data.styles[zone.index].enabled = !self.data.styles[zone.index].enabled; self.syncData(); self.graph.setDirtyCanvas(true, true); self.graph.changeTracker?.dispatchEvent(new Event("change")); return true; } if (zone.type === "delete") { self.data.styles.splice(zone.index, 1); self.syncData(); self.updateUI(); return true; } } } return false; }; this.showCSVSelector = function(clickEvent) { const list = self.data.csv_list || []; if (!list.length) return; const menu = document.createElement("div"); menu.style.cssText = `position:fixed;background:#1a1a1a;border:1px solid #444;border-radius:6px;max-height:300px;overflow-y:auto;z-index:10001;box-shadow:0 4px 20px rgba(0,0,0,0.5);min-width:200px;`; list.forEach(opt => { const item = document.createElement("div"); item.textContent = opt; item.style.cssText = `padding:10px 15px;cursor:pointer;color:#ddd;font-size:12px;border-bottom:1px solid #333;background:${opt === self.data.active_csv_file ? '#333' : '#1a1a1a'};`; item.onmouseover = () => item.style.background = "#444"; item.onmouseout = () => item.style.background = opt === self.data.active_csv_file ? '#333' : "#1a1a1a"; item.onclick = (ev) => { ev.stopPropagation(); self.data.active_csv_file = opt; self.syncData(); self.updateUI(); menu.remove(); }; menu.appendChild(item); }); if (clickEvent) { menu.style.left = (clickEvent.clientX + 8) + "px"; menu.style.top = clickEvent.clientY + "px"; } document.body.appendChild(menu); setTimeout(() => { const closeHandler = (ev) => { if (!menu.contains(ev.target)) { menu.remove(); document.removeEventListener("mousedown", closeHandler); } }; document.addEventListener("mousedown", closeHandler); }, 100); }; this.uploadCSVFile = async function() { const fileInput = document.createElement("input"); fileInput.type = "file"; fileInput.accept = ".csv"; fileInput.style.display = "none"; fileInput.addEventListener("change", async (e) => { const file = e.target.files?.[0]; if (file) { try { const formData = new FormData(); formData.append("file", file); const response = await api.fetchApi("/rayko_upload_csv_file", { method: "POST", body: formData }); if (response.ok) { const data = await response.json(); await self.loadCSVList(); self.data.active_csv_file = data.filename; self.syncData(); self.graph.setDirtyCanvas(true, true); self.graph.changeTracker?.dispatchEvent(new Event("change")); } else { const errorText = await response.text(); alert("Failed: " + errorText); } } catch (error) { alert("Error: " + error.message); } } if (fileInput.parentNode) fileInput.parentNode.removeChild(fileInput); }); document.body.appendChild(fileInput); fileInput.click(); }; this.showStyleSelector = async function(clickEvent) { if (!self.data.active_csv_file || self.data.active_csv_file === "No CSV files") { alert("⚠️ Upload CSV first!"); return; } try { const response = await api.fetchApi("/rayko_get_styles_from_file", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ filename: self.data.active_csv_file }) }); if (response.ok) { const data = await response.json(); const allStyles = data.styles || []; if (allStyles.length === 0) { alert("⚠️ No styles in CSV!"); return; } const tree = { _folders: [], _styles: [] }; let currentFolder = null; for (const style of allStyles) { if (!style) continue; if (style.startsWith("|||")) { const folderName = style.replace("|||", "").trim(); currentFolder = folderName; if (!tree[folderName]) { tree[folderName] = { _folders: [], _styles: [] }; tree._folders.push(folderName); } } else if (currentFolder && style.includes("|")) { const styleFullName = style.trim(); const parts = styleFullName.split("|"); const styleDisplayName = parts[parts.length - 1].trim(); if (tree[currentFolder]) { tree[currentFolder]._styles.push({ fullName: styleFullName, displayName: styleDisplayName }); } } else { tree._styles.push({ fullName: style.trim(), displayName: style.trim() }); } } self.menuPosition = { left: (clickEvent.clientX + 8) + "px", top: clickEvent.clientY + "px" }; self.menuScrollPosition = 0; self.showStyleTreeMenu(tree, {}, self.menuPosition); } } catch (e) { alert("Error: " + e.message); } }; this.showStyleTreeMenu = function(tree, expandedFolders, fixedPosition) { const self = this; const currentScroll = self.menuScrollPosition || 0; const menu = document.createElement("div"); menu.style.cssText = `position:fixed;background:#1a1a1a;border:1px solid #444;border-radius:6px;max-height:400px;overflow-y:auto;z-index:10001;box-shadow:0 4px 20px rgba(0,0,0,0.5);min-width:250px;`; menu.id = "rayko-style-menu"; const header = document.createElement("div"); header.textContent = `📁 ${self.data.active_csv_file}`; header.style.cssText = `padding:10px 12px;color:#fff;font-weight:bold;border-bottom:1px solid #333;background:#252525;position:sticky;top:0;`; menu.appendChild(header); const contentContainer = document.createElement("div"); contentContainer.id = "rayko-style-content"; menu.appendChild(contentContainer); function renderContent() { contentContainer.innerHTML = ""; if (tree._styles && tree._styles.length > 0) { for (const styleObj of tree._styles) { const item = document.createElement("div"); item.textContent = "🎨 " + styleObj.displayName; item.style.cssText = `padding:10px 12px;cursor:pointer;color:#ddd;border-bottom:1px solid #333;font-size:13px;`; item.style.backgroundColor = "#1a1a1a"; item.onmouseover = () => item.style.backgroundColor = "#333"; item.onmouseout = () => item.style.backgroundColor = "#1a1a1a"; item.onclick = (ev) => { ev.stopPropagation(); self.addStyleRow(styleObj.fullName, styleObj.displayName); menu.remove(); }; contentContainer.appendChild(item); } const separator = document.createElement("div"); separator.style.cssText = `height:1px;background:#333;margin:5px 0;`; contentContainer.appendChild(separator); } if (tree._folders && tree._folders.length > 0) { for (const folderName of tree._folders) { renderFolderItem(folderName, tree[folderName], contentContainer, 0); } } } function renderFolderItem(folderName, folderData, container, level) { const folderPath = folderName; const isExpanded = expandedFolders[folderPath] || false; const folderHeader = document.createElement("div"); folderHeader.style.cssText = `padding:10px 12px;cursor:pointer;color:#ffd700;font-weight:bold;border-bottom:1px solid #333;background:${level === 0 ? '#252525' : '#1f1f1f'};font-size:13px;padding-left:${12 + level * 16}px;`; folderHeader.innerHTML = `${isExpanded ? "▼" : "▶"} 📁 ${folderName}`; folderHeader.onclick = (ev) => { ev.stopPropagation(); const savedScroll = menu.scrollTop; expandedFolders[folderPath] = !isExpanded; renderContent(); setTimeout(() => { menu.scrollTop = savedScroll; }, 0); }; container.appendChild(folderHeader); if (isExpanded) { if (folderData._styles && folderData._styles.length > 0) { for (const styleObj of folderData._styles) { const styleItem = document.createElement("div"); styleItem.textContent = "🎨 " + styleObj.displayName; styleItem.style.cssText = `padding:10px 12px;cursor:pointer;color:#ddd;border-bottom:1px solid #333;font-size:13px;padding-left:${12 + (level + 1) * 16}px;`; styleItem.style.backgroundColor = "#1a1a1a"; styleItem.onmouseover = () => styleItem.style.backgroundColor = "#333"; styleItem.onmouseout = () => styleItem.style.backgroundColor = "#1a1a1a"; styleItem.onclick = (ev) => { ev.stopPropagation(); self.addStyleRow(styleObj.fullName, styleObj.displayName); menu.remove(); }; container.appendChild(styleItem); } } if (folderData._folders && folderData._folders.length > 0) { for (const subFolderName of folderData._folders) { renderFolderItem(subFolderName, folderData[subFolderName], container, level + 1); } } } } renderContent(); if (fixedPosition) { menu.style.left = fixedPosition.left; menu.style.top = fixedPosition.top; } setTimeout(() => { const rect = menu.getBoundingClientRect(); if (rect.right > window.innerWidth) { menu.style.left = (window.innerWidth - rect.width - 10) + "px"; } if (rect.bottom > window.innerHeight) { menu.style.top = (window.innerHeight - rect.height - 10) + "px"; } if (currentScroll > 0) { menu.scrollTop = currentScroll; } }, 10); document.body.appendChild(menu); menu.addEventListener("scroll", () => { self.menuScrollPosition = menu.scrollTop; }); setTimeout(() => { const closeHandler = (ev) => { if (!menu.contains(ev.target)) { self.menuPosition = null; self.menuScrollPosition = 0; menu.remove(); document.removeEventListener("mousedown", closeHandler); } }; document.addEventListener("mousedown", closeHandler); }, 100); }; this.addStyleRow = function(styleFullName, styleDisplayName) { const exists = self.data.styles.some(row => row.name === styleFullName && row.file === self.data.active_csv_file); if (exists) { alert("⚠️ Already added!"); return; } self.data.styles.push({ name: styleFullName, display_name: styleDisplayName, file: self.data.active_csv_file, enabled: true }); self.syncData(); self.updateUI(); self.menuPosition = null; self.menuScrollPosition = 0; }; this.syncData = function() { if (this.hiddenWidget) { this.hiddenWidget.value = JSON.stringify(self.data); } }; this.updateUI = function() { self.syncData(); if (self.graph) { self.graph.setDirtyCanvas(true, true); self.graph.changeTracker?.dispatchEvent(new Event("change")); } }; const onSerialize = this.onSerialize; this.onSerialize = function(o) { self.syncData(); return onSerialize ? onSerialize.apply(this, arguments) : undefined; }; const onExecute = this.onExecute; this.onExecute = function() { self.syncData(); return onExecute ? onExecute.apply(this, arguments) : undefined; }; this.loadCSVList(); return result; }; } } });