document.addEventListener("DOMContentLoaded", function () { // Создание диаграммы (из модального окна на всех страницах) let createDiagramForm = document.getElementById("create-diagram-form"); if (createDiagramForm) { createDiagramForm.onsubmit = async function (e) { e.preventDefault(); let modal = bootstrap.Modal.getOrCreateInstance(document.getElementById("createDiagramModal")); const formData = new FormData(createDiagramForm); const name = formData.get("name"); if (!name.trim()) return; let resp = await fetch("/diagram/create", { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({"name": name}) }); let data = await resp.json(); if (data.id) { modal.hide(); window.location.href = `/diagram/${data.id}`; } else { alert(data.error || "Ошибка"); } }; } // Редактор майндмэп if (window.DIAGRAM_ID && document.getElementById("mindmap-canvas")) { let NODE_INDENT_PX = 20; let maxLevel = 6; let mindmapCanvas = document.getElementById("mindmap-canvas"); let nodesData = {}; let rootId = null; function renderMindMapTree() { mindmapCanvas.innerHTML = ""; function _renderNode(nodeId, level) { let node = nodesData[nodeId]; let div = document.createElement("div"); div.className = "mindmap-node"; div.style.marginLeft = (level * NODE_INDENT_PX) + "px"; div.dataset.nodeId = nodeId; div.textContent = node.name; // Контекстное меню (иконка) let menuBtn = document.createElement("span"); menuBtn.className = "node-menu-btn"; menuBtn.style.float = "left"; menuBtn.style.cursor = "pointer"; menuBtn.style.marginLeft = "10px"; menuBtn.innerHTML = " ⋮ "; div.appendChild(menuBtn); menuBtn.onclick = function (ev) { ev.stopPropagation(); showNodeContextMenu(nodeId, div); }; mindmapCanvas.appendChild(div); if (level < maxLevel) for (let childId of Object.keys(nodesData).filter(fid => nodesData[fid].parent_id == nodeId) .sort((a,b) => nodesData[a].position - nodesData[b].position)) { _renderNode(childId, level+1); } } if (rootId) _renderNode(rootId, 0); } function fetchNodesAndRender() { fetch(`/diagram/${window.DIAGRAM_ID}/nodes`).then(resp=>resp.json()).then(data=>{ nodesData = {}; data.forEach(n => { nodesData[n.id] = n; if (n.parent_id === null) rootId = n.id; }); renderMindMapTree(); }); } fetchNodesAndRender(); // --- Модальное окно действий с узлом let nodeActionModal = new bootstrap.Modal(document.getElementById('nodeActionModal')); let nodeActionForm = document.getElementById('node-action-form'); let nodeActionInput = document.getElementById('node-action-input'); let nodeActionSaveBtn = document.getElementById('node-action-save-btn'); let nodeModalAction = null; // "add", "rename" let nodeActionTargetId = null; function showNodeContextMenu(nodeId, nodeDiv) { let node = nodesData[nodeId]; let menuWrap = document.createElement("div"); menuWrap.className = "bg-white border rounded p-2 position-absolute"; menuWrap.style.zIndex = "1000"; menuWrap.style.boxShadow = "0 2px 8px #6664"; menuWrap.innerHTML = ` ${node.parent_id !== null ? '' : ''} `; function hide() { menuWrap.remove(); } menuWrap.onclick = function(ev) { let act = ev.target.dataset.act; if (!act) return; hide(); if (act === "add") { nodeActionInput.value = ""; nodeModalAction = "add"; nodeActionTargetId = nodeId; nodeActionSaveBtn.innerText = "Добавить"; nodeActionModal.show(); } else if (act === "rename") { nodeActionInput.value = node.name; nodeModalAction = "rename"; nodeActionTargetId = nodeId; nodeActionSaveBtn.innerText = "Сохранить"; nodeActionModal.show(); } else if (act === "del") { if (confirm("Удалить этот узел и все дочерние элементы?")) { fetch(`/diagram/${window.DIAGRAM_ID}/node/delete`, {method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({id: nodeId})}) .then(r=>r.json()).then(()=>fetchNodesAndRender()); } } }; document.body.appendChild(menuWrap); let rect = nodeDiv.getBoundingClientRect(); menuWrap.style.top = (window.scrollY + rect.top + 30) + "px"; menuWrap.style.left = (window.scrollX + rect.left + 30) + "px"; let handler = e => { if (!menuWrap.contains(e.target)) {hide(); document.removeEventListener("mousedown", handler) }}; setTimeout(()=>document.addEventListener("mousedown", handler), 50); } nodeActionForm.onsubmit = function (e) { e.preventDefault(); let val = nodeActionInput.value.trim(); if (!val) { nodeActionInput.classList.add("is-invalid"); return; } nodeActionInput.classList.remove("is-invalid"); if (nodeModalAction === "add") { fetch(`/diagram/${window.DIAGRAM_ID}/node/create`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({parent_id: nodeActionTargetId, name: val}) }).then(r=>r.json()).then(r=>{ nodeActionModal.hide(); fetchNodesAndRender(); }); } else if (nodeModalAction === "rename") { fetch(`/diagram/${window.DIAGRAM_ID}/node/rename`, { method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify({id: nodeActionTargetId, name: val}) }).then(r=>r.json()).then(r=>{ nodeActionModal.hide(); fetchNodesAndRender(); }); } }; } // --- SVG export button --- let getSvgBtn = document.getElementById('get-svg-btn'); if (getSvgBtn && window.DIAGRAM_ID) { getSvgBtn.addEventListener('click', function() { // 1. Получаем .puml с backend (по REST) fetch(`/diagram/${window.DIAGRAM_ID}/puml_inline`) .then(r => { if (!r.ok) throw new Error("Не удалось получить .puml"); return r.text(); }) .then(pumlText => { // 2. Кодируем .puml в plantuml-формат let plantumlServer = "https://plantuml.1qq.su/plantuml"; let encoded = encode64(deflate(pumlText)); console.log(pumlText) // 3. Открываем ссылку в новом окне (SVG) let svgUrl = `${plantumlServer}/svg/${encoded}`; window.open(svgUrl, "_blank"); }) .catch(e => alert("Ошибка: "+e)); }); } // --- PlantUML text deflate+base64 encoder --- // © plantuml encoder, public domain or MIT, адаптация на чистом JS // Источник: https://plantuml.com/codejavascript // --BEGIN plantuml encoder-- /* eslint-disable */ // compressed deflate + base64 encode // https://plantuml.com/codejavascript function encode64(data) { var r = ""; for (var i = 0; i < data.length; i += 3) { if (i + 2 === data.length) { r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), 0); } else if (i + 1 === data.length) { r += append3bytes(data.charCodeAt(i), 0, 0); } else { r += append3bytes(data.charCodeAt(i), data.charCodeAt(i + 1), data.charCodeAt(i + 2)); } } return r; } function append3bytes(b1, b2, b3) { var c1 = b1 >> 2; var c2 = ((b1 & 0x3) << 4) | (b2 >> 4); var c3 = ((b2 & 0xF) << 2) | (b3 >> 6); var c4 = b3 & 0x3F; var r = ""; r += encode6bit(c1 & 0x3F); r += encode6bit(c2 & 0x3F); r += encode6bit(c3 & 0x3F); r += encode6bit(c4 & 0x3F); return r; } function encode6bit(b) { if (b < 10) { return String.fromCharCode(48 + b); } b -= 10; if (b < 26) { return String.fromCharCode(65 + b); } b -= 26; if (b < 26) { return String.fromCharCode(97 + b); } b -= 26; if (b === 0) { return '-'; } if (b === 1) { return '_'; } return '?'; } // Deflate (`raw` mode, no gzip/header) — используем tiny-inflate из CDN // Но тут мы вставляем быструю версию средствами pako (или fflate) либо полифилла: // Подключаем библиотеку pako через CDN if (typeof window.pako === "undefined") { var script = document.createElement("script"); script.src = "https://unpkg.com/pako@2.1.0/dist/pako.min.js"; script.onload = function() {}; document.head.appendChild(script); } function deflate(str) { if (typeof window.pako === "undefined") { alert('Не удалось инициализировать deflate (pako), попробуйте обновить страницу'); throw new Error("No pako.js"); } // Кодируем в UTF-8, а не "unescape(encodeURIComponent(str))" var utf8 = new TextEncoder().encode(str); var data = window.pako.deflateRaw(utf8); var res = ""; for (var i = 0; i < data.length; i++) { res += String.fromCharCode(data[i]); } return res; } // --END plantuml encoder-- });