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--
});