Добавляем все файлы

This commit is contained in:
2025-11-06 19:41:55 +02:00
parent 235d6a3a18
commit 2e5aaec307
218 changed files with 79015 additions and 0 deletions

View File

@@ -0,0 +1,88 @@
#message {
color: rgb(255, 255, 255);
font-size: 3em;
text-align: center;
}
.sidebar {
display: flex;
flex-direction: column;
height: 80vh;
}
.sidebar__menu {
width: 100px;
float: left;
background-color: #f0f0f0;
box-sizing: border-box;
}
#main {
margin: 7px 10px 7px 110px;
background-color: #ffffff;
box-sizing: border-box;
}
.sidebar__title {
font-size: 1.5em;
text-align: center;
}
.sidebar__buttons {
font-size: 1.3em;
display: flex;
flex-direction: column;
}
.sidebar__button {
border: solid;
text-align: center;
padding: 5px;
margin: 5px;
border: 1px solid rgb(0, 0, 0);
}
.sidebar__shapes {
font-size: 1.3em;
margin-top: 0.5em;
display: flex;
flex-direction: column;
}
.sidebar__shape {
border: solid;
text-align: center;
padding: 5px;
margin: 5px;
border: 1px solid rgb(0, 0, 0);
}
.sidebar__selectors {
display: flex;
flex-direction: column;
margin-top: 0.5em;
font-size: 1.3em;
}
.sidebar__selector {
border: solid;
text-align: center;
padding: 5px;
margin: 5px;
border: 1px solid rgb(0, 0, 0);
}
.sidebar__selector.active {
background-color: #e4e4e4;
}
.sidebar__footer {
text-align: center;
margin-top: auto;
}
.main {
height: 600px;
}
.selection {
position: absolute;
display: none;
outline: rgb(153, 153, 255) solid 2px;
pointer-events: none;
left: 333.617px;
top: 55px;
width: 175.5px;
height: 175.5px;
}

View File

@@ -0,0 +1,762 @@
/**
* @file SvgEditorM.js
* @brief Основной файл SvgEditorM, реализует функционал редактирования SVG-графики на сайте
*/
/**
* @brief Базовый SVG-элемент
* @param width Ширина
* @param height Высота
* @param posX Позиция X
* @param posY Позиция Y
* @param id ID элемента
* @param type Тип SVG-элемента
*/
class SVGElement {
constructor({ width, height, posX, posY, id, type } = {}) {
this.width = width || 100;
this.height = height || 100;
this.posX = posX || 100;
this.posY = posY || 100;
this.id = id || `svgElement_${Date.now()}`;
this.type = type || 'svg';
this.domNode = this.createSVGDOMNode(this.type);
}
createSVGDOMNode(type) {
let { height, width, posY, posX, id } = this;
let svg = document.createElementNS('http://www.w3.org/2000/svg', type);
svg.setAttribute('style', 'border: 1px solid black; box-sizing: border-box;');
svg.setAttribute('id', id);
if (type !== 'svg') {
svg.setAttribute('x', posX);
svg.setAttribute('y', posY);
svg.setAttribute('width', width);
svg.setAttribute('height', height);
} else {
svg.setAttribute('width', width);
svg.setAttribute('height', height);
}
svg.setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink');
return svg;
}
}
/**
* @brief Класс для модификации SVG-элементов
*/
class SVGElementModifier {
static moveSelectedElement({ handler, event }) {
const { selected, selector, editor, offset } = handler;
const { clientX, clientY } = event;
if (selected) {
if (selected.tagName.toLowerCase() === 'circle') {
selected.setAttribute('cx', clientX + offset.x);
selected.setAttribute('cy', clientY + offset.y);
} else {
selected.setAttribute('x', clientX + offset.x);
selected.setAttribute('y', clientY + offset.y);
selector.updateSelection(selected, editor);
}
selector.updateSelection(selected);
}
}
static deselectElement({ handler }) {
handler.selected = null;
}
static toggelHoveredElement({ handler, target }) {
return handler.selector.updateSelection(target, handler.editor);
}
static selectElement({ handler, event }) {
let { target, clientX, clientY } = event;
const { selector, editor } = handler;
if (selector.isSelectable(target, editor)) {
handler.offset.x = parseFloat(target.getAttribute('x')) - clientX || 0;
handler.offset.y = parseFloat(target.getAttribute('y')) - clientY || 0;
handler.selected = target;
}
}
}
/**
* @brief Прямоугольник
* @param posX Позиция X
* @param posY Позиция Y
* @param width Ширина
* @param height Высота
* @param id ID элемента
*/
class Rect extends SVGElement {
constructor({ posX, posY, width, height, id } = {}) {
super({ type: 'rect', posX, posY, width, height, id });
this.domNode.setAttribute('width', width || 100);
this.domNode.setAttribute('height', height || 100);
this.domNode.setAttribute('x', posX || 100);
this.domNode.setAttribute('y', posY || 100);
this.domNode.setAttribute('fill', 'rgba(0,120,200,0.2)');
this.domNode.setAttribute('stroke', '#0078c8');
}
}
/**
* @brief Класс выбора фигур
* @param id ID селектора
*/
class ShapeSelector {
constructor(id) {
this.id = id;
this.selectorElem = document.getElementById(this.id);
}
isSelectable(element, area) {
if (!element) return false;
if (element.isSameNode(area)) return false;
const tag = element.tagName ? element.tagName.toLowerCase() : '';
return (tag === 'rect' || tag === 'circle' || tag === 'g' || tag === 'path' || tag === 'ellipse');
}
toggleSelectionOverElement(element) {
let bound = element.getBoundingClientRect();
const { style } = this.selectorElem;
style.left = bound.left + 'px';
style.top = bound.top + 'px';
style.width = bound.width + 'px';
style.height = bound.height + 'px';
style.display = 'block';
}
updateSelection(element, area) {
if (this.isSelectable(element, area)) {
return this.toggleSelectionOverElement(element);
}
return this.hideSelection();
}
hideSelection() {
if (this.selectorElem) this.selectorElem.style.display = 'none';
}
}
/**
* @brief Обработчик событий редактора
*/
class EditorEventHandler {
constructor() {
this.editor = null;
this.selector = null;
this.selected = null;
this.offset = { x: 0, y: 0 };
}
startListening({ editor, selector }) {
this.editor = editor;
this.selector = selector;
this.handleEditorEvents();
}
handleEditorEvents() {
this.handleElementsHover();
this.handleElementSelect();
this.handleMoveElement();
this.handleElementDeselect();
}
handleElementsHover() {
this.editor.addEventListener('mouseover', ({ target }) => {
SVGElementModifier.toggelHoveredElement({ handler: this, target });
});
}
handleElementSelect() {
this.editor.addEventListener('mousedown', event => {
SVGElementModifier.selectElement({ handler: this, event });
});
}
handleElementDeselect() {
this.editor.addEventListener('mouseup', event => {
SVGElementModifier.deselectElement({ handler: this });
});
}
handleMoveElement() {
this.editor.addEventListener('mousemove', event => {
SVGElementModifier.moveSelectedElement({ handler: this, event });
});
}
}
/**
* @brief Область для рисования
* @param options Параметры области
*/
class Area extends SVGElement {
constructor(options = {}) {
super({ type: 'svg', width: options.width || 800, height: options.height || 400, id: options.id || 'mySVG' });
this.domNode.style.border = '3px solid black';
}
appendElement(element) {
this.domNode.appendChild(element);
}
setDimentions() { }
}
/**
* @brief Редактор SVG
* @param targetNode Узел-цель
* @param playGround Игровая область
* @param eventHandler Обработчик событий
* @param selector Селектор фигур
*/
class Editor {
constructor({ targetNode, playGround, eventHandler, selector }) {
this.targetNode = targetNode;
this.playGround = playGround;
this.domNode = this.createEditor();
this.selector = selector;
this.svgElements = [];
this.EditorEventHandler = eventHandler;
this.EditorEventHandler.startListening({
editor: this.domNode,
selector: this.selector
});
}
setDimentions(node, width, height) {
node.style.width = '-webkit-fill-available';
node.style.height = '-webkit-fill-available';
return node;
}
createEditor() {
let editor = this.playGround.domNode;
const { width, height } = this.getEditorParams();
this.setDimentions(editor, width, height);
return editor;
}
getEditorParams() {
let width = Math.max(this.targetNode.offsetWidth - 10, 0);
let height = Math.max(this.targetNode.offsetHeight - 10, 0);
return { width, height };
}
addElement(element) {
if (!element || !element.domNode) return;
this.playGround.appendElement(element.domNode);
this.svgElements.push(element);
}
}
/* -----------------------------
Инициализация (выполняется после загрузки DOM)
----------------------------- */
window.addEventListener('DOMContentLoaded', () => {
let main = document.getElementById('main') || document.body;
let selection = document.getElementById('selector');
if (!selection) {
selection = document.createElement('div');
selection.id = 'selector';
Object.assign(selection.style, {
position: 'absolute',
display: 'none',
border: '1px dashed #333',
pointerEvents: 'none',
zIndex: 9999
});
document.body.appendChild(selection);
}
const area = new Area({ id: 'mySVG', width: 800, height: 400 });
main.appendChild(area.domNode);
const selector = new ShapeSelector('selector');
const editorEventHandler = new EditorEventHandler();
const editor = new Editor({
targetNode: main,
playGround: area,
selector,
eventHandler: editorEventHandler
});
const testRect = new Rect({ posX: 20, posY: 20, width: 120, height: 80, id: 'r1' });
const testRect2 = new Rect({ posX: 200, posY: 150, width: 140, height: 90, id: 'r2' });
editor.addElement(testRect);
editor.addElement(testRect2);
area.domNode.addEventListener('mouseover', (e) => {
const tgt = e.target;
selector.updateSelection(tgt, area.domNode);
});
window.addEventListener('resize', () => {
const { width, height } = editor.getEditorParams();
editor.setDimentions(area.domNode, width, height);
});
console.log('SVG Editor initialized', { area, editor, selector });
});
/* -----------------------------
edit
----------------------------- */
/** @brief Селектор основного контейнера */
const MAIN_SEL = "#main";
/** @brief ID элемента outline */
const OUTLINE_ID = "outline";
/** @brief Селектор боковой панели с фигурами */
const SIDEBAR_SHAPES = ".sidebar__shapes";
/** @brief Селектор боковой панели с режимами */
const SIDEBAR_SELECTORS = ".sidebar__selectors";
/** @brief Ключ для сохранения количества изменений */
const saveCountKey = 'SvgEditorM_saveCount';
/** @brief Основной контейнер DOM */
const main = document.querySelector(MAIN_SEL);
if (!main) throw new Error("Не найден #main");
/** @brief Массив svg элементов внутри main */
let svgsInMain = Array.from(main.querySelectorAll("svg"));
/** @brief Рабочий svg элемент */
let svg;
if (svgsInMain.length === 0) {
svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("style", "border: 1px solid #ccc; box-sizing: border-box; width: -webkit-fill-available; height: -webkit-fill-available;");
svg.setAttribute("width", "1000");
svg.setAttribute("height", "600");
main.appendChild(svg);
} else {
// keep first svg, remove extras to avoid duplicates
svg = svgsInMain[0];
for (let i = 1; i < svgsInMain.length; i++) svgsInMain[i].remove();
}
/** @brief Создает и добавляет маркер стрелки к SVG */
(function ensureArrowMarker() {
let defs = svg.querySelector("defs");
if (!defs) {
defs = document.createElementNS("http://www.w3.org/2000/svg", "defs");
svg.appendChild(defs);
}
if (!svg.querySelector("#arrowhead")) {
const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
marker.setAttribute("id", "arrowhead");
marker.setAttribute("markerWidth", "6");
marker.setAttribute("markerHeight", "4");
marker.setAttribute("refX", "4.8");
marker.setAttribute("refY", "2");
marker.setAttribute("orient", "auto");
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
path.setAttribute("d", "M0,0 L6,2 L0,4 z");
path.setAttribute("fill", "#495057");
marker.appendChild(path);
defs.appendChild(marker);
}
})();
/** @brief Получает элемент outline из DOM */
const outline = document.getElementById(OUTLINE_ID);
/** @brief Счетчик для генерации уникальных ID */
let uid = 1;
/**
* @brief Генерирует уникальный идентификатор для элемента
* @param prefix префикс для ID
*/
function makeId(prefix = "el") { return `${prefix}_${Date.now().toString(36)}_${uid++}`; }
/**
* @brief Преобразует координаты из клиентских в координаты SVG
* @param clientX координата X в пикселях
* @param clientY координата Y в пикселях
*/
function clientToSvgPoint(clientX, clientY) {
const pt = svg.createSVGPoint();
pt.x = clientX; pt.y = clientY;
const ctm = svg.getScreenCTM();
if (!ctm) return { x: clientX, y: clientY };
const inv = ctm.inverse();
const p = pt.matrixTransform(inv);
return { x: p.x, y: p.y };
}
/**
* @brief Возвращает размеры и позицию элемента в координатах экрана
* @param el элемент SVG
*/
function screenBBoxOf(el) {
try {
const bbox = el.getBBox();
const p1 = svg.createSVGPoint(); p1.x = bbox.x; p1.y = bbox.y;
const p2 = svg.createSVGPoint(); p2.x = bbox.x + bbox.width; p2.y = bbox.y + bbox.height;
const s1 = p1.matrixTransform(svg.getScreenCTM());
const s2 = p2.matrixTransform(svg.getScreenCTM());
return {
left: Math.min(s1.x, s2.x),
top: Math.min(s1.y, s2.y),
width: Math.abs(s2.x - s1.x),
height: Math.abs(s2.y - s1.y)
};
} catch (e) {
const r = el.getBoundingClientRect();
return { left: r.left, top: r.top, width: r.width, height: r.height };
}
}
/**
* @brief Показывает или скрывает outline вокруг элемента
* @param el элемент SVG или null
*/
function showOutlineFor(el) {
if (!outline) return;
if (!el) { outline.style.display = "none"; return; }
const b = screenBBoxOf(el);
outline.style.display = "block";
outline.style.position = "fixed";
outline.style.left = `${b.left-2}px`;
outline.style.top = `${b.top-2}px`;
outline.style.width = `${b.width}px`;
outline.style.height = `${b.height}px`;
outline.style.border = "2px solid rgba(0,120,255,0.6)";
outline.style.pointerEvents = "none";
}
/** @brief Режим работы редактора */
let mode = "Move";
/** @brief Текущий выбранный элемент */
let currentSelected = null;
/** @brief Состояние перетаскивания элемента */
let dragState = null;
document.querySelectorAll(`${SIDEBAR_SHAPES} .sidebar__shape`).forEach(node => {
node.addEventListener("click", () => {
const shapeName = node.textContent.trim().toLowerCase();
createShape(shapeName);
});
});
document.querySelectorAll(`${SIDEBAR_SELECTORS} .sidebar__selector`).forEach(node => {
node.addEventListener("click", () => {
document.querySelectorAll(`${SIDEBAR_SELECTORS} .sidebar__selector`).forEach(n => n.classList.remove("active"));
node.classList.add("active");
mode = node.textContent.trim();
});
});
const defaultSel = document.querySelector(`${SIDEBAR_SELECTORS} .sidebar__selector`);
if (defaultSel) {
defaultSel.classList.add("active");
mode = defaultSel.textContent.trim();
}
/**
* @brief Создает фигуру указанного типа в центре SVG
* @param kind тип фигуры: rect, circle, line, arrow
*/
function createShape(kind) {
const center = { x: parseFloat(svg.getAttribute("width"))/2 || 200, y: parseFloat(svg.getAttribute("height"))/2 || 150 };
const id = makeId(kind);
let el = null;
if (kind === "rect") {
el = document.createElementNS("http://www.w3.org/2000/svg", "rect");
el.setAttribute("x", center.x - 50);
el.setAttribute("y", center.y - 25);
el.setAttribute("width", 100);
el.setAttribute("height", 50);
el.setAttribute("fill", "#1aaee5");
} else if (kind === "circle") {
el = document.createElementNS("http://www.w3.org/2000/svg", "circle");
el.setAttribute("cx", center.x);
el.setAttribute("cy", center.y);
el.setAttribute("r", 30);
el.setAttribute("fill", "#ff6600");
} else if (kind === "line") {
el = document.createElementNS("http://www.w3.org/2000/svg", "line");
el.setAttribute("x1", center.x - 60);
el.setAttribute("y1", center.y);
el.setAttribute("x2", center.x + 60);
el.setAttribute("y2", center.y);
el.setAttribute("stroke", "#495057");
el.setAttribute("stroke-width", 4);
} else if (kind === "arrow") {
el = document.createElementNS("http://www.w3.org/2000/svg", "line");
el.setAttribute("x1", center.x - 60);
el.setAttribute("y1", center.y - 10);
el.setAttribute("x2", center.x + 60);
el.setAttribute("y2", center.y + 10);
el.setAttribute("stroke", "#495057");
el.setAttribute("stroke-width", 4);
el.setAttribute("marker-end", "url(#arrowhead)");
} else {
alert("Unknown shape: " + kind);
return;
}
el.setAttribute("id", id);
el.classList.add("draggable");
svg.appendChild(el);
attachShapeListeners(el);
selectElement(el);
}
/**
* @brief Подключает обработчики событий для фигуры
* @param el элемент SVG
*/
function attachShapeListeners(el) {
el.addEventListener("mousedown", (ev) => {
ev.stopPropagation();
const startClient = { x: ev.clientX, y: ev.clientY };
const startSvg = clientToSvgPoint(ev.clientX, ev.clientY);
const startAttrs = readElementAttrs(el);
const action = mode === "Resize" ? "resize" : "move";
dragState = { type: action, el, startClient, startSvg, startAttrs };
selectElement(el);
window.addEventListener("mousemove", onWindowMouseMove);
window.addEventListener("mouseup", onWindowMouseUp);
});
el.addEventListener("click", (e) => { e.stopPropagation(); selectElement(el); });
}
/**
* @brief Считывает атрибуты элемента SVG
* @param el элемент SVG
*/
function readElementAttrs(el) {
const tag = el.tagName.toLowerCase();
if (tag === "rect") {
return {
x: parseFloat(el.getAttribute("x")||0),
y: parseFloat(el.getAttribute("y")||0),
width: parseFloat(el.getAttribute("width")||0),
height: parseFloat(el.getAttribute("height")||0)
};
} else if (tag === "circle") {
return {
cx: parseFloat(el.getAttribute("cx")||0),
cy: parseFloat(el.getAttribute("cy")||0),
r: parseFloat(el.getAttribute("r")||0)
};
} else if (tag === "line") {
return {
x1: parseFloat(el.getAttribute("x1")||0),
y1: parseFloat(el.getAttribute("y1")||0),
x2: parseFloat(el.getAttribute("x2")||0),
y2: parseFloat(el.getAttribute("y2")||0)
};
}
return {};
}
/**
* @brief Обрабатывает движение мыши при перетаскивании/изменении размера
* @param ev событие мыши
*/
function onWindowMouseMove(ev) {
if (!dragState) return;
const { type, el, startSvg, startAttrs } = dragState;
const curSvg = clientToSvgPoint(ev.clientX, ev.clientY);
const dx = curSvg.x - startSvg.x;
const dy = curSvg.y - startSvg.y;
const tag = el.tagName.toLowerCase();
if (type === "move") {
if (tag === "rect") {
el.setAttribute("x", startAttrs.x + dx);
el.setAttribute("y", startAttrs.y + dy);
} else if (tag === "circle") {
el.setAttribute("cx", startAttrs.cx + dx);
el.setAttribute("cy", startAttrs.cy + dy);
} else if (tag === "line") {
el.setAttribute("x1", startAttrs.x1 + dx);
el.setAttribute("y1", startAttrs.y1 + dy);
el.setAttribute("x2", startAttrs.x2 + dx);
el.setAttribute("y2", startAttrs.y2 + dy);
}
} else {
if (tag === "rect") {
const newW = Math.max(6, startAttrs.width + dx);
const newH = Math.max(6, startAttrs.height + dy);
el.setAttribute("width", newW);
el.setAttribute("height", newH);
} else if (tag === "circle") {
const center = { x: startAttrs.cx, y: startAttrs.cy };
const newR = Math.max(4, Math.hypot(curSvg.x - center.x, curSvg.y - center.y));
el.setAttribute("r", newR);
} else if (tag === "line") {
el.setAttribute("x2", startAttrs.x2 + dx);
el.setAttribute("y2", startAttrs.y2 + dy);
}
}
showOutlineFor(el);
}
/**
* @brief Обрабатывает отпускание кнопки мыши при перетаскивании/изменении размера
*/
function onWindowMouseUp() {
if (dragState) {
dragState = null;
window.removeEventListener("mousemove", onWindowMouseMove);
window.removeEventListener("mouseup", onWindowMouseUp);
}
}
svg.addEventListener("mousedown", (ev) => {
if (ev.target === svg) selectElement(null);
});
/**
* @brief Выбирает элемент или снимает выбор
* @param el элемент SVG или null
*/
function selectElement(el) {
if (currentSelected === el) return;
if (currentSelected) currentSelected.classList.remove("selected");
currentSelected = el;
if (el) {
el.classList.add("selected");
showOutlineFor(el);
} else {
showOutlineFor(null);
}
}
Array.from(svg.querySelectorAll(".draggable, rect, circle, line")).forEach(attachShapeListeners);
window.addEventListener("resize", () => showOutlineFor(currentSelected));
window.addEventListener("scroll", () => showOutlineFor(currentSelected));
window.addEventListener("keydown", (ev) => {
if ((ev.key === "Delete" || ev.key === "Backspace") && currentSelected) {
currentSelected.remove();
selectElement(null);
}
});
/** @brief Кнопки панели сайдбара */
const buttons = Array.from(document.querySelectorAll('.sidebar__button'));
/** @brief Кнопка сохранения */
const saveBtn = buttons.find(b => b.textContent.trim().toLowerCase() === 'save');
/** @brief Кнопка загрузки */
const loadBtn = buttons.find(b => b.textContent.trim().toLowerCase() === 'load');
/** @brief Генерирует следующее имя файла для сохранения */
function nextFilename() {
localStorage.setItem(saveCountKey, String((parseInt(localStorage.getItem(saveCountKey) || '0', 10) || 0) + 1));
const n = parseInt(localStorage.getItem(saveCountKey), 10) || 1;
return n === 1 ? 'SvgEditorM.svg' : `SvgEditorM-${n}.svg`;
}
/** @brief Возвращает SVG внутри main */
function getMainSvg() {
return main.querySelector('svg');
}
/**
* @brief Устанавливает атрибуты xmlns для элемента SVG, если их нет
* @param el элемент SVG
*/
function ensureXmlns(el) {
if (!el.getAttribute('xmlns')) el.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
if (!el.getAttribute('xmlns:xlink')) el.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
return el;
}
/** @brief Сохраняет SVG из main в файл */
function saveSvg() {
const svg = getMainSvg();
if (!svg) return;
const clone = svg.cloneNode(true);
ensureXmlns(clone);
const xml = new XMLSerializer().serializeToString(clone);
const blob = new Blob([xml], { type: 'image/svg+xml;charset=utf-8' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = nextFilename();
document.body.appendChild(a);
a.click();
a.remove();
setTimeout(() => URL.revokeObjectURL(url), 10000);
}
/** @brief Элемент input для загрузки файлов */
const fileInput = document.createElement('input');
fileInput.type = 'file';
fileInput.accept = '.svg,image/svg+xml';
fileInput.style.display = 'none';
document.body.appendChild(fileInput);
/**
* @brief Делает узел и его потомков интерактивными
* @param node элемент SVG
*/
function makeInteractiveForNode(node) {
if (!(node instanceof Element)) return;
const tag = node.tagName.toLowerCase();
if (['rect','circle','line','path','polyline','polygon','g'].includes(tag)) {
if (!node.classList.contains('draggable')) node.classList.add('draggable');
if (typeof attachShapeListeners === 'function') {
attachShapeListeners(node);
}
}
Array.from(node.children || []).forEach(ch => makeInteractiveForNode(ch));
}
/**
* @brief Загружает SVG из файла в main
* @param file объект File
*/
async function loadSvgFile(file) {
if (!file) return;
const text = await file.text();
const parser = new DOMParser();
const doc = parser.parseFromString(text, 'image/svg+xml');
const parsedSvg = doc.querySelector('svg');
if (!parsedSvg) {
alert('Выбранный файл не содержит SVG');
return;
}
const targetSvg = getMainSvg();
if (!targetSvg) {
const imported = document.importNode(parsedSvg, true);
ensureXmlns(imported);
main.appendChild(imported);
Array.from(imported.querySelectorAll('*')).forEach(n => makeInteractiveForNode(n));
} else {
const preservedOutline = document.getElementById('outline');
while (targetSvg.firstChild) targetSvg.removeChild(targetSvg.firstChild);
Array.from(parsedSvg.childNodes).forEach(n => {
const imp = document.importNode(n, true);
targetSvg.appendChild(imp);
});
Array.from(parsedSvg.attributes).forEach(attr => {
if (attr.name === 'xmlns' || attr.name === 'xmlns:xlink') return;
targetSvg.setAttribute(attr.name, attr.value);
});
ensureXmlns(targetSvg);
Array.from(targetSvg.querySelectorAll('*')).forEach(n => makeInteractiveForNode(n));
if (preservedOutline && preservedOutline.parentElement !== document.body) {
}
}
main.dispatchEvent(new CustomEvent('svg-loaded', { detail: { fileName: file.name } }));
}
fileInput.addEventListener('change', () => {
const f = fileInput.files && fileInput.files[0];
loadSvgFile(f).finally(() => { fileInput.value = ''; });
});
if (saveBtn) saveBtn.addEventListener('click', (e) => { e.stopPropagation(); saveSvg(); });
if (loadBtn) loadBtn.addEventListener('click', (e) => { e.stopPropagation(); fileInput.click(); });

View File

@@ -0,0 +1,30 @@
<?php
/**
* @file SvgEditorM/index.php
* @brief Интерфейс SVG редактора: боковая панель с кнопками действий, фигурами и селекторами, основная область для работы с SVG
*/
?>
<?php /** @brief Основной контейнер SVG редактора */ $sidebar; ?>
<div id="sidebar" class="bfloat">
<div class="btitle" style="background-color: transparent; padding-bottom: 10px;">SVG редактор</div>
<div class="sidebar__menu">
<div class="sidebar__buttons">
<span class="sidebar__button">Save</span>
<span class="sidebar__button">Load</span>
</div>
<div class="sidebar__shapes">
<span class="sidebar__shape">Circle</span>
<span class="sidebar__shape">Rect</span>
<span class="sidebar__shape">Line</span>
<span class="sidebar__shape">Arrow</span>
</div>
<div class="sidebar__selectors">
<span class="sidebar__selector">Move</span>
<span class="sidebar__selector">Resize</span>
</div>
</div>
<div class="main" id="main">
<span class="selection" id="outline"></span>
</div>
</div>

25
main_plugin/SvgEditorM/plug.php Executable file
View File

@@ -0,0 +1,25 @@
<?php
/**
* @file plug.php
* @brief Подключает плагин SvgEditorM для администраторов, перемещает sidebar и подгружает JS-модуль
*/
global $path, $_SESSION, $configAdmins;
if (in_array($_SESSION['username'], $configAdmins, true)) {
include $path . 'main_plugin/SvgEditorM/index.php';
echo '<link rel="stylesheet" type="text/css" href="/main_plugin/SvgEditorM/SvgEditorM.css">';
echo "<script type='module'>
document.addEventListener('DOMContentLoaded', () => {
const c = document.querySelector('.center-float');
const d = document.getElementById('sidebar');
if (c && d) {
c.appendChild(document.createElement('br'));
c.appendChild(d);
import('/main_plugin/SvgEditorM/SvgEditorM.js');
} else if (d) {
d.remove();
}
});
</script>";
}
?>