Добавляем все файлы
This commit is contained in:
88
main_plugin/SvgEditorM/SvgEditorM.css
Executable file
88
main_plugin/SvgEditorM/SvgEditorM.css
Executable 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;
|
||||
}
|
||||
762
main_plugin/SvgEditorM/SvgEditorM.js
Executable file
762
main_plugin/SvgEditorM/SvgEditorM.js
Executable 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(); });
|
||||
30
main_plugin/SvgEditorM/index.php
Executable file
30
main_plugin/SvgEditorM/index.php
Executable 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
25
main_plugin/SvgEditorM/plug.php
Executable 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>";
|
||||
}
|
||||
?>
|
||||
88
main_plugin/auth/auth.css
Executable file
88
main_plugin/auth/auth.css
Executable file
@@ -0,0 +1,88 @@
|
||||
#authorizationButton {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
float: right;
|
||||
margin-left: 5px;
|
||||
}
|
||||
#authorizationButton:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
}
|
||||
#loginButton {
|
||||
margin: 0px 3px;
|
||||
}
|
||||
|
||||
#authorizationDiv {
|
||||
font-size: 0.85em;
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
user-select: none;
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
width: 370px;
|
||||
border-radius: 5px;
|
||||
height: 230px;
|
||||
box-shadow: 0px 0px 5px #777;
|
||||
|
||||
color: #000000;
|
||||
text-shadow: none;
|
||||
z-index: 100;
|
||||
min-height: fit-content;
|
||||
}
|
||||
#authorizationDiv input, #authorizationDiv textarea, #authorizationDiv select, #authorizationDiv button {
|
||||
font-size: 0.85em;
|
||||
}
|
||||
|
||||
.authorizationDivTop {
|
||||
text-align: center;
|
||||
border: none;
|
||||
border-bottom: inherit;
|
||||
padding: 5px;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
.authorizationDivCloseFun {
|
||||
background-image: url(../../img/pict/b_iconslyb.svg);
|
||||
float: right;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
background-position: -159px -120px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.authorizationDivMainDiv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 200px;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 10px 0px;
|
||||
}
|
||||
.formRow {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: -webkit-fill-available;
|
||||
margin: 5px 45px;
|
||||
}
|
||||
.formRow label {
|
||||
margin-right: 10px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.formRow input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
#BackArrow {
|
||||
background-position: -77px -37px;
|
||||
background-image: url(../../img/pict/b_iconslyb.svg);
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
position: absolute;
|
||||
left: 0px;
|
||||
top: 33px;
|
||||
}
|
||||
#BackArrow:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
cursor: pointer;
|
||||
}
|
||||
161
main_plugin/auth/auth.js
Executable file
161
main_plugin/auth/auth.js
Executable file
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* @file auth.js
|
||||
* @brief Скрипт авторизации, содержит функции и переменные для входа, регистрации и управления блоком авторизации
|
||||
*/
|
||||
|
||||
addEventListener("load", function() {
|
||||
movementMenu("authorizationDiv");
|
||||
|
||||
let authorizationButtonId = document.getElementById("authorizationButton");
|
||||
const authorizationDivId = document.getElementById("authorizationDiv");
|
||||
|
||||
if (getCookie('Login') == "true") {
|
||||
authorizationButtonId.style.background = "url(../../img/pict/mc_iconslyb.svg) -1038px 1px";
|
||||
document.documentElement.style.setProperty('--autButBackX', '-1038');
|
||||
document.documentElement.style.setProperty('--autButBackY', '1');
|
||||
authorizationButtonId.onclick = function() {
|
||||
jsonrpcRequest("logoutUser", { logoff: "Выйти" })
|
||||
.then(response => {
|
||||
location.reload();
|
||||
})
|
||||
};
|
||||
} else if (getCookie('Login') == "false") {
|
||||
authorizationButtonId.style.background = "url(../../img/pict/mc_iconslyb.svg) -756px 1px";
|
||||
document.documentElement.style.setProperty('--autButBackX', '-756');
|
||||
document.documentElement.style.setProperty('--autButBackY', '1');
|
||||
authorizationButtonId.onclick = function() {
|
||||
const el = authorizationDivId;
|
||||
if (el.style.visibility === "visible") {
|
||||
el.style.visibility = "hidden";
|
||||
} else {
|
||||
el.style.visibility = "visible";
|
||||
el.style.top = "20%";
|
||||
el.style.left = "50%";
|
||||
el.style.transform = "translate(-50%, -20%)";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
/**
|
||||
* @brief Закрывает блок авторизации
|
||||
*/
|
||||
function authorizationDivCloseFun() {
|
||||
document.getElementById("authorizationDiv").style.visibility = "hidden";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Создаёт форму кнопки входа
|
||||
*/
|
||||
function loginButtonFunCreate() {
|
||||
document.querySelector(".authorizationDivMainDiv").innerHTML = `
|
||||
<div id="BackArrow" onClick="mainButtonFunCreate()"></div>
|
||||
<div class="formRow">
|
||||
<label>{{login_label}}:</label>
|
||||
<input type="text" id="loginInput" name="login">
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label>{{password_label}}:</label>
|
||||
<input type="password" id="passInput" name="pass" autocomplete="">
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<button type="button" id="loginButton" onClick="loginButtonFun()">{{login}}</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const inputLogin = document.getElementById('loginInput');
|
||||
const inputPass = document.getElementById('passInput');
|
||||
const loginBtn = document.getElementById('loginButton');
|
||||
[inputLogin, inputPass].forEach(input => {
|
||||
input.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Enter') {
|
||||
if (loginBtn.getAttribute('onClick') === 'loginButtonFun()') {
|
||||
loginBtn.click();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
/**
|
||||
* @brief Обрабатывает нажатие кнопки входа
|
||||
*/
|
||||
function loginButtonFun() {
|
||||
var login = document.getElementById("loginInput").value;
|
||||
var pass = document.getElementById("passInput").value;
|
||||
jsonrpcRequest("loginUser", { login: login, pass: pass, log: "Войти" })
|
||||
.then(response => {
|
||||
if (response == "true") {
|
||||
location.reload();
|
||||
} else {
|
||||
messageFunction("{{incorrect_login_password}}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Создаёт форму кнопки регистрации
|
||||
*/
|
||||
function registrationButtonFunCreate() {
|
||||
document.querySelector(".authorizationDivMainDiv").innerHTML = `
|
||||
<div id="BackArrow" onClick="mainButtonFunCreate()"></div>
|
||||
<div class="formRow">
|
||||
<label>{{login_label}}:</label>
|
||||
<input type="text" id="loginInput" name="login">
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label>{{password_label}}:</label>
|
||||
<input type="text" id="passInput" name="pass">
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label>{{repeat_password_label}}:</label>
|
||||
<input type="text" id="passСheckInput" name="pass">
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<label>{{email_label}}:</label>
|
||||
<input id="emailInput">
|
||||
</div>
|
||||
<div class="formRow">
|
||||
<button type="button" id="loginButton" onClick="registrationButtonFun()">{{register}}</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
/**
|
||||
* @brief Обрабатывает нажатие кнопки регистрации
|
||||
*/
|
||||
function registrationButtonFun() {
|
||||
var login = document.getElementById("loginInput").value;
|
||||
var pass = document.getElementById("passInput").value;
|
||||
var passСheck = document.getElementById("passСheckInput").value;
|
||||
var email = document.getElementById("emailInput").value;
|
||||
if (login.trim() == "" || pass.trim() == "" || passСheck.trim() == "" || email.trim() == "" ) {
|
||||
messageFunction("{{fill_all_fields}}");
|
||||
return;
|
||||
}
|
||||
if (pass != passСheck) {
|
||||
messageFunction("{{passwords_do_not_match}}");
|
||||
return;
|
||||
}
|
||||
jsonrpcRequest("registerUser", { login: login, pass: pass, email: email }).then(response => {
|
||||
if (response == "true") {
|
||||
messageFunction("{{account_creation_request_sent}}");
|
||||
} else if (response == "name_exists") {
|
||||
messageFunction("{{user_exists}}");
|
||||
} else {
|
||||
messageFunction("{{account_creation_request_error}}");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Создаёт главную форму выбора между входом и регистрацией
|
||||
*/
|
||||
function mainButtonFunCreate() {
|
||||
document.querySelector(".authorizationDivMainDiv").innerHTML = `
|
||||
<div class="formRow">{{account_authorization}}</div>
|
||||
<div class="formRow">
|
||||
<button type="button" id="loginButton" onClick="loginButtonFunCreate()">{{login}}</button>
|
||||
<button type="button" id="loginButton" onClick="registrationButtonFunCreate()">{{register}}</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
43
main_plugin/auth/auth.php
Executable file
43
main_plugin/auth/auth.php
Executable file
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
/**
|
||||
* @file auth.php
|
||||
* @brief Содержит интерфейс авторизации пользователя
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php /** @brief Кнопка вызова окна авторизации */ $authorizationButton; ?>
|
||||
<span id="authorizationButton"></span>
|
||||
|
||||
<?php /** @brief Основной контейнер окна авторизации */ $authorizationDiv; ?>
|
||||
<div id="authorizationDiv" class="borderStyle" style="visibility: hidden; top: 20%; left: 50%; transform: translate(-50%, -20%);">
|
||||
<div class="authorizationDivTop" class="borderStyle">
|
||||
<span class="authorizationDivTopTitle">{{authorization}}</span>
|
||||
<span class="authorizationDivCloseFun" onClick="authorizationDivCloseFun()" class="editib"></span>
|
||||
</div>
|
||||
<div class="authorizationDivMainDiv">
|
||||
<div class="formRow">{{account_authorization}}</div>
|
||||
<div class="formRow">
|
||||
<button type="button" id="loginButton" onClick="loginButtonFunCreate()">{{login}}</button>
|
||||
<button type="button" id="loginButton" onClick="registrationButtonFunCreate()">{{register}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.addEventListener("load", function() {
|
||||
try {
|
||||
var buttonHTML = document.getElementById("authorizationButton").outerHTML;
|
||||
var divHTML = document.getElementById("authorizationDiv").outerHTML;
|
||||
|
||||
document.getElementById("authorizationButton").remove();
|
||||
document.getElementById("authorizationDiv").remove();
|
||||
|
||||
var container = document.getElementById("hbody");
|
||||
if (!container) throw new Error("#hbody не найден для authorization");
|
||||
container.insertAdjacentHTML("beforeend", buttonHTML);
|
||||
container.insertAdjacentHTML("beforeend", divHTML);
|
||||
} catch (e) {
|
||||
console.error("Ошибка в блоке authorization:", e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
82
main_plugin/auth/func.auth.php
Executable file
82
main_plugin/auth/func.auth.php
Executable file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
/**
|
||||
* @file func.auth.php
|
||||
* @brief Функции для регистрации пользователей и отправки уведомлений по email
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Регистрирует нового пользователя
|
||||
* @param array $params Массив с ключами 'login', 'pass', 'email'
|
||||
* @return string "name_exists" если пользователь уже есть, "true" если регистрация прошла успешно
|
||||
* @throws Exception Если произошла ошибка при создании запроса на регистрацию
|
||||
*/
|
||||
function registerUser($params) {
|
||||
global $path, $config;
|
||||
$exists = false;
|
||||
$requestFile = simplexml_load_file($path . $config['usersRequest']);
|
||||
foreach ($requestFile->users->user as $child) {
|
||||
if ((string)$child['name'] === $params['login']) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
$usersFile = simplexml_load_file($path . $config['users']);
|
||||
foreach ($usersFile->users->user as $child) {
|
||||
if ((string)$child['name'] === $params['login']) {
|
||||
$exists = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ($exists) {
|
||||
return "name_exists";
|
||||
} else {
|
||||
$requestFile = simplexml_load_file($path . $config['usersRequest']);
|
||||
$requestFilePath = $path . $config['usersRequest'];
|
||||
|
||||
$newUser = $requestFile->users->addChild('user');
|
||||
$newUser->addAttribute('name', $params['login']);
|
||||
$newUser->addAttribute('pass', $params['pass']);
|
||||
$newUser->addAttribute('access', '');
|
||||
$newUser->addAttribute('email', $params['email']);
|
||||
$newUser->addAttribute('link', md5($params['login'].$params['pass']));
|
||||
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$dom->formatOutput = true;
|
||||
$dom->loadXML($requestFile->asXML());
|
||||
|
||||
if ($dom->save($requestFilePath)) {
|
||||
return sendRegistrationEmail($params);
|
||||
} else {
|
||||
throw new Exception("Error while creating user request", -32003);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Отправляет уведомление о регистрации пользователю и администратору
|
||||
* @param array $paramsEmail Массив с ключами 'login', 'pass', 'email'
|
||||
* @return string "true" при успешной отправке
|
||||
* @throws Exception Если произошла ошибка при отправке email
|
||||
*/
|
||||
function sendRegistrationEmail($paramsEmail) {
|
||||
global $config;
|
||||
|
||||
$to = $config['emailAdmin'];
|
||||
$subject = '=?UTF-8?B?'.base64_encode('Запрос на создание аккаунта на сайте ').'?=' . $_SERVER['HTTP_HOST'];
|
||||
$message = 'Логин: ' . $paramsEmail['login'] . "\r\n";
|
||||
$message .= 'Пароль: ' . $paramsEmail['pass'] . "\r\n";
|
||||
$message .= 'Почта: ' . $paramsEmail['email'] . "\r\n";
|
||||
$message .= 'Сайт: ' . $config['server'] . "\r\n";
|
||||
$headers = "MIME-Version: 1.0" . "\r\n";
|
||||
$headers .= "Content-type: text/html; charset=UTF-8" . "\r\n";
|
||||
$headers .= "From: info@oblat.lv";
|
||||
|
||||
if (!mail($to, $subject, $message, $headers)) {
|
||||
throw new Exception("Error sending email", -32002);
|
||||
}
|
||||
|
||||
return "true";
|
||||
}
|
||||
?>
|
||||
23
main_plugin/auth/lang.js.php
Executable file
23
main_plugin/auth/lang.js.php
Executable file
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
/**
|
||||
* @file lang.js.php
|
||||
* @brief Генерирует JavaScript-файл с языковыми строками для плагина авторизации
|
||||
*/
|
||||
|
||||
global $path, $_SESSION;
|
||||
|
||||
/** @brief Загружает языковой массив для плагина авторизации */
|
||||
$lang = include $path . 'lang.php';
|
||||
|
||||
/** @brief Определяет текущий язык пользователя, берется из GET-параметра или сессии, по умолчанию 'en' */
|
||||
$lng = $_GET['lng'] ?? ($_SESSION['lng'] ?? 'en');
|
||||
|
||||
/** @brief Массив для подстановки языковых строк в шаблон JS */
|
||||
$placeholders = [];
|
||||
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$placeholders['{{' . $key . '}}'] = $value;
|
||||
}
|
||||
|
||||
echo strtr(file_get_contents($path . 'auth.js'), $placeholders);
|
||||
?>
|
||||
61
main_plugin/auth/lang.php
Executable file
61
main_plugin/auth/lang.php
Executable file
@@ -0,0 +1,61 @@
|
||||
<?php
|
||||
$lang = [
|
||||
'ru' => [
|
||||
'authorization' => 'Авторизация',
|
||||
'login_label' => 'Логин',
|
||||
'password_label' => 'Пароль',
|
||||
'incorrect_login_password' => 'Неверный логин или пароль!',
|
||||
'repeat_password_label' => 'Повторите пароль',
|
||||
'email_label' => 'Почта',
|
||||
'fill_all_fields' => 'Вы должны заполнить все поля!',
|
||||
'passwords_do_not_match' => 'Пароли не совпадают!',
|
||||
'account_creation_request_sent' => 'Запрос на создание аккаунта отправлен!',
|
||||
'user_exists' => 'Пользователь с таким логином уже существует!',
|
||||
'account_creation_error' => 'Ошибка при создании аккаунта!',
|
||||
'incorrect_email' => 'Вы ввели неправильную почту!',
|
||||
'account_creation_request_error' => 'Ошибка при отправке запроса на создание аккаунта!',
|
||||
'account_authorization' => 'Авторизация аккаунта',
|
||||
'login' => 'Войти',
|
||||
'register' => 'Зарегистрироваться',
|
||||
'logoff' => 'Выйти',
|
||||
],
|
||||
'en' => [
|
||||
'authorization' => 'Authorization',
|
||||
'login_label' => 'Login',
|
||||
'password_label' => 'Password',
|
||||
'incorrect_login_password' => 'Incorrect login or password!',
|
||||
'repeat_password_label' => 'Repeat password',
|
||||
'email_label' => 'Email',
|
||||
'fill_all_fields' => 'You must fill out all fields!',
|
||||
'passwords_do_not_match' => 'Passwords do not match!',
|
||||
'account_creation_request_sent' => 'Account creation request sent!',
|
||||
'user_exists' => 'User with this login already exists!',
|
||||
'account_creation_error' => 'Error while creating the account!',
|
||||
'incorrect_email' => 'Incorrect email!',
|
||||
'account_creation_request_error' => 'Error sending account creation request!',
|
||||
'account_authorization' => 'Account authorization',
|
||||
'login' => 'Login',
|
||||
'register' => 'Register',
|
||||
'logoff' => 'Log out',
|
||||
],
|
||||
'lv' => [
|
||||
'authorization' => 'Autentifikācija',
|
||||
'login_label' => 'Lietotājvārds',
|
||||
'password_label' => 'Parole',
|
||||
'incorrect_login_password' => 'Nepareizs lietotājvārds vai parole!',
|
||||
'repeat_password_label' => 'Atkārtot paroli',
|
||||
'email_label' => 'E-pasts',
|
||||
'fill_all_fields' => 'Jums jāaizpilda visi lauki!',
|
||||
'passwords_do_not_match' => 'Paroles nesakrīt!',
|
||||
'account_creation_request_sent' => 'Pieprasījums par konta izveidi nosūtīts!',
|
||||
'user_exists' => 'Lietotājs ar šo lietotājvārdu jau pastāv!',
|
||||
'account_creation_error' => 'Kļūda, izveidojot kontu!',
|
||||
'incorrect_email' => 'Nepareizs e-pasts!',
|
||||
'account_creation_request_error' => 'Kļūda, nosūtot pieprasījumu par konta izveidi!',
|
||||
'account_authorization' => 'Konta autentifikācija',
|
||||
'login' => 'Ieiet',
|
||||
'register' => 'Reģistrēties',
|
||||
'logoff' => 'Iziet',
|
||||
],
|
||||
];
|
||||
return $lang;
|
||||
27
main_plugin/auth/plug.php
Executable file
27
main_plugin/auth/plug.php
Executable file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* @file plug.php
|
||||
* @brief Подключает плагин авторизации, подставляет языковые строки и выводит HTML-код формы авторизации
|
||||
*/
|
||||
|
||||
global $path, $_SESSION;
|
||||
|
||||
/** @brief Загружает языковой массив для плагина авторизации */
|
||||
$lang = include $path . 'main_plugin/auth/lang.php';
|
||||
|
||||
/** @brief Определяет текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_SESSION['lng'] ?? 'en';
|
||||
|
||||
include_once $path . 'main_plugin/auth/func.auth.php';
|
||||
|
||||
/** @brief Загружает HTML-шаблон формы авторизации */
|
||||
$Html = file_get_contents($path . 'main_plugin/auth/auth.php');
|
||||
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$Html = str_replace('{{' . $key . '}}', $value, $Html);
|
||||
}
|
||||
|
||||
echo $Html;
|
||||
echo '<link rel="stylesheet" type="text/css" href="/main_plugin/auth/auth.css">';
|
||||
echo '<script type="text/javascript" src="/main_plugin/auth/lang.js.php?lng=' . $lng . '"></script>';
|
||||
?>
|
||||
149
main_plugin/dgrm/dgrm.css
Executable file
149
main_plugin/dgrm/dgrm.css
Executable file
@@ -0,0 +1,149 @@
|
||||
#diagram {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
user-select: none;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 16px;
|
||||
color: rgb(73, 80, 87);
|
||||
outline: none;
|
||||
height: calc(95% + 2px);
|
||||
}
|
||||
|
||||
a {
|
||||
color: #0d6efd;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#dgrmDiv {
|
||||
display: inline-block;
|
||||
user-select: none;
|
||||
width: -webkit-fill-available;
|
||||
border-radius: 5px;
|
||||
height: 600px;
|
||||
font-size: 1em;
|
||||
max-width: calc(100% - 20px);
|
||||
overflow-y: hidden;
|
||||
transform: translate(0%, 0%);
|
||||
}
|
||||
|
||||
#dgrmTop {
|
||||
text-align: center;
|
||||
border-bottom: 1px #40464d solid;
|
||||
padding: 5px;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
#dgrmTopTitle {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#options {
|
||||
position: absolute !important;
|
||||
}
|
||||
.menu {
|
||||
position: absolute !important;
|
||||
}
|
||||
#menu {
|
||||
position: absolute !important;
|
||||
}
|
||||
|
||||
|
||||
|
||||
text {
|
||||
white-space: pre-wrap;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 16px;
|
||||
color: rgb(73, 80, 87);
|
||||
}
|
||||
|
||||
textarea {
|
||||
text-align: center;
|
||||
border: none;;
|
||||
padding: 10px;
|
||||
padding-top: 0.8em;
|
||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-size: 16px;
|
||||
background-color: transparent;
|
||||
color: transparent;
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
resize: none;
|
||||
line-height: 1em;
|
||||
caret-color: #fff;
|
||||
}
|
||||
|
||||
[data-connect] { display: none; }
|
||||
|
||||
.select path[data-key="selected"],
|
||||
.select .path-end,
|
||||
.select [data-connect],
|
||||
.highlight-e [data-key="end"] .path-end,
|
||||
.highlight-s [data-key="start"] .path-end,
|
||||
.hover [data-connect] {
|
||||
display: unset;
|
||||
opacity: 0.51;
|
||||
stroke: rgb(108 187 247);
|
||||
fill: rgb(108 187 247);
|
||||
}
|
||||
[data-connect].hover { stroke-width: 25px; }
|
||||
|
||||
.select path[data-key="selected"] { fill:none; }
|
||||
|
||||
.highlight [data-key="main"]{
|
||||
paint-order: stroke;
|
||||
stroke-width: 10px;
|
||||
stroke: rgb(108 187 247 / 51%);
|
||||
}
|
||||
|
||||
.shpath [data-key="end"] .path,
|
||||
.shpath [data-key="start"] .path { display: none;}
|
||||
.shpath.arw-e [data-key="end"] .path,
|
||||
.shpath.arw-s [data-key="start"] .path { display: unset;}
|
||||
.shpath.dash [data-key="path"] { stroke-dasharray:5; }
|
||||
|
||||
@media (pointer: coarse) {
|
||||
circle.path-end { r: 20px; }
|
||||
.ative-elem {
|
||||
stroke: rgb(108 187 247 / 51%);
|
||||
stroke-width: 70px;
|
||||
}
|
||||
|
||||
[data-connect] { stroke-width: 15px; }
|
||||
[data-connect].hover { stroke-width: 70px; }
|
||||
}
|
||||
|
||||
|
||||
.shrect.ta-1 text, .shtxt.ta-1 text { text-anchor: start; }
|
||||
.shrect.ta-2 text, .shtxt.ta-2 text { text-anchor: middle; }
|
||||
.shrect.ta-3 text, .shtxt.ta-3 text { text-anchor: end; }
|
||||
.shrect.ta-1 textarea, .shtxt.ta-1 textarea { text-align: left; }
|
||||
.shrect.ta-2 textarea, .shtxt.ta-2 textarea { text-align: center; }
|
||||
.shrect.ta-3 textarea, .shtxt.ta-3 textarea { text-align: right; }
|
||||
.shtxt textarea { caret-color: rgb(73, 80, 87); }
|
||||
.shtxt text { fill:rgb(73, 80, 87); }
|
||||
.shtxt [data-key="main"] { fill: transparent; stroke: transparent; }
|
||||
.shtxt.select [data-key="main"], .shtxt.highlight [data-key="main"] { stroke: rgb(108 187 247 / 51%); stroke-width: 2px; }
|
||||
|
||||
.shrhomb.highlight [data-key="border"] { stroke-width: 28px; stroke: rgb(108 187 247 / 51%); }
|
||||
.shrhomb.highlight [data-key="main"] { stroke-width:18px; stroke:#1D809F; }
|
||||
|
||||
.cl-red [data-key="main"] { fill: #E74C3C; } .cl-red .path { stroke: #E74C3C;}
|
||||
.cl-orange [data-key="main"] { fill: #ff6600;} .cl-orange .path { stroke: #ff6600;}
|
||||
.cl-green [data-key="main"] { fill: #19bc9b;} .cl-green .path { stroke: #19bc9b;}
|
||||
.cl-blue [data-key="main"] { fill: #1aaee5;} .cl-blue .path { stroke: #1aaee5;}
|
||||
.cl-dblue [data-key="main"] { fill: #1D809F;} .cl-dblue .path { stroke: #1D809F;}
|
||||
.cl-dgray [data-key="main"] { fill: #495057;} .cl-dgray .path { stroke: #495057;}
|
||||
|
||||
.shtxt.cl-red [data-key="main"] { fill: transparent; } .shtxt.cl-red text { fill: #E74C3C; }
|
||||
.shtxt.cl-orange [data-key="main"] { fill: transparent; } .shtxt.cl-orange text { fill: #ff6600; }
|
||||
.shtxt.cl-green [data-key="main"] { fill: transparent; } .shtxt.cl-green text { fill: #19bc9b; }
|
||||
.shtxt.cl-blue [data-key="main"] { fill: transparent; } .shtxt.cl-blue text { fill: #1aaee5; }
|
||||
.shtxt.cl-dblue [data-key="main"] { fill: transparent; } .shtxt.cl-dblue text { fill: #1D809F; }
|
||||
.shtxt.cl-dgray [data-key="main"] { fill: transparent; } .shtxt.cl-dgray text { fill: #495057; }
|
||||
|
||||
.shrhomb.cl-red [data-key="main"] { stroke-width:18px; stroke:#E74C3C; }
|
||||
.shrhomb.cl-orange [data-key="main"] { stroke-width:18px; stroke:#ff6600; }
|
||||
.shrhomb.cl-green [data-key="main"] { stroke-width:18px; stroke:#19bc9b; }
|
||||
.shrhomb.cl-blue [data-key="main"] { stroke-width:18px; stroke:#1aaee5; }
|
||||
.shrhomb.cl-dblue [data-key="main"] { stroke-width:18px; stroke:#1D809F; }
|
||||
.shrhomb.cl-dgray [data-key="main"] { stroke-width:18px; stroke:#495057; }
|
||||
26
main_plugin/dgrm/diagram/canvas-clear.js
Executable file
26
main_plugin/dgrm/diagram/canvas-clear.js
Executable file
@@ -0,0 +1,26 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { PathSmbl } from '../shapes/path-smbl.js';
|
||||
import { ShapeSmbl } from '../shapes/shape-smbl.js';
|
||||
|
||||
/** @param {CanvasElement} canvas */
|
||||
export function canvasClear(canvas) {
|
||||
while (canvas.firstChild) {
|
||||
(canvas.firstChild[ShapeSmbl] || canvas.firstChild[PathSmbl]).del();
|
||||
}
|
||||
canvas[CanvasSmbl].move(0, 0, 1);
|
||||
}
|
||||
|
||||
//
|
||||
// selection clear function
|
||||
|
||||
/** @param {CanvasElement} canvas */
|
||||
export function canvasSelectionClear(canvas) {
|
||||
if (canvas[CanvasSmbl].selectClear) { canvas[CanvasSmbl].selectClear(); };
|
||||
}
|
||||
|
||||
/** @param {CanvasElement} canvas, @param {()=>void} clearFn */
|
||||
export function canvasSelectionClearSet(canvas, clearFn) {
|
||||
canvas[CanvasSmbl].selectClear = clearFn;
|
||||
}
|
||||
|
||||
/** @typedef { import('../infrastructure/move-scale-applay.js').CanvasElement } CanvasElement */
|
||||
30
main_plugin/dgrm/diagram/dgrm-png.js
Executable file
30
main_plugin/dgrm/diagram/dgrm-png.js
Executable file
@@ -0,0 +1,30 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {any} serializedData
|
||||
* @param {function(Blob):void} callBack
|
||||
*/
|
||||
export function fileSaveSvg(canvas, serializedData, callBack) {
|
||||
const svgVirtual = /** @type {SVGSVGElement} */(canvas.ownerSVGElement.cloneNode(true));
|
||||
|
||||
svgVirtual.style.backgroundImage = null;
|
||||
svgVirtual.querySelectorAll('.select, .highlight').forEach(el => el.classList.remove('select', 'highlight'));
|
||||
|
||||
const nonSvgElems = svgVirtual.getElementsByTagName('foreignObject');
|
||||
while (nonSvgElems[0]) { nonSvgElems[0].parentNode.removeChild(nonSvgElems[0]); }
|
||||
|
||||
/* svgVirtual.querySelectorAll('g.hovertrack.shtxt.ta-1').forEach(group => {
|
||||
group.querySelectorAll('text, tspan').forEach(el => console.log(el));
|
||||
}); */
|
||||
|
||||
const svgStr = new XMLSerializer().serializeToString(svgVirtual);
|
||||
const blob = new Blob([svgStr], { type: 'image/svg+xml;charset=utf-8' });
|
||||
callBack(blob);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
108
main_plugin/dgrm/diagram/dgrm-serialization.js
Executable file
108
main_plugin/dgrm/diagram/dgrm-serialization.js
Executable file
@@ -0,0 +1,108 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { PathSmbl } from '../shapes/path-smbl.js';
|
||||
import { ShapeSmbl } from '../shapes/shape-smbl.js';
|
||||
import { canvasClear } from './canvas-clear.js';
|
||||
|
||||
const v = '1.1';
|
||||
|
||||
/** @param {Element} canvas */
|
||||
export const serialize = (canvas) => serializeShapes(/** @type {Array<ShapeElement & PathElement>} */([...canvas.children]));
|
||||
|
||||
/** @param {Array<ShapeElement & PathElement>} shapes */
|
||||
export function serializeShapes(shapes) {
|
||||
/** @type {DiagramSerialized} */
|
||||
const diagramSerialized = { v, s: [] };
|
||||
for (const shape of shapes) {
|
||||
if (shape[ShapeSmbl]) {
|
||||
// shape
|
||||
diagramSerialized.s.push(shape[ShapeSmbl].data);
|
||||
} else {
|
||||
// path
|
||||
|
||||
/** @param {PathEnd} pathEnd */
|
||||
function pathSerialize(pathEnd) {
|
||||
const shapeIndex = shapes.indexOf(pathEnd.shape?.shapeEl);
|
||||
return (shapeIndex !== -1)
|
||||
? { s: shapeIndex, k: pathEnd.shape.connectorKey }
|
||||
: { p: pathEnd.data };
|
||||
}
|
||||
|
||||
const pathData = shape[PathSmbl].data;
|
||||
const pathJson = { type: 0, s: pathSerialize(pathData.s), e: pathSerialize(pathData.e) };
|
||||
if (pathData.styles) { pathJson.c = pathData.styles; }
|
||||
|
||||
diagramSerialized.s.push(pathJson);
|
||||
}
|
||||
}
|
||||
|
||||
return diagramSerialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {DiagramSerialized} data
|
||||
* @param {Boolean=} dontClear
|
||||
*/
|
||||
export function deserialize(canvas, data, dontClear) {
|
||||
if (data.v !== v) { alert('Wrong format'); return null; }
|
||||
if (!dontClear) { canvasClear(canvas); }
|
||||
|
||||
/** @type {Map<ShapeData, ShapeElement>} */
|
||||
const shapeDataToElem = new Map();
|
||||
|
||||
/** @param {ShapeData} shapeData */
|
||||
function shapeEnsure(shapeData) {
|
||||
let shapeEl = shapeDataToElem.get(shapeData);
|
||||
if (!shapeEl) {
|
||||
shapeEl = canvas[CanvasSmbl].shapeMap[shapeData.type].create(shapeData);
|
||||
canvas.append(shapeEl);
|
||||
shapeDataToElem.set(shapeData, shapeEl);
|
||||
}
|
||||
return shapeEl;
|
||||
}
|
||||
|
||||
/** @param {number?} index */
|
||||
const shapeByIndex = index => shapeEnsure(/** @type {ShapeData} */(data.s[index]));
|
||||
|
||||
/** @type {PathElement[]} */
|
||||
const paths = [];
|
||||
for (const shape of data.s) {
|
||||
switch (shape.type) {
|
||||
// path
|
||||
case 0: {
|
||||
/** @param {PathEndSerialized} pathEnd */
|
||||
const pathDeserialize = pathEnd => pathEnd.p
|
||||
? { data: pathEnd.p }
|
||||
: { shape: { shapeEl: shapeByIndex(pathEnd.s), connectorKey: pathEnd.k } };
|
||||
|
||||
const path = canvas[CanvasSmbl].shapeMap[0].create({
|
||||
styles: /** @type {PathSerialized} */(shape).c,
|
||||
s: pathDeserialize(/** @type {PathSerialized} */(shape).s),
|
||||
e: pathDeserialize(/** @type {PathSerialized} */(shape).e)
|
||||
});
|
||||
paths.push(path);
|
||||
canvas.append(path);
|
||||
break;
|
||||
}
|
||||
default: shapeEnsure(/** @type {ShapeData} */(shape)); break;
|
||||
}
|
||||
}
|
||||
|
||||
return [...shapeDataToElem.values(), ...paths];
|
||||
}
|
||||
|
||||
/** @typedef {{v:string, s: Array<ShapeData | PathSerialized>}} DiagramSerialized */
|
||||
|
||||
/** @typedef { import("../shapes/shape-smbl").ShapeElement } ShapeElement */
|
||||
/** @typedef { import('../shapes/shape-evt-proc').ShapeData } ShapeData */
|
||||
|
||||
/** @typedef { import("../shapes/path-smbl").PathElement } PathElement */
|
||||
/** @typedef { import('../shapes/path').PathEndData } PathEndData */
|
||||
/** @typedef { import('../shapes/path').PathEnd } PathEnd */
|
||||
/** @typedef { import('../shapes/path').PathData } PathData */
|
||||
|
||||
/** @typedef { {s?:number, k?:string, p?:PathEndData} } PathEndSerialized */
|
||||
/** @typedef { {type:number, c?:string, s:PathEndSerialized, e:PathEndSerialized} } PathSerialized */
|
||||
|
||||
/** @typedef { import('../shapes/shape-evt-proc').CanvasData } CanvasData */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
32
main_plugin/dgrm/diagram/dgrm-srv.js
Executable file
32
main_plugin/dgrm/diagram/dgrm-srv.js
Executable file
@@ -0,0 +1,32 @@
|
||||
const svrApi = 'https://localhost:7156/api';
|
||||
|
||||
/**
|
||||
* @param {string} key
|
||||
* @param {DiagramSerialized} serialized
|
||||
* @returns {Promise}
|
||||
*/
|
||||
export async function srvSave(key, serialized) {
|
||||
return await fetch(`${svrApi}/${key}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json;charset=utf-8' },
|
||||
body: JSON.stringify(serialized)
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* get diagram json by key
|
||||
* @param {string} key
|
||||
* @returns {Promise<DiagramSerialized>}
|
||||
*/
|
||||
export async function srvGet(key) {
|
||||
return (await fetch(`${svrApi}/${key}`)).json();
|
||||
}
|
||||
|
||||
export function generateKey() {
|
||||
const arr = new Uint8Array((8 / 2));
|
||||
window.crypto.getRandomValues(arr);
|
||||
const date = new Date();
|
||||
return `${date.getUTCFullYear()}${(date.getUTCMonth() + 1).toString().padStart(2, '0')}${Array.from(arr, dec => dec.toString(16).padStart(2, '0')).join('')}`;
|
||||
}
|
||||
|
||||
/** @typedef { import("./dgrm-serialization").DiagramSerialized } DiagramSerialized */
|
||||
61
main_plugin/dgrm/diagram/group-move.js
Executable file
61
main_plugin/dgrm/diagram/group-move.js
Executable file
@@ -0,0 +1,61 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { placeToCell, pointInCanvas } from '../infrastructure/move-scale-applay.js';
|
||||
import { pointShift } from '../infrastructure/util.js';
|
||||
|
||||
/** @param {CanvasElement} canvas, @param {DiagramSerialized} data */
|
||||
export function groupMoveToCenter(canvas, data) {
|
||||
const screenCenter = pointInCanvas(canvas[CanvasSmbl].data, window.innerWidth / 2, window.innerHeight / 2);
|
||||
placeToCell(screenCenter, canvas[CanvasSmbl].data.cell);
|
||||
|
||||
const shift = pointShift(screenCenter, centerCalc(data), -1);
|
||||
iteratePoints(data, point => { if (point) { pointShift(point, shift); } });
|
||||
}
|
||||
|
||||
/** @param {DiagramSerialized} data */
|
||||
function centerCalc(data) {
|
||||
const minMax = maxAndMinPoint(data);
|
||||
return {
|
||||
x: minMax.min.x + (minMax.max.x - minMax.min.x) / 2,
|
||||
y: minMax.min.y + (minMax.max.y - minMax.min.y) / 2
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {DiagramSerialized} data */
|
||||
function maxAndMinPoint(data) {
|
||||
/** @type {Point} */
|
||||
const min = { x: Infinity, y: Infinity };
|
||||
|
||||
/** @type {Point} */
|
||||
const max = { x: -Infinity, y: -Infinity };
|
||||
|
||||
iteratePoints(data, point => {
|
||||
if (!point) { return; }
|
||||
|
||||
if (min.x > point.x) { min.x = point.x; }
|
||||
if (min.y > point.y) { min.y = point.y; }
|
||||
|
||||
if (max.x < point.x) { max.x = point.x; }
|
||||
if (max.y < point.y) { max.y = point.y; }
|
||||
});
|
||||
return { min, max };
|
||||
}
|
||||
|
||||
/** @param {DiagramSerialized} data, @param {(point:Point)=>void} callbackfn */
|
||||
function iteratePoints(data, callbackfn) {
|
||||
data.s.forEach(shapeOrPath => {
|
||||
if (shapeOrPath.type === 0) {
|
||||
// path
|
||||
callbackfn(/** @type {PathSerialized} */(shapeOrPath).s.p?.position);
|
||||
callbackfn(/** @type {PathSerialized} */(shapeOrPath).e.p?.position);
|
||||
} else {
|
||||
// shape
|
||||
callbackfn(/** @type {ShapeData} */(shapeOrPath).position);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('./dgrm-serialization.js').DiagramSerialized } DiagramSerialized */
|
||||
/** @typedef { import('./dgrm-serialization.js').PathSerialized } PathSerialized */
|
||||
/** @typedef { import('../shapes/shape-evt-proc.js').ShapeData } ShapeData */
|
||||
396
main_plugin/dgrm/diagram/group-select-applay.js
Executable file
396
main_plugin/dgrm/diagram/group-select-applay.js
Executable file
@@ -0,0 +1,396 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { movementApplay, ProcessedSmbl, shapeSelect } from '../infrastructure/move-evt-proc.js';
|
||||
import { placeToCell, pointInCanvas } from '../infrastructure/move-scale-applay.js';
|
||||
import { arrPop, classAdd, classDel, deepCopy, listen, listenDel, positionSet, svgEl } from '../infrastructure/util.js';
|
||||
import { PathSmbl } from '../shapes/path-smbl.js';
|
||||
import { ShapeSmbl } from '../shapes/shape-smbl.js';
|
||||
import { GroupSettings } from './group-settings.js';
|
||||
import { modalCreate } from '../shapes/modal-create.js';
|
||||
import { groupMoveToCenter } from './group-move.js';
|
||||
import { deserialize, serializeShapes } from './dgrm-serialization.js';
|
||||
import { canvasSelectionClear, canvasSelectionClearSet } from './canvas-clear.js';
|
||||
import { tipShow } from '../ui/ui.js';
|
||||
|
||||
//
|
||||
// copy past
|
||||
|
||||
const clipboardDataKey = 'dgrm';
|
||||
|
||||
/** @param {() => Array<ShapeElement & PathElement>} shapesToClipboardGetter */
|
||||
export function listenCopy(shapesToClipboardGetter) {
|
||||
/** @param {ClipboardEvent & {target:HTMLElement | SVGElement}} evt */
|
||||
function onCopy(evt) {
|
||||
const shapes = shapesToClipboardGetter();
|
||||
if (document.activeElement === shapes[0].ownerSVGElement) {
|
||||
evt.preventDefault();
|
||||
evt.clipboardData.setData(
|
||||
clipboardDataKey,
|
||||
JSON.stringify(copyDataCreate(shapes)));
|
||||
}
|
||||
}
|
||||
document.addEventListener('copy', onCopy);
|
||||
|
||||
// dispose fn
|
||||
return function() {
|
||||
listenDel(document, 'copy', onCopy);
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {CanvasElement} canvas */
|
||||
export function copyPastApplay(canvas) {
|
||||
listen(document, 'paste', /** @param {ClipboardEvent & {target:HTMLElement | SVGElement}} evt */ evt => {
|
||||
if (evt.target.tagName.toUpperCase() === 'TEXTAREA') { return; }
|
||||
// if (document.activeElement !== canvas.ownerSVGElement) { return; }
|
||||
|
||||
const dataStr = evt.clipboardData.getData(clipboardDataKey);
|
||||
if (!dataStr) { return; }
|
||||
|
||||
tipShow(false);
|
||||
canvasSelectionClear(canvas);
|
||||
past(canvas, JSON.parse(dataStr));
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {CanvasElement} canvas, @param {Array<ShapeElement & PathElement>} shapes */
|
||||
export const copyAndPast = (canvas, shapes) => past(canvas, copyDataCreate(shapes));
|
||||
|
||||
/** @param {Array<ShapeElement & PathElement>} shapes */
|
||||
const copyDataCreate = shapes => deepCopy(serializeShapes(shapes));
|
||||
|
||||
/** @param {CanvasElement} canvas, @param {DiagramSerialized} data */
|
||||
function past(canvas, data) {
|
||||
canvasSelectionClear(canvas);
|
||||
groupMoveToCenter(canvas, data);
|
||||
groupSelect(canvas, deserialize(canvas, data, true));
|
||||
}
|
||||
|
||||
//
|
||||
// group select
|
||||
|
||||
const highlightSClass = 'highlight-s';
|
||||
const highlightEClass = 'highlight-e';
|
||||
const highlightClass = 'highlight';
|
||||
|
||||
/** wait long press and draw selected rectangle
|
||||
* @param {CanvasElement} canvas
|
||||
*/
|
||||
export function groupSelectApplay(canvas) {
|
||||
const svg = canvas.ownerSVGElement;
|
||||
let timer;
|
||||
/** @type {Point} */ let selectStart;
|
||||
/** @type {SVGCircleElement} */ let startCircle;
|
||||
/** @type {SVGRectElement} */ let selectRect;
|
||||
/** @type {Point} */ let selectRectPos;
|
||||
|
||||
/** @param {PointerEvent} evt */
|
||||
function onMove(evt) {
|
||||
if (evt[ProcessedSmbl] || !selectRect) { reset(); return; }
|
||||
evt[ProcessedSmbl] = true;
|
||||
|
||||
if (startCircle) { startCircle.remove(); startCircle = null; }
|
||||
|
||||
// draw rect
|
||||
const x = evt.clientX - selectStart.x;
|
||||
const y = evt.clientY - selectStart.y;
|
||||
selectRect.width.baseVal.value = Math.abs(x);
|
||||
selectRect.height.baseVal.value = Math.abs(y);
|
||||
if (x < 0) { selectRectPos.x = evt.clientX; }
|
||||
if (y < 0) { selectRectPos.y = evt.clientY; }
|
||||
selectRect.style.transform = `translate(${selectRectPos.x}px, ${selectRectPos.y}px)`;
|
||||
}
|
||||
|
||||
function onUp() {
|
||||
if (selectRect) {
|
||||
/** @param {Point} point */
|
||||
const inRect = point => pointInRect(
|
||||
pointInCanvas(canvas[CanvasSmbl].data, selectRectPos.x, selectRectPos.y),
|
||||
selectRect.width.baseVal.value / canvas[CanvasSmbl].data.scale,
|
||||
selectRect.height.baseVal.value / canvas[CanvasSmbl].data.scale,
|
||||
point.x, point.y);
|
||||
|
||||
// select shapes in rect
|
||||
groupSelect(
|
||||
canvas,
|
||||
/** @type {Iterable<ShapeOrPathElement>} */(canvas.children),
|
||||
inRect);
|
||||
}
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
clearTimeout(timer); timer = null;
|
||||
startCircle?.remove(); startCircle = null;
|
||||
selectRect?.remove(); selectRect = null;
|
||||
|
||||
listenDel(svg, 'pointermove', onMove);
|
||||
listenDel(svg, 'wheel', reset);
|
||||
listenDel(svg, 'pointerup', onUp);
|
||||
}
|
||||
|
||||
listen(svg, 'pointerdown', /** @param {PointerEvent} evt */ evt => {
|
||||
if (evt[ProcessedSmbl] || !evt.isPrimary) { reset(); return; }
|
||||
|
||||
listen(svg, 'pointermove', onMove);
|
||||
listen(svg, 'wheel', reset, true);
|
||||
listen(svg, 'pointerup', onUp, true);
|
||||
|
||||
timer = setTimeout(_ => {
|
||||
canvasSelectionClear(canvas);
|
||||
|
||||
startCircle = svgEl('circle');
|
||||
classAdd(startCircle, 'ative-elem');
|
||||
startCircle.style.cssText = 'r:10px; fill: rgb(108 187 247 / 51%)';
|
||||
positionSet(startCircle, { x: evt.clientX, y: evt.clientY });
|
||||
svg.append(startCircle);
|
||||
|
||||
selectStart = { x: evt.clientX, y: evt.clientY };
|
||||
selectRectPos = { x: evt.clientX, y: evt.clientY };
|
||||
selectRect = svgEl('rect');
|
||||
selectRect.style.cssText = 'rx:10px; fill: rgb(108 187 247 / 51%)';
|
||||
positionSet(selectRect, selectRectPos);
|
||||
svg.append(selectRect);
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Highlight selected shapes and procces group operations (move, del, copy)
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {Iterable<ShapeOrPathElement>} elems
|
||||
* @param {{(position:Point):boolean}=} inRect
|
||||
*/
|
||||
export function groupSelect(canvas, elems, inRect) {
|
||||
/** @param {{position:Point}} data */
|
||||
const shapeInRect = data => inRect ? inRect(data.position) : true;
|
||||
|
||||
/** @type {Selected} */
|
||||
const selected = {
|
||||
shapes: [],
|
||||
shapesPaths: [],
|
||||
pathEnds: [],
|
||||
pathEndsPaths: []
|
||||
};
|
||||
|
||||
/**
|
||||
* @param {ShapeOrPathElement} pathEl, @param {PathEnd} pathEnd, @param {string} highlightClass
|
||||
* @returns {1|2|0}
|
||||
*/
|
||||
function pathEndInRect(pathEl, pathEnd, highlightClass) {
|
||||
if (!pathEnd.shape && shapeInRect(pathEnd.data)) {
|
||||
selected.pathEnds.push(pathEnd);
|
||||
classAdd(pathEl, highlightClass);
|
||||
return 1; // connect to end in rect
|
||||
} else if (pathEnd.shape && shapeInRect(pathEnd.shape.shapeEl[ShapeSmbl].data)) {
|
||||
return 2; // connect to shape in rect
|
||||
}
|
||||
return 0; // not in rect
|
||||
}
|
||||
|
||||
for (const shapeEl of elems) {
|
||||
if (shapeEl[ShapeSmbl]) {
|
||||
if (shapeInRect(shapeEl[ShapeSmbl].data)) {
|
||||
classAdd(shapeEl, highlightClass);
|
||||
selected.shapes.push(shapeEl);
|
||||
}
|
||||
} else if (shapeEl[PathSmbl]) {
|
||||
const isStartIn = pathEndInRect(shapeEl, shapeEl[PathSmbl].data.s, highlightSClass);
|
||||
const isEndIn = pathEndInRect(shapeEl, shapeEl[PathSmbl].data.e, highlightEClass);
|
||||
|
||||
if (isStartIn === 1 || isEndIn === 1) {
|
||||
selected.pathEndsPaths.push(shapeEl);
|
||||
}
|
||||
|
||||
if (isStartIn === 2 || isEndIn === 2) {
|
||||
selected.shapesPaths.push(shapeEl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
groupEvtProc(canvas, selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {Selected} selected
|
||||
*/
|
||||
function groupEvtProc(canvas, selected) {
|
||||
// only one shape selected
|
||||
if (selected.shapes?.length === 1 && !selected.pathEnds?.length) {
|
||||
classDel(selected.shapes[0], 'highlight');
|
||||
shapeSelect(selected.shapes[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// only one pathEnd selected
|
||||
if (!selected.shapes?.length && selected.pathEnds?.length === 1) {
|
||||
pathUnhighlight(selected.pathEndsPaths[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
// only one path selected
|
||||
if (!selected.shapes?.length && selected.pathEnds?.length === 2 && selected.pathEndsPaths?.length === 1) {
|
||||
pathUnhighlight(selected.pathEndsPaths[0]);
|
||||
shapeSelect(selected.pathEndsPaths[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = canvas.ownerSVGElement;
|
||||
let isMove = false;
|
||||
let isDownOnSelectedShape = false;
|
||||
|
||||
/** @type {{del():void}} */
|
||||
let settingsPnl;
|
||||
const pnlDel = () => { settingsPnl?.del(); settingsPnl = null; };
|
||||
|
||||
/** @param {PointerEvent & {target:Node}} evt */
|
||||
function down(evt) {
|
||||
pnlDel();
|
||||
isDownOnSelectedShape =
|
||||
selected.shapes?.some(shapeEl => shapeEl.contains(evt.target)) ||
|
||||
selected.pathEnds?.some(pathEnd => pathEnd.el.contains(evt.target));
|
||||
|
||||
// down on not selected shape
|
||||
if (!isDownOnSelectedShape && evt.target !== svg) {
|
||||
dispose();
|
||||
return;
|
||||
}
|
||||
|
||||
if (isDownOnSelectedShape) {
|
||||
evt.stopImmediatePropagation();
|
||||
}
|
||||
|
||||
svg.setPointerCapture(evt.pointerId);
|
||||
listen(svg, 'pointerup', up, true);
|
||||
listen(svg, 'pointermove', move);
|
||||
}
|
||||
|
||||
/** @param { {(point:Point):void} } pointMoveFn */
|
||||
function drawSelection(pointMoveFn) {
|
||||
selected.shapes?.forEach(shapeEl => {
|
||||
pointMoveFn(shapeEl[ShapeSmbl].data.position);
|
||||
shapeEl[ShapeSmbl].drawPosition();
|
||||
});
|
||||
selected.pathEnds?.forEach(pathEnd => pointMoveFn(pathEnd.data.position));
|
||||
selected.pathEndsPaths?.forEach(path => path[PathSmbl].draw());
|
||||
}
|
||||
|
||||
/** @param {PointerEvent} evt */
|
||||
function up(evt) {
|
||||
if (!isMove) {
|
||||
// click on canvas
|
||||
if (!isDownOnSelectedShape) { dispose(); return; }
|
||||
|
||||
// click on selected shape - show settings panel
|
||||
settingsPnl = modalCreate(evt.clientX - 10, evt.clientY - 10, new GroupSettings(cmd => {
|
||||
switch (cmd) {
|
||||
case 'del':
|
||||
arrPop(selected.shapes, shapeEl => shapeEl[ShapeSmbl].del());
|
||||
arrPop(selected.pathEndsPaths, pathEl => pathEl[PathSmbl].del());
|
||||
dispose();
|
||||
break;
|
||||
case 'copy': {
|
||||
copyAndPast(canvas, elemsToCopyGet(selected)); // will call dispose
|
||||
break;
|
||||
}
|
||||
}
|
||||
}));
|
||||
} else {
|
||||
// move end
|
||||
drawSelection(point => placeToCell(point, canvas[CanvasSmbl].data.cell));
|
||||
}
|
||||
|
||||
dispose(true);
|
||||
}
|
||||
|
||||
/** @param {PointerEventFixMovement} evt */
|
||||
function move(evt) {
|
||||
// move canvas
|
||||
if (!isDownOnSelectedShape) { dispose(true); return; }
|
||||
|
||||
// move selected shapes
|
||||
isMove = true;
|
||||
drawSelection(point => movementApplay(point, canvas[CanvasSmbl].data.scale, evt));
|
||||
}
|
||||
|
||||
/** @param {boolean=} saveOnDown */
|
||||
function dispose(saveOnDown) {
|
||||
listenDel(svg, 'pointerup', up);
|
||||
listenDel(svg, 'pointermove', move);
|
||||
isMove = false;
|
||||
isDownOnSelectedShape = false;
|
||||
|
||||
if (!saveOnDown) {
|
||||
canvasSelectionClearSet(canvas, null);
|
||||
if (listenCopyDispose) { listenCopyDispose(); listenCopyDispose = null; }
|
||||
|
||||
listenDel(svg, 'pointerdown', down, true);
|
||||
pnlDel();
|
||||
arrPop(selected.shapes, shapeEl => classDel(shapeEl, highlightClass));
|
||||
arrPop(selected.pathEndsPaths, pathEl => pathUnhighlight(pathEl));
|
||||
selected.pathEnds = null;
|
||||
selected.shapesPaths = null;
|
||||
}
|
||||
}
|
||||
|
||||
svg.addEventListener('pointerdown', down, { passive: true, capture: true });
|
||||
|
||||
canvasSelectionClearSet(canvas, dispose);
|
||||
let listenCopyDispose = listenCopy(() => elemsToCopyGet(selected));
|
||||
}
|
||||
|
||||
/** @param {Selected} selected */
|
||||
function elemsToCopyGet(selected) {
|
||||
/** @type {Set<PathElement>} */
|
||||
const fullSelectedPaths = new Set();
|
||||
|
||||
/** @param {PathEnd} pathEnd */
|
||||
const pathEndSelected = pathEnd =>
|
||||
selected.shapes.includes(pathEnd.shape?.shapeEl) || selected.pathEnds.includes(pathEnd);
|
||||
|
||||
/** @param {PathElement} pathEl */
|
||||
function fullSelectedPathAdd(pathEl) {
|
||||
if (pathEndSelected(pathEl[PathSmbl].data.s) && pathEndSelected(pathEl[PathSmbl].data.e)) {
|
||||
fullSelectedPaths.add(pathEl);
|
||||
}
|
||||
}
|
||||
|
||||
selected.shapesPaths?.forEach(fullSelectedPathAdd);
|
||||
selected.pathEndsPaths?.forEach(fullSelectedPathAdd);
|
||||
|
||||
return [...selected.shapes, ...fullSelectedPaths];
|
||||
}
|
||||
|
||||
/** @param {PathElement} pathEl`` */
|
||||
function pathUnhighlight(pathEl) {
|
||||
classDel(pathEl, highlightSClass);
|
||||
classDel(pathEl, highlightEClass);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Point} rectPosition
|
||||
* @param {number} rectWidth, @param {number} rectHeight
|
||||
* @param {number} x, @param {number} y
|
||||
*/
|
||||
const pointInRect = (rectPosition, rectWidth, rectHeight, x, y) =>
|
||||
rectPosition.x <= x && x <= rectPosition.x + rectWidth &&
|
||||
rectPosition.y <= y && y <= rectPosition.y + rectHeight;
|
||||
|
||||
/**
|
||||
* @typedef { {
|
||||
* shapes:ShapeElement[]
|
||||
* shapesPaths:PathElement[]
|
||||
* pathEnds: PathEnd[]
|
||||
* pathEndsPaths: PathElement[]
|
||||
* } } Selected
|
||||
*/
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('../shapes/shape-smbl').ShapeElement } ShapeElement */
|
||||
/** @typedef { import('../shapes/shape-evt-proc').Shape } Shape */
|
||||
/** @typedef { import('../shapes/path').Path } Path */
|
||||
/** @typedef { import('../shapes/path').PathEnd } PathEnd */
|
||||
/** @typedef { import('../shapes/path-smbl').PathElement } PathElement */
|
||||
/** @typedef { SVGGraphicsElement & { [ShapeSmbl]?: Shape, [PathSmbl]?:Path }} ShapeOrPathElement */
|
||||
/** @typedef { import('../infrastructure/move-evt-mobile-fix.js').PointerEventFixMovement} PointerEventFixMovement */
|
||||
/** @typedef { import('./dgrm-serialization.js').DiagramSerialized } DiagramSerialized */
|
||||
32
main_plugin/dgrm/diagram/group-settings.js
Executable file
32
main_plugin/dgrm/diagram/group-settings.js
Executable file
@@ -0,0 +1,32 @@
|
||||
import { copySvg, delSvg } from '../infrastructure/assets.js';
|
||||
import { clickForAll, evtTargetAttr } from '../infrastructure/util.js';
|
||||
|
||||
export class GroupSettings extends HTMLElement {
|
||||
/** @param {(cms:string)=>void} cmdHandler */
|
||||
constructor(cmdHandler) {
|
||||
super();
|
||||
/** @private */
|
||||
this._cmdHandler = cmdHandler;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
.ln { display: flex; }
|
||||
.ln > * {
|
||||
height: 24px;
|
||||
padding: 10px;
|
||||
}
|
||||
[data-cmd] { cursor: pointer; }
|
||||
</style>
|
||||
<div class="ln">
|
||||
${copySvg}
|
||||
${delSvg}
|
||||
</div>`;
|
||||
|
||||
clickForAll(shadow, '[data-cmd]',
|
||||
evt => this._cmdHandler(evtTargetAttr(evt, 'data-cmd')));
|
||||
}
|
||||
}
|
||||
customElements.define('ap-grp-settings', GroupSettings);
|
||||
52
main_plugin/dgrm/index.js
Executable file
52
main_plugin/dgrm/index.js
Executable file
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* @file index.js
|
||||
* @brief Основной файл диаграммного редактора, содержит инициализацию канваса, загрузку диаграмм и подключение модулей UI
|
||||
*/
|
||||
|
||||
import { moveEvtMobileFix } from './infrastructure/move-evt-mobile-fix.js';
|
||||
import { CanvasSmbl } from './infrastructure/canvas-smbl.js';
|
||||
import { moveScaleApplay } from './infrastructure/move-scale-applay.js';
|
||||
import { evtRouteApplay } from './infrastructure/evt-route-applay.js';
|
||||
import { tipShow, uiDisable } from './ui/ui.js';
|
||||
import { srvGet } from './diagram/dgrm-srv.js';
|
||||
import { deserialize } from './diagram/dgrm-serialization.js';
|
||||
import { copyPastApplay, groupSelectApplay } from './diagram/group-select-applay.js';
|
||||
import { shapeTypeMap } from './shapes/shape-type-map.js';
|
||||
import './ui/menu.js';
|
||||
import './ui/shape-menu.js';
|
||||
|
||||
// @ts-ignore
|
||||
/** @type {import('./infrastructure/canvas-smbl.js').CanvasElement} */
|
||||
/** @brief Элемент канваса */
|
||||
const canvas = document.getElementById('canvas');
|
||||
/** @brief Данные канваса и отображение фигур */
|
||||
canvas[CanvasSmbl] = {
|
||||
data: {
|
||||
position: { x: 0, y: 0 },
|
||||
scale: 1,
|
||||
cell: 24
|
||||
},
|
||||
shapeMap: shapeTypeMap(canvas)
|
||||
};
|
||||
|
||||
moveEvtMobileFix(canvas.ownerSVGElement);
|
||||
evtRouteApplay(canvas.ownerSVGElement);
|
||||
copyPastApplay(canvas);
|
||||
groupSelectApplay(canvas); // groupSelectApplay must go before moveScaleApplay
|
||||
moveScaleApplay(canvas);
|
||||
|
||||
/** @type { import('./ui/menu').Menu } */(document.getElementById('menu')).init(canvas);
|
||||
/** @type { import('./ui/shape-menu').ShapeMenu } */(document.getElementById('menu-shape')).init(canvas);
|
||||
|
||||
/** @brief Загружает диаграмму по ссылке, если указан параметр k */
|
||||
let url = new URL(window.location.href);
|
||||
if (url.searchParams.get('k')) {
|
||||
uiDisable(true);
|
||||
srvGet(url.searchParams.get('k')).then(appData => {
|
||||
url.searchParams.delete('k');
|
||||
if (deserialize(canvas, appData)) { tipShow(false); }
|
||||
history.replaceState(null, null, url);
|
||||
uiDisable(false);
|
||||
url = null;
|
||||
});
|
||||
} else { url = null; }
|
||||
17
main_plugin/dgrm/index.php
Executable file
17
main_plugin/dgrm/index.php
Executable file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* @file index.php
|
||||
* @brief Контейнер для блока схемы
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php /** @brief Основной контейнер диаграммы */ $dgrmDiv; ?>
|
||||
<div id="dgrmDiv" class="bfloat">
|
||||
<div class="btitle" style="background-color: transparent;">Блок схема</div>
|
||||
<ap-menu id="menu"></ap-menu>
|
||||
<ap-menu-shape id="menu-shape"></ap-menu-shape>
|
||||
<div id="tip"></div>
|
||||
<svg id="diagram" tabindex="0" style="background-position: 0px 0px; touch-action: none; background-color: #fff; display:block; user-select: none; -webkit-user-select: none; -webkit-touch-callout: none; pointer-events: none;">
|
||||
<g id="canvas" style="transform: matrix(1, 0, 0, 1, 0, 0);"></g>
|
||||
</svg>
|
||||
</div>
|
||||
2
main_plugin/dgrm/infrastructure/assets.js
Executable file
2
main_plugin/dgrm/infrastructure/assets.js
Executable file
@@ -0,0 +1,2 @@
|
||||
export const delSvg = '<svg data-cmd="del" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M17 6h5v2h-2v13a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V8H2V6h5V3a1 1 0 0 1 1-1h8a1 1 0 0 1 1 1v3zm1 2H6v12h12V8zm-9 3h2v6H9v-6zm4 0h2v6h-2v-6zM9 4v2h6V4H9z" fill="rgb(52,71,103)"/></svg>';
|
||||
export const copySvg = '<svg data-cmd="copy" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7 6V3a1 1 0 0 1 1-1h12a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1.001 1.001 0 0 1 3 21l.003-14c0-.552.45-1 1.007-1H7zM5.003 8L5 20h10V8H5.003zM9 6h8v10h2V4H9v2z" fill="rgb(52,71,103)"/></svg>';
|
||||
15
main_plugin/dgrm/infrastructure/canvas-smbl.js
Executable file
15
main_plugin/dgrm/infrastructure/canvas-smbl.js
Executable file
@@ -0,0 +1,15 @@
|
||||
export const CanvasSmbl = Symbol('Canvas');
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef {{position:Point, scale:number, cell: number}} CanvasData */
|
||||
/** @typedef {SVGGElement & { [CanvasSmbl]?: Canvas }} CanvasElement */
|
||||
/**
|
||||
@typedef {{
|
||||
move?(x:number, y:number, scale:number): void
|
||||
data: CanvasData
|
||||
|
||||
// TODO: it is not infrastructure methods -> shouldn't be here
|
||||
selectClear?(): void
|
||||
shapeMap: Record<number, import("../shapes/shape-type-map").ShapeType>
|
||||
}} Canvas
|
||||
*/
|
||||
35
main_plugin/dgrm/infrastructure/evt-route-applay.js
Executable file
35
main_plugin/dgrm/infrastructure/evt-route-applay.js
Executable file
@@ -0,0 +1,35 @@
|
||||
/** @param {Element} elem */
|
||||
export function evtRouteApplay(elem) {
|
||||
elem.addEventListener('pointerdown', /** @param {RouteEvent} evt */ evt => {
|
||||
if (!evt.isPrimary || evt[RouteedSmbl] || !evt.isTrusted) { return; }
|
||||
|
||||
evt.stopImmediatePropagation();
|
||||
|
||||
const newEvt = new PointerEvent('pointerdown', evt);
|
||||
newEvt[RouteedSmbl] = true;
|
||||
activeElemFromPoint(evt).dispatchEvent(newEvt);
|
||||
}, { capture: true, passive: true });
|
||||
}
|
||||
|
||||
/** @param { {clientX:number, clientY:number} } evt */
|
||||
function activeElemFromPoint(evt) {
|
||||
return elemFromPointByPrioity(evt).find(el => !el.hasAttribute('data-evt-no'));
|
||||
}
|
||||
|
||||
/** @param { {clientX:number, clientY:number} } evt */
|
||||
export function priorityElemFromPoint(evt) {
|
||||
return elemFromPointByPrioity(evt)[0];
|
||||
}
|
||||
|
||||
/** @param { {clientX:number, clientY:number} } evt */
|
||||
function elemFromPointByPrioity(evt) {
|
||||
return document.elementsFromPoint(evt.clientX, evt.clientY)
|
||||
.sort((a, b) => {
|
||||
const ai = a.getAttribute('data-evt-index');
|
||||
const bi = b.getAttribute('data-evt-index');
|
||||
return (ai === bi) ? 0 : ai > bi ? -1 : 1;
|
||||
});
|
||||
}
|
||||
|
||||
const RouteedSmbl = Symbol('routeed');
|
||||
/** @typedef {PointerEvent & { [RouteedSmbl]?: boolean }} RouteEvent */
|
||||
60
main_plugin/dgrm/infrastructure/file.js
Executable file
60
main_plugin/dgrm/infrastructure/file.js
Executable file
@@ -0,0 +1,60 @@
|
||||
/**
|
||||
* save file to user
|
||||
* @param {Blob} blob
|
||||
* @param {string} name
|
||||
*/
|
||||
export function fileSave(blob, name) { ('showSaveFilePicker' in window) ? fileSaveAs(blob) : fileDownload(blob, name); }
|
||||
|
||||
/**
|
||||
* save file with "File save as" dialog
|
||||
* @param {Blob} blob
|
||||
*/
|
||||
async function fileSaveAs(blob) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
const writable = await (await window.showSaveFilePicker({
|
||||
types: [
|
||||
{
|
||||
description: 'PNG Image',
|
||||
accept: {
|
||||
'image/png': ['.png']
|
||||
}
|
||||
}
|
||||
]
|
||||
})).createWritable();
|
||||
await writable.write(blob);
|
||||
await writable.close();
|
||||
} catch {
|
||||
alert('File not saved');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* save file with default download process
|
||||
* @param {Blob} blob
|
||||
* @param {string} name
|
||||
*/
|
||||
function fileDownload(blob, name) {
|
||||
const link = document.createElement('a');
|
||||
link.download = name;
|
||||
link.href = URL.createObjectURL(blob);
|
||||
link.click();
|
||||
URL.revokeObjectURL(link.href);
|
||||
link.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} accept
|
||||
* @param {BlobCallback} callBack
|
||||
*/
|
||||
export function fileOpen(accept, callBack) {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'file';
|
||||
input.multiple = false;
|
||||
input.accept = accept;
|
||||
input.onchange = async function() {
|
||||
callBack((!input.files?.length) ? null : input.files[0]);
|
||||
};
|
||||
input.click();
|
||||
input.remove();
|
||||
}
|
||||
50
main_plugin/dgrm/infrastructure/move-evt-mobile-fix.js
Executable file
50
main_plugin/dgrm/infrastructure/move-evt-mobile-fix.js
Executable file
@@ -0,0 +1,50 @@
|
||||
import { listenDel } from './util.js';
|
||||
|
||||
/** @param {Element} elem */
|
||||
export function moveEvtMobileFix(elem) {
|
||||
/** @type {Point} */ let pointDown;
|
||||
/** @type {number} */ let prevX;
|
||||
/** @type {number} */ let prevY;
|
||||
|
||||
/** @param {PointerEventFixMovement} evt */
|
||||
function move(evt) {
|
||||
if (!evt.isPrimary || !evt.isTrusted) { return; }
|
||||
|
||||
// fix old Android
|
||||
if (pointDown &&
|
||||
Math.abs(pointDown.x - evt.clientX) < 3 &&
|
||||
Math.abs(pointDown.y - evt.clientY) < 3) {
|
||||
evt.stopImmediatePropagation();
|
||||
return;
|
||||
}
|
||||
pointDown = null;
|
||||
|
||||
// fix iOS
|
||||
if (evt.movementX === undefined) {
|
||||
evt[MovementXSmbl] = (prevX ? evt.clientX - prevX : 0);
|
||||
evt[MovementYSmbl] = (prevY ? evt.clientY - prevY : 0);
|
||||
prevX = evt.clientX;
|
||||
prevY = evt.clientY;
|
||||
} else {
|
||||
evt[MovementXSmbl] = evt.movementX;
|
||||
evt[MovementYSmbl] = evt.movementY;
|
||||
}
|
||||
}
|
||||
|
||||
elem.addEventListener('pointerdown', /** @param {PointerEvent} evt */ evt => {
|
||||
pointDown = { x: evt.clientX, y: evt.clientY };
|
||||
prevX = null;
|
||||
prevY = null;
|
||||
elem.addEventListener('pointermove', move, { capture: true, passive: true });
|
||||
|
||||
elem.addEventListener('pointerup', _ => {
|
||||
listenDel(elem, 'pointermove', move, true);
|
||||
}, { capture: true, once: true, passive: true });
|
||||
}, { capture: true, passive: true });
|
||||
}
|
||||
|
||||
export const MovementXSmbl = Symbol('movementX');
|
||||
export const MovementYSmbl = Symbol('movementY');
|
||||
/** @typedef {PointerEvent & { [MovementXSmbl]: number, [MovementYSmbl]: number }} PointerEventFixMovement */
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
117
main_plugin/dgrm/infrastructure/move-evt-proc.js
Executable file
117
main_plugin/dgrm/infrastructure/move-evt-proc.js
Executable file
@@ -0,0 +1,117 @@
|
||||
import { MovementXSmbl, MovementYSmbl } from './move-evt-mobile-fix.js';
|
||||
import { listenDel, listen } from './util.js';
|
||||
|
||||
/**
|
||||
* @param { Element } elemTrackOutdown poitdows in this element will be tracking to fire {onOutdown} callback
|
||||
* @param { Element } elem
|
||||
* @param { {scale:number} } canvasScale
|
||||
* @param { Point } shapePosition
|
||||
* @param { {(evt:PointerEvent):void} } onMoveStart
|
||||
* @param { {(evt:PointerEvent):void} } onMove
|
||||
* @param { {(evt:PointerEvent):void} } onMoveEnd
|
||||
* @param { {(evt:PointerEvent):void} } onClick
|
||||
* @param { {():void} } onOutdown
|
||||
*/
|
||||
export function moveEvtProc(elemTrackOutdown, elem, canvasScale, shapePosition, onMoveStart, onMove, onMoveEnd, onClick, onOutdown) {
|
||||
let isMoved = false;
|
||||
let isInit = false;
|
||||
/** @type {Element} */ let target;
|
||||
|
||||
/** @param {PointerEventFixMovement} evt */
|
||||
function move(evt) {
|
||||
if (!isInit) { return; }
|
||||
|
||||
if (!isMoved) {
|
||||
onMoveStart(evt);
|
||||
|
||||
// if reset
|
||||
if (!isInit) { return; }
|
||||
}
|
||||
|
||||
movementApplay(shapePosition, canvasScale.scale, evt);
|
||||
isMoved = true;
|
||||
onMove(evt);
|
||||
}
|
||||
|
||||
/** @param {PointerEvent} evt */
|
||||
function cancel(evt) {
|
||||
if (isMoved) {
|
||||
onMoveEnd(evt);
|
||||
} else {
|
||||
onClick(evt);
|
||||
}
|
||||
reset(true);
|
||||
}
|
||||
|
||||
/** @param {PointerEvent & { target:Node}} docEvt */
|
||||
function docDown(docEvt) {
|
||||
if (!elem.contains(docEvt.target)) {
|
||||
reset();
|
||||
onOutdown();
|
||||
}
|
||||
}
|
||||
|
||||
function wheel() {
|
||||
reset();
|
||||
onOutdown();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ProcEvent} evt
|
||||
*/
|
||||
function init(evt) {
|
||||
if (evt[ProcessedSmbl] || !evt.isPrimary) {
|
||||
return;
|
||||
}
|
||||
|
||||
evt[ProcessedSmbl] = true;
|
||||
target = /** @type {Element} */(evt.target);
|
||||
if (evt.pointerId !== fakePointerId) { target.setPointerCapture(evt.pointerId); }
|
||||
listen(target, 'pointercancel', cancel, true);
|
||||
listen(target, 'pointerup', cancel, true);
|
||||
listen(target, 'pointermove', move);
|
||||
|
||||
listen(elemTrackOutdown, 'wheel', wheel, true);
|
||||
listen(elemTrackOutdown, 'pointerdown', docDown);
|
||||
|
||||
isInit = true;
|
||||
}
|
||||
|
||||
listen(elem, 'pointerdown', init);
|
||||
|
||||
/** @param {boolean=} saveOutTrack */
|
||||
function reset(saveOutTrack) {
|
||||
listenDel(target, 'pointercancel', cancel);
|
||||
listenDel(target, 'pointerup', cancel);
|
||||
listenDel(target, 'pointermove', move);
|
||||
if (!saveOutTrack) {
|
||||
listenDel(elemTrackOutdown, 'pointerdown', docDown);
|
||||
listenDel(elemTrackOutdown, 'wheel', wheel);
|
||||
}
|
||||
target = null;
|
||||
isMoved = false;
|
||||
isInit = false;
|
||||
}
|
||||
|
||||
return reset;
|
||||
}
|
||||
|
||||
/** @param {Point} point, @param {number} scale, @param {PointerEventFixMovement} evt */
|
||||
export function movementApplay(point, scale, evt) {
|
||||
point.x += evt[MovementXSmbl] / scale;
|
||||
point.y += evt[MovementYSmbl] / scale;
|
||||
}
|
||||
|
||||
const fakePointerId = 42; // random number
|
||||
/** @param {SVGGraphicsElement} shapeOrPathEl */
|
||||
export function shapeSelect(shapeOrPathEl) {
|
||||
shapeOrPathEl.ownerSVGElement.focus();
|
||||
shapeOrPathEl.dispatchEvent(new PointerEvent('pointerdown', { isPrimary: true, pointerId: fakePointerId }));
|
||||
shapeOrPathEl.dispatchEvent(new PointerEvent('pointerup', { isPrimary: true }));
|
||||
}
|
||||
|
||||
export const ProcessedSmbl = Symbol('processed');
|
||||
|
||||
/** @typedef {PointerEvent & { [ProcessedSmbl]?: boolean }} ProcEvent */
|
||||
/** @typedef {import('./move-evt-mobile-fix.js').PointerEventFixMovement} PointerEventFixMovement */
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
223
main_plugin/dgrm/infrastructure/move-scale-applay.js
Executable file
223
main_plugin/dgrm/infrastructure/move-scale-applay.js
Executable file
@@ -0,0 +1,223 @@
|
||||
import { CanvasSmbl } from './canvas-smbl.js';
|
||||
import { ProcessedSmbl } from './move-evt-proc.js';
|
||||
import { listen, listenDel } from './util.js';
|
||||
|
||||
/**
|
||||
* Get point in canvas given the scale and position of the canvas
|
||||
* @param {{position:{x:number, y:number}, scale:number}} canvasData
|
||||
* @param {number} x, @param {number} y
|
||||
*/
|
||||
export const pointInCanvas = (canvasData, x, y) => ({
|
||||
x: (x - canvasData.position.x) / canvasData.scale,
|
||||
y: (y - canvasData.position.y) / canvasData.scale
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {Point} point
|
||||
* @param {number} cell
|
||||
*/
|
||||
export function placeToCell(point, cell) {
|
||||
const cellSizeHalf = cell / 2;
|
||||
function placeToCell(coordinate) {
|
||||
const coor = (Math.round(coordinate / cell) * cell);
|
||||
return (coordinate - coor > 0) ? coor + cellSizeHalf : coor - cellSizeHalf;
|
||||
}
|
||||
|
||||
point.x = placeToCell(point.x);
|
||||
point.y = placeToCell(point.y);
|
||||
}
|
||||
|
||||
/** @param { CanvasElement } canvas */
|
||||
export function moveScaleApplay(canvas) {
|
||||
const canvasData = canvas[CanvasSmbl].data;
|
||||
|
||||
const gripUpdate = applayGrid(canvas.ownerSVGElement, canvasData);
|
||||
|
||||
function transform() {
|
||||
canvas.style.transform = `matrix(${canvasData.scale}, 0, 0, ${canvasData.scale}, ${canvasData.position.x}, ${canvasData.position.y})`;
|
||||
gripUpdate();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {number} nextScale
|
||||
* @param {Point} originPoint
|
||||
*/
|
||||
function scale(nextScale, originPoint) {
|
||||
if (nextScale < 0.25 || nextScale > 4) { return; }
|
||||
|
||||
const divis = nextScale / canvasData.scale;
|
||||
canvasData.scale = nextScale;
|
||||
|
||||
canvasData.position.x = divis * (canvasData.position.x - originPoint.x) + originPoint.x;
|
||||
canvasData.position.y = divis * (canvasData.position.y - originPoint.y) + originPoint.y;
|
||||
|
||||
transform();
|
||||
}
|
||||
|
||||
// move, scale with fingers
|
||||
applayFingers(canvas.ownerSVGElement, canvasData, scale, transform);
|
||||
|
||||
// scale with mouse wheel
|
||||
canvas.ownerSVGElement.addEventListener('wheel', /** @param {WheelEvent} evt */ evt => {
|
||||
evt.preventDefault();
|
||||
const delta = evt.deltaY || evt.deltaX;
|
||||
const scaleStep = Math.abs(delta) < 50
|
||||
? 0.05 // trackpad pitch
|
||||
: 0.25; // mouse wheel
|
||||
|
||||
scale(
|
||||
canvasData.scale + (delta < 0 ? scaleStep : -scaleStep),
|
||||
evtPoint(evt));
|
||||
});
|
||||
|
||||
canvas[CanvasSmbl].move = function (x, y, scale) {
|
||||
canvasData.position.x = x;
|
||||
canvasData.position.y = y;
|
||||
canvasData.scale = scale;
|
||||
transform();
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { SVGSVGElement } svg
|
||||
* @param { {position:Point, scale:number} } canvasData
|
||||
* @param { {(nextScale:number, originPoint:Point):void} } scaleFn
|
||||
* @param { {():void} } transformFn
|
||||
* @return
|
||||
*/
|
||||
function applayFingers(svg, canvasData, scaleFn, transformFn) {
|
||||
/** @type { Pointer } */
|
||||
let firstPointer;
|
||||
|
||||
/** @type { Pointer} */
|
||||
let secondPointer;
|
||||
|
||||
/** @type {number} */
|
||||
let distance;
|
||||
|
||||
/** @type {Point} */
|
||||
let center;
|
||||
|
||||
/** @param {PointerEvent} evt */
|
||||
function cancel(evt) {
|
||||
distance = null;
|
||||
center = null;
|
||||
if (firstPointer?.id === evt.pointerId) { firstPointer = null; }
|
||||
if (secondPointer?.id === evt.pointerId) { secondPointer = null; }
|
||||
|
||||
if (!firstPointer && !secondPointer) {
|
||||
listenDel(svg, 'pointermove', move);
|
||||
listenDel(svg, 'pointercancel', cancel);
|
||||
listenDel(svg, 'pointerup', cancel);
|
||||
}
|
||||
};
|
||||
|
||||
/** @param {PointerEvent} evt */
|
||||
function move(evt) {
|
||||
if (evt[ProcessedSmbl]) { return; }
|
||||
|
||||
if ((firstPointer && !secondPointer) || (!firstPointer && secondPointer)) {
|
||||
// move with one pointer
|
||||
canvasData.position.x = evt.clientX + (firstPointer || secondPointer).shift.x;
|
||||
canvasData.position.y = evt.clientY + (firstPointer || secondPointer).shift.y;
|
||||
transformFn();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!secondPointer || !firstPointer || (secondPointer?.id !== evt.pointerId && firstPointer?.id !== evt.pointerId)) { return; }
|
||||
|
||||
const distanceNew = Math.hypot(firstPointer.pos.x - secondPointer.pos.x, firstPointer.pos.y - secondPointer.pos.y);
|
||||
const centerNew = {
|
||||
x: (firstPointer.pos.x + secondPointer.pos.x) / 2,
|
||||
y: (firstPointer.pos.y + secondPointer.pos.y) / 2
|
||||
};
|
||||
|
||||
// not first move
|
||||
if (distance) {
|
||||
canvasData.position.x = canvasData.position.x + centerNew.x - center.x;
|
||||
canvasData.position.y = canvasData.position.y + centerNew.y - center.y;
|
||||
|
||||
scaleFn(
|
||||
canvasData.scale / distance * distanceNew,
|
||||
centerNew);
|
||||
}
|
||||
|
||||
distance = distanceNew;
|
||||
center = centerNew;
|
||||
|
||||
if (firstPointer.id === evt.pointerId) { firstPointer = evtPointer(evt, canvasData); }
|
||||
if (secondPointer.id === evt.pointerId) { secondPointer = evtPointer(evt, canvasData); }
|
||||
}
|
||||
|
||||
listen(svg, 'pointerdown', /** @param {PointerEvent} evt */ evt => {
|
||||
if (evt[ProcessedSmbl] || (!firstPointer && !evt.isPrimary) || (firstPointer && secondPointer)) {
|
||||
return;
|
||||
}
|
||||
|
||||
svg.setPointerCapture(evt.pointerId);
|
||||
if (!firstPointer) {
|
||||
listen(svg, 'pointermove', move);
|
||||
listen(svg, 'pointercancel', cancel);
|
||||
listen(svg, 'pointerup', cancel);
|
||||
}
|
||||
|
||||
if (!firstPointer) { firstPointer = evtPointer(evt, canvasData); return; }
|
||||
if (!secondPointer) { secondPointer = evtPointer(evt, canvasData); }
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { SVGSVGElement } svg
|
||||
* @param { import('./canvas-smbl.js').CanvasData } canvasData
|
||||
*/
|
||||
function applayGrid(svg, canvasData) {
|
||||
let curOpacity;
|
||||
/** @param {number} opacity */
|
||||
function backImg(opacity) {
|
||||
if (curOpacity !== opacity) {
|
||||
curOpacity = opacity;
|
||||
svg.style.backgroundImage = `radial-gradient(rgb(73 80 87 / ${opacity}) 1px, transparent 0)`;
|
||||
}
|
||||
}
|
||||
|
||||
backImg(0.7);
|
||||
svg.style.backgroundSize = `${canvasData.cell}px ${canvasData.cell}px`;
|
||||
|
||||
return function() {
|
||||
const size = canvasData.cell * canvasData.scale;
|
||||
|
||||
if (canvasData.scale < 0.5) { backImg(0); } else
|
||||
if (canvasData.scale <= 0.9) { backImg(0.3); } else { backImg(0.7); }
|
||||
|
||||
svg.style.backgroundSize = `${size}px ${size}px`;
|
||||
svg.style.backgroundPosition = `${canvasData.position.x}px ${canvasData.position.y}px`;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent | MouseEvent} evt
|
||||
* @return {Point}
|
||||
*/
|
||||
function evtPoint(evt) { return { x: evt.clientX, y: evt.clientY }; }
|
||||
|
||||
/**
|
||||
* @param { PointerEvent } evt
|
||||
* @param { {position:Point, scale:number} } canvasData
|
||||
* @return { Pointer }
|
||||
*/
|
||||
function evtPointer(evt, canvasData) {
|
||||
return {
|
||||
id: evt.pointerId,
|
||||
pos: evtPoint(evt),
|
||||
shift: {
|
||||
x: canvasData.position.x - evt.clientX,
|
||||
y: canvasData.position.y - evt.clientY
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { {id:number, pos:Point, shift:Point} } Pointer */
|
||||
/** @typedef { import("./move-evt-proc").ProcEvent } DgrmEvent */
|
||||
/** @typedef { import('./canvas-smbl.js').CanvasData } CanvasData */
|
||||
/** @typedef { import('./canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
93
main_plugin/dgrm/infrastructure/png-chunk.js
Executable file
93
main_plugin/dgrm/infrastructure/png-chunk.js
Executable file
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* @param {Blob} png
|
||||
* @param {string} chunkName 4 symbol string
|
||||
* @returns {Promise<DataView | null>} chunk data
|
||||
*/
|
||||
export async function pngChunkGet(png, chunkName) {
|
||||
return chunkGet(
|
||||
await png.arrayBuffer(),
|
||||
toUit32(chunkName));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Blob} png
|
||||
* @param {string} chunkName 4 symbol string
|
||||
* @param {Uint8Array} data
|
||||
* @returns {Promise<Blob>} new png
|
||||
*/
|
||||
export async function pngChunkSet(png, chunkName, data) {
|
||||
return chunkSet(
|
||||
await png.arrayBuffer(),
|
||||
toUit32(chunkName),
|
||||
data
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} pngData
|
||||
* @param {number} chunkNameUint32 chunk name as Uint32
|
||||
* @param {Uint8Array} data
|
||||
* @returns {Blob} new png
|
||||
*/
|
||||
function chunkSet(pngData, chunkNameUint32, data) {
|
||||
/** @type {DataView} */
|
||||
let startPart;
|
||||
/** @type {DataView} */
|
||||
let endPart;
|
||||
|
||||
const existingChunk = chunkGet(pngData, chunkNameUint32);
|
||||
if (existingChunk) {
|
||||
startPart = new DataView(pngData, 0, existingChunk.byteOffset - 8);
|
||||
endPart = new DataView(pngData, existingChunk.byteOffset + existingChunk.byteLength + 4);
|
||||
} else {
|
||||
const endChunkStart = pngData.byteLength - 12; // 12 - end chunk length
|
||||
startPart = new DataView(pngData, 0, endChunkStart);
|
||||
endPart = new DataView(pngData, endChunkStart);
|
||||
}
|
||||
|
||||
const chunkHeader = new DataView(new ArrayBuffer(8));
|
||||
chunkHeader.setUint32(0, data.length);
|
||||
chunkHeader.setUint32(4, chunkNameUint32);
|
||||
|
||||
return new Blob([
|
||||
startPart,
|
||||
|
||||
// new chunk
|
||||
chunkHeader,
|
||||
data,
|
||||
new Uint32Array([0]), // CRC fake - not calculated
|
||||
|
||||
endPart
|
||||
],
|
||||
{ type: 'image/png' });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {ArrayBuffer} pngData
|
||||
* @param {number} chunkNameUint32 chunk name as Uint32
|
||||
* @returns {DataView | null} chunk data
|
||||
*/
|
||||
function chunkGet(pngData, chunkNameUint32) {
|
||||
const dataView = new DataView(pngData, 8); // 8 byte - png signature
|
||||
|
||||
let chunkPosition = 0;
|
||||
let chunkUint = dataView.getUint32(4);
|
||||
let chunkLenght;
|
||||
while (chunkUint !== 1229278788) { // last chunk 'IEND'
|
||||
chunkLenght = dataView.getUint32(chunkPosition);
|
||||
if (chunkUint === chunkNameUint32) {
|
||||
return new DataView(pngData, chunkPosition + 16, chunkLenght);
|
||||
}
|
||||
chunkPosition = chunkPosition + 12 + chunkLenght;
|
||||
chunkUint = dataView.getUint32(chunkPosition + 4);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} chunkName 4 symbol string
|
||||
* @return {number} uit32
|
||||
*/
|
||||
function toUit32(chunkName) {
|
||||
return new DataView((new TextEncoder()).encode(chunkName).buffer).getUint32(0);
|
||||
}
|
||||
70
main_plugin/dgrm/infrastructure/svg-text-area.js
Executable file
70
main_plugin/dgrm/infrastructure/svg-text-area.js
Executable file
@@ -0,0 +1,70 @@
|
||||
import { svgTextDraw } from './svg-text-draw.js';
|
||||
import { svgEl } from './util.js';
|
||||
|
||||
/**
|
||||
* Create teaxtArea above SVGTextElement 'textEl'
|
||||
* update 'textEl' with text from teaxtArea
|
||||
* resize teaxtArea - so teaxtArea always cover all 'textEl'
|
||||
* @param {SVGTextElement} textEl
|
||||
* @param {number} verticalMiddle em
|
||||
* @param {string} val
|
||||
* @param {{(val:string):void}} onchange
|
||||
* @param {{(val:string):void}} onblur
|
||||
*/
|
||||
export function textareaCreate(textEl, verticalMiddle, val, onchange, onblur) {
|
||||
let foreign = svgEl('foreignObject');
|
||||
const textarea = document.createElement('textarea');
|
||||
const draw = () => foreignWidthSet(textEl, foreign, textarea, textareaPaddingAndBorder, textareaStyle.textAlign);
|
||||
|
||||
textarea.value = val || '';
|
||||
textarea.oninput = function() {
|
||||
svgTextDraw(textEl, verticalMiddle, textarea.value);
|
||||
onchange(textarea.value);
|
||||
draw();
|
||||
};
|
||||
textarea.onblur = function() {
|
||||
onblur(textarea.value);
|
||||
};
|
||||
textarea.onpointerdown = function(evt) {
|
||||
evt.stopImmediatePropagation();
|
||||
};
|
||||
|
||||
foreign.appendChild(textarea);
|
||||
textEl.parentElement.appendChild(foreign);
|
||||
|
||||
const textareaStyle = getComputedStyle(textarea);
|
||||
// must be in px
|
||||
const textareaPaddingAndBorder = parseInt(textareaStyle.paddingLeft) + parseInt(textareaStyle.borderWidth);
|
||||
draw();
|
||||
|
||||
textarea.focus();
|
||||
|
||||
return {
|
||||
dispose: () => { foreign.remove(); foreign = null; },
|
||||
draw
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {SVGTextElement} textEl
|
||||
* @param {SVGForeignObjectElement} foreign
|
||||
* @param {HTMLTextAreaElement} textarea
|
||||
* @param {number} textareaPaddingAndBorder
|
||||
* @param {string} textAlign
|
||||
*/
|
||||
function foreignWidthSet(textEl, foreign, textarea, textareaPaddingAndBorder, textAlign) {
|
||||
const textBbox = textEl.getBBox();
|
||||
const width = textBbox.width + 20; // +20 paddings for iPhone
|
||||
|
||||
foreign.width.baseVal.value = width + 2 * textareaPaddingAndBorder + 2; // +2 magic number for FireFox
|
||||
foreign.x.baseVal.value = textBbox.x - textareaPaddingAndBorder - (
|
||||
textAlign === 'center'
|
||||
? 10
|
||||
: textAlign === 'right' ? 20 : 0);
|
||||
|
||||
foreign.height.baseVal.value = textBbox.height + 2 * textareaPaddingAndBorder + 3; // +3 magic number for FireFox
|
||||
foreign.y.baseVal.value = textBbox.y - textareaPaddingAndBorder;
|
||||
|
||||
textarea.style.width = `${width}px`;
|
||||
textarea.style.height = `${textBbox.height}px`;
|
||||
}
|
||||
43
main_plugin/dgrm/infrastructure/svg-text-draw.js
Executable file
43
main_plugin/dgrm/infrastructure/svg-text-draw.js
Executable file
@@ -0,0 +1,43 @@
|
||||
/**
|
||||
* @param {SVGTextElement} textEl target text element
|
||||
* @param {number} verticalMiddle
|
||||
* @param {string} str
|
||||
* @returns {void}
|
||||
*/
|
||||
export function svgTextDraw(textEl, verticalMiddle, str) {
|
||||
const strData = svgStrToTspan(
|
||||
(str || ''),
|
||||
textEl.x?.baseVal[0]?.value ?? 0);
|
||||
|
||||
textEl.innerHTML = strData.s;
|
||||
|
||||
textEl.y.baseVal[0].newValueSpecifiedUnits(
|
||||
textEl.y.baseVal[0].SVG_LENGTHTYPE_EMS, // em
|
||||
strData.c > 0 ? verticalMiddle - (strData.c) / 2 : verticalMiddle);
|
||||
}
|
||||
|
||||
/**
|
||||
* create multiline tspan markup
|
||||
* @param {string} str
|
||||
* @param {number} x
|
||||
* @returns { {s:string, c:number} }
|
||||
*/
|
||||
function svgStrToTspan(str, x) {
|
||||
let c = 0;
|
||||
return {
|
||||
s: str.split('\n')
|
||||
.map((t, i) => {
|
||||
c = i;
|
||||
return `<tspan x="${x}" dy="${i === 0 ? 0.41 : 1}em" ${t.length === 0 ? 'visibility="hidden"' : ''}>${t.length === 0 ? '.' : escapeHtml(t).replaceAll(' ', ' ')}</tspan>`;
|
||||
}).join(''),
|
||||
c
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function escapeHtml(str) {
|
||||
return str.replaceAll('&', '&').replaceAll('<', '<').replaceAll('>', '>').replaceAll('"', '"').replaceAll("'", ''');
|
||||
}
|
||||
93
main_plugin/dgrm/infrastructure/svg-to-png.js
Executable file
93
main_plugin/dgrm/infrastructure/svg-to-png.js
Executable file
@@ -0,0 +1,93 @@
|
||||
// src/infrastructure/svg-to-png.js
|
||||
/**
|
||||
* @param {SVGElement} svg - виртуальный SVG (готовый для рендеринга)
|
||||
* @param {{x:number,y:number,width:number,height:number}} rect - область в единицах SVG user units
|
||||
* @param {number} scale - множитель (вызов передаёт, например, 3)
|
||||
* @param {(blob:Blob|null)=>void} callBack
|
||||
*/
|
||||
export function svgToPng(svg, rect, scale, callBack) {
|
||||
if (!svg || !rect || rect.width <= 0 || rect.height <= 0) {
|
||||
callBack(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// output размеры с учётом devicePixelRatio
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const outputWidth = Math.round(rect.width * scale * dpr);
|
||||
const outputHeight = Math.round(rect.height * scale * dpr);
|
||||
|
||||
// Сериализуем svg в строку и делаем blob/url
|
||||
let svgString;
|
||||
try {
|
||||
svgString = new XMLSerializer().serializeToString(svg);
|
||||
} catch (e) {
|
||||
callBack(null);
|
||||
return;
|
||||
}
|
||||
|
||||
let url;
|
||||
try {
|
||||
url = URL.createObjectURL(new Blob([svgString], { type: 'image/svg+xml;charset=utf-8' }));
|
||||
} catch (e) {
|
||||
callBack(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const img = new Image();
|
||||
// crossOrigin можно добавить, если нужно: img.crossOrigin = 'anonymous';
|
||||
img.onload = function () {
|
||||
try {
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = outputWidth;
|
||||
canvas.height = outputHeight;
|
||||
canvas.style.width = `${outputWidth}px`;
|
||||
canvas.style.height = `${outputHeight}px`;
|
||||
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) {
|
||||
URL.revokeObjectURL(url);
|
||||
callBack(null);
|
||||
return;
|
||||
}
|
||||
ctx.imageSmoothingEnabled = false;
|
||||
|
||||
// Если rect.x/rect.y отрицательные => смещение исходного изображения внутри canvas
|
||||
const sx = Math.max(0, rect.x * scale * dpr);
|
||||
const sy = Math.max(0, rect.y * scale * dpr);
|
||||
const sWidth = rect.width * scale * dpr;
|
||||
const sHeight = rect.height * scale * dpr;
|
||||
|
||||
// dx/dy: смещение на canvas (если rect.x < 0, то мы сдвигаем вправо)
|
||||
const dx = rect.x < 0 ? -rect.x * scale * dpr : 0;
|
||||
const dy = rect.y < 0 ? -rect.y * scale * dpr : 0;
|
||||
|
||||
// drawImage с указанием исходной области и целевой области
|
||||
ctx.drawImage(
|
||||
img,
|
||||
sx, // sx
|
||||
sy, // sy
|
||||
sWidth, // sWidth
|
||||
sHeight, // sHeight
|
||||
dx, // dx (на canvas)
|
||||
dy, // dy
|
||||
outputWidth, // dWidth
|
||||
outputHeight // dHeight
|
||||
);
|
||||
|
||||
URL.revokeObjectURL(url);
|
||||
canvas.toBlob(blob => {
|
||||
callBack(blob);
|
||||
}, 'image/png');
|
||||
} catch (err) {
|
||||
URL.revokeObjectURL(url);
|
||||
callBack(null);
|
||||
}
|
||||
};
|
||||
|
||||
img.onerror = function () {
|
||||
try { URL.revokeObjectURL(url); } catch (e) {}
|
||||
callBack(null);
|
||||
};
|
||||
|
||||
img.src = url;
|
||||
}
|
||||
146
main_plugin/dgrm/infrastructure/util.js
Executable file
146
main_plugin/dgrm/infrastructure/util.js
Executable file
@@ -0,0 +1,146 @@
|
||||
//
|
||||
// dom utils
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Element} parent
|
||||
* @param {string} key
|
||||
* @returns T
|
||||
*/
|
||||
export const child = (parent, key) => /** @type {T} */(parent.querySelector(`[data-key="${key}"]`));
|
||||
|
||||
/** @param {HTMLElement|SVGElement} crcl, @param {Point} pos */
|
||||
export function positionSet(crcl, pos) { crcl.style.transform = `translate(${pos.x}px, ${pos.y}px)`; }
|
||||
|
||||
/** @param {Element} el, @param {string[]} cl */
|
||||
export const classAdd = (el, ...cl) => el?.classList.add(...cl);
|
||||
|
||||
/** @param {Element} el, @param {string} cl */
|
||||
export const classDel = (el, cl) => el?.classList.remove(cl);
|
||||
|
||||
/** @param {Element} el, @param {string} cl */
|
||||
export const classHas = (el, cl) => el?.classList.contains(cl);
|
||||
|
||||
/** @param {Element} shapeEl, @param {{styles?:string[]}} shapeData, @param {string} classPrefix, @param {string} classToAdd */
|
||||
export function classSingleAdd(shapeEl, shapeData, classPrefix, classToAdd) {
|
||||
if (!shapeData.styles) { shapeData.styles = []; }
|
||||
|
||||
const currentClass = shapeData.styles.findIndex(ss => ss.startsWith(classPrefix));
|
||||
if (currentClass > -1) {
|
||||
classDel(shapeEl, shapeData.styles[currentClass]);
|
||||
shapeData.styles.splice(currentClass, 1);
|
||||
}
|
||||
shapeData.styles.push(classToAdd);
|
||||
classAdd(shapeEl, classToAdd);
|
||||
}
|
||||
|
||||
/** @param {Element | GlobalEventHandlers} el, @param {string} type, @param {EventListenerOrEventListenerObject} listener, @param {boolean?=} once */
|
||||
export const listen = (el, type, listener, once) => {
|
||||
if (el) el.addEventListener(type, listener, { passive: true, once });
|
||||
};
|
||||
|
||||
/** @param {Element | GlobalEventHandlers} el, @param {string} type, @param {EventListenerOrEventListenerObject} listener, @param {boolean?=} capture */
|
||||
export const listenDel = (el, type, listener, capture) => el?.removeEventListener(type, listener, { capture });
|
||||
|
||||
/** @param {ParentNode} el, @param {string} selector, @param {(this: GlobalEventHandlers, ev: PointerEvent & { currentTarget: Element }) => any} handler */
|
||||
export function clickForAll(el, selector, handler) { el.querySelectorAll(selector).forEach(/** @param {HTMLElement} el */ el => { el.onclick = handler; }); }
|
||||
|
||||
/** @param {PointerEvent & { currentTarget: Element }} evt, @param {string} attr */
|
||||
export const evtTargetAttr = (evt, attr) => evt.currentTarget.getAttribute(attr);
|
||||
|
||||
/**
|
||||
* @template {keyof SVGElementTagNameMap} T
|
||||
* @param {T} qualifiedName
|
||||
* @param {string?=} innerHTML
|
||||
* @returns {SVGElementTagNameMap[T]}
|
||||
*/
|
||||
export function svgEl(qualifiedName, innerHTML) {
|
||||
const svgGrp = document.createElementNS('http://www.w3.org/2000/svg', qualifiedName);
|
||||
if (innerHTML) { svgGrp.innerHTML = innerHTML; }
|
||||
return svgGrp;
|
||||
}
|
||||
|
||||
/**
|
||||
* calc farthest point of <tspan>s bbox in {textEl}
|
||||
* origin is in the center
|
||||
* @param {SVGTextElement} textEl
|
||||
*/
|
||||
export function svgTxtFarthestPoint(textEl) {
|
||||
/** @type {Point} */
|
||||
let maxPoint;
|
||||
let maxAbsSum = 0;
|
||||
for (const span of textEl.getElementsByTagName('tspan')) {
|
||||
for (const point of boxPoints(span.getBBox())) {
|
||||
const pointAbsSum = Math.abs(point.x) + Math.abs(point.y);
|
||||
if (maxAbsSum < pointAbsSum) {
|
||||
maxPoint = point;
|
||||
maxAbsSum = pointAbsSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
return maxPoint;
|
||||
}
|
||||
|
||||
/** @param {DOMRect} box */
|
||||
const boxPoints = (box) => [
|
||||
{ x: box.x, y: box.y },
|
||||
{ x: box.right, y: box.y },
|
||||
{ x: box.x, y: box.bottom },
|
||||
{ x: box.right, y: box.bottom }
|
||||
];
|
||||
|
||||
//
|
||||
// math, arr utils
|
||||
|
||||
/**
|
||||
* Get the ceiling for a number {val} with a given floor height {step}
|
||||
* @param {number} min
|
||||
* @param {number} step
|
||||
* @param {number} val
|
||||
*/
|
||||
export function ceil(min, step, val) {
|
||||
if (val <= min) { return min; }
|
||||
return min + Math.ceil((val - min) / step) * step;
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} arr
|
||||
* @param {{(el:T):void}} action
|
||||
*/
|
||||
export function arrPop(arr, action) {
|
||||
let itm = arr.pop();
|
||||
while (itm) { action(itm); itm = arr.pop(); };
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {Array<T>} arr
|
||||
* @param {T} el
|
||||
*/
|
||||
export function arrDel(arr, el) {
|
||||
const index = arr.indexOf(el);
|
||||
if (index > -1) {
|
||||
arr.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {Point} point, @param {Point} shift, @param {number=} coef */
|
||||
export function pointShift(point, shift, coef) {
|
||||
const _coef = coef ?? 1;
|
||||
point.x += _coef * shift.x;
|
||||
point.y += _coef * shift.y;
|
||||
return point;
|
||||
}
|
||||
|
||||
//
|
||||
// object utils
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T} obj
|
||||
* @returns {T}
|
||||
*/
|
||||
export const deepCopy = obj => JSON.parse(JSON.stringify(obj));
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
25
main_plugin/dgrm/plug.php
Executable file
25
main_plugin/dgrm/plug.php
Executable file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* @file plug.php
|
||||
* @brief Подключает плагин dgrm для администраторов и выводит соответствующий HTML и JS
|
||||
*/
|
||||
|
||||
global $path, $_SESSION, $configAdmins;
|
||||
if (in_array($_SESSION['username'], $configAdmins, true)) {
|
||||
echo file_get_contents($path . 'main_plugin/dgrm/index.php');
|
||||
echo "<script type='module'>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const c = document.querySelector('.center-float');
|
||||
const d = document.getElementById('dgrmDiv');
|
||||
if (c && d) {
|
||||
c.appendChild(document.createElement('br'));
|
||||
c.appendChild(d);
|
||||
import('/main_plugin/dgrm/index.js');
|
||||
} else if (d) {
|
||||
d.remove();
|
||||
}
|
||||
});
|
||||
</script>";
|
||||
echo '<link rel="stylesheet" type="text/css" href="/main_plugin/dgrm/dgrm.css">';
|
||||
}
|
||||
?>
|
||||
69
main_plugin/dgrm/shapes/circle.js
Executable file
69
main_plugin/dgrm/shapes/circle.js
Executable file
@@ -0,0 +1,69 @@
|
||||
import { ceil, child, positionSet, svgTxtFarthestPoint } from '../infrastructure/util.js';
|
||||
import { shapeCreate } from './shape-evt-proc.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {CircleData} circleData
|
||||
*/
|
||||
export function circle(canvas, circleData) {
|
||||
const templ = `
|
||||
<circle data-key="outer" data-evt-no data-evt-index="2" r="72" fill="transparent" stroke-width="0" />
|
||||
<circle data-key="main" r="48" fill="#ff6600" stroke="#fff" stroke-width="1" />
|
||||
<text data-key="text" x="0" y="0" text-anchor="middle" style="pointer-events: none;" fill="#fff"> </text>`;
|
||||
|
||||
const shape = shapeCreate(canvas, circleData, templ,
|
||||
{
|
||||
right: { dir: 'right', position: { x: 48, y: 0 } },
|
||||
left: { dir: 'left', position: { x: -48, y: 0 } },
|
||||
bottom: { dir: 'bottom', position: { x: 0, y: 48 } },
|
||||
top: { dir: 'top', position: { x: 0, y: -48 } }
|
||||
},
|
||||
// onTextChange
|
||||
txtEl => {
|
||||
const newRadius = textElRadius(txtEl, 48, 24);
|
||||
if (newRadius !== circleData.r) {
|
||||
circleData.r = newRadius;
|
||||
resize();
|
||||
}
|
||||
});
|
||||
|
||||
function resize() {
|
||||
shape.cons.right.position.x = circleData.r;
|
||||
shape.cons.left.position.x = -circleData.r;
|
||||
shape.cons.bottom.position.y = circleData.r;
|
||||
shape.cons.top.position.y = -circleData.r;
|
||||
|
||||
for (const connectorKey in shape.cons) {
|
||||
positionSet(child(shape.el, connectorKey), shape.cons[connectorKey].position);
|
||||
}
|
||||
|
||||
radiusSet(shape.el, 'outer', circleData.r + 24);
|
||||
radiusSet(shape.el, 'main', circleData.r);
|
||||
shape.draw();
|
||||
}
|
||||
|
||||
if (!!circleData.r && circleData.r !== 48) { resize(); } else { shape.draw(); }
|
||||
|
||||
return shape.el;
|
||||
}
|
||||
|
||||
/** @param {Element} svgGrp, @param {string} key, @param {number} r */
|
||||
function radiusSet(svgGrp, key, r) { /** @type {SVGCircleElement} */(child(svgGrp, key)).r.baseVal.value = r; }
|
||||
|
||||
/**
|
||||
* calc radius that cover all <tspan> in SVGTextElement
|
||||
* origin is in the center of the circle
|
||||
* @param {SVGTextElement} textEl
|
||||
* @param {*} minR
|
||||
* @param {*} step
|
||||
*/
|
||||
function textElRadius(textEl, minR, step) {
|
||||
const farthestPoint = svgTxtFarthestPoint(textEl);
|
||||
return ceil(minR, step, Math.sqrt(farthestPoint.x ** 2 + farthestPoint.y ** 2));
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('./shape-evt-proc').CanvasData } CanvasData */
|
||||
/** @typedef { import('./shape-evt-proc').ConnectorsData } ConnectorsData */
|
||||
/** @typedef { {type:number, position: Point, title?: string, styles?: string[], r?:number} } CircleData */
|
||||
27
main_plugin/dgrm/shapes/modal-create.js
Executable file
27
main_plugin/dgrm/shapes/modal-create.js
Executable file
@@ -0,0 +1,27 @@
|
||||
/** @type {HTMLDivElement} */
|
||||
let editModalDiv;
|
||||
|
||||
/** @param {number} bottomX, @param {number} bottomY, @param {HTMLElement} elem */
|
||||
export function modalCreate(bottomX, bottomY, elem) {
|
||||
editModalDiv = document.createElement('div');
|
||||
editModalDiv.style.cssText = 'position: fixed; box-shadow: 0px 0px 58px 2px rgb(34 60 80 / 20%); border-radius: 16px; background-color: rgba(255,255,255, .9);';
|
||||
editModalDiv.append(elem);
|
||||
document.body.append(editModalDiv);
|
||||
|
||||
function position(btmX, btmY) {
|
||||
editModalDiv.style.left = `${btmX}px`;
|
||||
editModalDiv.style.top = `${btmY - 35}px`;
|
||||
}
|
||||
|
||||
position(bottomX, bottomY);
|
||||
|
||||
return {
|
||||
position,
|
||||
del: () => { editModalDiv.remove(); editModalDiv = null; }
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {number} dif */
|
||||
export function modalChangeTop(dif) {
|
||||
editModalDiv.style.top = `${editModalDiv.getBoundingClientRect().top - 90}px`;
|
||||
}
|
||||
75
main_plugin/dgrm/shapes/path-settings.js
Executable file
75
main_plugin/dgrm/shapes/path-settings.js
Executable file
@@ -0,0 +1,75 @@
|
||||
import { copyAndPast } from '../diagram/group-select-applay.js';
|
||||
import { classAdd, classDel, clickForAll, listen, classSingleAdd, evtTargetAttr } from '../infrastructure/util.js';
|
||||
import { PathSmbl } from './path-smbl.js';
|
||||
|
||||
export class PathSettings extends HTMLElement {
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {PathElement} pathElement
|
||||
*/
|
||||
constructor(canvas, pathElement) {
|
||||
super();
|
||||
/** @private */
|
||||
this._pathElement = pathElement;
|
||||
|
||||
/** @private */
|
||||
this._canvas = canvas;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const pathStyles = this._pathElement[PathSmbl].data.styles;
|
||||
const actStyle = style => this._pathElement[PathSmbl].data.styles?.includes(style) ? 'class="actv"' : '';
|
||||
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
.ln { display: flex; }
|
||||
.ln > * {
|
||||
height: 24px;
|
||||
padding: 10px;
|
||||
fill-opacity: 0.3;
|
||||
stroke-opacity: 0.3;
|
||||
}
|
||||
[data-cmd] { cursor: pointer; }
|
||||
.actv {
|
||||
fill-opacity: 1;
|
||||
stroke-opacity: 1;
|
||||
}
|
||||
</style>
|
||||
<ap-shape-edit id="edit" edit-btn="true">
|
||||
<div class="ln">
|
||||
<svg data-cmd data-cmd-arg="arw-s" ${actStyle('arw-s')} viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M7.828 11H20v2H7.828l5.364 5.364-1.414 1.414L4 12l7.778-7.778 1.414 1.414z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-cmd data-cmd-arg="arw-e" ${actStyle('arw-e')} viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M16.172 11l-5.364-5.364 1.414-1.414L20 12l-7.778 7.778-1.414-1.414L16.172 13H4v-2z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-cmd data-cmd-arg="dash" ${actStyle('dash')} viewBox="0 0 24 24" width="24" height="24"><path d="M 2,11 L 20,11" stroke="rgb(52,71,103)" style="stroke-dasharray: 4,3; stroke-width: 3;"></path></svg>
|
||||
</div>
|
||||
</ap-shape-edit>`;
|
||||
|
||||
// colors, del
|
||||
listen(shadow.getElementById('edit'), 'cmd', /** @param {CustomEvent<{cmd:string, arg:string}>} evt */ evt => {
|
||||
switch (evt.detail.cmd) {
|
||||
case 'style': classSingleAdd(this._pathElement, this._pathElement[PathSmbl].data, 'cl-', evt.detail.arg); break;
|
||||
case 'del': this._pathElement[PathSmbl].del(); break;
|
||||
case 'copy': copyAndPast(this._canvas, [this._pathElement]); break;
|
||||
}
|
||||
});
|
||||
|
||||
// arrows, dotted
|
||||
clickForAll(shadow, '[data-cmd]', evt => {
|
||||
const argStyle = evtTargetAttr(evt, 'data-cmd-arg');
|
||||
const currentArr = pathStyles.indexOf(argStyle);
|
||||
if (currentArr > -1) {
|
||||
classDel(this._pathElement, argStyle);
|
||||
pathStyles.splice(currentArr, 1);
|
||||
classDel(evt.currentTarget, 'actv');
|
||||
} else {
|
||||
classAdd(this._pathElement, argStyle);
|
||||
pathStyles.push(argStyle);
|
||||
classAdd(evt.currentTarget, 'actv');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('ap-path-settings', PathSettings);
|
||||
|
||||
/** @typedef { import('./path-smbl').PathElement } PathElement */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
2
main_plugin/dgrm/shapes/path-smbl.js
Executable file
2
main_plugin/dgrm/shapes/path-smbl.js
Executable file
@@ -0,0 +1,2 @@
|
||||
export const PathSmbl = Symbol('path');
|
||||
/** @typedef {SVGGraphicsElement & { [PathSmbl]?: import("./path").Path }} PathElement */
|
||||
402
main_plugin/dgrm/shapes/path.js
Executable file
402
main_plugin/dgrm/shapes/path.js
Executable file
@@ -0,0 +1,402 @@
|
||||
import { child, classAdd, classDel, classHas, listen, listenDel, svgEl } from '../infrastructure/util.js';
|
||||
import { moveEvtProc, movementApplay } from '../infrastructure/move-evt-proc.js';
|
||||
import { placeToCell, pointInCanvas } from '../infrastructure/move-scale-applay.js';
|
||||
import { priorityElemFromPoint } from '../infrastructure/evt-route-applay.js';
|
||||
import { ShapeSmbl } from './shape-smbl.js';
|
||||
import { PathSettings } from './path-settings.js';
|
||||
import { PathSmbl } from './path-smbl.js';
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { modalCreate } from './modal-create.js';
|
||||
import { canvasSelectionClearSet } from '../diagram/canvas-clear.js';
|
||||
import { listenCopy } from '../diagram/group-select-applay.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {PathData} pathData
|
||||
*/
|
||||
export function path(canvas, pathData) {
|
||||
|
||||
/** @type {PathElement} */
|
||||
const svgGrp = svgEl('g', `
|
||||
<path data-key="outer" d="M0 0" stroke="transparent" stroke-width="20" fill="none" />
|
||||
<path data-key="path" class="path" d="M0 0" stroke="#495057" stroke-width="1.8" fill="none" style="pointer-events: none;" />
|
||||
<path data-key="selected" d="M0 0" stroke="transparent" stroke-width="10" fill="none" style="pointer-events: none;" />
|
||||
<g data-key="start">
|
||||
<circle data-evt-index="1" class="path-end" r="10" stroke-width="0" fill="transparent" />
|
||||
<path class="path" d="M-7 7 l 7 -7 l -7 -7" stroke="#495057" stroke-width="1.8" fill="none" style="pointer-events: none;"></path>
|
||||
</g>
|
||||
<g data-key="end">
|
||||
<circle data-evt-index="1" class="path-end" r="10" stroke-width="0" fill="transparent" />
|
||||
<path class="path" d="M-7 7 l 7 -7 l -7 -7" stroke="#495057" stroke-width="1.8" fill="none" style="pointer-events: none;"></path>
|
||||
</g>`);
|
||||
classAdd(svgGrp, 'shpath');
|
||||
|
||||
pathData.s.el = child(svgGrp, 'start');
|
||||
pathData.e.el = child(svgGrp, 'end');
|
||||
pathData.styles = pathData.styles ?? ['arw-e'];
|
||||
const paths = childs(svgGrp, 'path', 'outer', 'selected');
|
||||
|
||||
function draw() {
|
||||
const endDir = dirByAngle(pathData.s.data.position, pathData.e.data.position);
|
||||
pathData.e.data.dir = endDir;
|
||||
pathData.s.data.dir = dirReverse(endDir);
|
||||
|
||||
const dAttr = pathCalc(pathData);
|
||||
paths.forEach(pp => pp.setAttribute('d', dAttr));
|
||||
|
||||
endDraw(pathData.s);
|
||||
endDraw(pathData.e);
|
||||
}
|
||||
|
||||
|
||||
/** @param {PathEnd} pathEnd */
|
||||
function pathDelFromShape(pathEnd) { shapeObj(pathEnd.shape)?.pathDel(svgGrp); }
|
||||
|
||||
/** @param {PathEnd} pathEnd */
|
||||
function pathAddToShape(pathEnd) {
|
||||
if (pathEnd.shape) {
|
||||
pathEnd.data = shapeObj(pathEnd.shape).pathAdd(pathEnd.shape.connectorKey, svgGrp);
|
||||
}
|
||||
};
|
||||
|
||||
/** @type { {position:(bottomX:number, bottomY:number)=>void, del:()=>void} } */
|
||||
let settingsPnl;
|
||||
function del() {
|
||||
unSelect();
|
||||
reset();
|
||||
pathDelFromShape(pathData.s);
|
||||
pathDelFromShape(pathData.e);
|
||||
svgGrp.remove();
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {0|1|2}
|
||||
* 0 - init, 1 - selected, 2 - edit
|
||||
*/
|
||||
let state = 0;
|
||||
|
||||
/** @type {()=>void} */
|
||||
let listenCopyDispose;
|
||||
|
||||
/** @param {PointerEvent} evt */
|
||||
function select(evt) {
|
||||
// in edit mode
|
||||
if (state === 2) { return; }
|
||||
|
||||
// to edit mode
|
||||
if (state === 1) {
|
||||
state = 2;
|
||||
settingsPnl = modalCreate(evt.clientX - 10, evt.clientY - 10, new PathSettings(canvas, svgGrp));
|
||||
return;
|
||||
}
|
||||
|
||||
// to select mode
|
||||
state = 1;
|
||||
classAdd(svgGrp, 'select');
|
||||
endSetEvtIndex(pathData.s, 2);
|
||||
endSetEvtIndex(pathData.e, 2);
|
||||
|
||||
canvasSelectionClearSet(canvas, unSelect);
|
||||
listenCopyDispose = listenCopy(() => [svgGrp]);
|
||||
};
|
||||
|
||||
/** @type { {():void} } */
|
||||
let hoverEmulateDispose;
|
||||
function unSelect() {
|
||||
state = 0;
|
||||
classDel(svgGrp, 'select');
|
||||
endSetEvtIndex(pathData.s, 1);
|
||||
endSetEvtIndex(pathData.e, 1);
|
||||
|
||||
settingsPnl?.del(); settingsPnl = null;
|
||||
|
||||
if (hoverEmulateDispose) {
|
||||
hoverEmulateDispose();
|
||||
hoverEmulateDispose = null;
|
||||
svgGrp.style.pointerEvents = 'unset';
|
||||
}
|
||||
|
||||
canvasSelectionClearSet(canvas, null);
|
||||
if (listenCopyDispose) { listenCopyDispose(); listenCopyDispose = null; }
|
||||
};
|
||||
|
||||
/** @type {'s'|'e'} */
|
||||
let movedEnd;
|
||||
|
||||
const reset = moveEvtProc(
|
||||
canvas.ownerSVGElement,
|
||||
svgGrp,
|
||||
canvas[CanvasSmbl].data,
|
||||
// data.end.position,
|
||||
{
|
||||
get x() { return pathData[movedEnd]?.data.position.x; },
|
||||
set x(val) { if (movedEnd) { pathData[movedEnd].data.position.x = val; } },
|
||||
|
||||
get y() { return pathData[movedEnd]?.data.position.y; },
|
||||
set y(val) { if (movedEnd) { pathData[movedEnd].data.position.y = val; } }
|
||||
},
|
||||
// onMoveStart
|
||||
/** @param {PointerEvent & { target: Element} } evt */ evt => {
|
||||
unSelect();
|
||||
|
||||
movedEnd = pathData.e.el.contains(evt.target) ? 'e' : pathData.s.el.contains(evt.target) ? 's' : null;
|
||||
|
||||
//
|
||||
// move whole path
|
||||
if (!movedEnd) {
|
||||
return;
|
||||
}
|
||||
|
||||
//
|
||||
// move path end
|
||||
|
||||
// disconnect from shape
|
||||
if (pathData[movedEnd].shape) {
|
||||
if (pathData[movedEnd].shape.shapeEl !== pathData[movedEnd === 's' ? 'e' : 's'].shape?.shapeEl) {
|
||||
pathDelFromShape(pathData[movedEnd]);
|
||||
}
|
||||
pathData[movedEnd].shape = null;
|
||||
pathData[movedEnd].data = {
|
||||
dir: pathData[movedEnd].data.dir,
|
||||
position: pointInCanvas(canvas[CanvasSmbl].data, evt.clientX, evt.clientY)
|
||||
};
|
||||
}
|
||||
|
||||
// hover emulation - start
|
||||
svgGrp.style.pointerEvents = 'none';
|
||||
hoverEmulateDispose = hoverEmulate(svgGrp.parentElement);
|
||||
},
|
||||
// onMove
|
||||
/** @param {PointerEventFixMovement} evt */
|
||||
evt => {
|
||||
if (!movedEnd) {
|
||||
moveWholePath(canvas[CanvasSmbl].data, pathData, draw, evt);
|
||||
} else {
|
||||
const diagram = document.getElementById('diagram');
|
||||
const rect = diagram.getBoundingClientRect();
|
||||
pathData[movedEnd].data.position = {
|
||||
x: evt.clientX - rect.left,
|
||||
y: evt.clientY - rect.top
|
||||
};
|
||||
draw();
|
||||
}
|
||||
},
|
||||
// onMoveEnd
|
||||
evt => {
|
||||
if (!movedEnd) {
|
||||
moveWholePathFinish(canvas[CanvasSmbl].data, pathData, draw);
|
||||
} else {
|
||||
// connect to shape
|
||||
const elemFromPoint = priorityElemFromPoint(evt);
|
||||
const connectorKey = elemFromPoint?.getAttribute('data-connect');
|
||||
if (connectorKey) {
|
||||
// @ts-ignore
|
||||
pathData[movedEnd].shape = { shapeEl: elemFromPoint.parentElement, connectorKey };
|
||||
pathAddToShape(pathData[movedEnd]);
|
||||
} else {
|
||||
placeToCell(pathData[movedEnd].data.position, canvas[CanvasSmbl].data.cell);
|
||||
}
|
||||
draw();
|
||||
}
|
||||
|
||||
// hover emulation - end
|
||||
unSelect();
|
||||
},
|
||||
// onClick
|
||||
select,
|
||||
// onOutdown
|
||||
unSelect
|
||||
);
|
||||
|
||||
svgGrp[PathSmbl] = {
|
||||
draw,
|
||||
/** @param {PointerEventInit} evt */
|
||||
pointerCapture: evt => pathData.e.el.dispatchEvent(new PointerEvent('pointerdown', evt)),
|
||||
del,
|
||||
data: pathData
|
||||
};
|
||||
|
||||
if (pathData.styles) { classAdd(svgGrp, ...pathData.styles); }
|
||||
pathAddToShape(pathData.s);
|
||||
pathAddToShape(pathData.e);
|
||||
draw();
|
||||
|
||||
return svgGrp;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{scale:number}} canvasData
|
||||
* @param {PathData} pathData
|
||||
* @param {{():void}} draw
|
||||
* @param {PointerEventFixMovement} evt
|
||||
*/
|
||||
function moveWholePath(canvasData, pathData, draw, evt) {
|
||||
/** @param {Point} point */
|
||||
const move = point => movementApplay(point, canvasData.scale, evt);
|
||||
moveShapeOrEnd(pathData.s, move);
|
||||
moveShapeOrEnd(pathData.e, move);
|
||||
|
||||
// if any shape connected - shape will draw connected path
|
||||
if (!pathData.s.shape && !pathData.e.shape) { draw(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {{cell:number}} canvasData
|
||||
* @param {PathData} pathData
|
||||
* @param {{():void}} draw
|
||||
*/
|
||||
function moveWholePathFinish(canvasData, pathData, draw) {
|
||||
/** @param {Point} point */
|
||||
const toCell = point => placeToCell(point, canvasData.cell);
|
||||
moveShapeOrEnd(pathData.s, toCell);
|
||||
moveShapeOrEnd(pathData.e, toCell);
|
||||
|
||||
if (!pathData.s.shape || !pathData.e.shape) { draw(); }
|
||||
}
|
||||
|
||||
/**
|
||||
* applay moveFn to connected shape or to path end point
|
||||
* @param {PathEnd} pathEnd, @param {{(point:Point):void}} moveFn */
|
||||
function moveShapeOrEnd(pathEnd, moveFn) {
|
||||
if (pathEnd.shape) {
|
||||
moveFn(shapeObj(pathEnd.shape).data.position);
|
||||
shapeObj(pathEnd.shape).drawPosition();
|
||||
} else {
|
||||
moveFn(pathEnd.data.position);
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {PathConnectedShape} pathConnectedShape */
|
||||
const shapeObj = pathConnectedShape => pathConnectedShape?.shapeEl[ShapeSmbl];
|
||||
|
||||
/** @param {PathEnd} pathEnd */
|
||||
function endDraw(pathEnd) {
|
||||
pathEnd.el.style.transform = `translate(${pathEnd.data.position.x}px, ${pathEnd.data.position.y}px) rotate(${arrowAngle(pathEnd.data.dir)}deg)`;
|
||||
}
|
||||
|
||||
/** @param {PathEnd} pathEnd, @param {number} index */
|
||||
function endSetEvtIndex(pathEnd, index) { pathEnd.el.firstElementChild.setAttribute('data-evt-index', index.toString()); }
|
||||
|
||||
/** @param {Dir} dir */
|
||||
const arrowAngle = dir => dir === 'right'
|
||||
? 180
|
||||
: dir === 'left'
|
||||
? 0
|
||||
: dir === 'bottom'
|
||||
? 270
|
||||
: 90;
|
||||
|
||||
/** @param {Dir} dir, @return {Dir} */
|
||||
export const dirReverse = dir => dir === 'left'
|
||||
? 'right'
|
||||
: dir === 'right'
|
||||
? 'left'
|
||||
: dir === 'top' ? 'bottom' : 'top';
|
||||
|
||||
/** @param {Point} s, @param {Point} e, @return {Dir} */
|
||||
function dirByAngle(s, e) {
|
||||
const rad = Math.atan2(e.y - s.y, e.x - s.x);
|
||||
return numInRangeIncludeEnds(rad, -0.8, 0.8)
|
||||
? 'left'
|
||||
: numInRangeIncludeEnds(rad, 0.8, 2.4)
|
||||
? 'top'
|
||||
: numInRangeIncludeEnds(rad, 2.4, 3.2) || numInRangeIncludeEnds(rad, -3.2, -2.4) ? 'right' : 'bottom';
|
||||
}
|
||||
|
||||
/** @param {PathData} data */
|
||||
function pathCalc(data) {
|
||||
let coef = Math.hypot(
|
||||
data.s.data.position.x - data.e.data.position.x,
|
||||
data.s.data.position.y - data.e.data.position.y) * 0.5;
|
||||
coef = coef > 70
|
||||
? 70
|
||||
: coef < 15 ? 15 : coef;
|
||||
|
||||
/** @param {PathEndData} pathEnd */
|
||||
function cx(pathEnd) {
|
||||
return (pathEnd.dir === 'right' || pathEnd.dir === 'left')
|
||||
? pathEnd.dir === 'right' ? pathEnd.position.x + coef : pathEnd.position.x - coef
|
||||
: pathEnd.position.x;
|
||||
}
|
||||
|
||||
/** @param {PathEndData} pathEnd */
|
||||
function cy(pathEnd) {
|
||||
return (pathEnd.dir === 'right' || pathEnd.dir === 'left')
|
||||
? pathEnd.position.y
|
||||
: pathEnd.dir === 'bottom' ? pathEnd.position.y + coef : pathEnd.position.y - coef;
|
||||
}
|
||||
|
||||
return `M ${data.s.data.position.x} ${data.s.data.position.y} C ${cx(data.s.data)} ${cy(data.s.data)}, ` +
|
||||
`${cx(data.e.data)} ${cy(data.e.data)}, ${data.e.data.position.x} ${data.e.data.position.y}`;
|
||||
}
|
||||
|
||||
/** @param {Element} element */
|
||||
function hoverEmulate(element) {
|
||||
/** @type {Element} */
|
||||
let elemFromPoint = null;
|
||||
|
||||
/** @param {PointerEvent} evt */
|
||||
function move(evt) {
|
||||
const elemFromPointNew = priorityElemFromPoint(evt);
|
||||
if (elemFromPoint !== elemFromPointNew) {
|
||||
if (classHas(elemFromPointNew, 'hovertrack')) {
|
||||
classAdd(elemFromPointNew, 'hover');
|
||||
}
|
||||
let parentHover = false;
|
||||
if (classHas(elemFromPointNew?.parentElement, 'hovertrack')) {
|
||||
classAdd(elemFromPointNew.parentElement, 'hover');
|
||||
parentHover = true;
|
||||
}
|
||||
|
||||
classDel(elemFromPoint, 'hover');
|
||||
if (elemFromPoint?.parentElement !== elemFromPointNew?.parentElement || !parentHover) {
|
||||
classDel(elemFromPoint?.parentElement, 'hover');
|
||||
}
|
||||
|
||||
elemFromPoint = elemFromPointNew;
|
||||
}
|
||||
}
|
||||
|
||||
listen(element, 'pointermove', move);
|
||||
// dispose fn
|
||||
return function() {
|
||||
listenDel(element, 'pointermove', move);
|
||||
classDel(elemFromPoint, 'hover');
|
||||
classDel(elemFromPoint?.parentElement, 'hover');
|
||||
elemFromPoint = null;
|
||||
};
|
||||
}
|
||||
|
||||
/** @param {Element} el, @param {...string} keys */
|
||||
const childs = (el, ...keys) => keys.map(kk => child(el, kk));
|
||||
|
||||
/** @param {number} num, @param {number} a, @param {number} b */
|
||||
const numInRangeIncludeEnds = (num, a, b) => a <= num && num <= b;
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { 'left' | 'right' | 'top' | 'bottom' } Dir */
|
||||
/** @typedef { {shapeEl: ShapeElement, connectorKey: string} } PathConnectedShape */
|
||||
/** @typedef { {position: Point, dir: Dir }} PathEndData */
|
||||
/** @typedef { {shape?:PathConnectedShape, data?:PathEndData, el?:SVGElement} } PathEnd */
|
||||
/**
|
||||
@typedef {{
|
||||
s: PathEnd,
|
||||
e: PathEnd,
|
||||
styles?: string[],
|
||||
}} PathData
|
||||
*/
|
||||
/** @typedef { {shape?:PathConnectedShape, data?:PathEndData, oppositeShape?:PathConnectedShape, type:number} } MovedEnd */
|
||||
/**
|
||||
@typedef {{
|
||||
draw(): void
|
||||
pointerCapture: (evt:PointerEventInit)=>void
|
||||
del(): void
|
||||
data: PathData
|
||||
}} Path
|
||||
*/
|
||||
|
||||
/** @typedef { import('./path-smbl.js').PathElement } PathElement */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('./shape-smbl').ShapeElement } ShapeElement */
|
||||
/** @typedef { import('./shape-evt-proc').Shape } Shape */
|
||||
/** @typedef { import('../infrastructure/move-evt-mobile-fix.js').PointerEventFixMovement } PointerEventFixMovement */
|
||||
86
main_plugin/dgrm/shapes/rect-txt-settings.js
Executable file
86
main_plugin/dgrm/shapes/rect-txt-settings.js
Executable file
@@ -0,0 +1,86 @@
|
||||
import { copyAndPast } from '../diagram/group-select-applay.js';
|
||||
import { classAdd, classDel, clickForAll, listen, classSingleAdd, evtTargetAttr } from '../infrastructure/util.js';
|
||||
import { modalCreate } from './modal-create.js';
|
||||
import { ShapeSmbl } from './shape-smbl.js';
|
||||
|
||||
/**
|
||||
* @param {import('../infrastructure/canvas-smbl.js').CanvasElement} canvas
|
||||
* @param {import('./shape-smbl').ShapeElement} shapeElement
|
||||
* @param {number} bottomX positon of the bottom left corner of the panel
|
||||
* @param {number} bottomY positon of the bottom left corner of the panel
|
||||
*/
|
||||
export const rectTxtSettingsPnlCreate = (canvas, shapeElement, bottomX, bottomY) =>
|
||||
modalCreate(bottomX, bottomY, new RectTxtSettings(canvas, shapeElement));
|
||||
|
||||
class RectTxtSettings extends HTMLElement {
|
||||
/**
|
||||
* @param {import('../infrastructure/canvas-smbl.js').CanvasElement} canvas
|
||||
* @param {import('./shape-smbl').ShapeElement} rectElement
|
||||
*/
|
||||
constructor(canvas, rectElement) {
|
||||
super();
|
||||
/** @private */
|
||||
this._rectElement = rectElement;
|
||||
|
||||
/** @private */
|
||||
this._canvas = canvas;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
.ln { display: flex; }
|
||||
.ln > * {
|
||||
height: 24px;
|
||||
padding: 10px;
|
||||
fill-opacity: 0.3;
|
||||
stroke-opacity: 0.3;
|
||||
}
|
||||
[data-cmd] { cursor: pointer; }
|
||||
|
||||
.ta-1 [data-cmd-arg="1"],
|
||||
.ta-2 [data-cmd-arg="2"],
|
||||
.ta-3 [data-cmd-arg="3"]
|
||||
{ fill-opacity: 1; stroke-opacity: 1; }
|
||||
</style>
|
||||
<ap-shape-edit id="F" edit-btn="true">
|
||||
<div class="ln">
|
||||
<svg data-cmd data-cmd-arg="1" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 15h14v2H3v-2zm0-5h18v2H3v-2zm0-5h14v2H3V9z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-cmd data-cmd-arg="2" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm2 15h14v2H5v-2zm-2-5h18v2H3v-2zm2-5h14v2H5V9z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-cmd data-cmd-arg="3" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm4 15h14v2H7v-2zm-4-5h18v2H3v-2zm4-5h14v2H7V9z" fill="rgb(52,71,103)"/></svg>
|
||||
</div>
|
||||
</ap-shape-edit>`;
|
||||
|
||||
const rectData = /** @type {import('./rect.js').RectData} */(this._rectElement[ShapeSmbl].data);
|
||||
|
||||
const editEl = shadow.getElementById('edit');
|
||||
classAdd(editEl, `ta-${rectData.a}`);
|
||||
|
||||
// colors, del
|
||||
listen(editEl, 'cmd', /** @param {CustomEvent<{cmd:string, arg:string}>} evt */ evt => {
|
||||
switch (evt.detail.cmd) {
|
||||
case 'style': classSingleAdd(this._rectElement, rectData, 'cl-', evt.detail.arg); break;
|
||||
case 'del': this._rectElement[ShapeSmbl].del(); break;
|
||||
case 'copy': copyAndPast(this._canvas, [this._rectElement]); break;
|
||||
}
|
||||
});
|
||||
|
||||
// text align
|
||||
clickForAll(shadow, '[data-cmd]', evt => {
|
||||
const alignNew = /** @type {1|2|3} */(Number.parseInt(evtTargetAttr(evt, 'data-cmd-arg')));
|
||||
if (alignNew === rectData.a) { return; }
|
||||
|
||||
const alignOld = rectData.a;
|
||||
|
||||
// applay text align to shape
|
||||
rectData.a = alignNew;
|
||||
this._rectElement[ShapeSmbl].draw();
|
||||
|
||||
// highlight text align btn in settings panel
|
||||
classDel(editEl, `ta-${alignOld}`);
|
||||
classAdd(editEl, `ta-${rectData.a}`);
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('ap-rect-txt-settings', RectTxtSettings);
|
||||
142
main_plugin/dgrm/shapes/rect.js
Executable file
142
main_plugin/dgrm/shapes/rect.js
Executable file
@@ -0,0 +1,142 @@
|
||||
import { ceil, child, classAdd, classDel, positionSet } from '../infrastructure/util.js';
|
||||
import { rectTxtSettingsPnlCreate } from './rect-txt-settings.js';
|
||||
import { shapeCreate } from './shape-evt-proc.js';
|
||||
import { settingsPnlCreate } from './shape-settings.js';
|
||||
import { ShapeSmbl } from './shape-smbl.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {RectData} rectData
|
||||
*/
|
||||
export function rect(canvas, rectData) {
|
||||
rectData.w = rectData.w ?? 96;
|
||||
rectData.h = rectData.h ?? 48;
|
||||
rectData.a = rectData.a ?? (rectData.t ? 1 : 2);
|
||||
|
||||
const templ = `
|
||||
<rect data-key="outer" data-evt-no data-evt-index="2" width="144" height="96" x="-72" y="-48" fill="transparent" stroke="transparent" stroke-width="0" />
|
||||
<rect data-key="main" width="96" height="48" x="-48" y="-24" rx="15" ry="15" fill="#1aaee5" stroke="#fff" stroke-width="1" />
|
||||
<text data-key="text" y="0" x="${rectTxtXByAlign(rectData)}" style="pointer-events: none;" fill="#fff"> </text>`;
|
||||
|
||||
const shape = shapeCreate(canvas, rectData, templ,
|
||||
{
|
||||
right: { dir: 'right', position: { x: 48, y: 0 } },
|
||||
left: { dir: 'left', position: { x: -48, y: 0 } },
|
||||
bottom: { dir: 'bottom', position: { x: 0, y: 24 } },
|
||||
top: { dir: 'top', position: { x: 0, y: -24 } }
|
||||
},
|
||||
// onTextChange
|
||||
txtEl => {
|
||||
const textBox = txtEl.getBBox();
|
||||
const newWidth = ceil(96, 48, textBox.width + (rectData.t ? 6 : 0)); // 6 px right padding for text shape
|
||||
const newHeight = ceil(48, 48, textBox.height);
|
||||
|
||||
if (rectData.w !== newWidth || rectData.h !== newHeight) {
|
||||
rectData.w = newWidth;
|
||||
rectData.h = newHeight;
|
||||
resize();
|
||||
}
|
||||
},
|
||||
// settingsPnlCreateFn
|
||||
rectData.t ? rectTxtSettingsPnlCreate : settingsPnlCreate);
|
||||
|
||||
classAdd(shape.el, rectData.t ? 'shtxt' : 'shrect');
|
||||
|
||||
let currentW = rectData.w;
|
||||
let currentTxtAlign = rectData.a;
|
||||
/** @param {boolean?=} fixTxtAlign */
|
||||
function resize(fixTxtAlign) {
|
||||
const mainX = rectData.w / -2;
|
||||
const mainY = rectData.h / -2;
|
||||
const middleX = 0;
|
||||
|
||||
shape.cons.right.position.x = -mainX;
|
||||
shape.cons.left.position.x = mainX;
|
||||
shape.cons.bottom.position.y = -mainY;
|
||||
shape.cons.bottom.position.x = middleX;
|
||||
shape.cons.top.position.y = mainY;
|
||||
shape.cons.top.position.x = middleX;
|
||||
for (const connectorKey in shape.cons) {
|
||||
positionSet(child(shape.el, connectorKey), shape.cons[connectorKey].position);
|
||||
}
|
||||
|
||||
rectSet(shape.el, 'main', rectData.w, rectData.h, mainX, mainY);
|
||||
rectSet(shape.el, 'outer', rectData.w + 48, rectData.h + 48, mainX - 24, mainY - 24);
|
||||
|
||||
// if text align or width changed
|
||||
// fix text align
|
||||
if (fixTxtAlign || currentTxtAlign !== rectData.a || currentW !== rectData.w) {
|
||||
let txtX;
|
||||
let posXDelta;
|
||||
switch (rectData.a) {
|
||||
// text align left
|
||||
case 1:
|
||||
txtX = mainX + 8;
|
||||
posXDelta = (rectData.w - currentW) / 2;
|
||||
break;
|
||||
case 2:
|
||||
txtX = 0;
|
||||
posXDelta = 0;
|
||||
break;
|
||||
// text align right
|
||||
case 3:
|
||||
txtX = -mainX - 8;
|
||||
posXDelta = (rectData.w - currentW) / -2;
|
||||
break;
|
||||
}
|
||||
|
||||
const txtEl = child(shape.el, 'text');
|
||||
txtEl.x.baseVal[0].value = txtX;
|
||||
txtEl.querySelectorAll('tspan').forEach(ss => { ss.x.baseVal[0].value = txtX; });
|
||||
|
||||
rectData.position.x += posXDelta;
|
||||
|
||||
classDel(shape.el, `ta-${currentTxtAlign}`);
|
||||
classAdd(shape.el, `ta-${rectData.a}`);
|
||||
|
||||
currentTxtAlign = rectData.a;
|
||||
currentW = rectData.w;
|
||||
}
|
||||
|
||||
shape.draw();
|
||||
}
|
||||
|
||||
classAdd(shape.el, `ta-${rectData.a}`);
|
||||
if (rectData.w !== 96 || rectData.h !== 48) { resize(true); } else { shape.draw(); }
|
||||
|
||||
shape.el[ShapeSmbl].draw = resize;
|
||||
|
||||
return shape.el;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} svgGrp, @param {string} key,
|
||||
* @param {number} w, @param {number} h
|
||||
* @param {number} x, @param {number} y
|
||||
*/
|
||||
function rectSet(svgGrp, key, w, h, x, y) {
|
||||
/** @type {SVGRectElement} */ const rect = child(svgGrp, key);
|
||||
rect.width.baseVal.value = w;
|
||||
rect.height.baseVal.value = h;
|
||||
rect.x.baseVal.value = x;
|
||||
rect.y.baseVal.value = y;
|
||||
}
|
||||
|
||||
/** @param {RectData} rectData */
|
||||
const rectTxtXByAlign = rectData => rectData.a === 1
|
||||
? -40 // text align keft
|
||||
: rectData.a === 2
|
||||
? 0 // text align middle
|
||||
: 40; // text align right
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('./shape-evt-proc').CanvasData } CanvasData */
|
||||
/** @typedef { import('./shape-evt-proc').ConnectorsData } ConnectorsData */
|
||||
/**
|
||||
@typedef {{
|
||||
type:number, position: Point, title?: string, styles?: string[],
|
||||
w?:number, h?:number
|
||||
t?:boolean,
|
||||
a?: 1|2|3
|
||||
}} RectData */
|
||||
101
main_plugin/dgrm/shapes/rhomb.js
Executable file
101
main_plugin/dgrm/shapes/rhomb.js
Executable file
@@ -0,0 +1,101 @@
|
||||
import { ceil, child, classAdd, positionSet, svgTxtFarthestPoint } from '../infrastructure/util.js';
|
||||
import { shapeCreate } from './shape-evt-proc.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {RhombData} rhombData
|
||||
*/
|
||||
export function rhomb(canvas, rhombData) {
|
||||
const templ = `
|
||||
<path data-key="outer" data-evt-no data-evt-index="2" d="M-72 0 L0 -72 L72 0 L0 72 Z" stroke-width="0" fill="transparent" />
|
||||
<path data-key="border" d="M-39 0 L0 -39 L39 0 L0 39 Z" stroke-width="20" stroke="#fff" fill="transparent" stroke-linejoin="round" />
|
||||
<path data-key="main" d="M-39 0 L0 -39 L39 0 L0 39 Z" stroke-width="18" stroke-linejoin="round" stroke="#1D809F" fill="#1D809F" />
|
||||
<text data-key="text" x="0" y="0" text-anchor="middle" style="pointer-events: none;" fill="#fff"> </text>`;
|
||||
|
||||
const shape = shapeCreate(canvas, rhombData, templ,
|
||||
{
|
||||
right: { dir: 'right', position: { x: 48, y: 0 } },
|
||||
left: { dir: 'left', position: { x: -48, y: 0 } },
|
||||
bottom: { dir: 'bottom', position: { x: 0, y: 48 } },
|
||||
top: { dir: 'top', position: { x: 0, y: -48 } }
|
||||
},
|
||||
// onTextChange
|
||||
txtEl => {
|
||||
const newWidth = ceil(96, 48, textElRhombWidth(txtEl) - 20); // -20 experemental val
|
||||
if (newWidth !== rhombData.w) {
|
||||
rhombData.w = newWidth;
|
||||
resize();
|
||||
}
|
||||
});
|
||||
classAdd(shape.el, 'shrhomb');
|
||||
|
||||
function resize() {
|
||||
const connectors = rhombCalc(rhombData.w, 0);
|
||||
shape.cons.right.position.x = connectors.r.x;
|
||||
shape.cons.left.position.x = connectors.l.x;
|
||||
shape.cons.bottom.position.y = connectors.b.y;
|
||||
shape.cons.top.position.y = connectors.t.y;
|
||||
for (const connectorKey in shape.cons) {
|
||||
positionSet(child(shape.el, connectorKey), shape.cons[connectorKey].position);
|
||||
}
|
||||
|
||||
const mainRhomb = rhombCalc(rhombData.w, 9);
|
||||
rhombSet(shape.el, 'main', mainRhomb);
|
||||
rhombSet(shape.el, 'border', mainRhomb);
|
||||
rhombSet(shape.el, 'outer', rhombCalc(rhombData.w, -24));
|
||||
|
||||
shape.draw();
|
||||
}
|
||||
|
||||
if (!!rhombData.w && rhombData.w !== 96) { resize(); } else { shape.draw(); }
|
||||
|
||||
return shape.el;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Element} svgGrp, @param {string} key,
|
||||
* @param {RhombPoints} rhomb
|
||||
*/
|
||||
function rhombSet(svgGrp, key, rhomb) {
|
||||
/** @type {SVGPathElement} */(child(svgGrp, key)).setAttribute('d', `M${rhomb.l.x} ${rhomb.l.y} L${rhomb.t.x} ${rhomb.t.y} L${rhomb.r.x} ${rhomb.r.y} L${rhomb.b.x} ${rhomb.b.y} Z`);
|
||||
}
|
||||
|
||||
/**
|
||||
* calc square rhomb points by width
|
||||
* origin is in the center of the rhomb
|
||||
* @param {number} width, @param {number} margin
|
||||
* @returns {RhombPoints}
|
||||
*/
|
||||
function rhombCalc(width, margin) {
|
||||
const half = width / 2;
|
||||
const mrgnMinHalf = margin - half;
|
||||
const halfMinMrgn = half - margin;
|
||||
return {
|
||||
l: { x: mrgnMinHalf, y: 0 },
|
||||
t: { x: 0, y: mrgnMinHalf },
|
||||
r: { x: halfMinMrgn, y: 0 },
|
||||
b: { x: 0, y: halfMinMrgn }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* calc width of the square rhomb that cover all tspan in {textEl}
|
||||
* origin is in the center of the rhomb
|
||||
* @param {SVGTextElement} textEl
|
||||
*/
|
||||
function textElRhombWidth(textEl) {
|
||||
const farthestPoint = svgTxtFarthestPoint(textEl);
|
||||
return 2 * (Math.abs(farthestPoint.x) + Math.abs(farthestPoint.y));
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef { import('./shape-evt-proc').CanvasData } CanvasData */
|
||||
/** @typedef { import('./shape-evt-proc').ConnectorsData } ConnectorsData */
|
||||
/**
|
||||
@typedef {{
|
||||
type:number, position: Point, title?: string, styles?: string[]
|
||||
w?:number
|
||||
}} RhombData
|
||||
*/
|
||||
/** @typedef { { l:Point, t:Point, r:Point, b:Point } } RhombPoints */
|
||||
306
main_plugin/dgrm/shapes/shape-evt-proc.js
Executable file
306
main_plugin/dgrm/shapes/shape-evt-proc.js
Executable file
@@ -0,0 +1,306 @@
|
||||
import { child, classAdd, classDel, deepCopy, svgEl } from '../infrastructure/util.js';
|
||||
import { moveEvtProc } from '../infrastructure/move-evt-proc.js';
|
||||
import { path, dirReverse } from './path.js';
|
||||
import { textareaCreate } from '../infrastructure/svg-text-area.js';
|
||||
import { settingsPnlCreate } from './shape-settings.js';
|
||||
import { placeToCell, pointInCanvas } from '../infrastructure/move-scale-applay.js';
|
||||
import { ShapeSmbl } from './shape-smbl.js';
|
||||
import { svgTextDraw } from '../infrastructure/svg-text-draw.js';
|
||||
import { PathSmbl } from './path-smbl.js';
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { canvasSelectionClearSet } from '../diagram/canvas-clear.js';
|
||||
import { listenCopy } from '../diagram/group-select-applay.js';
|
||||
|
||||
/**
|
||||
* provides:
|
||||
* - shape move
|
||||
* - connectors
|
||||
*
|
||||
* - text editor
|
||||
* - standard edit panel
|
||||
* - onTextChange callback
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {string} shapeHtml must have '<text data-key="text">'
|
||||
* @param {ShapeData & { title?: string, styles?: string[]}} shapeData
|
||||
* @param {ConnectorsData} cons
|
||||
* @param {SettingsPnlCreateFn=} settingsPnlCreateFn
|
||||
* @param {{(txtEl:SVGTextElement):void}} onTextChange
|
||||
*/
|
||||
export function shapeCreate(canvas, shapeData, shapeHtml, cons, onTextChange, settingsPnlCreateFn) {
|
||||
/** @type {ShapeElement} */
|
||||
const el = svgEl('g', `${shapeHtml}
|
||||
${Object.entries(cons)
|
||||
.map(cc => `<circle data-key="${cc[0]}" data-connect="${cc[1].dir}" class="hovertrack" data-evt-index="2" r="10" cx="0" cy="0" style="transform: translate(${cc[1].position.x}px, ${cc[1].position.y}px);" />`)
|
||||
.join()}`);
|
||||
|
||||
const textSettings = {
|
||||
/** @type {SVGTextElement} */
|
||||
el: child(el, 'text'),
|
||||
/** vericale middle, em */
|
||||
vMid: 0
|
||||
};
|
||||
|
||||
svgTextDraw(textSettings.el, textSettings.vMid, shapeData.title);
|
||||
|
||||
const shapeProc = shapeEditEvtProc(canvas, el, shapeData, cons, textSettings,
|
||||
settingsPnlCreateFn,
|
||||
// onTextChange
|
||||
() => onTextChange(textSettings.el));
|
||||
|
||||
return {
|
||||
el,
|
||||
cons,
|
||||
draw: shapeProc.draw
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* provides:
|
||||
* - shape move
|
||||
* - connectors
|
||||
* - copy fn
|
||||
*
|
||||
* - text editor
|
||||
* - standard edit panel
|
||||
* - onTextChange callback
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {ShapeElement} svgGrp
|
||||
* @param {ShapeData & { title?: string, styles?: string[]}} shapeData
|
||||
* @param {ConnectorsData} connectorsInnerPosition
|
||||
* @param { {el:SVGTextElement, vMid: number} } textSettings vMid in em
|
||||
* @param {{():void}} onTextChange
|
||||
* @param {SettingsPnlCreateFn} settingsPnlCreateFn
|
||||
*/
|
||||
function shapeEditEvtProc(canvas, svgGrp, shapeData, connectorsInnerPosition, textSettings, settingsPnlCreateFn, onTextChange) {
|
||||
/** @type {{dispose():void, draw():void}} */
|
||||
let textEditor;
|
||||
|
||||
/** @type { {position:(bottomX:number, bottomY:number)=>void, del:()=>void} } */
|
||||
let settingsPnl;
|
||||
|
||||
function unSelect() {
|
||||
textEditor?.dispose(); textEditor = null;
|
||||
settingsPnl?.del(); settingsPnl = null;
|
||||
}
|
||||
|
||||
/** @param {string} txt */
|
||||
function onTxtChange(txt) {
|
||||
shapeData.title = txt;
|
||||
onTextChange();
|
||||
}
|
||||
|
||||
const settingPnlCreate = settingsPnlCreateFn ?? settingsPnlCreate;
|
||||
const shapeProc = shapeEvtProc(canvas, svgGrp, shapeData, connectorsInnerPosition,
|
||||
// onEdit
|
||||
() => {
|
||||
textEditor = textareaCreate(textSettings.el, textSettings.vMid, shapeData.title, onTxtChange, onTxtChange);
|
||||
|
||||
const position = svgGrp.getBoundingClientRect();
|
||||
settingsPnl = settingPnlCreate(canvas, svgGrp, position.left + 10, position.top + 10);
|
||||
},
|
||||
// onUnselect
|
||||
unSelect
|
||||
);
|
||||
|
||||
if (shapeData.styles) { classAdd(svgGrp, ...shapeData.styles); }
|
||||
|
||||
svgGrp[ShapeSmbl].del = function() {
|
||||
shapeProc.del();
|
||||
svgGrp.remove();
|
||||
};
|
||||
|
||||
return {
|
||||
draw: () => {
|
||||
shapeProc.drawPosition();
|
||||
|
||||
if (settingsPnl) {
|
||||
const position = svgGrp.getBoundingClientRect();
|
||||
settingsPnl.position(position.left + 10, position.top + 10);
|
||||
}
|
||||
|
||||
if (textEditor) { textEditor.draw(); }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* provides:
|
||||
* - shape move
|
||||
* - connectors
|
||||
* - copy fn
|
||||
* - onEdit, onEditStop callbacks
|
||||
* @param {CanvasElement} canvas
|
||||
* @param {ShapeElement} svgGrp
|
||||
* @param {ShapeData} shapeData
|
||||
* @param {ConnectorsData} connectorsInnerPosition
|
||||
* @param {{():void}} onEdit
|
||||
* @param {{():void}} onUnselect
|
||||
*/
|
||||
function shapeEvtProc(canvas, svgGrp, shapeData, connectorsInnerPosition, onEdit, onUnselect) {
|
||||
classAdd(svgGrp, 'hovertrack');
|
||||
|
||||
/** @type {ConnectorsData} */
|
||||
const connectorsData = deepCopy(connectorsInnerPosition);
|
||||
|
||||
/** @type { Set<PathElement> } */
|
||||
const paths = new Set();
|
||||
|
||||
function drawPosition() {
|
||||
svgGrp.style.transform = `translate(${shapeData.position.x}px, ${shapeData.position.y}px)`;
|
||||
|
||||
// paths
|
||||
for (const connectorKey in connectorsInnerPosition) {
|
||||
connectorsData[connectorKey].position = {
|
||||
x: connectorsInnerPosition[connectorKey].position.x + shapeData.position.x,
|
||||
y: connectorsInnerPosition[connectorKey].position.y + shapeData.position.y
|
||||
};
|
||||
}
|
||||
|
||||
for (const path of paths) {
|
||||
path[PathSmbl].draw();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {0|1|2}
|
||||
* 0 - init, 1 - selected, 2 - edit
|
||||
*/
|
||||
let state = 0;
|
||||
|
||||
/** @type {()=>void} */
|
||||
let listenCopyDispose;
|
||||
|
||||
function unSelect() {
|
||||
onUnselect();
|
||||
|
||||
state = 0;
|
||||
classDel(svgGrp, 'select');
|
||||
classDel(svgGrp, 'highlight');
|
||||
|
||||
canvasSelectionClearSet(canvas, null);
|
||||
if (listenCopyDispose) { listenCopyDispose(); listenCopyDispose = null; }
|
||||
}
|
||||
|
||||
const moveProcReset = moveEvtProc(
|
||||
canvas.ownerSVGElement,
|
||||
svgGrp,
|
||||
canvas[CanvasSmbl].data,
|
||||
shapeData.position,
|
||||
// onMoveStart
|
||||
/** @param {PointerEvent & { target: Element} } evt */
|
||||
evt => {
|
||||
unSelect();
|
||||
|
||||
const connectorKey = evt.target.getAttribute('data-connect');
|
||||
if (connectorKey) {
|
||||
moveProcReset();
|
||||
|
||||
const diagramEl = document.getElementById('diagram');
|
||||
const rect = diagramEl.getBoundingClientRect();
|
||||
const x = evt.clientX - rect.left;
|
||||
const y = evt.clientY - rect.top;
|
||||
|
||||
const pathEl = path(canvas, {
|
||||
s: { shape: { shapeEl: svgGrp, connectorKey } },
|
||||
e: {
|
||||
data: {
|
||||
dir: dirReverse(connectorsData[connectorKey].dir),
|
||||
position: pointInCanvas(canvas[CanvasSmbl].data, x, y)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
svgGrp.parentNode.append(pathEl);
|
||||
pathEl[PathSmbl].pointerCapture(evt);
|
||||
paths.add(pathEl);
|
||||
}
|
||||
},
|
||||
// onMove
|
||||
drawPosition,
|
||||
// onMoveEnd
|
||||
_ => {
|
||||
placeToCell(shapeData.position, canvas[CanvasSmbl].data.cell);
|
||||
drawPosition();
|
||||
},
|
||||
// onClick
|
||||
_ => {
|
||||
// in edit mode
|
||||
if (state === 2) { return; }
|
||||
|
||||
// to edit mode
|
||||
if (state === 1) {
|
||||
state = 2;
|
||||
classDel(svgGrp, 'select');
|
||||
classAdd(svgGrp, 'highlight');
|
||||
// edit mode
|
||||
onEdit();
|
||||
return;
|
||||
}
|
||||
|
||||
// to select mode
|
||||
state = 1;
|
||||
classAdd(svgGrp, 'select');
|
||||
|
||||
canvasSelectionClearSet(canvas, unSelect);
|
||||
listenCopyDispose = listenCopy(() => [svgGrp]);
|
||||
},
|
||||
// onOutdown
|
||||
unSelect);
|
||||
|
||||
svgGrp[ShapeSmbl] = {
|
||||
/**
|
||||
* @param {string} connectorKey
|
||||
* @param {PathElement} pathEl
|
||||
*/
|
||||
pathAdd: function(connectorKey, pathEl) {
|
||||
paths.add(pathEl);
|
||||
return connectorsData[connectorKey];
|
||||
},
|
||||
|
||||
/** @param {PathElement} pathEl */
|
||||
pathDel: function(pathEl) {
|
||||
paths.delete(pathEl);
|
||||
},
|
||||
|
||||
drawPosition,
|
||||
|
||||
data: shapeData
|
||||
};
|
||||
|
||||
return {
|
||||
drawPosition,
|
||||
del: () => {
|
||||
unSelect();
|
||||
moveProcReset();
|
||||
for (const path of paths) {
|
||||
path[PathSmbl].del();
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { {position:Point, scale:number, cell:number} } CanvasData */
|
||||
|
||||
/** @typedef { 'left' | 'right' | 'top' | 'bottom' } PathDir */
|
||||
/** @typedef { {position: Point, dir: PathDir} } PathEnd */
|
||||
/** @typedef { Object.<string, PathEnd> } ConnectorsData */
|
||||
|
||||
/** @typedef { {type: number, position: Point, styles?:string[]} } ShapeData */
|
||||
/**
|
||||
@typedef {{
|
||||
pathAdd(connectorKey:string, pathEl:PathElement): PathEnd
|
||||
pathDel(pathEl:PathElement): void
|
||||
drawPosition: ()=>void
|
||||
data: ShapeData
|
||||
del?: ()=>void
|
||||
draw?: ()=>void
|
||||
}} Shape
|
||||
*/
|
||||
|
||||
/** @typedef { {(canvas:CanvasElement, shapeElement:ShapeElement, bottomX:number, bottomY:number):{position(btmX:number, btmY:number):void, del():void} } } SettingsPnlCreateFn */
|
||||
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/** @typedef {import('./shape-smbl').ShapeElement} ShapeElement */
|
||||
/** @typedef {import('./path').Path} Path */
|
||||
/** @typedef {import('./path-smbl').PathElement} PathElement */
|
||||
121
main_plugin/dgrm/shapes/shape-settings.js
Executable file
121
main_plugin/dgrm/shapes/shape-settings.js
Executable file
@@ -0,0 +1,121 @@
|
||||
import { copyAndPast } from '../diagram/group-select-applay.js';
|
||||
import { copySvg, delSvg } from '../infrastructure/assets.js';
|
||||
import { clickForAll, listen, classSingleAdd, evtTargetAttr } from '../infrastructure/util.js';
|
||||
import { modalChangeTop, modalCreate } from './modal-create.js';
|
||||
import { ShapeSmbl } from './shape-smbl.js';
|
||||
|
||||
/**
|
||||
* @param {import('../infrastructure/canvas-smbl').CanvasElement} canvas
|
||||
* @param {import('./shape-smbl').ShapeElement} shapeElement
|
||||
* @param {number} bottomX positon of the bottom left corner of the panel
|
||||
* @param {number} bottomY positon of the bottom left corner of the panel
|
||||
*/
|
||||
export function settingsPnlCreate(canvas, shapeElement, bottomX, bottomY) {
|
||||
const shapeSettings = new ShapeEdit();
|
||||
listen(shapeSettings, 'cmd', /** @param {CustomEvent<{cmd:string, arg:string}>} evt */ evt => {
|
||||
switch (evt.detail.cmd) {
|
||||
case 'style': classSingleAdd(shapeElement, shapeElement[ShapeSmbl].data, 'cl-', evt.detail.arg); break;
|
||||
case 'del': shapeElement[ShapeSmbl].del(); break;
|
||||
case 'copy': copyAndPast(canvas, [shapeElement]); break;
|
||||
}
|
||||
});
|
||||
return modalCreate(bottomX, bottomY, shapeSettings);
|
||||
}
|
||||
|
||||
class ShapeEdit extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML =
|
||||
`<style>
|
||||
.ln { display: flex; }
|
||||
.ln > * {
|
||||
height: 24px;
|
||||
padding: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#prop { padding-bottom: 10px; }
|
||||
|
||||
.crcl { width: 25px; height: 25px; border-radius: 50%; }
|
||||
</style>
|
||||
<div id="pnl">
|
||||
<div id="clr" style="display: none;">
|
||||
<div class="ln">
|
||||
<div data-cmd="style" data-cmd-arg="cl-red">
|
||||
<div class="crcl" style="background: #E74C3C"></div>
|
||||
</div>
|
||||
<div data-cmd="style" data-cmd-arg="cl-orange">
|
||||
<div class="crcl" style="background: #ff6600"></div>
|
||||
</div>
|
||||
<div data-cmd="style" data-cmd-arg="cl-green">
|
||||
<div class="crcl" style="background: #19bc9b"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ln">
|
||||
<div data-cmd="style" data-cmd-arg="cl-blue">
|
||||
<div class="crcl" style="background: #1aaee5"></div>
|
||||
</div>
|
||||
<div data-cmd="style" data-cmd-arg="cl-dblue">
|
||||
<div class="crcl" style="background: #1D809F"></div>
|
||||
</div>
|
||||
<div data-cmd="style" data-cmd-arg="cl-dgray">
|
||||
<div class="crcl" style="background: #495057"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="prop" style="display: none;"><slot id="slot"></slot></div>
|
||||
</div>
|
||||
<div class="ln">
|
||||
<svg data-toggle="clr" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M19.228 18.732l1.768-1.768 1.767 1.768a2.5 2.5 0 1 1-3.535 0zM8.878 1.08l11.314 11.313a1 1 0 0 1 0 1.415l-8.485 8.485a1 1 0 0 1-1.414 0l-8.485-8.485a1 1 0 0 1 0-1.415l7.778-7.778-2.122-2.121L8.88 1.08zM11 6.03L3.929 13.1 11 20.173l7.071-7.071L11 6.029z" fill="rgb(52,71,103)"/></svg>
|
||||
<svg data-toggle="prop" ${this.getAttribute('edit-btn') ? '' : 'style="display: none;"'} viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M12.9 6.858l4.242 4.243L7.242 21H3v-4.243l9.9-9.9zm1.414-1.414l2.121-2.122a1 1 0 0 1 1.414 0l2.829 2.829a1 1 0 0 1 0 1.414l-2.122 2.121-4.242-4.242z" fill="rgb(52,71,103)"/></svg>
|
||||
${copySvg}
|
||||
${delSvg}
|
||||
</div>`;
|
||||
|
||||
//
|
||||
// tabs
|
||||
|
||||
{
|
||||
const pnl = shadow.getElementById('pnl');
|
||||
|
||||
/** @param {1|-1} coef */
|
||||
function modalSetTop(coef) {
|
||||
modalChangeTop(window.scrollY + coef * pnl.getBoundingClientRect().height); // window.scrollY fix IPhone keyboard
|
||||
}
|
||||
|
||||
/** @type {HTMLElement} */
|
||||
let currentTab;
|
||||
|
||||
clickForAll(shadow, '[data-toggle]', evt => {
|
||||
if (currentTab) {
|
||||
modalSetTop(1);
|
||||
display(currentTab, false);
|
||||
}
|
||||
|
||||
const tab = shadow.getElementById(evtTargetAttr(evt, 'data-toggle'));
|
||||
if (currentTab !== tab) {
|
||||
display(tab, true);
|
||||
modalSetTop(-1);
|
||||
currentTab = tab;
|
||||
} else {
|
||||
currentTab = null;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
//
|
||||
// commands
|
||||
|
||||
clickForAll(shadow, '[data-cmd]', evt => {
|
||||
this.dispatchEvent(new CustomEvent('cmd', {
|
||||
detail: {
|
||||
cmd: evtTargetAttr(evt, 'data-cmd'),
|
||||
arg: evtTargetAttr(evt, 'data-cmd-arg')
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('ap-shape-edit', ShapeEdit);
|
||||
|
||||
/** @param {ElementCSSInlineStyle} el, @param {boolean} isDisp */
|
||||
function display(el, isDisp) { el.style.display = isDisp ? 'unset' : 'none'; }
|
||||
3
main_plugin/dgrm/shapes/shape-smbl.js
Executable file
3
main_plugin/dgrm/shapes/shape-smbl.js
Executable file
@@ -0,0 +1,3 @@
|
||||
export const ShapeSmbl = Symbol('shape');
|
||||
|
||||
/** @typedef {SVGGraphicsElement & { [ShapeSmbl]?: import('./shape-evt-proc').Shape }} ShapeElement */
|
||||
27
main_plugin/dgrm/shapes/shape-type-map.js
Executable file
27
main_plugin/dgrm/shapes/shape-type-map.js
Executable file
@@ -0,0 +1,27 @@
|
||||
import { circle } from './circle.js';
|
||||
import { path } from './path.js';
|
||||
import { rect } from './rect.js';
|
||||
import { rhomb } from './rhomb.js';
|
||||
|
||||
/**
|
||||
* @param {CanvasElement} canvas
|
||||
* @returns {Record<number, ShapeType>}
|
||||
*/
|
||||
export function shapeTypeMap(canvas) {
|
||||
return {
|
||||
0: { create: shapeData => path(canvas, shapeData) },
|
||||
1: { create: shapeData => circle(canvas, shapeData) },
|
||||
2: { create: shapeData => rect(canvas, shapeData) },
|
||||
3: { create: shapeData => { /** @type {RectData} */(shapeData).t = true; return rect(canvas, shapeData); } },
|
||||
4: { create: shapeData => rhomb(canvas, shapeData) }
|
||||
};
|
||||
}
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('./rect.js').RectData } RectData */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
/**
|
||||
@typedef {{
|
||||
create: (shapeData)=>SVGGraphicsElement
|
||||
}} ShapeType
|
||||
*/
|
||||
371
main_plugin/dgrm/ui/menu.js
Executable file
371
main_plugin/dgrm/ui/menu.js
Executable file
@@ -0,0 +1,371 @@
|
||||
import { canvasClear } from '../diagram/canvas-clear.js';
|
||||
import { fileSaveSvg } from '../diagram/dgrm-png.js';
|
||||
import { deserialize, serialize } from '../diagram/dgrm-serialization.js';
|
||||
import { generateKey, srvSave } from '../diagram/dgrm-srv.js';
|
||||
import { fileOpen, fileSave } from '../infrastructure/file.js';
|
||||
import { tipShow, uiDisable } from './ui.js';
|
||||
|
||||
export class Menu extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
.menu {
|
||||
top: 15px;
|
||||
left: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#options {
|
||||
padding: 15px;
|
||||
box-shadow: 0px 0px 58px 2px rgb(34 60 80 / 20%);
|
||||
background-color: rgba(255,255,255, .9);
|
||||
|
||||
position: absolute !important;
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
width: 200px;
|
||||
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
#options div, #options a {
|
||||
color: rgb(13, 110, 253);
|
||||
cursor: pointer; margin: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
line-height: 25px;
|
||||
text-decoration: none;
|
||||
}
|
||||
#options div svg, #options a svg { margin-right: 10px; }
|
||||
</style>
|
||||
<svg id="menu" class="menu" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z" fill="rgb(52,71,103)"/></svg>
|
||||
<div id="options" style="visibility: hidden;">
|
||||
<div id="menu2" style="margin: 0 0 15px;"><svg viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 4h18v2H3V4zm0 7h18v2H3v-2zm0 7h18v2H3v-2z" fill="rgb(52,71,103)"/></svg></div>
|
||||
<div id="new"><svg viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M9 2.003V2h10.998C20.55 2 21 2.455 21 2.992v18.016a.993.993 0 0 1-.993.992H3.993A1 1 0 0 1 3 20.993V8l6-5.997zM5.83 8H9V4.83L5.83 8zM11 4v5a1 1 0 0 1-1 1H5v10h14V4h-8z" fill="rgb(52,71,103)"/></svg>New diagram</div>
|
||||
<div id="open"><svg viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 21a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h7.414l2 2H20a1 1 0 0 1 1 1v3h-2V7h-7.414l-2-2H4v11.998L5.5 11h17l-2.31 9.243a1 1 0 0 1-.97.757H3zm16.938-8H7.062l-1.5 6h12.876l1.5-6z" fill="rgb(52,71,103)"/></svg>Open diagram image</div>
|
||||
<div id="save"><svg viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M3 19h18v2H3v-2zm10-5.828L19.071 7.1l1.414 1.414L12 17 3.515 8.515 4.929 7.1 11 13.17V2h2v11.172z" fill="rgb(52,71,103)"/></svg>Save diagram image</div>
|
||||
<div id="link" style="display: none;"><svg viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13.06 8.11l1.415 1.415a7 7 0 0 1 0 9.9l-.354.353a7 7 0 0 1-9.9-9.9l1.415 1.415a5 5 0 1 0 7.071 7.071l.354-.354a5 5 0 0 0 0-7.07l-1.415-1.415 1.415-1.414zm6.718 6.011l-1.414-1.414a5 5 0 1 0-7.071-7.071l-.354.354a5 5 0 0 0 0 7.07l1.415 1.415-1.415 1.414-1.414-1.414a7 7 0 0 1 0-9.9l.354-.353a7 7 0 0 1 9.9 9.9z" fill="rgb(52,71,103)"/></svg>Copy link to diagram</div>
|
||||
<a style="display: none;" href="/donate.html" target="_blank" style="margin-bottom: 0;">
|
||||
<svg viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0H24V24H0z"/><path d="M12.001 4.529c2.349-2.109 5.979-2.039 8.242.228 2.262 2.268 2.34 5.88.236 8.236l-8.48 8.492-8.478-8.492c-2.104-2.356-2.025-5.974.236-8.236 2.265-2.264 5.888-2.34 8.244-.228zm6.826 1.641c-1.5-1.502-3.92-1.563-5.49-.153l-1.335 1.198-1.336-1.197c-1.575-1.412-3.99-1.35-5.494.154-1.49 1.49-1.565 3.875-.192 5.451L12 18.654l7.02-7.03c1.374-1.577 1.299-3.959-.193-5.454z" fill="rgb(255,66,77)"/></svg>Donate
|
||||
</a>
|
||||
</div>`;
|
||||
|
||||
const options = shadow.getElementById('options');
|
||||
function toggle() { options.style.visibility = options.style.visibility === 'visible' ? 'hidden' : 'visible'; }
|
||||
|
||||
/** @param {string} id, @param {()=>void} handler */
|
||||
function click(id, handler) {
|
||||
shadow.getElementById(id).onclick = _ => {
|
||||
uiDisable(true);
|
||||
handler();
|
||||
toggle();
|
||||
uiDisable(false);
|
||||
};
|
||||
}
|
||||
|
||||
shadow.getElementById('menu').onclick = toggle;
|
||||
shadow.getElementById('menu2').onclick = toggle;
|
||||
|
||||
click('new', () => { canvasClear(this._canvas); });
|
||||
|
||||
click('save', () => {
|
||||
const serialized = serialize(this._canvas);
|
||||
if (serialized.s.length === 0) { alertEmpty(); return; }
|
||||
|
||||
fileSaveSvg(this._canvas, serialized, blob => {
|
||||
fileSave(blob, 'dgrm.svg');
|
||||
});
|
||||
});
|
||||
|
||||
click('open', () =>
|
||||
fileOpen('.svg', async svgFile => await loadData(this._canvas, svgFile))
|
||||
);
|
||||
|
||||
click('link', async () => {
|
||||
const serialized = serialize(this._canvas);
|
||||
if (serialized.s.length === 0) { alertEmpty(); return; }
|
||||
|
||||
const key = generateKey();
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.set('k', key);
|
||||
// use clipboard before server call - to fix 'Document is not focused'
|
||||
await navigator.clipboard.writeText(url.toString());
|
||||
await srvSave(key, serialized);
|
||||
|
||||
alert('Link to diagram copied to clipboard');
|
||||
});
|
||||
}
|
||||
|
||||
/** @param {CanvasElement} canvas */
|
||||
init(canvas) {
|
||||
/** @private */ this._canvas = canvas;
|
||||
|
||||
// file drag to window
|
||||
document.body.addEventListener('dragover', evt => { evt.preventDefault(); });
|
||||
document.body.addEventListener('drop', async evt => {
|
||||
evt.preventDefault();
|
||||
|
||||
if (evt.dataTransfer?.items?.length !== 1 ||
|
||||
evt.dataTransfer.items[0].kind !== 'file' ||
|
||||
evt.dataTransfer.items[0].type !== 'image/png') {
|
||||
alertCantOpen(); return;
|
||||
}
|
||||
|
||||
await loadData(this._canvas, evt.dataTransfer.items[0].getAsFile());
|
||||
});
|
||||
}
|
||||
};
|
||||
customElements.define('ap-menu', Menu);
|
||||
|
||||
|
||||
|
||||
|
||||
/** @param {CanvasElement} canvas, @param {File|Blob} svgFile */
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
|
||||
function parseTranslateFromString(str) {
|
||||
if (!str) return { x: 0, y: 0 };
|
||||
const m = /translate\(\s*([-\d.]+)(?:px)?\s*(?:,|\s)\s*([-\d.]+)(?:px)?\s*\)/.exec(str);
|
||||
if (m) return { x: parseFloat(m[1]), y: parseFloat(m[2]) };
|
||||
const mm = /matrix\(\s*([-\d.e]+)[,\s]+([-\d.e]+)[,\s]+([-\d.e]+)[,\s]+([-\d.e]+)[,\s]+([-\d.e]+)[,\s]+([-\d.e]+)\s*\)/.exec(str);
|
||||
if (mm && mm.length >= 7) return { x: parseFloat(mm[5]), y: parseFloat(mm[6]) };
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
function getEffectiveTranslate(el) {
|
||||
// priority: style attribute "transform: translate(...)" -> attribute transform -> HTMLElement.style.transform
|
||||
if (!el || !el.getAttribute) return { x: 0, y: 0 };
|
||||
const styleAttr = el.getAttribute('style');
|
||||
if (styleAttr) {
|
||||
const m = /transform\s*:\s*([^;]+)/.exec(styleAttr);
|
||||
if (m) {
|
||||
const p = parseTranslateFromString(m[1]);
|
||||
if (Number.isFinite(p.x) || Number.isFinite(p.y)) return p;
|
||||
}
|
||||
}
|
||||
const trAttr = el.getAttribute('transform');
|
||||
if (trAttr) {
|
||||
const p = parseTranslateFromString(trAttr);
|
||||
if (Number.isFinite(p.x) || Number.isFinite(p.y)) return p;
|
||||
}
|
||||
if (el instanceof HTMLElement && el.style && el.style.transform) {
|
||||
const p = parseTranslateFromString(el.style.transform);
|
||||
if (Number.isFinite(p.x) || Number.isFinite(p.y)) return p;
|
||||
}
|
||||
return { x: 0, y: 0 };
|
||||
}
|
||||
|
||||
function extractCoordsFromPathD(d) {
|
||||
if (!d) return { start: { x: 0, y: 0 }, end: { x: 0, y: 0 } };
|
||||
const nums = (d.match(/-?\d+(\.\d+)?/g) || []).map(Number);
|
||||
if (nums.length >= 4) {
|
||||
return { start: { x: nums[0], y: nums[1] }, end: { x: nums[nums.length - 2], y: nums[nums.length - 1] } };
|
||||
}
|
||||
return { start: { x: 0, y: 0 }, end: { x: 0, y: 0 } };
|
||||
}
|
||||
|
||||
function detectMainTag(el) {
|
||||
if (!el || !el.querySelector) return null;
|
||||
const main = el.querySelector('[data-key="main"]');
|
||||
if (main) return main.tagName?.toLowerCase() || null;
|
||||
// fallback: check for presence of common tags
|
||||
if (el.querySelector('circle')) return 'circle';
|
||||
if (el.querySelector('rect')) return 'rect';
|
||||
if (el.querySelector('path')) return 'path';
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function loadData(canvas, svgFile) {
|
||||
try {
|
||||
const text = await svgFile.text();
|
||||
|
||||
const parser = new DOMParser();
|
||||
const svgDoc = parser.parseFromString(text, 'image/svg+xml');
|
||||
const svgElement = svgDoc.documentElement;
|
||||
|
||||
const srcCanvasContainer = svgElement.querySelector('#canvas') || svgElement;
|
||||
const canvasContainerOffset = getEffectiveTranslate(srcCanvasContainer);
|
||||
|
||||
const sourceElements = Array.from(srcCanvasContainer.children || []);
|
||||
|
||||
const shapeMap = canvas[CanvasSmbl] && canvas[CanvasSmbl].shapeMap;
|
||||
if (!shapeMap) {
|
||||
console.error('canvas[CanvasSmbl].shapeMap not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const mapKeys = Object.keys(shapeMap);
|
||||
|
||||
// Найдём ключ для стрелок (проверкой create с s/e данными)
|
||||
let arrowKey = null;
|
||||
for (const k of mapKeys) {
|
||||
try {
|
||||
const testData = {
|
||||
s: { data: { dir: 'right', position: { x: 0, y: 0 } } },
|
||||
e: { data: { dir: 'right', position: { x: 10, y: 0 } } }
|
||||
};
|
||||
const proto = shapeMap[k].create(testData);
|
||||
// если create вернул элемент и в нём есть path или g[data-key="start"]
|
||||
if (proto && (proto.querySelector && (proto.querySelector('path') || proto.querySelector('[data-key="start"]')))) {
|
||||
arrowKey = k;
|
||||
proto.remove && proto.remove();
|
||||
break;
|
||||
}
|
||||
proto.remove && proto.remove();
|
||||
} catch (err) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
// fallback обычно '0'
|
||||
if (arrowKey == null) arrowKey = mapKeys.find(mk => String(mk) === '0') || null;
|
||||
|
||||
// Сопоставление kind(tag) -> key: создаём один proto на ключ и смотрим main tag
|
||||
const keyByTag = {};
|
||||
for (const k of mapKeys) {
|
||||
try {
|
||||
const proto = shapeMap[k].create({ type: k, position: { x: 0, y: 0 }, title: 'T' });
|
||||
const tag = detectMainTag(proto);
|
||||
if (tag) {
|
||||
if (!keyByTag[tag]) keyByTag[tag] = k;
|
||||
}
|
||||
proto.remove && proto.remove();
|
||||
} catch (e) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
const createdShapes = [];
|
||||
|
||||
// 1) создаём фигуры (non-path)
|
||||
for (const node of sourceElements) {
|
||||
if (!(node instanceof Element)) continue;
|
||||
const looksLikePath = node.classList.contains('shpath') || !!node.querySelector('path.path') || !!node.querySelector('path[data-key="path"]');
|
||||
if (looksLikePath) continue;
|
||||
|
||||
const groupPos = getEffectiveTranslate(node); // visible transform of group
|
||||
const absPos = { x: (groupPos.x || 0) + (canvasContainerOffset.x || 0), y: (groupPos.y || 0) + (canvasContainerOffset.y || 0) };
|
||||
|
||||
const tag = detectMainTag(node) || 'unknown';
|
||||
const key = keyByTag[tag] || null;
|
||||
|
||||
if (!key) {
|
||||
// fallback: просто клонируем — визуал будет, но интерактивность может отсутствовать
|
||||
try {
|
||||
const clone = node.cloneNode(true);
|
||||
canvas.append(clone);
|
||||
} catch (e) {
|
||||
console.error('clone failed', e);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const title = node.querySelector('text')?.textContent || 'Title';
|
||||
const shapeData = { type: key, position: { x: absPos.x, y: absPos.y }, title };
|
||||
|
||||
try {
|
||||
const shapeEl = shapeMap[key].create(shapeData);
|
||||
// apply visible transform (absolute)
|
||||
shapeEl.setAttribute && shapeEl.setAttribute('transform', `translate(${absPos.x},${absPos.y})`);
|
||||
canvas.append(shapeEl);
|
||||
shapeEl.dispatchEvent && shapeEl.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
|
||||
|
||||
// collect connectors absolute positions from source node
|
||||
const connectors = {};
|
||||
const connNodes = Array.from(node.querySelectorAll('circle[data-connect]') || []);
|
||||
connNodes.forEach(cn => {
|
||||
const connName = cn.getAttribute('data-connect') || cn.getAttribute('data-key') || 'conn';
|
||||
const connTr = getEffectiveTranslate(cn);
|
||||
// connector absolute = canvasContainerOffset + groupPos + connTr
|
||||
const cpos = {
|
||||
x: (canvasContainerOffset.x || 0) + (groupPos.x || 0) + (connTr.x || 0),
|
||||
y: (canvasContainerOffset.y || 0) + (groupPos.y || 0) + (connTr.y || 0)
|
||||
};
|
||||
connectors[connName] = cpos;
|
||||
});
|
||||
|
||||
createdShapes.push({ key, el: shapeEl, tag, absPos, connectors });
|
||||
} catch (e) {
|
||||
console.error('create shape failed', e);
|
||||
}
|
||||
}
|
||||
|
||||
// 2) создаём стрелки точно как в _shapeCreate: формируем s/e с data.position и вызываем create
|
||||
for (const node of sourceElements) {
|
||||
if (!(node instanceof Element)) continue;
|
||||
const looksLikePath = node.classList.contains('shpath') || !!node.querySelector('path.path') || !!node.querySelector('path[data-key="path"]');
|
||||
if (!looksLikePath) continue;
|
||||
|
||||
let sPos = { x: 0, y: 0 }, ePos = { x: 0, y: 0 };
|
||||
|
||||
const startG = node.querySelector('[data-key="start"]');
|
||||
const endG = node.querySelector('[data-key="end"]');
|
||||
|
||||
if (startG) {
|
||||
const p = getEffectiveTranslate(startG);
|
||||
sPos.x = (p.x || 0) + (canvasContainerOffset.x || 0);
|
||||
sPos.y = (p.y || 0) + (canvasContainerOffset.y || 0);
|
||||
}
|
||||
if (endG) {
|
||||
const p = getEffectiveTranslate(endG);
|
||||
ePos.x = (p.x || 0) + (canvasContainerOffset.x || 0);
|
||||
ePos.y = (p.y || 0) + (canvasContainerOffset.y || 0);
|
||||
}
|
||||
|
||||
if ((sPos.x === 0 && sPos.y === 0) || (ePos.x === 0 && ePos.y === 0)) {
|
||||
const pathEl = node.querySelector('path[data-key="path"]') || node.querySelector('path');
|
||||
if (pathEl) {
|
||||
const coords = extractCoordsFromPathD(pathEl.getAttribute('d'));
|
||||
sPos.x = coords.start?.x || sPos.x;
|
||||
sPos.y = coords.start?.y || sPos.y;
|
||||
ePos.x = coords.end?.x || ePos.x;
|
||||
ePos.y = coords.end?.y || ePos.y;
|
||||
}
|
||||
}
|
||||
|
||||
// Найти реальные shape и connector для s/e
|
||||
const findConnector = (pos) => {
|
||||
let best = null;
|
||||
let minDist = Infinity;
|
||||
for (const sh of createdShapes) {
|
||||
for (const key in sh.connectors) {
|
||||
const c = sh.connectors[key];
|
||||
const dx = c.x - pos.x;
|
||||
const dy = c.y - pos.y;
|
||||
const dist = dx*dx + dy*dy;
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
best = { shapeEl: sh.el, connectorKey: key };
|
||||
}
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
|
||||
const sShape = findConnector(sPos);
|
||||
const eShape = findConnector(ePos);
|
||||
|
||||
const shapeData = {
|
||||
s: sShape ? { shape: sShape, data: { dir:'right', position: {...sPos} } } : { data: { dir:'right', position: {...sPos} } },
|
||||
e: eShape ? { shape: eShape, data: { dir:'right', position: {...ePos} } } : { data: { dir:'right', position: {...ePos} } }
|
||||
};
|
||||
|
||||
const shapeEl = arrowKey != null ? shapeMap[arrowKey].create(shapeData) : node.cloneNode(true);
|
||||
canvas.append(shapeEl);
|
||||
shapeEl.dispatchEvent && shapeEl.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }));
|
||||
}
|
||||
|
||||
|
||||
console.log('loadData finished. shapes created:', createdShapes.length);
|
||||
} catch (e) {
|
||||
console.error('Ошибка при загрузке SVG', e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const alertCantOpen = () => alert('File cannot be read. Use the exact image file you got from the application.');
|
||||
const alertEmpty = () => alert('Diagram is empty');
|
||||
|
||||
/** @typedef { {x:number, y:number} } Point */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
183
main_plugin/dgrm/ui/shape-menu.js
Executable file
183
main_plugin/dgrm/ui/shape-menu.js
Executable file
@@ -0,0 +1,183 @@
|
||||
import { CanvasSmbl } from '../infrastructure/canvas-smbl.js';
|
||||
import { pointInCanvas } from '../infrastructure/move-scale-applay.js';
|
||||
import { listen } from '../infrastructure/util.js';
|
||||
import { tipShow } from './ui.js';
|
||||
|
||||
export class ShapeMenu extends HTMLElement {
|
||||
connectedCallback() {
|
||||
const shadow = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML =
|
||||
`<style>
|
||||
.menu {
|
||||
position: absolute !important;
|
||||
overflow-x: auto;
|
||||
padding: 0;
|
||||
top: 50%;
|
||||
left: 13px;
|
||||
transform: translateY(-50%);
|
||||
box-shadow: 0px 0px 58px 2px rgba(34, 60, 80, 0.2);
|
||||
border-radius: 16px;
|
||||
background-color: rgba(255,255,255, .9);
|
||||
}
|
||||
|
||||
.content {
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
[data-cmd] {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu svg { padding: 10px; }
|
||||
.stroke {
|
||||
stroke: #344767;
|
||||
stroke-width: 2px;
|
||||
fill: transparent;
|
||||
}
|
||||
|
||||
.menu .big {
|
||||
width: 62px;
|
||||
min-width: 62px;
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 700px) {
|
||||
.menu {
|
||||
width: 100%;
|
||||
border-radius: 0;
|
||||
bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
top: unset;
|
||||
left: unset;
|
||||
transform: unset;
|
||||
}
|
||||
|
||||
.content {
|
||||
align-self: center;
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<div id="menu" class="menu" style="touch-action: none;">
|
||||
<div class="content">
|
||||
<svg class="stroke" data-cmd="shapeAdd" data-cmd-arg="1" viewBox="0 0 24 24" width="24" height="24"><circle r="9" cx="12" cy="12"></circle></svg>
|
||||
<svg class="stroke" data-cmd="shapeAdd" data-cmd-arg="4" viewBox="0 0 24 24" width="24" height="24"><path d="M2 12 L12 2 L22 12 L12 22 Z" stroke-linejoin="round"></path></svg>
|
||||
<svg class="stroke" data-cmd="shapeAdd" data-cmd-arg="2" viewBox="0 0 24 24" width="24" height="24"><rect x="2" y="4" width="20" height="16" rx="3" ry="3"></rect></svg>
|
||||
<svg data-cmd="shapeAdd" data-cmd-arg="0" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13 8v8a3 3 0 0 1-3 3H7.83a3.001 3.001 0 1 1 0-2H10a1 1 0 0 0 1-1V8a3 3 0 0 1 3-3h3V2l5 4-5 4V7h-3a1 1 0 0 0-1 1z" fill="rgba(52,71,103,1)"/></svg>
|
||||
<svg data-cmd="shapeAdd" data-cmd-arg="3" viewBox="0 0 24 24" width="24" height="24"><path fill="none" d="M0 0h24v24H0z"/><path d="M13 6v15h-2V6H5V4h14v2z" fill="rgba(52,71,103,1)"/></svg>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
const menu = shadow.getElementById('menu');
|
||||
menu.querySelectorAll('[data-cmd="shapeAdd"]').forEach(el => listen(el, 'pointerdown', this));
|
||||
listen(menu, 'pointerleave', this);
|
||||
listen(menu, 'pointerup', this);
|
||||
listen(menu, 'pointermove', this);
|
||||
};
|
||||
|
||||
/** @param {CanvasElement} canvas */
|
||||
init(canvas) {
|
||||
/** @private */ this._canvas = canvas;
|
||||
}
|
||||
|
||||
/** @param {PointerEvent & { currentTarget: Element }} evt */
|
||||
handleEvent(evt) {
|
||||
switch (evt.type) {
|
||||
case 'pointermove':
|
||||
if (!this._isNativePointerleaveTriggered) {
|
||||
// emulate pointerleave for mobile
|
||||
|
||||
const pointElem = document.elementFromPoint(evt.clientX, evt.clientY);
|
||||
if (pointElem === this._pointElem) {
|
||||
return;
|
||||
}
|
||||
|
||||
// pointerleave
|
||||
if (this._parentElem === this._pointElem) {
|
||||
// TODO: check mobile
|
||||
this._canvas.ownerSVGElement.setPointerCapture(evt.pointerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* @type {Element}
|
||||
* @private
|
||||
*/
|
||||
this._pointElem = pointElem;
|
||||
}
|
||||
break;
|
||||
case 'pointerleave':
|
||||
this._isNativePointerleaveTriggered = true;
|
||||
if (this._pressedShapeTemplKey != null) {
|
||||
// when shape drag out from menu panel
|
||||
this._shapeCreate(evt);
|
||||
}
|
||||
this._clean();
|
||||
break;
|
||||
case 'pointerdown':
|
||||
this._pressedShapeTemplKey = parseInt(evt.currentTarget.getAttribute('data-cmd-arg'));
|
||||
|
||||
// for emulate pointerleave
|
||||
this._parentElem = document.elementFromPoint(evt.clientX, evt.clientY);
|
||||
this._pointElem = this._parentElem;
|
||||
this._isNativePointerleaveTriggered = null;
|
||||
break;
|
||||
case 'pointerup':
|
||||
this._clean();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {PointerEvent} evt
|
||||
* @private
|
||||
*/
|
||||
_shapeCreate(evt) {
|
||||
tipShow(false);
|
||||
if (this._pressedShapeTemplKey == null) return;
|
||||
|
||||
const svg = this._canvas.ownerSVGElement;
|
||||
const pt = svg.createSVGPoint();
|
||||
pt.x = evt.clientX; pt.y = evt.clientY;
|
||||
const loc = pt.matrixTransform(this._canvas.getScreenCTM().inverse());
|
||||
const x = loc.x, y = loc.y;
|
||||
|
||||
let shapeData;
|
||||
if (this._pressedShapeTemplKey === 0) {
|
||||
shapeData = {
|
||||
s: { data: { dir: 'right', position: { x: x - 24, y } } },
|
||||
e: { data: { dir: 'right', position: { x: x + 24, y } } }
|
||||
};
|
||||
} else {
|
||||
shapeData = {
|
||||
type: this._pressedShapeTemplKey,
|
||||
position: { x, y },
|
||||
title: 'Title'
|
||||
};
|
||||
}
|
||||
|
||||
const shapeEl = this._canvas[CanvasSmbl].shapeMap[this._pressedShapeTemplKey].create(shapeData);
|
||||
this._canvas.append(shapeEl);
|
||||
|
||||
// Для стрелки и других форм диспатчим pointerdown
|
||||
shapeEl.dispatchEvent(new PointerEvent('pointerdown', evt));
|
||||
|
||||
// transform ставим только для фигур, которые не path
|
||||
if (this._pressedShapeTemplKey !== 0) {
|
||||
shapeEl.setAttribute('transform', `translate(${x},${y})`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @private */
|
||||
_clean() {
|
||||
this._pressedShapeTemplKey = null;
|
||||
this._parentElem = null;
|
||||
this._pointElem = null;
|
||||
}
|
||||
}
|
||||
customElements.define('ap-menu-shape', ShapeMenu);
|
||||
|
||||
/** @typedef { import('../shapes/shape-type-map.js').ShapeType } ShapeType */
|
||||
/** @typedef { import('../infrastructure/canvas-smbl.js').CanvasElement } CanvasElement */
|
||||
29
main_plugin/dgrm/ui/ui.js
Executable file
29
main_plugin/dgrm/ui/ui.js
Executable file
@@ -0,0 +1,29 @@
|
||||
/** @type {HTMLDivElement} */ let overlay;
|
||||
/** @param {boolean} isDisable */
|
||||
export function uiDisable(isDisable) {
|
||||
/* if (isDisable && !overlay) {
|
||||
overlay = document.createElement('div');
|
||||
overlay.style.cssText = 'z-index: 2; position: fixed; left: 0; top: 0; width:100%; height:100%; background: #fff; opacity: 0';
|
||||
overlay.innerHTML =
|
||||
`<style>
|
||||
@keyframes blnk {
|
||||
0% { opacity: 0; }
|
||||
50% { opacity: 0.7; }
|
||||
100% {opacity: 0;}
|
||||
}
|
||||
.blnk { animation: blnk 1.6s linear infinite; }
|
||||
</style>`;
|
||||
overlay.classList.add('blnk');
|
||||
document.body.append(overlay);
|
||||
} else if (!isDisable) {
|
||||
overlay.remove();
|
||||
overlay = null;
|
||||
} */
|
||||
}
|
||||
|
||||
/** @param {boolean} show */
|
||||
tipShow(false);
|
||||
export function tipShow(show) {
|
||||
document.getElementById('diagram').style.pointerEvents = show ? 'none' : 'unset';
|
||||
document.getElementById('tip').style.display = show ? 'unset' : 'none';
|
||||
}
|
||||
787
main_plugin/editor/editor.css
Executable file
787
main_plugin/editor/editor.css
Executable file
@@ -0,0 +1,787 @@
|
||||
/* Главное */
|
||||
#basis3 {
|
||||
left: 0 !important;
|
||||
top: 33px !important;
|
||||
transform: none !important;
|
||||
}
|
||||
#copyr_sym *, .deploy *, .elementEditPanelElement * {
|
||||
margin-top: 2px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.selectedColor {
|
||||
background: #3a6e62;
|
||||
color: #ffffff;
|
||||
}
|
||||
.textGrey:hover {
|
||||
color: #787878;
|
||||
cursor: pointer;
|
||||
}
|
||||
.toolbar-container {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
#panel {
|
||||
position: absolute;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
max-width: -webkit-fill-available;
|
||||
border-radius: 5px;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
overflow-x: clip;
|
||||
overflow-y: visible;
|
||||
z-index: 100;
|
||||
}
|
||||
.toolbar-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
white-space: nowrap;
|
||||
padding: 4px;
|
||||
margin: 0;
|
||||
flex-shrink: 0;
|
||||
padding: 4px 0px;
|
||||
width: 100%;
|
||||
}
|
||||
.arrow {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 24px;
|
||||
height: 36px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 10;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
#arrow-left {
|
||||
left: 0;
|
||||
border: none;
|
||||
border-right: inherit;
|
||||
}
|
||||
#arrow-right {
|
||||
right: 0;
|
||||
border: none;
|
||||
border-left: inherit;
|
||||
}
|
||||
|
||||
/* Стили контейнера для показа увеличенных изображений */
|
||||
#bas,
|
||||
#C {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
#bas {
|
||||
z-index: 1;
|
||||
background: #FFFFFF;
|
||||
opacity: 0.95;
|
||||
}
|
||||
|
||||
#view {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Стили контейнера, содержащего страницу */
|
||||
#basis {
|
||||
position: relative;
|
||||
width: -webkit-fill-available;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
/* Стили таблицы, содержащей страницу */
|
||||
#tab {
|
||||
width: 1000px;
|
||||
}
|
||||
|
||||
/* Стили меню */
|
||||
|
||||
#sect {
|
||||
display: flex;
|
||||
margin-bottom: 15px;
|
||||
font-family: Tahoma;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Стили пунктов меню */
|
||||
.lin {
|
||||
margin: 9px;
|
||||
width: 180px;
|
||||
height: 50px;
|
||||
color: #FFFFFF;
|
||||
text-align: center;
|
||||
box-shadow: 5px 5px 10px #000066;
|
||||
background: #660033;
|
||||
line-height: 50px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Стили ячейки для рекламы */
|
||||
#rec {
|
||||
width: 300px;
|
||||
text-align: center;
|
||||
vertical-align: top;
|
||||
font-weight: bold;
|
||||
color: #660033;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Стили контейнера, содержащего страницу */
|
||||
#basis3 {
|
||||
position: fixed;
|
||||
width: -webkit-fill-available;
|
||||
margin: auto;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
font-size: 0.97em;
|
||||
}
|
||||
|
||||
/* Стили вкладок с настройками */
|
||||
.cust {
|
||||
height: auto;
|
||||
padding: 8px;
|
||||
position: fixed;
|
||||
visibility: hidden;
|
||||
z-index: 100;
|
||||
background: url(../../img/img/backgr.jpg);
|
||||
border-radius: 5px;
|
||||
top: 30%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
width: calc(100% - 20px);
|
||||
max-width: max-content;
|
||||
}
|
||||
.cust2 {
|
||||
height: auto;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
top: 236%;
|
||||
width: 1118px;
|
||||
border: 1px solid #000000;
|
||||
visibility: hidden;
|
||||
z-index: 100;
|
||||
background: url(../../img/img/backgr.jpg);
|
||||
border-radius: 5px;
|
||||
max-width: -webkit-fill-available;
|
||||
}
|
||||
|
||||
/* Стили кнопок (крестиков) для закрытия вкладок */
|
||||
.cldiv {
|
||||
margin: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Стили вкладки с изображениями с сервера */
|
||||
|
||||
#p_list {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.roster {
|
||||
width: 180px;
|
||||
margin: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Стили главной панели редактора */
|
||||
.toolbar-group {
|
||||
position: relative;
|
||||
padding: 0px 5px;
|
||||
margin: 0px 2px;
|
||||
display: inline-block;
|
||||
}
|
||||
.toolbar-group::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: -3.1px;
|
||||
width: 5px;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
#000 0px 1px,
|
||||
transparent 1px 2px,
|
||||
#000 2px 3px,
|
||||
transparent 3px 4px,
|
||||
#000 4px 5px
|
||||
);
|
||||
}
|
||||
.toolbar-group.open {
|
||||
margin-left: 7px;
|
||||
}
|
||||
.toolbar-group-button {
|
||||
transition: transform 0.1s ease, left 0.1s ease;
|
||||
}
|
||||
.toolbar-group.open .toolbar-group-button {
|
||||
position: absolute;
|
||||
left: -9px;
|
||||
transform: scale(0.6);
|
||||
transform-origin: top left;
|
||||
z-index: 1;
|
||||
}
|
||||
.toolbar-group-content {
|
||||
transition: margin-left 0.1s ease;
|
||||
}
|
||||
.toolbar-group.open .toolbar-group-content {
|
||||
margin-left: 7px;
|
||||
}
|
||||
|
||||
#toolbar-group-button-main {
|
||||
background-position: -318px -117px;
|
||||
}
|
||||
#toolbar-group-button-text {
|
||||
background-position: -237px 1664px;
|
||||
}
|
||||
#toolbar-group-button-paste {
|
||||
background-position: 166px 517px;
|
||||
}
|
||||
|
||||
.align-dropdown, .align-dropdown-oneImg {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
}
|
||||
.align-dropdown-text {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
border: 1px solid #000000;
|
||||
height: 26px;
|
||||
border-radius: 5px;
|
||||
vertical-align: middle;
|
||||
background: #FFFFFF;
|
||||
padding-right: 13px;
|
||||
}
|
||||
.align-dropdown-text .dropdown-arrow {
|
||||
position: absolute;
|
||||
right: 5px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.align-dropdown-text .current {
|
||||
padding: 0 3px;
|
||||
height: 28px;
|
||||
line-height: 28px;
|
||||
display: inline-block;
|
||||
cursor: pointer;
|
||||
}
|
||||
.align-dropdown .current {
|
||||
cursor: pointer;
|
||||
}
|
||||
.align-list {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: -9px;
|
||||
background: #fff;
|
||||
border: 1px solid #000000;
|
||||
display: none;
|
||||
z-index: 10;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
.align-list li {
|
||||
padding: 4px 8px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.align-list li:hover {
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Стили выпадающего списка панели */
|
||||
#sel {
|
||||
font-size: 1.1em;
|
||||
font-family: Tahoma;
|
||||
border: 1px solid #000000;
|
||||
}
|
||||
|
||||
/* Стили кнопок навигации по этапам редактирования */
|
||||
#forw,
|
||||
#bac {
|
||||
right: 10px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* Стили кнопок на главной панели редактора */
|
||||
.but,
|
||||
.pers,
|
||||
.swit,
|
||||
.swit2,
|
||||
.pers2,
|
||||
.pers3 {
|
||||
border: 1px solid #000000;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.editt {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.editi {
|
||||
border-radius: 5px;
|
||||
background: #FFFFFF;
|
||||
display: inline-block;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.edits {
|
||||
border-radius: 5px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.editib {
|
||||
background-image: url(../../img/pict/b_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editimc {
|
||||
background-image: url(../../img/pict/mc_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editib:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editimc:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editf.active {
|
||||
background-image: url(../../img/pict/b_iconslyb.svg);
|
||||
background-color: #e7e7e7;
|
||||
}
|
||||
.editf.active:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
background-color: #e7e7e7;
|
||||
}
|
||||
|
||||
#settingsMain {
|
||||
background-position: -197px -597px;
|
||||
}
|
||||
|
||||
#forma {
|
||||
background-position: 45px -117px;
|
||||
}
|
||||
|
||||
#htm {
|
||||
background-position: -318px 1824px;
|
||||
}
|
||||
|
||||
#bac {
|
||||
background-position: 645px 716px;
|
||||
}
|
||||
|
||||
#forw {
|
||||
background-position: 605px 716px;
|
||||
}
|
||||
|
||||
#img, #oneImg {
|
||||
background-position: 645px 477px;
|
||||
}
|
||||
|
||||
#hr {
|
||||
background-position: 766px 676px;
|
||||
}
|
||||
|
||||
#tabl {
|
||||
background-position: 1006px 597px;
|
||||
}
|
||||
|
||||
#link {
|
||||
background-position: 245px 277px;
|
||||
}
|
||||
|
||||
#linkdel {
|
||||
background-position: 205px 277px;
|
||||
}
|
||||
|
||||
#delin {
|
||||
background-position: 205px 277px;
|
||||
}
|
||||
|
||||
#copyr {
|
||||
background-position: 445px 517px;
|
||||
}
|
||||
|
||||
#bol {
|
||||
background-position: 1006px 517px;
|
||||
}
|
||||
|
||||
#ital {
|
||||
background-position: 966px 517px;
|
||||
}
|
||||
|
||||
#under {
|
||||
background-position: 926px 517px;
|
||||
}
|
||||
|
||||
#col {
|
||||
background-position: -237px 1664px;
|
||||
}
|
||||
|
||||
#backgr {
|
||||
background-position: -278px 1664px;
|
||||
}
|
||||
|
||||
#font {
|
||||
background-position: 1046px 517px;
|
||||
}
|
||||
|
||||
#strik {
|
||||
background-position: 885px 517px;
|
||||
}
|
||||
|
||||
#sup {
|
||||
background-position: 846px 517px;
|
||||
}
|
||||
|
||||
#sub {
|
||||
background-position: 806px 517px;
|
||||
}
|
||||
|
||||
#titleEdit {
|
||||
background-position: 1045px 557px;
|
||||
}
|
||||
|
||||
/* #pluginSave {
|
||||
background-position: -479px 1665px;
|
||||
}
|
||||
#pluginCreateLeft {
|
||||
background-position: -598px 1665px;
|
||||
}
|
||||
#pluginAddLeft {
|
||||
background-position: -677px 1665px;
|
||||
}
|
||||
#pluginCreateRight {
|
||||
background-position: -558px 1665px;
|
||||
}
|
||||
#pluginAddRight {
|
||||
background-position: -638px 1665px;
|
||||
}
|
||||
#pluginDelete {
|
||||
background-position: -518px 1665px;
|
||||
}
|
||||
#pluginMove {
|
||||
background-position: -438px 1665px;
|
||||
} */
|
||||
|
||||
#contentPageCreate {
|
||||
background-position: 926px 676px;
|
||||
}
|
||||
#contentPageMove {
|
||||
background-position: -1077px 1704px;
|
||||
}
|
||||
#contentPageSettings {
|
||||
background-position: -156px 1704px;
|
||||
}
|
||||
|
||||
#elementEdit {
|
||||
background-position: 965px 557px;
|
||||
}
|
||||
|
||||
#coup {
|
||||
background-position: -357px 1662px;
|
||||
}
|
||||
|
||||
#contentPageCreateUrlManager {
|
||||
background-position: -679px 1820px;
|
||||
}
|
||||
|
||||
.indent {
|
||||
display: inline-block;
|
||||
width: 5px;
|
||||
}
|
||||
|
||||
/* картинки списков */
|
||||
#equar, .align-dropdown .current.equar {
|
||||
background-position: 685px 517px;
|
||||
}
|
||||
#equac, .align-dropdown .current.equac {
|
||||
background-position: 725px 517px;
|
||||
}
|
||||
#equal, .align-dropdown .current.equal {
|
||||
background-position: 765px 517px;
|
||||
}
|
||||
#equaj, .align-dropdown .current.equaj {
|
||||
background-position: 645px 517px;
|
||||
}
|
||||
|
||||
#listNone, .align-dropdown .current.listNone {
|
||||
background-position: -798px 1666px;
|
||||
}
|
||||
#listDots, .align-dropdown .current.listDots {
|
||||
background-position: 764px 437px;
|
||||
}
|
||||
#listNumbers, .align-dropdown .current.listNumbers {
|
||||
background-position: 724px 437px;
|
||||
}
|
||||
#listLetters, .align-dropdown .current.listLetters {
|
||||
background-position: -757px 1666px;
|
||||
}
|
||||
|
||||
/* Стили маленьких окон на главной панели редактора */
|
||||
.smbut {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
visibility: hidden;
|
||||
}
|
||||
#ccc {
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
left: 470px;
|
||||
top: 17px;
|
||||
pointer-events: none;
|
||||
}
|
||||
#list {
|
||||
width: 100px;
|
||||
height: 28px;
|
||||
}
|
||||
#fs {
|
||||
width: 50px;
|
||||
height: 28px;
|
||||
}
|
||||
#ff {
|
||||
width: 145px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
#mainActions {
|
||||
height: 28px;
|
||||
}
|
||||
#pluginDropdownContent {
|
||||
height: 28px;
|
||||
}
|
||||
#elementDropdownContent {
|
||||
width: 170px;
|
||||
height: 28px;
|
||||
}
|
||||
|
||||
/* Стили контейнеров вкладок с настройками */
|
||||
.deploy {
|
||||
text-align: center;
|
||||
line-height: 25px;
|
||||
}
|
||||
|
||||
/* Стили textarea */
|
||||
.textar {
|
||||
width: 400px;
|
||||
height: 70px;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid #000000;
|
||||
margin-left: 5px;
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
/* Стили коротких текстовых полей */
|
||||
.inptx {
|
||||
width: 51px;
|
||||
height: 22px;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid #000000;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Стили средних текстовых полей */
|
||||
.inpmid {
|
||||
width: 120px;
|
||||
height: 18px;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid #000000;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Стили длинных текстовых полей */
|
||||
.inpbig {
|
||||
width: 190px;
|
||||
height: 18px;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid #000000;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
/* Стили для формы */
|
||||
.inform {
|
||||
display: inline-block;
|
||||
margin-block-end: 0em;
|
||||
}
|
||||
|
||||
/* Стили кнопок вставки и просмотра на вкладках с настройками */
|
||||
.butin {
|
||||
height: 22px;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid #000000;
|
||||
background: #FFFFFF;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.butinAuto {
|
||||
width: min-content;
|
||||
height: 22px;
|
||||
font-size: 0.92em;
|
||||
cursor: pointer;
|
||||
padding: 1px 5px 1px 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.butinAuto:hover {
|
||||
color: #787878;
|
||||
}
|
||||
.buttonEditTable {
|
||||
height: 22px;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid #000000;
|
||||
background: #FFFFFF;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.buttonEditTable.active {
|
||||
background-color: #e7e7e7;
|
||||
}
|
||||
|
||||
/* Стили выпадающих списков */
|
||||
.sele {
|
||||
height: 22px;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid #000000;
|
||||
margin-right: 10px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.colored {
|
||||
width: 28px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Стили выпадающих списков без зазора справа */
|
||||
.selena {
|
||||
height: 22px;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid #000000;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
/* Стили кнопок выбора цвета на вкладках с настройками */
|
||||
.colored {
|
||||
width: 28px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Стиль кнопок зарузок изоброжений */
|
||||
#selectImgForm {
|
||||
width: 77px;
|
||||
height: 22px;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid #000000;
|
||||
background: #FFFFFF;
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
margin: 0px 5px 0px -15px;
|
||||
}
|
||||
|
||||
/* Стиль окна текстового редактора */
|
||||
#tex {
|
||||
position: absolute;
|
||||
top: 236%;
|
||||
width: 1100px;
|
||||
height: 380px;
|
||||
border: 1px solid #000000;
|
||||
padding: 10px;
|
||||
font-size: 1.2em;
|
||||
font-family: Tahoma;
|
||||
visibility: hidden;
|
||||
z-index: 6;
|
||||
border-radius: 5px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
/* Стили контейнера с кнопками для символов и самих кнопок */
|
||||
#copyr_sym {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.sym {
|
||||
vertical-align: middle;
|
||||
width: 25px;
|
||||
height: 25px;
|
||||
margin: 1px;
|
||||
border: 1px solid #000000;
|
||||
font-size: 1em;
|
||||
font-family: Tahoma;
|
||||
background: #FFFFFF;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#pluginRulesButton1, #pluginRulesButton2 {
|
||||
width: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
#pluginRulesBlock1, #pluginRulesBlock2 {
|
||||
width: 240px;
|
||||
position: absolute;
|
||||
display: inline-block;
|
||||
background: #efefef;
|
||||
font-size: 0.9em;
|
||||
padding: 2px;
|
||||
line-height: 1.6;
|
||||
border-radius: 5px;
|
||||
opacity: 0;
|
||||
transition: max-height 0.18s ease-out, opacity 0.18s ease-out;
|
||||
pointer-events: none;
|
||||
}
|
||||
#pluginRulesBlock1.show, #pluginRulesBlock2.show {
|
||||
opacity: 1;
|
||||
pointer-events: all;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block-start: 5px;
|
||||
margin-block-end: 5px;
|
||||
}
|
||||
|
||||
.pluginEditable {
|
||||
outline-offset: 15px;
|
||||
overflow-x: hidden;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
}
|
||||
|
||||
/* поля для редактирование элементов */
|
||||
#editingMenuItems {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
user-select: none;
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
box-shadow: 0px 0px 5px #777;
|
||||
padding: 5px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.editingMenuItemsElement {
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.editingMenuItemsElement:hover {
|
||||
color: #787878;
|
||||
}
|
||||
2272
main_plugin/editor/editor.js
Executable file
2272
main_plugin/editor/editor.js
Executable file
File diff suppressed because it is too large
Load Diff
377
main_plugin/editor/editor.php
Executable file
377
main_plugin/editor/editor.php
Executable file
@@ -0,0 +1,377 @@
|
||||
<?php
|
||||
/**
|
||||
* @file editor.php
|
||||
* @brief Контейнеры и элементы интерфейса визуального редактора: основная панель, вкладки с символами, настройки ссылок, цвета, плагинов, страницы и элементы редактирования (текст, изображения, таблицы, линии)
|
||||
*/
|
||||
?>
|
||||
|
||||
<div style="position: absolute; display: none;">
|
||||
<div id="bas"></div>
|
||||
<div id="">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Основной контейнер редактора */ $basis3; ?>
|
||||
<div id="basis3" style="visibility: hidden; top: 20%; left: 10px; transform: translate(0%, -20%);">
|
||||
|
||||
<?php /** @brief Вкладка с символами */ $copyr_d; ?>
|
||||
<div id="copyr_d" class="cust sb borderStyle">
|
||||
<div id="copyr_sym">
|
||||
<input type="button" class="sym" id="s1" value="$">
|
||||
<input type="button" class="sym" id="s2" value="£">
|
||||
<input type="button" class="sym" id="s3" value="¥">
|
||||
<input type="button" class="sym" id="s4" value="€">
|
||||
<input type="button" class="sym" id="s5" value="₹">
|
||||
<input type="button" class="sym" id="s6" value="₽">
|
||||
<input type="button" class="sym" id="s7" value="₱">
|
||||
<input type="button" class="sym" id="s8" value="฿">
|
||||
<br>
|
||||
<input type="button" class="sym" id="s9" value="±">
|
||||
<input type="button" class="sym" id="s10" value="×">
|
||||
<input type="button" class="sym" id="s11" value="÷">
|
||||
<input type="button" class="sym" id="s12" value="−">
|
||||
<input type="button" class="sym" id="s13" value="√">
|
||||
<input type="button" class="sym" id="s14" value="≠">
|
||||
<input type="button" class="sym" id="s15" value="≤">
|
||||
<input type="button" class="sym" id="s16" value="≥">
|
||||
<input type="button" class="sym" id="s17" value="∑">
|
||||
<br>
|
||||
<input type="button" class="sym" id="s26" value="∞">
|
||||
<input type="button" class="sym" id="s27" value="Ω">
|
||||
<input type="button" class="sym" id="s28" value="Ω">
|
||||
<input type="button" class="sym" id="s29" value="α">
|
||||
<input type="button" class="sym" id="s30" value="β">
|
||||
<input type="button" class="sym" id="s31" value="™">
|
||||
<input type="button" class="sym" id="s32" value="∙">
|
||||
<input type="button" class="sym" id="s33" value="≈">
|
||||
<br>
|
||||
<input type="button" class="sym" id="s18" value="←">
|
||||
<input type="button" class="sym" id="s19" value="↑">
|
||||
<input type="button" class="sym" id="s20" value="→">
|
||||
<input type="button" class="sym" id="s21" value="↓">
|
||||
<input type="button" class="sym" id="s22" value="↖">
|
||||
<input type="button" class="sym" id="s23" value="↗">
|
||||
<input type="button" class="sym" id="s24" value="↘">
|
||||
<input type="button" class="sym" id="s25" value="↙">
|
||||
<br>
|
||||
<input type="button" class="sym" id="s34" value="■">
|
||||
<input type="button" class="sym" id="s35" value="▲">
|
||||
<input type="button" class="sym" id="s37" value="○">
|
||||
<input type="button" class="sym" id="s38" value="►">
|
||||
<input type="button" class="sym" id="s39" value="▼">
|
||||
<input type="button" class="sym" id="s40" value="●">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Блок настроек ссылок */ $link_d; ?>
|
||||
<div id="link_d" class="cust sb borderStyle">
|
||||
<div class="deploy">
|
||||
{{link}}: <input id="link2" class="inpbig" style="margin-right: 0px;"> <input id="link2FromPage" onClick="linkFromPage()" type="button" value="{{select_page_from_site}}" style="margin-right: 10px;">
|
||||
{{open_in_new_window}}: <select id="link3" class="sele">
|
||||
<option selected value="yes">{{yes}}</option>
|
||||
<option value="no">{{no}}</option>
|
||||
</select><br>
|
||||
</select>
|
||||
{{underline}}: <select id="link5" class="sele">
|
||||
<option value="yes">{{yes}}</option>
|
||||
<option selected value="no">{{no}}</option>
|
||||
</select>
|
||||
{{color}}: <input type="color" id="link6c" class="colored" value="#0645AD" title="{{choose_color}}">
|
||||
<input type="button" id="butlink" class="butin" value="{{insert}}" title="{{insert_link}}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Окно для изменения цвета текста */ $col_d; ?>
|
||||
<div id="col_d" class="cust sb borderStyle">
|
||||
<div class="deploy">
|
||||
{{change_text_color}}:
|
||||
<input type="color" id="colColor" class="colored" alt="{{color}}" title="{{choose_color}}">
|
||||
<button id="colFun_d" class="butin">{{select}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Окно для изменения цвета фона текста */ $backgr_d; ?>
|
||||
<div id="backgr_d" class="cust sb borderStyle">
|
||||
<div class="deploy">
|
||||
{{change_text_background}}:
|
||||
<input type="color" id="backgrColor" class="colored" alt="{{color}}" title="{{choose_color}}">
|
||||
<button id="backgrFun_d" class="butin">{{select}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Окно для создания плагина слева */ $pluginCreateLeft_d; ?>
|
||||
<div id="pluginCreateLeft_d" class="cust sb borderStyle">
|
||||
<div class="deploy">
|
||||
{{create_plugin_left}}:<br>
|
||||
<label for="pluginCreateLeftName">{{enter_plugin_name}}</label>
|
||||
<label id="pluginRulesButton1" class="butin">!</label>
|
||||
<div id="pluginRulesBlock1">
|
||||
<div>{{plugin_name_guidelines}}</div>
|
||||
<hr>
|
||||
<div>{{used_plugin_names}}</div>
|
||||
</div>
|
||||
<input type="text" id="pluginCreateLeftName" class="inpbig">
|
||||
<label for="pluginCreateLeftTitle">{{enter_plugin_title}}</label>
|
||||
<input type="text" id="pluginCreateLeftTitle" class="inpbig">
|
||||
<button id="pluginCreateLeftFun_d" class="butin">{{add}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php /** @brief Список выбора плагина слева */ $pluginAddLeft_d; ?>
|
||||
<div id="pluginAddLeft_d" class="cust sb borderStyle">
|
||||
<div class="deploy">
|
||||
{{add_plugin_left}}:<br>
|
||||
<label for="pluginAddLeftName">{{select_plugin}}</label>
|
||||
<select id="pluginAddLeftName" class="sele"></select>
|
||||
<label for="pluginAddLeftTitle">{{enter_plugin_title}}</label>
|
||||
<input type="text" id="pluginAddLeftTitle" class="inpbig">
|
||||
<button id="pluginAddLeftFun_d" class="butin">{{select}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php /** @brief Окно для создания плагина справа */ $pluginCreateRight_d; ?>
|
||||
<div id="pluginCreateRight_d" class="cust sb borderStyle">
|
||||
<div class="deploy">
|
||||
{{create_plugin_right}}:<br>
|
||||
<label for="pluginCreateRightName">{{enter_plugin_name}}</label>
|
||||
<label id="pluginRulesButton2" class="butin">!</label>
|
||||
<div id="pluginRulesBlock2">
|
||||
<div>{{plugin_name_guidelines}}</div>
|
||||
<hr>
|
||||
<div>{{used_plugin_names}}</div>
|
||||
</div>
|
||||
<input type="text" id="pluginCreateRightName" class="inpbig">
|
||||
<label for="pluginCreateRightTitle">{{enter_plugin_title}}</label>
|
||||
<input type="text" id="pluginCreateRightTitle" class="inpbig">
|
||||
<button id="pluginCreateRightFun_d" class="butin">{{add}}</button>
|
||||
</div>
|
||||
</div>
|
||||
<?php /** @brief Список выбора плагина справа */ $pluginAddRight_d; ?>
|
||||
<div id="pluginAddRight_d" class="cust sb borderStyle">
|
||||
<div class="deploy">
|
||||
{{add_plugin_right}}:<br>
|
||||
<label for="pluginAddRightName">{{select_plugin}}</label>
|
||||
<select id="pluginAddRightName" class="sele"></select>
|
||||
<label for="pluginAddRightTitle">{{enter_plugin_title}}</label>
|
||||
<input type="text" id="pluginAddRightTitle" class="inpbig">
|
||||
<button id="pluginAddRightFun_d" class="butin">{{select}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Редактирование меню страницы */ $contentPageCreate_d; ?>
|
||||
<div id="contentPageCreate_d" class="cust sb borderStyle">
|
||||
<div class="deploy">
|
||||
{{create_page}}:<br>
|
||||
{{select_or_create_file}} <input id="contentPageCreateUrl" class="inpbig">
|
||||
<div id="contentPageCreateUrlManager" class="editi editib pers" style="margin: -3px 0px 0px -16px; width: 24px; height: 20px;"></div><br>
|
||||
{{menu_name}}: <input id="contentPageCreateName" class="inpbig">
|
||||
{{tab_title}}: <input id="contentPageCreateTitle" class="inpbig">
|
||||
{{design}}: <select id="contentPageCreateTemplate" class="sele">
|
||||
<?php ?>
|
||||
</select>
|
||||
<br>
|
||||
<button id="contentPageCreateFun_d" class="butin">{{create}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Настройки страницы */ $contentPageSettings_d; ?>
|
||||
<div id="contentPageSettings_d" class="cust sb borderStyle">
|
||||
<div class="deploy">
|
||||
<button id="contentPageSettingsFun_d" class="butin">{{edit}}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Основные настройки страницы */ $settingsMain_d; ?>
|
||||
<div id="settingsMain_d" class="cust2 sb borderStyle" style="width: 200px;">
|
||||
<div class="deploy" style="text-align: left;">
|
||||
<option id="save" class="butinAuto">{{save}}</option>
|
||||
<option id="saveHow" class="butinAuto">{{save_as}}</option>
|
||||
<option id="getPage" class="butinAuto">{{open}}</option>
|
||||
<option id="newPage" class="butinAuto">{{new}}</option>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<?php /** @brief Главная панель редактора */ $panel; ?>
|
||||
<div id="panel" class="borderStyle"><div id="arrow-left" class="arrow borderStyle">⊲</div><div class="toolbar-container">
|
||||
<div class="toolbar-group" style="margin-left: 14px;">
|
||||
<span id="toolbar-group-button-main" class="editi editib pers toolbar-group-button"></span>
|
||||
<span class="toolbar-group-content">
|
||||
|
||||
<div id="settingsMain" class="editi editib swit"></div>
|
||||
<div id="htm" onclick="showHtmlCode()" class="editi editib pers" alt="{{html_code_main_block_alt}}" title="{{html_code_main_block_title}}"></div>
|
||||
<div id="bac" class="editi editib pers" alt="{{undo_action_alt}}" title="{{undo_action_title}}"></div>
|
||||
<div id="forw" class="editi editib pers" alt="{{redo_action_alt}}" title="{{redo_action_title}}"></div>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<span id="toolbar-group-button-text" class="editi editib pers toolbar-group-button"></span>
|
||||
<span class="toolbar-group-content">
|
||||
|
||||
<div class="align-dropdown-text" id="ff">
|
||||
<div class="current" data-cmd="'Roboto', sans-serif">Roboto</div><span class="dropdown-arrow">▾</span>
|
||||
<ul class="align-list" style="left: -1px;">
|
||||
<li data-cmd="'Roboto', sans-serif"><div>Roboto</div></li>
|
||||
<li data-cmd="'Open Sans', sans-serif"><div>Open Sans</div></li>
|
||||
<li data-cmd="'Montserrat', sans-serif"><div>Montserrat</div></li>
|
||||
<li data-cmd="'Lora', serif"><div>Lora</div></li>
|
||||
<li data-cmd="'Source Sans 3', sans-serif"><div>Source Sans Pro</div></li>
|
||||
<li data-cmd="'Merriweather', serif"><div>Merriweather</div></li>
|
||||
<li data-cmd="'PT Serif', serif"><div>PT Serif</div></li>
|
||||
<li data-cmd="'Playfair Display', serif"><div>Playfair Display</div></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="align-dropdown-text" id="fs">
|
||||
<div class="current" data-cmd="15px">15px</div><span class="dropdown-arrow">▾</span>
|
||||
<ul class="align-list" style="left: -1px;">
|
||||
<li data-cmd="1px"><div>1px</div></li><li data-cmd="2px"><div>2px</div></li><li data-cmd="4px"><div>4px</div></li><li data-cmd="6px"><div>6px</div></li><li data-cmd="8px"><div>8px</div></li><li data-cmd="10px"><div>10px</div></li><li data-cmd="11px"><div>11px</div></li><li data-cmd="12px"><div>12px</div></li><li data-cmd="13px"><div>13px</div></li><li data-cmd="14px"><div>14px</div></li><li data-cmd="15px"><div>15px</div></li><li data-cmd="16px"><div>16px</div></li><li data-cmd="17px"><div>17px</div></li><li data-cmd="18px"><div>18px</div></li><li data-cmd="19px"><div>19px</div></li><li data-cmd="20px"><div>20px</div></li><li data-cmd="22px"><div>22px</div></li><li data-cmd="24px"><div>24px</div></li><li data-cmd="28px"><div>28px</div></li><li data-cmd="32px"><div>32px</div></li><li data-cmd="48px"><div>48px</div></li><li data-cmd="56px"><div>56px</div></li><li data-cmd="64px"><div>64px</div></li><li data-cmd="96px"><div>96px</div></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div id="bol" class="editi editib pers editf" alt="{{bold_alt}}" title="{{bold_title}}"></div>
|
||||
<div id="ital" class="editi editib pers editf" alt="{{italic_alt}}" title="{{italic_title}}"></div>
|
||||
<div id="under" class="editi editib pers editf" alt="{{underline_alt}}" title="{{underline_title}}"></div>
|
||||
<div id="strik" class="editi editib pers editf" alt="{{strikethrough_alt}}" title="{{strikethrough_title}}"></div>
|
||||
<div style="display: inline-block;">
|
||||
<div id="col" class="editi editimc pers" alt="{{font_color_alt}}" title="{{font_color_title}}"></div>
|
||||
<div id="backgr" class="editi editimc pers" alt="{{background_color_alt}}" title="{{background_color_title}}"></div>
|
||||
<div class="pickr-container"></div>
|
||||
</div>
|
||||
<div id="sup" class="editi editib pers editf" alt="{{superscript_alt}}" title="{{superscript_title}}"></div>
|
||||
<div id="sub" class="editi editib pers editf" alt="{{subscript_alt}}" title="{{subscript_title}}"></div>
|
||||
<div class="align-dropdown">
|
||||
<div class="current editi editib pers" style="background-position:-798px 1666px;" data-cmd="listNone"></div>
|
||||
<ul class="align-list">
|
||||
<li data-cmd="1"><div class="editi editib pers" id="listNone"></div></li>
|
||||
<li data-cmd="2"><div class="editi editib pers" id="listDots"></div></li>
|
||||
<li data-cmd="3"><div class="editi editib pers" id="listNumbers"></div></li>
|
||||
<li data-cmd="4"><div class="editi editib pers" id="listLetters"></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="align-dropdown">
|
||||
<div class="current editi editib pers" style="background-position: 765px 517px;" data-cmd="equal"></div>
|
||||
<ul class="align-list">
|
||||
<li data-cmd="equal"><div class="editi editib pers" id="equal"></div></li>
|
||||
<li data-cmd="equac"><div class="editi editib pers" id="equac"></div></li>
|
||||
<li data-cmd="equar"><div class="editi editib pers" id="equar"></div></li>
|
||||
<li data-cmd="equaj"><div class="editi editib pers" id="equaj"></div></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="forma" class="editi editib pers" alt="{{remove_format_alt}}" title="{{remove_format_title}}"></div>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar-group">
|
||||
<span id="toolbar-group-button-paste" class="editi editib pers toolbar-group-button"></span>
|
||||
<span class="toolbar-group-content">
|
||||
|
||||
<div id="link" class="editi editib swit" alt="{{insert_link_alt}}" title="{{insert_link_title}}"></div>
|
||||
<div id="linkdel" class="editi editib pers" alt="{{remove_link_alt}}" title="{{remove_link_title}}"></div>
|
||||
|
||||
<div class="align-dropdown-oneImg">
|
||||
<div class="current editi editib pers" id="oneImg"></div>
|
||||
<ul class="align-list" style="left: -1px;">
|
||||
<li data-cmd="imgLink"><div id="imgLink">{{insert_image_link}}</div></li>
|
||||
<li data-cmd="imgPc"><div id="imgPc">{{insert_image_PC}}</div></li>
|
||||
<li data-cmd="imgManager"><div id="imgManager">{{insert_image_manager}}</div></li>
|
||||
</ul>
|
||||
</div>
|
||||
<div id="hr" class="editi editib pers editf"></div>
|
||||
<div id="tabl" class="editi editib pers editf"></div>
|
||||
<div id="copyr" class="editi editib swit"></div>
|
||||
|
||||
<select id="pluginDropdownContent" class="edits pers3">
|
||||
<option id="pluginAddLeft" value="pluginAddLeft" class="swit">{{add_plugin_left}}</option>
|
||||
<option id="pluginAddRight" value="pluginAddRight" class="swit">{{add_plugin_right}}</option>
|
||||
<option id="pluginDelete" value="pluginDelete" class="editfText">{{delete_plugin}}</option>
|
||||
<option id="pluginMove" value="pluginMove" class="editfText">{{move_plugin}}</option>
|
||||
</select>
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div><div id="arrow-right" class="arrow borderStyle">⊳</div></div>
|
||||
|
||||
<!-- текстовый редактор -->
|
||||
<form name="dat">
|
||||
<textarea id="tex" name="tex" class="sb"></textarea>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Поля для редактирования элементов */ $editingMenuItems; ?>
|
||||
<div id="editingMenuItems" class="borderStyle">
|
||||
<div id="editingMenuItemsCopy" class="editingMenuItemsElement">{{copy}}</div>
|
||||
<div id="editingMenuItemsCut" class="editingMenuItemsElement">{{cut}}</div>
|
||||
<div id="editingMenuItemsPaste" class="editingMenuItemsElement">{{paste}}</div>
|
||||
<hr id="editingMenuItemsHr">
|
||||
|
||||
<div id="elementEditPanelHr" class="elementEditPanelElement">
|
||||
{{thickness_px}}: <input id="hr1Edit" class="inptx" value="1" type="number" title="{{thickness_tooltip}}"><br>
|
||||
{{width_px_percent}}: <input id="hr2Edit" class="inptx" value="95%" type="text" title="{{width_tooltip}}"><br>
|
||||
{{position}}: <select id="hr4Edit" class="sele" title="{{position_tooltip}}">
|
||||
<option value="left_clear">{{left_clear}}</option>
|
||||
<option value="right_clear">{{right_clear}}</option>
|
||||
<option value="center_clear">{{center_clear}}</option>
|
||||
<option selected value="left_text">{{left_text}}</option>
|
||||
<option value="right_text">{{right_text}}</option>
|
||||
<option value="text">{{in_text}}</option>
|
||||
</select><br>
|
||||
{{color}}: <input type="color" id="hr3cEdit" class="colored" title="{{choose_color}}"><br>
|
||||
{{margin_top_px}}: <input id="hrMarginTopEdit" class="inptx" value="0" type="number" title="{{padding_top_tooltip}}"><br>
|
||||
{{margin_right_px}}: <input id="hrMarginRightEdit" class="inptx" value="0" type="number" title="{{padding_right_tooltip}}"><br>
|
||||
{{margin_bottom_px}}: <input id="hrMarginBottomEdit" class="inptx" value="0" type="number" title="{{padding_bottom_tooltip}}"><br>
|
||||
{{margin_left_px}}: <input id="hrMarginLeftEdit" class="inptx" value="0" type="number" title="{{padding_left_tooltip}}"><br>
|
||||
<input type="button" id="buthrEdit" class="butin" value="{{edit}}" title="{{edit_hr}}">
|
||||
</div>
|
||||
|
||||
<div id="elementEditPanelImg" class="elementEditPanelElement">
|
||||
{{image_url}}: <input id="ima1Edit" class="inpbig" title="{{url_tooltip}}"><br>
|
||||
{{image_width_px}}: <input id="ima6Edit" class="inptx" placeholder="{{auto}}" value="250" type="number" title="{{width_tooltip}}"><br>
|
||||
{{image_height_px}}: <input id="ima6aEdit" class="inptx" placeholder="{{auto}}" value="" type="number" title="{{height_tooltip}}"><br>
|
||||
{{position}}: <select id="ima4Edit" class="sele" title="{{position_tooltip}}">
|
||||
<option value="left_clear">{{left_clear}}</option>
|
||||
<option value="right_clear">{{right_clear}}</option>
|
||||
<option value="center_clear">{{center_clear}}</option>
|
||||
<option selected value="left_text">{{left_text}}</option>
|
||||
<option value="right_text">{{right_text}}</option>
|
||||
<option value="text">{{in_text}}</option>
|
||||
</select><br>
|
||||
{{margin_top_px}}: <input id="imaMarginTopEdit" class="inptx" value="0" type="number" title="{{padding_top_tooltip}}"><br>
|
||||
{{margin_right_px}}: <input id="imaMarginRightEdit" class="inptx" value="0" type="number" title="{{padding_right_tooltip}}"><br>
|
||||
{{margin_bottom_px}}: <input id="imaMarginBottomEdit" class="inptx" value="0" type="number" title="{{padding_bottom_tooltip}}"><br>
|
||||
{{margin_left_px}}: <input id="imaMarginLeftEdit" class="inptx" value="0" type="number" title="{{padding_left_tooltip}}"><br>
|
||||
{{border_px}}: <input id="ima7Edit" class="inptx" value="0" type="number" title="{{border_tooltip}}"><br>
|
||||
{{border_color}}: <input type="color" id="ima8cEdit" class="colored" title="{{choose_color}}"><br>
|
||||
{{link_tooltip}}: <input id="imaLinkEdit" class="inpbig" type="url" title="{{link_tooltip}}"><br>
|
||||
<input type="button" id="butimaEdit" class="butin" value="{{edit}}" title="{{edit_image}}"><br>
|
||||
</div>
|
||||
|
||||
<div id="elementEditPanelTable" class="elementEditPanelElement">
|
||||
<input type="button" class="buttonEditTable" value="{{add_row}}" id="editTableButtonAddLine" title="{{add_row_tooltip}}"><br>
|
||||
<input type="button" class="buttonEditTable" value="{{delete_row}}" id="editTableButtonDeleteLine" title="{{delete_row_tooltip}}"><br>
|
||||
<input type="button" class="buttonEditTable" value="{{add_column}}" id="editTableButtonAddColumn" title="{{add_column_tooltip}}"><br>
|
||||
<input type="button" class="buttonEditTable" value="{{delete_column}}" id="editTableButtonDeleteColumn" title="{{delete_column_tooltip}}"><br>
|
||||
{{width_px_percent}}: <input id="tab1Edit" class="inptx" value="50%" type="text" title="{{width_tooltip}}"><br>
|
||||
{{position}}: <select id="tab6Edit" class="sele" title="{{position_tooltip}}">
|
||||
<option value="left_clear">{{left_clear}}</option>
|
||||
<option value="right_clear">{{right_clear}}</option>
|
||||
<option value="center_clear">{{center_clear}}</option>
|
||||
<option selected value="left_text">{{left_text}}</option>
|
||||
<option value="right_text">{{right_text}}</option>
|
||||
<option value="text">{{in_text}}</option>
|
||||
</select><br>
|
||||
{{padding_inner_px}}: <input id="tab4Edit" class="inptx" value="1" type="number" title="{{padding_inner_tooltip}}"><br>
|
||||
{{margin_top_px}}: <input id="tabMarginTopEdit" class="inptx" value="0" type="number" title="{{padding_top_tooltip}}"><br>
|
||||
{{margin_right_px}}: <input id="tabMarginRightEdit" class="inptx" value="0" type="number" title="{{padding_right_tooltip}}"><br>
|
||||
{{margin_bottom_px}}: <input id="tabMarginBottomEdit" class="inptx" value="0" type="number" title="{{padding_bottom_tooltip}}"><br>
|
||||
{{margin_left_px}}: <input id="tabMarginLeftEdit" class="inptx" value="0" type="number" title="{{padding_left_tooltip}}"><br>
|
||||
{{border_px}}: <input id="tab9Edit" class="inptx" value="1" type="number" title="{{border_tooltip}}"><br>
|
||||
{{border_color}}: <input type="color" id="tab10cEdit" class="colored" title="{{choose_color}}"><br>
|
||||
{{background_color}}: <input type="color" id="tab11cEdit" class="colored" value="#FFFFFF" title="{{choose_color}}"><br>
|
||||
<input type="button" id="butabEdit" class="butin" value="{{edit}}" title="{{edit_table}}"><br>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
292
main_plugin/editor/func.editor.php
Executable file
292
main_plugin/editor/func.editor.php
Executable file
@@ -0,0 +1,292 @@
|
||||
<?php
|
||||
/**
|
||||
* @file func.editor.php
|
||||
* @brief Содержит серверные функции для плагина редактора
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Сохраняет содержимое центрального блока страницы
|
||||
* @param array $params Массив с данными для сохранения, включая ключ 'saveContentIdData'
|
||||
* @return string 'true' в случае успешного сохранения
|
||||
* @throws Exception В случае проблем с сохранением контента
|
||||
*/
|
||||
function savePageCenterBlock($params) {
|
||||
global $config, $path, $_SESSION;
|
||||
|
||||
$saveContentIdXml = $path . $_SESSION['page_url'] . ".page.php";
|
||||
$saveContentIdData = $params['saveContentIdData'] ?? '';
|
||||
$file = @simplexml_load_file($saveContentIdXml);
|
||||
$node = $file->content->{$_SESSION['lng']};
|
||||
|
||||
$file->content->{$_SESSION['lng']} = '';
|
||||
$node = dom_import_simplexml($node);
|
||||
$doc = $node->ownerDocument;
|
||||
$appended = $node->appendChild($doc->createCDATASection("\n" . $saveContentIdData . "\n"));
|
||||
$saved = $file->asXML($saveContentIdXml);
|
||||
|
||||
if ($appended && $saved) {
|
||||
return 'true';
|
||||
}
|
||||
|
||||
throw new Exception("Problem saving content", -32003);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Сохраняет левый и правый блоки страницы
|
||||
* @param array $params Массив с данными блоков, ключ 'floatsBlock' содержит массив левых и правых блоков
|
||||
* @return string 'true' в случае успешного сохранения
|
||||
* @throws Exception В случае проблем с сохранением блоков
|
||||
*/
|
||||
function savePageSideBlocks($params) {
|
||||
global $config, $path, $_SESSION;
|
||||
|
||||
$saveContentIdXml = $path . $_SESSION['page_url'] . ".page.php";
|
||||
$file = @simplexml_load_file($saveContentIdXml);
|
||||
|
||||
$floatsBlock = json_decode($params['floatsBlock'] ?? '[]', true);
|
||||
$left = $floatsBlock['left'] ?? [];
|
||||
$right = $floatsBlock['right'] ?? [];
|
||||
|
||||
unset($file->lblock->block, $file->rblock->block);
|
||||
|
||||
foreach ($left as $d) {
|
||||
$b = $file->lblock->addChild('block');
|
||||
$b->addAttribute('url', htmlspecialchars($d['pluginUrl'] ?? '', ENT_QUOTES, 'UTF-8'));
|
||||
$b->addAttribute('title', htmlspecialchars($d['title'] ?? '', ENT_QUOTES, 'UTF-8'));
|
||||
$b->addAttribute('tclass',htmlspecialchars($d['tclass'] ?? '', ENT_QUOTES, 'UTF-8'));
|
||||
$b->addAttribute('bclass',htmlspecialchars($d['bclass'] ?? '', ENT_QUOTES, 'UTF-8'));
|
||||
}
|
||||
foreach ($right as $d) {
|
||||
$b = $file->rblock->addChild('block');
|
||||
$b->addAttribute('url', htmlspecialchars($d['pluginUrl'] ?? '', ENT_QUOTES, 'UTF-8'));
|
||||
$b->addAttribute('title', htmlspecialchars($d['title'] ?? '', ENT_QUOTES, 'UTF-8'));
|
||||
$b->addAttribute('tclass',htmlspecialchars($d['tclass'] ?? '', ENT_QUOTES, 'UTF-8'));
|
||||
$b->addAttribute('bclass',htmlspecialchars($d['bclass'] ?? '', ENT_QUOTES, 'UTF-8'));
|
||||
}
|
||||
|
||||
$dom = new DOMDocument('1.0', 'UTF-8');
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$dom->formatOutput = true;
|
||||
$dom->loadXML($file->asXML());
|
||||
$ok = $dom->save($saveContentIdXml);
|
||||
|
||||
if ($ok) {
|
||||
return 'true';
|
||||
}
|
||||
|
||||
throw new Exception("Problem saving floats block", -32003);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Сохраняет заголовок страницы
|
||||
* @param array $params Массив с данными для сохранения, ключ 'newTitle' содержит новый заголовок
|
||||
* @return string 'true' в случае успешного сохранения
|
||||
* @throws Exception В случае проблем с сохранением заголовка или если узел не найден
|
||||
*/
|
||||
function savePageTitle($params) {
|
||||
global $config, $path, $_SESSION;
|
||||
|
||||
$newTitle = htmlspecialchars($params['newTitle'] ?? '', ENT_QUOTES, 'UTF-8');
|
||||
$filePath = $path . "data/filepath." . $_SESSION['lng'] . ".php";
|
||||
$file = @simplexml_load_file($filePath);
|
||||
|
||||
$fileNode = rtrim($config['REQUEST_URI'], '.html');
|
||||
$nodes = array_filter(explode('/', $fileNode));
|
||||
$node = $file->index;
|
||||
$current = 'index';
|
||||
|
||||
foreach ($nodes as $n) {
|
||||
if ($n !== '' && isset($node->$n)) {
|
||||
$node = $node->$n;
|
||||
$current .= '->' . $n;
|
||||
} else {
|
||||
$error = "Node not found: $current->$n";
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($error)) {
|
||||
throw new Exception($error, -32602);
|
||||
}
|
||||
|
||||
$node['title'] = $newTitle;
|
||||
$saved = $file->asXML($filePath);
|
||||
|
||||
if ($saved) {
|
||||
return 'true';
|
||||
}
|
||||
|
||||
throw new Exception("Problem saving title", -32003);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Сохраняет весь контент страницы, включая центральный блок, боковые блоки и атрибуты
|
||||
* @param array $params Массив с данными страницы, включая 'page_url', 'nameFile', 'saveContentIdData', 'floatsBlock', 'title', 'pluginUrl', 'tclass', 'bclass'
|
||||
* @return string 'true' в случае успешного сохранения
|
||||
* @throws Exception В случае проблем с сохранением содержимого страницы
|
||||
*/
|
||||
function saveHowPageContent($params) {
|
||||
global $config, $path, $_SESSION;
|
||||
|
||||
$_SESSION['page_url'] = $params['page_url'] . str_replace('.page.php', '', $params['nameFile']);
|
||||
$filePath = $path . $params['page_url'] . $params['nameFile'];
|
||||
$contentData = $params['saveContentIdData'] ?? '';
|
||||
$file = simplexml_load_file($filePath);
|
||||
$langNode = $file->content->{$_SESSION['lng']};
|
||||
$file->content->{$_SESSION['lng']} = '';
|
||||
$node = dom_import_simplexml($langNode);
|
||||
$doc = $node->ownerDocument;
|
||||
$node->appendChild($doc->createCDATASection("\n" . $contentData . "\n"));
|
||||
|
||||
$file->rblock = '';
|
||||
$file->lblock = '';
|
||||
$blocks = $params['floatsBlock'] ?? [];
|
||||
$titles = $params['title'] ?? [];
|
||||
$urls = $params['pluginUrl'] ?? [];
|
||||
$tclasses = $params['tclass'] ?? [];
|
||||
$bclasses = $params['bclass'] ?? [];
|
||||
foreach ($blocks as $i => $blockName) {
|
||||
$newBlock = $file->$blockName->addChild('block');
|
||||
$newBlock->addAttribute('url', htmlspecialchars($urls[$i] ?? '', ENT_QUOTES,'UTF-8'));
|
||||
$newBlock->addAttribute('title', htmlspecialchars($titles[$i] ?? '', ENT_QUOTES,'UTF-8'));
|
||||
$newBlock->addAttribute('tclass',htmlspecialchars($tclasses[$i] ?? '',ENT_QUOTES,'UTF-8'));
|
||||
$newBlock->addAttribute('bclass',htmlspecialchars($bclasses[$i] ?? '',ENT_QUOTES,'UTF-8'));
|
||||
}
|
||||
|
||||
$dom = new DOMDocument('1.0','UTF-8');
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$dom->formatOutput = true;
|
||||
$dom->loadXML($file->asXML());
|
||||
|
||||
$saved = $dom->save($filePath);
|
||||
if ($saved === false) {
|
||||
throw new Exception("Failed to save content", -32003);
|
||||
}
|
||||
return 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Создаёт новую страницу на основе шаблона
|
||||
* @param array $params Массив с данными страницы, включая 'page_url', 'nameFile', 'saveContentIdData'
|
||||
* @return string 'true' в случае успешного создания страницы
|
||||
* @throws Exception В случае ошибок при создании файла или записи XML
|
||||
*/
|
||||
function createNewPage($params) {
|
||||
global $config, $path, $_SESSION;
|
||||
|
||||
$_SESSION['page_url'] = $params['page_url'] . str_replace('.page.php', '', $params['nameFile']);
|
||||
$saveContentIdXml = $path . $params['page_url'] . $params['nameFile'];
|
||||
$templatePage = $path . "data/template.page.php";
|
||||
$saveContentIdData = $params['saveContentIdData'] ?? '';
|
||||
$templateContent = file_get_contents($templatePage);
|
||||
if ($templateContent === false || file_put_contents($saveContentIdXml, $templateContent) === false) {
|
||||
throw new Exception("Failed to create file", -32004);
|
||||
}
|
||||
|
||||
$file = simplexml_load_file($saveContentIdXml);
|
||||
$node = $file->content->{$_SESSION['lng']};
|
||||
$file->content->{$_SESSION['lng']} = '';
|
||||
$node = dom_import_simplexml($node);
|
||||
$no = $node->ownerDocument;
|
||||
$node->appendChild($no->createCDATASection("\n" . $saveContentIdData . "\n"));
|
||||
|
||||
$saved = $file->asXML($saveContentIdXml);
|
||||
if ($saved === false) {
|
||||
throw new Exception("Failed to write XML", -32003);
|
||||
}
|
||||
return 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Проверяет существование файла по указанному пути
|
||||
* @param array $params Массив с данными проверки, включая 'path' и 'nameFile'
|
||||
* @return string "true" если файл существует, иначе "false"
|
||||
* @throws Exception В случае некорректного пути
|
||||
*/
|
||||
function checkFile($params) {
|
||||
global $path;
|
||||
|
||||
$rel = trim($params['path'], "/\\");
|
||||
if ($rel !== '' && strpos($rel, '..') !== false) {
|
||||
throw new Exception("Invalid path", -32602);
|
||||
}
|
||||
|
||||
$name = basename($params['nameFile']);
|
||||
$full = rtrim($path, DIRECTORY_SEPARATOR)
|
||||
. ($rel !== '' ? DIRECTORY_SEPARATOR . $rel : '')
|
||||
. DIRECTORY_SEPARATOR . $name;
|
||||
|
||||
return file_exists($full) ? "true" : "false";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Создаёт новый плагин с базовым шаблоном
|
||||
* @param array $params Массив с данными плагина, включая 'pluginName'
|
||||
* @return string Сообщение о создании плагина или "false", если плагин уже существует
|
||||
* @throws Exception В случае отсутствия имени плагина или ошибок записи файла
|
||||
*/
|
||||
function createPlugin($params) {
|
||||
$pluginName = $params['pluginName'] ?? '';
|
||||
$baseDir = __DIR__ . '/../../plugin/';
|
||||
|
||||
if ($pluginName === '') {
|
||||
throw new Exception("Missing pluginName", -32602);
|
||||
}
|
||||
|
||||
$dir = $baseDir . $pluginName;
|
||||
if (!is_dir($dir)) {
|
||||
umask(0);
|
||||
$stub = "<?php\n echo '<div class=\"pluginEditable\" contenteditable=\"true\"></div>'; \n?>";
|
||||
if (file_put_contents("$dir/plug.php", $stub) === false) {
|
||||
throw new Exception("Failed to write plug.php", -32004);
|
||||
}
|
||||
return "Plugin created: $pluginName";
|
||||
}
|
||||
|
||||
return "false";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Получает содержимое плагина
|
||||
* @param array $params Массив с данными плагина, включая 'pluginName2'
|
||||
* @return string HTML-контент плагина
|
||||
* @throws Exception В случае если файл плагина не найден
|
||||
*/
|
||||
function getPlugin($params) {
|
||||
$pluginName = $params['pluginName2'] ?? '';
|
||||
$file = __DIR__ . '/../../plugin/' . $pluginName . '/plug.php';
|
||||
if (!is_file($file)) {
|
||||
throw new Exception("Plugin not found", -32602);
|
||||
}
|
||||
ob_start();
|
||||
include $file;
|
||||
return ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Загружает изображение пользователя из Base64 и сохраняет на сервер
|
||||
* @param array $params Массив с данными изображения, включая 'userImgBase64' и 'userImgName'
|
||||
* @return string Путь к сохранённому изображению
|
||||
* @throws Exception В случае ошибок при сохранении изображения
|
||||
*/
|
||||
function uploadImage($params) {
|
||||
global $config, $path, $_SESSION;
|
||||
$base64 = $params['userImgBase64'] ?? '';
|
||||
$filename= $params['userImgName'] ?? 'image.png';
|
||||
$uploaddir = 'img/users_img/' . ($_SESSION['username'] ?? '') . '/';
|
||||
$name = pathinfo($filename, PATHINFO_FILENAME);
|
||||
$ext = pathinfo($filename, PATHINFO_EXTENSION) ?: 'png';
|
||||
$target = $uploaddir . $name . '.' . $ext;
|
||||
$i = 1;
|
||||
while (file_exists($target)) {
|
||||
$target = $uploaddir . $name . "_$i." . $ext;
|
||||
$i++;
|
||||
}
|
||||
$data = base64_decode($base64, true);
|
||||
if (file_put_contents($target, $data) === false) {
|
||||
throw new Exception("Error saving image", -32004);
|
||||
}
|
||||
return $target;
|
||||
}
|
||||
|
||||
?>
|
||||
24
main_plugin/editor/lang.js.php
Executable file
24
main_plugin/editor/lang.js.php
Executable file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
/**
|
||||
* @file lang.js.php
|
||||
* @brief Подготавливает языковые строки и подставляет их в JS-шаблон редактора
|
||||
*/
|
||||
|
||||
/** @brief Языковой массив для редактора */
|
||||
$lang = include $path . 'lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_GET['lng'] ?? ($_SESSION['lng'] ?? 'en');
|
||||
|
||||
/** @brief Массив подстановок для шаблона JS */
|
||||
$placeholders = [];
|
||||
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$placeholders['{{' . $key . '}}'] = $value;
|
||||
}
|
||||
|
||||
$js = file_get_contents($path . 'editor.js');
|
||||
$js = strtr($js, $placeholders);
|
||||
|
||||
echo "window.editorJs = (function() {\n" . $js . "\n})();";
|
||||
?>
|
||||
514
main_plugin/editor/lang.php
Executable file
514
main_plugin/editor/lang.php
Executable file
@@ -0,0 +1,514 @@
|
||||
<?php
|
||||
$lang = [
|
||||
'ru' => [
|
||||
'insert_line' => 'Вставить линию',
|
||||
'thickness_px' => 'Толщина (px)',
|
||||
'width_px_percent' => 'Ширина (px, %)',
|
||||
'position' => 'Положение',
|
||||
'left' => 'Слева',
|
||||
'center' => 'По центру',
|
||||
'right' => 'Справа',
|
||||
'in_text' => 'В тексте',
|
||||
'color' => 'Цвет',
|
||||
'choose_color' => 'Нажмите, чтобы выбрать цвет',
|
||||
'insert' => 'Вставить',
|
||||
'insert_hr' => 'Вставить горизонтальную линию',
|
||||
'insert_table' => 'Вставить таблицу',
|
||||
'rows' => 'Строки',
|
||||
'columns' => 'Колонки',
|
||||
'padding_inner_px' => 'Внутренний отступ (px)',
|
||||
'padding_outer_px' => 'Внешний отступ (px)',
|
||||
'border_px' => 'Рамка (px)',
|
||||
'border_color' => 'Цвет рамки',
|
||||
'background_color' => 'Цвет фона',
|
||||
'upload' => 'Загрузить',
|
||||
'select' => 'Выбрать',
|
||||
'image_url' => 'URL изображения',
|
||||
'image_width_px' => 'Ширина изображения (px)',
|
||||
'image_height_px' => 'Высота изображения (px)',
|
||||
'margin_outer_px' => 'Внешний отступ (px)',
|
||||
'link' => 'Ссылка',
|
||||
'select_page_from_site' => 'Выбрать страницу с сайта',
|
||||
'open_in_new_window' => 'Открывать в новом окне',
|
||||
'yes' => 'Да',
|
||||
'no' => 'Нет',
|
||||
'underline' => 'Подчеркивание',
|
||||
'insert_link' => 'Вставить ссылку',
|
||||
'change_text_color' => 'Изменить цвет текста',
|
||||
'change_text_background' => 'Изменить цвет фона текста',
|
||||
'create_plugin_left' => 'Создать плагин слева',
|
||||
'enter_plugin_name' => 'Введите имя плагина',
|
||||
'plugin_name_guidelines' => 'В имени плагина только англ. буквы, цифры, пробелы, _',
|
||||
'used_plugin_names' => 'Используемые имена плагинов',
|
||||
'enter_plugin_title' => 'Введите заголовок плагина',
|
||||
'add' => 'Добавить',
|
||||
'add_plugin_left' => 'Добавить левый плагин',
|
||||
'select_plugin' => 'Выберите плагин',
|
||||
'create_plugin_right' => 'Создать плагин справа',
|
||||
'add_plugin_right' => 'Добавить правый плагин',
|
||||
'create_page' => 'Создать страницу',
|
||||
'select_or_create_file' => 'Выбрать или создать файл',
|
||||
'menu_name' => 'Название в меню',
|
||||
'tab_title' => 'Название во вкладке',
|
||||
'design' => 'Дизайн',
|
||||
'create' => 'Создать',
|
||||
'edit' => 'Изменить',
|
||||
'save' => 'Сохранить',
|
||||
'save_as' => 'Сохранить как',
|
||||
'open' => 'Открыть',
|
||||
'new' => 'Новый',
|
||||
'html_code_main_block_alt' => 'HTML-код главного блока',
|
||||
'html_code_main_block_title' => 'HTML-код главного блока',
|
||||
'undo_action_alt' => 'Отменить действие',
|
||||
'undo_action_title' => 'Отменить действие',
|
||||
'redo_action_alt' => 'Повторить действие',
|
||||
'redo_action_title' => 'Повторить действие',
|
||||
'bold_alt' => 'Жирный',
|
||||
'bold_title' => 'Сделать текст жирным',
|
||||
'italic_alt' => 'Курсив',
|
||||
'italic_title' => 'Сделать текст курсивом',
|
||||
'underline_alt' => 'Подчеркнутый',
|
||||
'underline_title' => 'Сделать текст подчеркнутым',
|
||||
'strikethrough_alt' => 'Зачеркнутый',
|
||||
'strikethrough_title' => 'Сделать текст зачеркнутым',
|
||||
'font_color_alt' => 'Цвет шрифта',
|
||||
'font_color_title' => 'Сделать текст другим цветом',
|
||||
'background_color_alt' => 'Цвет фона',
|
||||
'background_color_title' => 'Сделать фон другим цветом',
|
||||
'superscript_alt' => 'Надстрочный',
|
||||
'superscript_title' => 'Сделать текст надстрочным',
|
||||
'subscript_alt' => 'Подстрочный',
|
||||
'subscript_title' => 'Сделать текст подстрочным',
|
||||
'list_none' => 'Без списка',
|
||||
'align_left_alt' => 'Выравнить влево',
|
||||
'align_left_title' => 'Выравнивание влево',
|
||||
'align_center_alt' => 'Выравнить по центру',
|
||||
'align_center_title' => 'Выравнивание по центру',
|
||||
'align_right_alt' => 'Выравнить вправо',
|
||||
'align_right_title' => 'Выравнивание вправо',
|
||||
'align_justify_alt' => 'Выровнять по ширине',
|
||||
'align_justify_title' => 'Выровнять по ширине',
|
||||
'remove_format_alt' => 'Удалить форматирование',
|
||||
'remove_format_title' => 'Удалить форматирование',
|
||||
'insert_link_alt' => 'Вставить ссылку',
|
||||
'insert_link_title' => 'Вставить ссылку',
|
||||
'remove_link_alt' => 'Удалить ссылку',
|
||||
'remove_link_title' => 'Удалить ссылку',
|
||||
'edit_title_title' => 'Изменить заголовок',
|
||||
'insert_image_link' => 'Вставить изображение по ссылке',
|
||||
'insert_image_PC' => 'Загрузить изображение с компьютера',
|
||||
'insert_image_manager' => 'Выбрать изображение через менеджер',
|
||||
'insert_symbol' => 'Вставить символ',
|
||||
'delete_plugin' => 'Удалить плагин',
|
||||
'move_plugin' => 'Переместить плагин',
|
||||
'copy' => 'Копировать',
|
||||
'cut' => 'Вырезать',
|
||||
'paste' => 'Вставить',
|
||||
'auto' => 'авто',
|
||||
|
||||
'insert_url' => 'Вставьте URL!',
|
||||
'delete_plugin_confirm' => 'Удалить плагин?',
|
||||
'action_not_defined' => 'Действие не определено',
|
||||
'plugin_name_empty_error' => 'Имя плагина не может быть пустым!',
|
||||
'plugin_not_selected_error' => 'Плагин не выбран!',
|
||||
'plugin_name_guidelines' => 'В имени плагина можно использовать только английские буквы, цифры, пробелы и подчеркивания!',
|
||||
'plugin_name_exists_suffix' => ' - это имя плагина уже используется!',
|
||||
'used_plugin_names' => 'Используемые имена плагина:',
|
||||
'plugin_created_left_suffix' => 'создан на левую',
|
||||
'plugin_added_left_suffix' => 'добавлен на левую',
|
||||
'plugin_created_right_suffix' => 'создан на правую',
|
||||
'plugin_added_right_suffix' => 'добавлен на правую',
|
||||
'img_upload_error' => 'Ошибка при загрузке изображения!',
|
||||
'rows_or_columns_not_specified' => 'Вы не указали количество строк или столбцов!',
|
||||
'insert_url_or_select_image' => 'Вставьте URL или выберите изображение!',
|
||||
'cursor_not_in_editable_field' => 'Курсор не в редактируемом поле!',
|
||||
'cursor_or_selection_not_in_editable_field' => 'Курсор или выделение не в редактируемом поле!',
|
||||
'no_text_selected' => 'Вы не выделили текст!',
|
||||
|
||||
'padding_top_px' => 'Отступ сверху (px)',
|
||||
'padding_right_px' => 'Отступ справа (px)',
|
||||
'padding_bottom_px' => 'Отступ снизу (px)',
|
||||
'padding_left_px' => 'Отступ слева (px)',
|
||||
'thickness_tooltip' => 'Толщина линии в пикселях',
|
||||
'width_tooltip' => 'Ширина в процентах или пикселях',
|
||||
'position_tooltip' => 'Позиция элемента',
|
||||
'padding_top_tooltip' => 'Задайте верхний отступ в пикселях',
|
||||
'padding_right_tooltip' => 'Задайте правый отступ в пикселях',
|
||||
'padding_bottom_tooltip' => 'Задайте нижний отступ в пикселях',
|
||||
'padding_left_tooltip' => 'Задайте левый отступ в пикселях',
|
||||
'url_tooltip' => 'Введите URL изображения',
|
||||
'height_tooltip' => 'Высота изображения в пикселях',
|
||||
'margin_tooltip' => 'Внешний отступ в пикселях',
|
||||
'border_tooltip' => 'Толщина рамки в пикселях',
|
||||
'link_tooltip' => 'Ссылка при клике на изображение',
|
||||
'caption' => 'Подпись к изображению',
|
||||
'caption_tooltip' => 'Текст подписи под изображением',
|
||||
'add_row_tooltip' => 'Добавить строку в таблицу',
|
||||
'delete_row_tooltip' => 'Удалить строку из таблицы',
|
||||
'add_column_tooltip' => 'Добавить столбец в таблицу',
|
||||
'delete_column_tooltip' => 'Удалить столбец из таблицы',
|
||||
'padding_inner_tooltip' => 'Внутренний отступ в пикселях',
|
||||
'padding_outer_tooltip' => 'Внешний отступ в пикселях',
|
||||
'plugin_title_empty_error' => 'Заголовок пустой!',
|
||||
'open_page' => 'Откройте страницу на сайте, а не через менеджер!',
|
||||
'enter_new_title' => 'Введите новый заголовок:',
|
||||
'title_saved' => 'Заголовок сохранён!',
|
||||
'new_file' => 'Новый файл',
|
||||
'add_row' => 'Добавить строку',
|
||||
'delete_row' => 'Удалить строку',
|
||||
'add_column' => 'Добавить столбец',
|
||||
'delete_column'=> 'Удалить столбец',
|
||||
'left_clear' => 'Слева (без текста)',
|
||||
'right_clear' => 'Справа (без текста)',
|
||||
'center_clear'=> 'По центру (без текста)',
|
||||
'left_text' => 'Слева с текстом',
|
||||
'right_text' => 'Справа с текстом',
|
||||
'link_tooltip' => 'Ссылка при клике на изображение',
|
||||
'margin_top_px' => 'Внешний отступ сверху (px)',
|
||||
'margin_right_px' => 'Внешний отступ справа (px)',
|
||||
'margin_bottom_px' => 'Внешний отступ снизу (px)',
|
||||
'margin_left_px' => 'Внешний отступ слева (px)',
|
||||
'in_text' => 'В тексте'
|
||||
],
|
||||
'en' => [
|
||||
'insert_line' => 'Insert line',
|
||||
'thickness_px' => 'Thickness (px)',
|
||||
'width_px_percent' => 'Width (px, %)',
|
||||
'position' => 'Position',
|
||||
'left' => 'Left',
|
||||
'center' => 'Center',
|
||||
'right' => 'Right',
|
||||
'in_text' => 'In text',
|
||||
'color' => 'Color',
|
||||
'choose_color' => 'Click to choose color',
|
||||
'insert' => 'Insert',
|
||||
'insert_hr' => 'Insert horizontal line',
|
||||
'insert_table' => 'Insert table',
|
||||
'rows' => 'Rows',
|
||||
'columns' => 'Columns',
|
||||
'padding_inner_px' => 'Inner padding (px)',
|
||||
'padding_outer_px' => 'Outer padding (px)',
|
||||
'border_px' => 'Border (px)',
|
||||
'border_color' => 'Border color',
|
||||
'background_color' => 'Background color',
|
||||
'upload' => 'Upload',
|
||||
'select' => 'Select',
|
||||
'image_url' => 'Image URL',
|
||||
'image_width_px' => 'Image width (px)',
|
||||
'image_height_px' => 'Image height (px)',
|
||||
'margin_outer_px' => 'Outer margin (px)',
|
||||
'link' => 'Link',
|
||||
'select_page_from_site' => 'Select page from site',
|
||||
'open_in_new_window' => 'Open in new window',
|
||||
'yes' => 'Yes',
|
||||
'no' => 'No',
|
||||
'underline' => 'Underline',
|
||||
'insert_link' => 'Insert link',
|
||||
'change_text_color' => 'Change text color',
|
||||
'change_text_background' => 'Change text background',
|
||||
'create_plugin_left' => 'Create plugin left',
|
||||
'enter_plugin_name' => 'Enter plugin name',
|
||||
'plugin_name_guidelines' => 'Plugin name: letters, digits, spaces, underscores',
|
||||
'used_plugin_names' => 'Used plugin names',
|
||||
'enter_plugin_title' => 'Enter plugin title',
|
||||
'add' => 'Add',
|
||||
'add_plugin_left' => 'Add left plugin',
|
||||
'select_plugin' => 'Select plugin',
|
||||
'create_plugin_right' => 'Create plugin right',
|
||||
'add_plugin_right' => 'Add right plugin',
|
||||
'create_page' => 'Create page',
|
||||
'select_or_create_file' => 'Select or create file',
|
||||
'menu_name' => 'Menu name',
|
||||
'tab_title' => 'Tab title',
|
||||
'design' => 'Design',
|
||||
'create' => 'Create',
|
||||
'edit' => 'Edit',
|
||||
'save' => 'Save',
|
||||
'save_as' => 'Save as',
|
||||
'open' => 'Open',
|
||||
'new' => 'New',
|
||||
'html_code_main_block_alt' => 'HTML code main block',
|
||||
'html_code_main_block_title' => 'HTML code main block',
|
||||
'undo_action_alt' => 'Undo action',
|
||||
'undo_action_title' => 'Undo action',
|
||||
'redo_action_alt' => 'Redo action',
|
||||
'redo_action_title' => 'Redo action',
|
||||
'bold_alt' => 'Bold',
|
||||
'bold_title' => 'Make text bold',
|
||||
'italic_alt' => 'Italic',
|
||||
'italic_title' => 'Make text italic',
|
||||
'underline_alt' => 'Underline',
|
||||
'underline_title' => 'Underline text',
|
||||
'strikethrough_alt' => 'Strikethrough',
|
||||
'strikethrough_title' => 'Strikethrough text',
|
||||
'font_color_alt' => 'Font color',
|
||||
'font_color_title' => 'Change font color',
|
||||
'background_color_alt' => 'Background color',
|
||||
'background_color_title' => 'Change background color',
|
||||
'superscript_alt' => 'Superscript',
|
||||
'superscript_title' => 'Make text superscript',
|
||||
'subscript_alt' => 'Subscript',
|
||||
'subscript_title' => 'Make text subscript',
|
||||
'list_none' => 'Without a list',
|
||||
'align_left_alt' => 'Align left',
|
||||
'align_left_title' => 'Align text left',
|
||||
'align_center_alt' => 'Align center',
|
||||
'align_center_title' => 'Align text center',
|
||||
'align_right_alt' => 'Align right',
|
||||
'align_right_title' => 'Align text right',
|
||||
'align_justify_alt' => 'Justify',
|
||||
'align_justify_title' => 'Justify text',
|
||||
'remove_format_alt' => 'Remove format',
|
||||
'remove_format_title' => 'Remove formatting',
|
||||
'insert_link_alt' => 'Insert link',
|
||||
'insert_link_title' => 'Insert link',
|
||||
'remove_link_alt' => 'Remove link',
|
||||
'remove_link_title' => 'Remove link',
|
||||
'edit_title_title' => 'Edit title',
|
||||
'insert_image_link' => 'Insert image by link',
|
||||
'insert_image_PC' => 'Upload image from PC',
|
||||
'insert_image_manager' => 'Choose image from manager',
|
||||
'insert_symbol' => 'Insert symbol',
|
||||
'delete_plugin' => 'Delete plugin',
|
||||
'move_plugin' => 'Move plugin',
|
||||
'copy' => 'Copy',
|
||||
'cut' => 'Cut',
|
||||
'paste' => 'Paste',
|
||||
'auto' => 'auto',
|
||||
|
||||
'insert_url' => 'Insert URL!',
|
||||
'delete_plugin_confirm' => 'Delete plugin?',
|
||||
'action_not_defined' => 'Action not defined',
|
||||
'plugin_name_empty_error' => 'Plugin name cannot be empty!',
|
||||
'plugin_not_selected_error' => 'Plugin not selected!',
|
||||
'plugin_name_guidelines' => 'Plugin name can only contain English letters, numbers, spaces, and underscores!',
|
||||
'plugin_name_exists_suffix' => ' - this plugin name is already taken!',
|
||||
'used_plugin_names' => 'Used plugin names:',
|
||||
'plugin_created_left_suffix' => 'created on the left',
|
||||
'plugin_added_left_suffix' => 'added on the left',
|
||||
'plugin_created_right_suffix' => 'created on the right',
|
||||
'plugin_added_right_suffix' => 'added on the right',
|
||||
'img_upload_error' => 'Image upload error!',
|
||||
'rows_or_columns_not_specified' => 'You didn\'t specify the number of rows or columns!',
|
||||
'insert_url_or_select_image' => 'Insert URL or select an image!',
|
||||
'cursor_not_in_editable_field' => 'Cursor is not in an editable field!',
|
||||
'cursor_or_selection_not_in_editable_field' => 'Cursor or selection is not in an editable field!',
|
||||
'no_text_selected' => 'No text selected!',
|
||||
|
||||
'padding_top_px' => 'Padding top (px)',
|
||||
'padding_right_px' => 'Padding right (px)',
|
||||
'padding_bottom_px' => 'Padding bottom (px)',
|
||||
'padding_left_px' => 'Padding left (px)',
|
||||
'thickness_tooltip' => 'Line thickness in pixels',
|
||||
'width_tooltip' => 'Width in percent or pixels',
|
||||
'position_tooltip' => 'Element position',
|
||||
'padding_top_tooltip' => 'Set top padding in pixels',
|
||||
'padding_right_tooltip' => 'Set right padding in pixels',
|
||||
'padding_bottom_tooltip' => 'Set bottom padding in pixels',
|
||||
'padding_left_tooltip' => 'Set left padding in pixels',
|
||||
'url_tooltip' => 'Enter image URL',
|
||||
'height_tooltip' => 'Image height in pixels',
|
||||
'margin_tooltip' => 'Outer margin in pixels',
|
||||
'border_tooltip' => 'Border width in pixels',
|
||||
'link_tooltip' => 'Link on image click',
|
||||
'caption' => 'Image caption',
|
||||
'caption_tooltip' => 'Text displayed under the image',
|
||||
'add_row_tooltip' => 'Add a row to the table',
|
||||
'delete_row_tooltip' => 'Delete a row from the table',
|
||||
'add_column_tooltip' => 'Add a column to the table',
|
||||
'delete_column_tooltip' => 'Delete a column from the table',
|
||||
'padding_inner_tooltip' => 'Inner padding in pixels',
|
||||
'padding_outer_tooltip' => 'Outer padding in pixels',
|
||||
'plugin_title_empty_error' => 'Title is empty!',
|
||||
'open_page' => 'Open the page on the website, not through the manager!',
|
||||
'enter_new_title' => 'Enter a new title:',
|
||||
'title_saved' => 'Title saved!',
|
||||
'new_file' => 'Jauns fails',
|
||||
'add_row' => 'Add row',
|
||||
'delete_row' => 'Delete row',
|
||||
'add_column' => 'Add column',
|
||||
'delete_column'=> 'Delete column',
|
||||
'left_clear' => 'Left (no text)',
|
||||
'right_clear' => 'Right (no text)',
|
||||
'center_clear'=> 'Center (no text)',
|
||||
'left_text' => 'Left with text',
|
||||
'link_tooltip' => 'Link on image click',
|
||||
'margin_top_px' => 'Margin top (px)',
|
||||
'margin_right_px' => 'Margin right (px)',
|
||||
'margin_bottom_px' => 'Margin bottom (px)',
|
||||
'margin_left_px' => 'Margin left (px)',
|
||||
'right_text' => 'Right with text'
|
||||
],
|
||||
'lv' => [
|
||||
'insert_line' => 'Ievietot līniju',
|
||||
'thickness_px' => 'Biezums (px)',
|
||||
'width_px_percent' => 'Platums (px, %)',
|
||||
'position' => 'Novietojums',
|
||||
'left' => 'Pa kreisi',
|
||||
'center' => 'Centrā',
|
||||
'right' => 'Pa labi',
|
||||
'in_text' => 'Tekstā',
|
||||
'color' => 'Krāsa',
|
||||
'choose_color' => 'Klikšķiniet, lai izvēlētos krāsu',
|
||||
'insert' => 'Ievietot',
|
||||
'insert_hr' => 'Ievietot horizontālu līniju',
|
||||
'insert_table' => 'Ievietot tabulu',
|
||||
'rows' => 'Rindas',
|
||||
'columns' => 'Kolonnas',
|
||||
'padding_inner_px' => 'Iekšējais atkāpums (px)',
|
||||
'padding_outer_px' => 'Ārējais atkāpums (px)',
|
||||
'border_px' => 'Rāmja biezums (px)',
|
||||
'border_color' => 'Rāmja krāsa',
|
||||
'background_color' => 'Fona krāsa',
|
||||
'upload' => 'Augšupielādēt',
|
||||
'select' => 'Izvēlēties',
|
||||
'image_url' => 'Attēla URL',
|
||||
'image_width_px' => 'Attēla platums (px)',
|
||||
'image_height_px' => 'Attēla augstums (px)',
|
||||
'margin_outer_px' => 'Ārējais atkāpums (px)',
|
||||
'link' => 'Saite',
|
||||
'select_page_from_site' => 'Izvēlēties lapu no vietnes',
|
||||
'open_in_new_window' => 'Atvērt jaunā logā',
|
||||
'yes' => 'Jā',
|
||||
'no' => 'Nē',
|
||||
'underline' => 'Pasvītrojums',
|
||||
'insert_link' => 'Ievietot saiti',
|
||||
'change_text_color' => 'Mainīt teksta krāsu',
|
||||
'change_text_background' => 'Mainīt teksta fona krāsu',
|
||||
'create_plugin_left' => 'Izveidot spraudni pa kreisi',
|
||||
'enter_plugin_name' => 'Ievadiet spraudņa nosaukumu',
|
||||
'plugin_name_guidelines' => 'Nosaukumā tikai angļu burtu, ciparu, atstarpju un apakšsvītru',
|
||||
'used_plugin_names' => 'Izmantotie spraudņu nosaukumi',
|
||||
'enter_plugin_title' => 'Ievadiet spraudņa virsrakstu',
|
||||
'add' => 'Pievienot',
|
||||
'add_plugin_left' => 'Pievienot kreiso spraudni',
|
||||
'select_plugin' => 'Izvēlēties spraudni',
|
||||
'create_plugin_right' => 'Izveidot spraudni pa labi',
|
||||
'add_plugin_right' => 'Pievienot labo spraudni',
|
||||
'create_page' => 'Izveidot lapu',
|
||||
'select_or_create_file' => 'Izvēlēties vai izveidot failu',
|
||||
'menu_name' => 'Nosaukums izvēlnē',
|
||||
'tab_title' => 'Cilnes virsraksts',
|
||||
'design' => 'Dizains',
|
||||
'create' => 'Izveidot',
|
||||
'edit' => 'Rediģēt',
|
||||
'save' => 'Saglabāt',
|
||||
'save_as' => 'Saglabāt kā',
|
||||
'open' => 'Atvērt',
|
||||
'new' => 'Jauns',
|
||||
'html_code_main_block_alt' => 'Galvenā bloka HTML kods',
|
||||
'html_code_main_block_title' => 'Galvenā bloka HTML kods',
|
||||
'undo_action_alt' => 'Atcelt darbību',
|
||||
'undo_action_title' => 'Atcelt darbību',
|
||||
'redo_action_alt' => 'Atkārtot darbību',
|
||||
'redo_action_title' => 'Atkārtot darbību',
|
||||
'bold_alt' => 'Treknraksts',
|
||||
'bold_title' => 'Padarīt tekstu treknu',
|
||||
'italic_alt' => 'Kursīvs',
|
||||
'italic_title' => 'Padarīt tekstu kursīvu',
|
||||
'underline_alt' => 'Pasvītrot',
|
||||
'underline_title' => 'Pasvītrot tekstu',
|
||||
'strikethrough_alt' => 'Pārvilkt',
|
||||
'strikethrough_title' => 'Pārvilkt tekstu',
|
||||
'font_color_alt' => 'Fonta krāsa',
|
||||
'font_color_title' => 'Mainīt fonta krāsu',
|
||||
'background_color_alt' => 'Fona krāsa',
|
||||
'background_color_title' => 'Mainīt fona krāsu',
|
||||
'superscript_alt' => 'Augšraksts',
|
||||
'superscript_title' => 'Padarīt tekstu augšrakstu',
|
||||
'subscript_alt' => 'Apakšraksts',
|
||||
'subscript_title' => 'Padarīt tekstu apakšrakstu',
|
||||
'list_none' => 'Bez saraksta',
|
||||
'align_left_alt' => 'Līdzināt pa kreisi',
|
||||
'align_left_title' => 'Līdzināt pa kreisi',
|
||||
'align_center_alt' => 'Centrēt',
|
||||
'align_center_title' => 'Centrēt tekstu',
|
||||
'align_right_alt' => 'Līdzināt pa labi',
|
||||
'align_right_title' => 'Līdzināt pa labi',
|
||||
'align_justify_alt' => 'Izlīdzināt',
|
||||
'align_justify_title' => 'Izlīdzināt tekstu',
|
||||
'remove_format_alt' => 'Noņemt formatējumu',
|
||||
'remove_format_title' => 'Noņemt formatējumu',
|
||||
'insert_link_alt' => 'Ievietot saiti',
|
||||
'insert_link_title' => 'Ievietot saiti',
|
||||
'remove_link_alt' => 'Noņemt saiti',
|
||||
'remove_link_title' => 'Noņemt saiti',
|
||||
'edit_title_title' => 'Rediģēt virsrakstu',
|
||||
'insert_image_link' => 'Ievietot attēlu pēc saites',
|
||||
'insert_image_PC' => 'Augšupielādēt attēlu no datora',
|
||||
'insert_image_manager' => 'Izvēlēties attēlu no pārvaldnieka',
|
||||
'insert_symbol' => 'Ievietot simbolu',
|
||||
'delete_plugin' => 'Dzēst spraudni',
|
||||
'move_plugin' => 'Pārvietot spraudni',
|
||||
'copy' => 'Kopēt',
|
||||
'cut' => 'Izgriezt',
|
||||
'paste' => 'Ielīmēt',
|
||||
'auto' => 'auto',
|
||||
|
||||
'insert_url' => 'Ievietojiet URL!',
|
||||
'delete_plugin_confirm' => 'Vai dzēst spraudni?',
|
||||
'action_not_defined' => 'Darbība nav definēta',
|
||||
'plugin_name_empty_error' => 'Spraudņa nosaukums nedrīkst būt tukšs!',
|
||||
'plugin_not_selected_error' => 'Spraudnis nav izvēlēts!',
|
||||
'plugin_name_guidelines' => 'Spraudņa nosaukumā var izmantot tikai angļu alfabēta burtus, ciparus, atstarpes un zemsvītras līnijas!',
|
||||
'plugin_name_exists_suffix' => ' - šis spraudņa nosaukums jau ir izmantots!',
|
||||
'used_plugin_names' => 'Izmantotie spraudņu nosaukumi:',
|
||||
'plugin_created_left_suffix' => 'izveidots kreisajā pusē',
|
||||
'plugin_added_left_suffix' => 'pievienots kreisajā pusē',
|
||||
'plugin_created_right_suffix' => 'izveidots labajā pusē',
|
||||
'plugin_added_right_suffix' => 'pievienots labajā pusē',
|
||||
'img_upload_error' => 'Attēla augšupielādes kļūda!',
|
||||
'rows_or_columns_not_specified' => 'Jūs neesat norādījis rindu vai kolonnu skaitu!',
|
||||
'insert_url_or_select_image' => 'Ievietojiet URL vai izvēlieties attēlu!',
|
||||
'cursor_not_in_editable_field' => 'Kursors nav rediģējamā laukā!',
|
||||
'cursor_or_selection_not_in_editable_field' => 'Kursors vai atlase nav rediģējamā laukā!',
|
||||
'no_text_selected' => 'Nav izvēlēts teksts!',
|
||||
|
||||
'padding_top_px' => 'Augšējais polsterējums (px)',
|
||||
'padding_right_px' => 'Labais polsterējums (px)',
|
||||
'padding_bottom_px' => 'Apakšējais polsterējums (px)',
|
||||
'padding_left_px' => 'Kreisais polsterējums (px)',
|
||||
'thickness_tooltip' => 'Līnijas biezums pikseļos',
|
||||
'width_tooltip' => 'Platums procentos vai pikseļos',
|
||||
'position_tooltip' => 'Elementa pozīcija',
|
||||
'padding_top_tooltip' => 'Iestatiet augšējo polsterējumu pikseļos',
|
||||
'padding_right_tooltip' => 'Iestatiet labo polsterējumu pikseļos',
|
||||
'padding_bottom_tooltip' => 'Iestatiet apakšējo polsterējumu pikseļos',
|
||||
'padding_left_tooltip' => 'Iestatiet kreiso polsterējumu pikseļos',
|
||||
'url_tooltip' => 'Ievadiet attēla URL',
|
||||
'height_tooltip' => 'Attēla augstums pikseļos',
|
||||
'margin_tooltip' => 'Ārējais attālums pikseļos',
|
||||
'border_tooltip' => 'Rāmja platums pikseļos',
|
||||
'link_tooltip' => 'Saite, kad klikšķina uz attēla',
|
||||
'caption' => 'Attēla paraksts',
|
||||
'caption_tooltip' => 'Teksts zem attēla',
|
||||
'add_row_tooltip' => 'Pievienot rindu tabulai',
|
||||
'delete_row_tooltip' => 'Dzēst rindu no tabulas',
|
||||
'add_column_tooltip' => 'Pievienot kolonnu tabulai',
|
||||
'delete_column_tooltip' => 'Dzēst kolonnu no tabulas',
|
||||
'padding_inner_tooltip' => 'Iekšējais polsterējums pikseļos',
|
||||
'padding_outer_tooltip' => 'Ārējais polsterējums pikseļos',
|
||||
'plugin_title_empty_error' => 'Virsraksts ir tukšs!',
|
||||
'open_page' => 'Atveriet lapu vietnē, nevis caur pārvaldnieku!',
|
||||
'enter_new_title' => 'Ievadiet jauno virsrakstu:',
|
||||
'title_saved' => 'Virsraksts saglabāts!',
|
||||
'new_file' => 'Jauns fails',
|
||||
'add_row' => 'Pievienot rindu',
|
||||
'delete_row' => 'Dzēst rindu',
|
||||
'add_column' => 'Pievienot kolonnu',
|
||||
'delete_column'=> 'Dzēst kolonnu',
|
||||
'left_clear' => 'Pa kreisi (bez teksta)',
|
||||
'right_clear' => 'Pa labi (bez teksta)',
|
||||
'center_clear'=> 'Centrēti (bez teksta)',
|
||||
'left_text' => 'Pa kreisi ar tekstu',
|
||||
'right_text' => 'Pa labi ar tekstu',
|
||||
'link_tooltip' => 'Saite kad klikšķina uz attēla',
|
||||
'margin_top_px' => 'Ārējais attālums augšā (px)',
|
||||
'margin_right_px' => 'Ārējais attālums pa labi (px)',
|
||||
'margin_bottom_px' => 'Ārējais attālums apakšā (px)',
|
||||
'margin_left_px' => 'Ārējais attālums pa kreisi (px)',
|
||||
'in_text' => 'Tekstā'
|
||||
],
|
||||
];
|
||||
|
||||
return $lang;
|
||||
26
main_plugin/editor/plug.php
Executable file
26
main_plugin/editor/plug.php
Executable file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
/**
|
||||
* @file plug.php
|
||||
* @brief Подключает плагин редактора для администраторов
|
||||
*/
|
||||
|
||||
global $path, $_SESSION, $configAdmins;
|
||||
|
||||
/** @brief Языковой массив для плагина редактора */
|
||||
$lang = include $path . 'main_plugin/editor/lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_SESSION['lng'] ?? 'en';
|
||||
|
||||
if (in_array($_SESSION['username'], $configAdmins, true)) {
|
||||
include_once $path . 'main_plugin/editor/func.editor.php';
|
||||
|
||||
$Html = file_get_contents($path . 'main_plugin/editor/editor.php');
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$Html = str_replace('{{' . $key . '}}', $value, $Html);
|
||||
}
|
||||
echo $Html;
|
||||
echo '<link rel="stylesheet" type="text/css" href="/main_plugin/editor/editor.css">';
|
||||
echo '<script type="text/javascript" src="/main_plugin/editor/lang.js.php?lng=' . $lng . '"></script>';
|
||||
}
|
||||
?>
|
||||
172
main_plugin/form_editor/form_editor.css
Executable file
172
main_plugin/form_editor/form_editor.css
Executable file
@@ -0,0 +1,172 @@
|
||||
#formEditorContent {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 800px;
|
||||
padding-top: 5px;
|
||||
}
|
||||
.formEditorArea {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
box-sizing: border-box;
|
||||
overflow: auto;
|
||||
position: relative;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#formEditorButtons,
|
||||
.formEditorProperties {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
#panelButtons {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background-color: #f0f0f0;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
z-index: 50;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
.fe-btn {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
border: 1px solid #000;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#panelProperties {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
right: 0;
|
||||
margin: 10px;
|
||||
width: 17%;
|
||||
background-color: #f0f0f0;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
z-index: 50;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
height: -webkit-fill-available;
|
||||
min-width: 160px;
|
||||
padding-top: 15px;
|
||||
overflow-y: auto;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.panelHeader {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
}
|
||||
.formEditorProperties {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
/* окно действий с элементами */
|
||||
#formEditorActions {
|
||||
display: none;
|
||||
position: absolute;
|
||||
background-color: #f0f0f0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 5px;
|
||||
box-sizing: border-box;
|
||||
z-index: 50;
|
||||
position: fixed;
|
||||
}
|
||||
|
||||
.formEditorActionPanel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.formEditorAction {
|
||||
text-align: center;
|
||||
border: 1px solid #000;
|
||||
border-radius: 3px;
|
||||
padding: 3px;
|
||||
cursor: pointer;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.image-chooser {
|
||||
position: absolute;
|
||||
z-index: 50;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
padding: 6px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.image-chooser button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
|
||||
#formMainPanel {
|
||||
position: relative;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
#menuToggle {
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f0f0f0;
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
z-index: 10;
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
#menuPanel {
|
||||
border-radius: 4px;
|
||||
position: absolute;
|
||||
top: 45px;
|
||||
left: 10px;
|
||||
display: none;
|
||||
background: #fff;
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f0f0f0;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
#menuPanel button {
|
||||
padding: 4px;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
#menuPanel label {
|
||||
display: block;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
#gridInput {
|
||||
width: 60px;
|
||||
}
|
||||
1595
main_plugin/form_editor/form_editor.js
Executable file
1595
main_plugin/form_editor/form_editor.js
Executable file
File diff suppressed because it is too large
Load Diff
55
main_plugin/form_editor/form_editor.php
Executable file
55
main_plugin/form_editor/form_editor.php
Executable file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
/**
|
||||
* @file form_editor.php
|
||||
* @brief Интерфейс визуального редактора форм: панель кнопок, область редактирования, свойства элементов, действия (копировать, вставить, удалить) и выбор изображений
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php /** @brief Основной контейнер визуального редактора форм */ $formEditor; ?>
|
||||
<div id="formEditor" class="bfloat">
|
||||
<div class="btitle">{{form_editor}}</div>
|
||||
<div id="formEditorContent">
|
||||
<div id="formMainPanel">
|
||||
<button id="menuToggle">☰</button>
|
||||
<div id="menuPanel">
|
||||
<button id="downloadBtn">{{download_html}}</button>
|
||||
<button id="loadBtn">{{load_html}}</button>
|
||||
<button id="downloadCssBtn">{{download_html_and_auto_css}}</button>
|
||||
Сетка (px): <input id="gridInput" type="number" value="20" min="1">
|
||||
</div>
|
||||
</div>
|
||||
<div class="formEditorPanel" id="panelButtons">
|
||||
<div id="formEditorButtons">
|
||||
<div class="fe-btn" id="addDiv">D</div>
|
||||
<div class="fe-btn" id="addText">T</div>
|
||||
<div class="fe-btn" id="addInput">I</div>
|
||||
<div class="fe-btn" id="addButton">B</div>
|
||||
<div class="fe-btn" id="addImg">Im</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="formEditorArea"></div>
|
||||
<div class="formEditorAreaStyle" style="display: none;"></div>
|
||||
<div class="formEditorPanel" id="panelProperties" style="display: none;">
|
||||
<div class="panelHeader">{{properties_and_actions}}</div>
|
||||
<div class="formEditorProperties">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="formEditorStyles" style="display: none;"><style></style></div>
|
||||
|
||||
<?php /** @brief Контейнер выбора изображения */ $image_chooser; ?>
|
||||
<div class="image-chooser" style="display: none;">
|
||||
<button id="imgByUrl">{{img_by_url}}</button>
|
||||
<button id="imgUpload">{{img_upload_to_server}}</button>
|
||||
<button id="imgData">{{img_insert_as_data_url}}</button>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Панель действий с элементами формы (копировать, вставить, удалить) */ $formEditorActions; ?>
|
||||
<div id="formEditorActions">
|
||||
<div class="formEditorActionPanel">
|
||||
<div class="formEditorAction" id="copyAction">{{copy}}</div>
|
||||
<div class="formEditorAction" id="pasteAction">{{paste}}</div>
|
||||
<div class="formEditorAction" id="deleteAction">{{delete}}</div>
|
||||
</div>
|
||||
</div>
|
||||
21
main_plugin/form_editor/lang.js.php
Executable file
21
main_plugin/form_editor/lang.js.php
Executable file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* @file lang.js.php
|
||||
* @brief Подготавливает языковые строки и подставляет их в JS-файл редактора форм
|
||||
*/
|
||||
|
||||
/** @brief Языковой массив для редактора */
|
||||
$lang = include $path . 'lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_GET['lng'] ?? ($_SESSION['lng'] ?? 'en');
|
||||
|
||||
/** @brief Массив подстановок для шаблона JS */
|
||||
$placeholders = [];
|
||||
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$placeholders['{{' . $key . '}}'] = $value;
|
||||
}
|
||||
|
||||
echo 'window.addEventListener("load", function() {' . strtr(file_get_contents($path . 'form_editor.js'), $placeholders) . '});';
|
||||
?>
|
||||
11
main_plugin/form_editor/lang.php
Executable file
11
main_plugin/form_editor/lang.php
Executable file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
$lang = [
|
||||
'ru' => [
|
||||
],
|
||||
'en' => [
|
||||
],
|
||||
'lv' => [
|
||||
],
|
||||
];
|
||||
|
||||
return $lang;
|
||||
40
main_plugin/form_editor/plug.php
Executable file
40
main_plugin/form_editor/plug.php
Executable file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
/**
|
||||
* @file plug.php
|
||||
* @brief Подключает плагин form_editor для администраторов, подставляет языковые строки и выводит HTML-код редактора форм
|
||||
*/
|
||||
|
||||
global $path, $_SESSION, $configAdmins;
|
||||
|
||||
/** @brief Языковой массив для плагина form_editor */
|
||||
$lang = include $path . 'main_plugin/form_editor/lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_SESSION['lng'] ?? 'en';
|
||||
|
||||
if (in_array($_SESSION['username'], $configAdmins, true)) {
|
||||
|
||||
$Html = file_get_contents($path . 'main_plugin/form_editor/form_editor.php');
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$Html = str_replace('{{' . $key . '}}', $value, $Html);
|
||||
}
|
||||
|
||||
echo $Html;
|
||||
|
||||
echo "<script>
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const c = document.querySelector('.center-float');
|
||||
const d = document.getElementById('formEditor');
|
||||
if (c && d) {
|
||||
c.appendChild(document.createElement('br'));
|
||||
c.appendChild(d);
|
||||
} else if (d) {
|
||||
d.remove();
|
||||
}
|
||||
});
|
||||
</script>";
|
||||
|
||||
echo '<link rel="stylesheet" type="text/css" href="/main_plugin/form_editor/form_editor.css">';
|
||||
echo '<script type="text/javascript" src="/main_plugin/form_editor/lang.js.php?lng=' . $lng . '"></script>';
|
||||
}
|
||||
?>
|
||||
309
main_plugin/manager/func.manager.php
Executable file
309
main_plugin/manager/func.manager.php
Executable file
@@ -0,0 +1,309 @@
|
||||
<?php
|
||||
/**
|
||||
* @file func.manager.php
|
||||
* @brief Функции управления файлами и папками для плагина manager
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Получает содержимое указанной папки
|
||||
* @param array $params Массив с ключом 'managerPathFolder' указывающим путь к папке
|
||||
* @return array Содержимое папки с информацией о файлах и папках
|
||||
* @throws Exception Если указанная директория недействительна
|
||||
*/
|
||||
function getFolderContents($params) {
|
||||
global $path, $_SESSION;
|
||||
$relPath = $params['managerPathFolder'] ?? '';
|
||||
$directory = realpath($path . $relPath);
|
||||
if (!$directory || !is_dir($directory)) {
|
||||
throw new Exception("Invalid directory", -32602);
|
||||
}
|
||||
$lang = include $path . 'main_plugin/manager/lang.php';
|
||||
$files = scandir($directory);
|
||||
$data = [];
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..') continue;
|
||||
$filePath = $directory . '/' . $file;
|
||||
if (is_dir($filePath)) {
|
||||
$name = $file;
|
||||
$type = $lang[$_SESSION['lng']]['file'];
|
||||
$size = count(array_diff(scandir($filePath), ['.','..']));
|
||||
} else {
|
||||
$extension = pathinfo($file, PATHINFO_EXTENSION);
|
||||
$name = $extension ? $file : pathinfo($file, PATHINFO_FILENAME);
|
||||
$type = $lang[$_SESSION['lng']]['folder'];
|
||||
$size = filesize($filePath);
|
||||
}
|
||||
$data[] = [
|
||||
'name' => $name,
|
||||
'path' => $relPath . "/" . $file,
|
||||
'type' => $type,
|
||||
'size' => $size,
|
||||
'creationTime' => date('Y-m-d H:i:s', filemtime($filePath)),
|
||||
];
|
||||
}
|
||||
array_unshift($data, ['rootFolder' => basename($path)]);
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Проверяет наличие конфликта имени файла или папки
|
||||
* @param array $params Массив с ключами 'name' и 'currentPath'
|
||||
* @return string "true" если файл/папка уже существует, "false" если нет
|
||||
* @throws Exception Если путь недействителен
|
||||
*/
|
||||
function checkNameConflict($params) {
|
||||
global $path;
|
||||
|
||||
$nameParam = $params['name'] ?? '';
|
||||
|
||||
if (preg_match('/^\$_COOKIE\[.*\]$/', $nameParam)) {
|
||||
eval('$nameParam = ' . $nameParam . ';');
|
||||
}
|
||||
|
||||
$currentPath = $path . ($params['currentPath'] ?? '') . '/';
|
||||
$name = basename($nameParam);
|
||||
$newPath = $currentPath . $name;
|
||||
|
||||
if (strpos(realpath(dirname($newPath)), realpath($path)) !== 0) {
|
||||
throw new Exception("Invalid path", -32602);
|
||||
}
|
||||
return file_exists($newPath) ? "true" : "false";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Вставляет файл или папку из буфера обмена в указанное место
|
||||
* @param array $params Массив с ключами 'managerSettingsInsert', 'clipboardPath', 'clipboardAction'
|
||||
* @return string "true" при успешной операции
|
||||
* @throws Exception Если действие некорректно или операция файла не удалась
|
||||
*/
|
||||
function insertClipboard($params) {
|
||||
global $path;
|
||||
$relDest = $params['managerSettingsInsert'] ?? '';
|
||||
$dest = realpath($path . '/' . $relDest);
|
||||
$clipboard = $params['clipboardPath'] ?? '';
|
||||
$action = $params['clipboardAction'] ?? '';
|
||||
|
||||
if (strpos($clipboard, '/') === 0) {
|
||||
$clipboard = $path . $clipboard;
|
||||
}
|
||||
$newPath = $dest . '/' . basename($clipboard);
|
||||
|
||||
$success = false;
|
||||
if ($action === 'cut') {
|
||||
$success = @rename($clipboard, $newPath);
|
||||
if ($success && !file_exists($newPath)) {
|
||||
$success = false;
|
||||
}
|
||||
} elseif ($action === 'copy') {
|
||||
$success = recursiveCopy($clipboard, $newPath) && file_exists($newPath);
|
||||
} else {
|
||||
throw new Exception("Invalid action", -32602);
|
||||
}
|
||||
if (!$success) {
|
||||
throw new Exception("File operation failed", -32004);
|
||||
}
|
||||
return "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Рекурсивно копирует файлы и папки
|
||||
* @param string $source Исходный путь
|
||||
* @param string $destination Путь назначения
|
||||
* @return bool true при успешном копировании, false при ошибке
|
||||
*/
|
||||
function recursiveCopy($source, $destination) {
|
||||
if (is_dir($source)) {
|
||||
if (!is_dir($destination) && !mkdir($destination, 0755, true)) {
|
||||
return false;
|
||||
}
|
||||
foreach (scandir($source) as $item) {
|
||||
if ($item === '.' || $item === '..') continue;
|
||||
if (!recursiveCopy("$source/$item", "$destination/$item")) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!copy($source, $destination)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Переименовывает файл или папку
|
||||
* @param array $params Массив с ключами 'managerSettingsRename' и 'managerNamePath'
|
||||
* @return string "true" при успешном переименовании
|
||||
* @throws Exception Если операция переименования не удалась
|
||||
*/
|
||||
function renameFile($params) {
|
||||
global $path;
|
||||
$currentFile = realpath($path . '/' . ($params['managerSettingsRename'] ?? ''));
|
||||
$newName = $params['managerNamePath'] ?? '';
|
||||
$target = dirname($currentFile) . '/' . $newName;
|
||||
if (!rename($currentFile, $target)) {
|
||||
throw new Exception("Rename failed", -32004);
|
||||
}
|
||||
return "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Удаляет файл или папку
|
||||
* @param array $params Массив с ключом 'managerSettingsDelete'
|
||||
* @return string "true" при успешном удалении
|
||||
* @throws Exception Если удаление файла или папки не удалось
|
||||
*/
|
||||
function deleteFile($params) {
|
||||
global $path;
|
||||
$target = realpath($path . '/' . ($params['managerSettingsDelete'] ?? ''));
|
||||
|
||||
$delete = function($p) use (&$delete) {
|
||||
if (is_dir($p)) {
|
||||
foreach (array_diff(scandir($p), ['.', '..']) as $f) {
|
||||
$delete($p . '/' . $f);
|
||||
}
|
||||
$ok = rmdir($p);
|
||||
} else {
|
||||
$ok = unlink($p);
|
||||
}
|
||||
if (!$ok) {
|
||||
throw new Exception("Failed to delete file or directory", -32004);
|
||||
}
|
||||
};
|
||||
|
||||
$delete($target);
|
||||
return "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Получает свойства файла или папки
|
||||
* @param array $params Массив с ключом 'managerSettingsProperties'
|
||||
* @return array Массив с информацией о файле или папке: имя, тип, путь, размер, время создания и изменения
|
||||
* @throws Exception Если файл или папка не найдены
|
||||
*/
|
||||
function getFileProperties($params) {
|
||||
global $path, $_SESSION;
|
||||
$lang = include $path . 'main_plugin/manager/lang.php';
|
||||
$target = realpath($path . '/' . ($params['managerSettingsProperties']));
|
||||
if (!$target || !file_exists($target)) {
|
||||
throw new Exception($lang[$_SESSION['lng']]['file_or_folder_not_found'], -32602);
|
||||
}
|
||||
$isDir = is_dir($target);
|
||||
return [
|
||||
['label' => $lang[$_SESSION['lng']]['name'], 'value' => basename($target)],
|
||||
['label' => $lang[$_SESSION['lng']]['type'], 'value' => $isDir ? $lang[$_SESSION['lng']]['folder'] : $lang[$_SESSION['lng']]['file']],
|
||||
['label' => $lang[$_SESSION['lng']]['location'], 'value' => str_replace($path, '', dirname($target))],
|
||||
['label' => $lang[$_SESSION['lng']]['size'], 'value' => $isDir ? (count(scandir($target)) - 2) . ' files' : filesize($target) . ' bytes'],
|
||||
['label' => $lang[$_SESSION['lng']]['creation_time'], 'value' => date('Y-m-d H:i:s', filectime($target))],
|
||||
['label' => $lang[$_SESSION['lng']]['last_modified_time'], 'value' => date('Y-m-d H:i:s', filemtime($target))],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Создаёт новый файл или папку
|
||||
* @param array $params Массив с ключами 'managerSettingsCreate', 'managerType', 'managerNamePath'
|
||||
* @return string "true" при успешном создании, "checkItemExists" если элемент уже существует
|
||||
* @throws Exception Если создание файла или папки не удалось
|
||||
*/
|
||||
function createFile($params) {
|
||||
global $config, $path;
|
||||
|
||||
$newItemName = $params['managerSettingsCreate'] ?? '';
|
||||
$type = $params['managerType'] ?? '';
|
||||
$parentPath = realpath($path . '/' . ($params['managerNamePath'] ?? ''));
|
||||
|
||||
$fullPath = $parentPath . '/' . $newItemName;
|
||||
if (file_exists($fullPath)) {
|
||||
return 'checkItemExists';
|
||||
}
|
||||
|
||||
$success = false;
|
||||
if ($type === 'папка') {
|
||||
$success = mkdir($fullPath);
|
||||
} elseif ($type === 'файл') {
|
||||
$success = file_put_contents($fullPath, '') !== false;
|
||||
}
|
||||
|
||||
if (!$success) {
|
||||
throw new Exception("Failed to create item", -32004);
|
||||
}
|
||||
return "true";
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Загружает страницу и возвращает её блоки и контент
|
||||
* @param array $params Массив с ключом 'newPath' указывающим путь к странице
|
||||
* @return array Массив с ключами 'right', 'left', 'content'
|
||||
* @throws Exception Если файл страницы не найден, не удалось загрузить XML или отсутствует контент для языка
|
||||
*/
|
||||
function getPage($params) {
|
||||
global $config, $path, $_SESSION;
|
||||
|
||||
$rel = $params['newPath'] ?? '';
|
||||
$file = $path . $rel . '.page.php';
|
||||
libxml_use_internal_errors(true);
|
||||
$pageXml = @simplexml_load_file($file);
|
||||
if (!$pageXml) {
|
||||
throw new Exception("Failed to load page file", -32004);
|
||||
}
|
||||
|
||||
if (!isset($_SESSION['lng']) || !isset($pageXml->content->{$_SESSION['lng']})) {
|
||||
throw new Exception("Missing language or content", -32602);
|
||||
}
|
||||
|
||||
$page = [];
|
||||
$page['right'] = GetBlock($pageXml->rblock->block, 'right');
|
||||
$page['left'] = GetBlock($pageXml->lblock->block, 'left');
|
||||
$page['content'] = (string)$pageXml->content->{$_SESSION['lng']};
|
||||
$_SESSION['page_url'] = $rel;
|
||||
session_write_close();
|
||||
return $page;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Загружает файл на сервер из base64-данных
|
||||
* @param array $params Массив с ключами 'fileData', 'fileName', 'pathLoad'
|
||||
* @return string Относительный путь загруженного файла
|
||||
* @throws Exception Если отсутствуют данные файла, некорректный путь загрузки, неверный base64 или сохранение файла не удалось
|
||||
*/
|
||||
function uploadFile($params) {
|
||||
global $config, $path;
|
||||
|
||||
$base64 = $params['fileData'] ?? '';
|
||||
$originalName = trim($params['fileName'] ?? '');
|
||||
$relDir = trim($params['pathLoad'] ?? '');
|
||||
|
||||
if ($base64 === '' || $originalName === '') {
|
||||
throw new Exception("Missing file data or name", -32602);
|
||||
}
|
||||
|
||||
$rootDir = realpath($path);
|
||||
$uploadDir = realpath($path . DIRECTORY_SEPARATOR . $relDir);
|
||||
if (!$uploadDir || strpos($uploadDir, $rootDir) !== 0 || !is_dir($uploadDir) || !is_writable($uploadDir)) {
|
||||
throw new Exception("Invalid upload directory", -32602);
|
||||
}
|
||||
|
||||
$basename = preg_replace('/[^\w\-]/u', '_', pathinfo($originalName, PATHINFO_FILENAME));
|
||||
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
|
||||
$counter = 0;
|
||||
do {
|
||||
$name = $basename . ($counter ? "_{$counter}" : '');
|
||||
$fullPath = $uploadDir . DIRECTORY_SEPARATOR . $name . ($extension ? ".{$extension}" : '');
|
||||
$counter++;
|
||||
} while (file_exists($fullPath));
|
||||
|
||||
$data = base64_decode($base64, true);
|
||||
if ($data === false) {
|
||||
throw new Exception("Invalid base64 data", -32602);
|
||||
}
|
||||
|
||||
if (file_put_contents($fullPath, $data) === false) {
|
||||
throw new Exception("Failed to save file", -32004);
|
||||
}
|
||||
|
||||
@chmod($fullPath, 0644);
|
||||
$relativePath = '/' . str_replace('\\', '/', substr($fullPath, strlen($rootDir)));
|
||||
return $relativePath;
|
||||
}
|
||||
|
||||
?>
|
||||
22
main_plugin/manager/lang.js.php
Executable file
22
main_plugin/manager/lang.js.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* @file lang.js.php
|
||||
* @brief Подготавливает языковые строки и подставляет их в JS-файл менеджера
|
||||
*/
|
||||
|
||||
/** @brief Языковой массив для менеджера */
|
||||
$lang = include $path . 'lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_GET['lng'] ?? ($_SESSION['lng'] ?? 'en');
|
||||
|
||||
/** @brief Массив подстановок для шаблона JS */
|
||||
$placeholders = [];
|
||||
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$placeholders['{{' . $key . '}}'] = $value;
|
||||
}
|
||||
|
||||
$js = 'window.addEventListener("LoadmanagerJs", function() {' . strtr(file_get_contents($path . 'manager.js'), $placeholders) . '}, { once: true });git checkout main';
|
||||
echo "window.managerJs = (function() {\n" . $js . "\n})();";
|
||||
?>
|
||||
169
main_plugin/manager/lang.php
Executable file
169
main_plugin/manager/lang.php
Executable file
@@ -0,0 +1,169 @@
|
||||
<?php
|
||||
$lang = [
|
||||
'ru' => [
|
||||
'copy' => 'Копировать',
|
||||
'cut' => 'Вырезать',
|
||||
'rename' => 'Переименовать',
|
||||
'delete' => 'Удалить',
|
||||
'properties' => 'Свойства',
|
||||
'upload_file' => 'Загрузить файл',
|
||||
'paste' => 'Вставить',
|
||||
'create_file' => 'Создать файл',
|
||||
'create_folder' => 'Создать папку',
|
||||
'rights' => 'Права',
|
||||
'ok' => 'Ок',
|
||||
'cancel' => 'Отмена',
|
||||
'file' => 'Файл',
|
||||
'to_clipboard' => 'в буфер обмена!',
|
||||
'error_when' => 'Ошибка при',
|
||||
'right_click_to_select_file' => 'Кликните правой кнопкой мыши на файл для выбора файла!',
|
||||
'file_with_same_name_exists' => 'Файл с таким именем уже существует!',
|
||||
'file_pasted_successfully' => 'Файл вставлен из буфера обмена!',
|
||||
'file_paste_unknown_error' => 'Неизвестная ошибка при вставке файла!',
|
||||
'error' => 'Ошибка',
|
||||
'enter_new_name' => 'Введите новое имя:',
|
||||
'rename_success' => 'Переименование выполнено успешно!',
|
||||
'rename_error' => 'Ошибка при переименовании!',
|
||||
'invalid_name_error' => 'Некорректное имя. Использование запрещенных символов...',
|
||||
'delete_confirm' => 'Подтвердите удаление!',
|
||||
'delete_success' => 'Удаление выполнено успешно!',
|
||||
'delete_error' => 'Ошибка при удалении!',
|
||||
'folder' => 'Папка',
|
||||
'enter_new_folder_name' => 'Введите имя новой папки:',
|
||||
'invalid_folder_name' => 'Некорректное имя папки',
|
||||
'folder_created_successfully' => 'Папка создана успешно!',
|
||||
'enter_new_file_name' => 'Введите имя нового файла:',
|
||||
'invalid_file_name' => 'Некорректное имя файла',
|
||||
'file_created_successfully' => 'Файл создан успешно!',
|
||||
'create' => 'Создать',
|
||||
'with_name' => 'с именем',
|
||||
'create_error' => 'Ошибка при создании',
|
||||
'item_already_exists' => 'с таким же именем уже есть!',
|
||||
'unknown_error' => 'Неизвестная ошибка!',
|
||||
'no_rights_yet' => 'прав пока что нету',
|
||||
'file_upload_error' => 'Ошибка загрузки файла!',
|
||||
'file_uploaded_successfully' => 'Файл успешно загружен!',
|
||||
'select_file_ending_with_page_php' => 'Выберите файл с расширением .page.php!',
|
||||
|
||||
'name' => 'Имя',
|
||||
'type' => 'Тип',
|
||||
'location' => 'Расположение',
|
||||
'size' => 'Размер',
|
||||
'creation_time' => 'Время создания',
|
||||
'last_modified_time' => 'Время последнего изменения',
|
||||
'error' => 'Ошибка',
|
||||
'file_or_folder_not_found' => 'Файл или папка не найдены'
|
||||
],
|
||||
'en' => [
|
||||
'copy' => 'Copy',
|
||||
'cut' => 'Cut',
|
||||
'rename' => 'Rename',
|
||||
'delete' => 'Delete',
|
||||
'properties' => 'Properties',
|
||||
'upload_file' => 'Upload file',
|
||||
'paste' => 'Paste',
|
||||
'create_file' => 'Create file',
|
||||
'create_folder' => 'Create folder',
|
||||
'rights' => 'Permissions',
|
||||
'ok' => 'OK',
|
||||
'cancel' => 'Cancel',
|
||||
'file' => 'File',
|
||||
'to_clipboard' => 'to clipboard!',
|
||||
'error_when' => 'Error when',
|
||||
'right_click_to_select_file' => 'Right-click to select a file!',
|
||||
'file_with_same_name_exists' => 'File with this name already exists!',
|
||||
'file_pasted_successfully' => 'File pasted successfully!',
|
||||
'file_paste_unknown_error' => 'Unknown error while pasting!',
|
||||
'error' => 'Error',
|
||||
'enter_new_name' => 'Enter new name:',
|
||||
'rename_success' => 'Renamed successfully!',
|
||||
'rename_error' => 'Error renaming!',
|
||||
'invalid_name_error' => 'Invalid name contains forbidden characters',
|
||||
'delete_confirm' => 'Confirm deletion!',
|
||||
'delete_success' => 'Deleted successfully!',
|
||||
'delete_error' => 'Error deleting!',
|
||||
'folder' => 'Folder',
|
||||
'enter_new_folder_name' => 'Enter folder name:',
|
||||
'invalid_folder_name' => 'Invalid folder name',
|
||||
'folder_created_successfully' => 'Folder created successfully!',
|
||||
'enter_new_file_name' => 'Enter file name:',
|
||||
'invalid_file_name' => 'Invalid file name',
|
||||
'file_created_successfully' => 'File created successfully!',
|
||||
'create' => 'Create',
|
||||
'with_name' => 'with name',
|
||||
'create_error' => 'Error creating',
|
||||
'item_already_exists' => 'already exists!',
|
||||
'unknown_error' => 'Unknown error!',
|
||||
'no_rights_yet' => 'No permissions set',
|
||||
'file_upload_error' => 'File upload error!',
|
||||
'file_uploaded_successfully' => 'File uploaded successfully!',
|
||||
'select_file_ending_with_page_php' => 'Select file ending with .page.php!',
|
||||
|
||||
'name' => 'Name',
|
||||
'type' => 'Type',
|
||||
'location' => 'Location',
|
||||
'size' => 'Size',
|
||||
'creation_time' => 'Creation Time',
|
||||
'last_modified_time' => 'Last Modified Time',
|
||||
'file' => 'File',
|
||||
'error' => 'Error',
|
||||
'file_or_folder_not_found' => 'File or folder not found'
|
||||
],
|
||||
'lv' => [
|
||||
'copy' => 'Kopēt',
|
||||
'cut' => 'Izgriezt',
|
||||
'rename' => 'Pārdēvēt',
|
||||
'delete' => 'Dzēst',
|
||||
'properties' => 'Īpašības',
|
||||
'upload_file' => 'Augšupielādēt failu',
|
||||
'paste' => 'Ielīmēt',
|
||||
'create_file' => 'Izveidot failu',
|
||||
'create_folder' => 'Izveidot mapi',
|
||||
'rights' => 'Tiesības',
|
||||
'ok' => 'Labi',
|
||||
'cancel' => 'Atcelt',
|
||||
'file' => 'Fails',
|
||||
'to_clipboard' => 'starpliktuvi!',
|
||||
'error_when' => 'Kļūda, mēģinot',
|
||||
'right_click_to_select_file' => 'Ar peles labo pogu atlasiet failu!',
|
||||
'file_with_same_name_exists' => 'Fails ar šādu nosaukumu jau pastāv!',
|
||||
'file_pasted_successfully' => 'Fails veiksmīgi ielīmēts!',
|
||||
'file_paste_unknown_error' => 'Nezināma kļūda ielīmējot!',
|
||||
'error' => 'Kļūda',
|
||||
'enter_new_name' => 'Ievadiet jaunu nosaukumu:',
|
||||
'rename_success' => 'Pārdēvēts veiksmīgi!',
|
||||
'rename_error' => 'Kļūda pārdēvējot!',
|
||||
'invalid_name_error' => 'Nederīgs nosaukums',
|
||||
'delete_confirm' => 'Apstipriniet dzēšanu!',
|
||||
'delete_success' => 'Dzēsts veiksmīgi!',
|
||||
'delete_error' => 'Kļūda dzēšot!',
|
||||
'folder' => 'Mape',
|
||||
'enter_new_folder_name' => 'Ievadiet mapes nosaukumu:',
|
||||
'invalid_folder_name' => 'Nederīgs mapes nosaukums',
|
||||
'folder_created_successfully' => 'Mape izveidota veiksmīgi!',
|
||||
'enter_new_file_name' => 'Ievadiet faila nosaukumu:',
|
||||
'invalid_file_name' => 'Nederīgs faila nosaukums',
|
||||
'file_created_successfully' => 'Fails izveidots veiksmīgi!',
|
||||
'create' => 'Izveidot',
|
||||
'with_name' => 'ar nosaukumu',
|
||||
'create_error' => 'Kļūda izveidojot',
|
||||
'item_already_exists' => 'jau eksistē!',
|
||||
'unknown_error' => 'Nezināma kļūda!',
|
||||
'no_rights_yet' => 'Nav piešķirtu tiesību',
|
||||
'file_upload_error' => 'Kļūda augšupielādējot failu!',
|
||||
'file_uploaded_successfully' => 'Fails augšupielādēts veiksmīgi!',
|
||||
'select_file_ending_with_page_php' => 'Atlasiet failu ar paplašinājumu .page.php!',
|
||||
|
||||
'name' => 'Vārds',
|
||||
'type' => 'Tips',
|
||||
'location' => 'Atrašanās vieta',
|
||||
'size' => 'Izmērs',
|
||||
'creation_time' => 'Izveidošanas laiks',
|
||||
'last_modified_time' => 'Pēdējās izmaiņas',
|
||||
'file' => 'Fails',
|
||||
'error' => 'Kļūda',
|
||||
'file_or_folder_not_found' => 'Fails vai mape nav atrasti'
|
||||
]
|
||||
];
|
||||
|
||||
return $lang;
|
||||
279
main_plugin/manager/manager.css
Executable file
279
main_plugin/manager/manager.css
Executable file
@@ -0,0 +1,279 @@
|
||||
/* менеджер */
|
||||
#managerDiv {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid #000000;
|
||||
width: 800px;
|
||||
border-radius: 5px;
|
||||
height: 600px;
|
||||
font-size: 1em;
|
||||
box-shadow: 0px 0px 5px #777;
|
||||
}
|
||||
#managerCloseFun {
|
||||
float: right;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-position: -159px -121px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#managerHistoryBackFun {
|
||||
margin-right: 2px;
|
||||
float: left;
|
||||
width: 20px;
|
||||
height: 14px;
|
||||
background-position: -400px -43px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#managerHistoryForwFun {
|
||||
margin-right: 2px;
|
||||
float: left;
|
||||
width: 20px;
|
||||
height: 14px;
|
||||
background-position: -441px -43px;
|
||||
cursor: pointer;
|
||||
}
|
||||
#managerBackFun {
|
||||
margin-right: 12px;
|
||||
float: left;
|
||||
width: 20px;
|
||||
height: 14px;
|
||||
background-position: -481px -43px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#managerTop {
|
||||
text-align: center;
|
||||
border-bottom: 1px #40464d solid;
|
||||
padding: 5px;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
#managerTopTitle {
|
||||
text-align: center;
|
||||
}
|
||||
#managerPath {
|
||||
display: inline-block;
|
||||
flex: 1;
|
||||
height: 18px;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 5px;
|
||||
padding: 2px;
|
||||
}
|
||||
#managerPath.active {
|
||||
border: 1px #40464d solid;
|
||||
}
|
||||
.managerPathButton {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.managerPathButton:hover {
|
||||
color: #787878;
|
||||
}
|
||||
|
||||
#managerTopDiv {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
#managerTableDiv {
|
||||
height: 480px;
|
||||
margin: 0px 20px 0px 20px;
|
||||
border: 1px #40464d solid;
|
||||
overflow-y: overlay;
|
||||
border-radius: 5px;
|
||||
}
|
||||
#managerTable {
|
||||
font-size: 1em;
|
||||
border-collapse: collapse;
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
width: -webkit-fill-available;
|
||||
}
|
||||
#managerTableTitle {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
font-weight: bold;
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
}
|
||||
#managerTable td {
|
||||
padding: 5px;
|
||||
}
|
||||
.managerTableDivFile:hover td {
|
||||
background-color: #c5e7f9;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* #managerTableDiv::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
}
|
||||
#managerTableDiv::-webkit-scrollbar-track {
|
||||
border: 1px solid #000;
|
||||
padding: 2px 0;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
#managerTableDiv::-webkit-scrollbar-thumb {
|
||||
border-radius: 10px;
|
||||
box-shadow: inset 0 0 6px rgba(0, 0, 0, 0.3);
|
||||
background-color: #ffffff;
|
||||
border: 1px solid #000;
|
||||
border-right: 0px solid #000;
|
||||
} */
|
||||
|
||||
/* сохронение как*/
|
||||
|
||||
#saveHowDiv {
|
||||
margin: 8px 20px 0px 20px;
|
||||
white-space: nowrap;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#saveHowName {
|
||||
border: 1px #40464d solid;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
width: 594px;
|
||||
font-size: 1em;
|
||||
display: inline-block;
|
||||
margin-left: 10px;
|
||||
}
|
||||
#saveHowButton {
|
||||
border: 1px #40464d solid;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
margin: 0px 0px 0px 10px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 91px;
|
||||
}
|
||||
#saveHowButton:hover {
|
||||
color: #787878;
|
||||
}
|
||||
|
||||
/* окно менеджер */
|
||||
#managerSettings {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid #000000;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
box-shadow: 0px 0px 5px #777;
|
||||
padding: 5px;
|
||||
animation: fadeIn 0.5s ease-in forwards;
|
||||
}
|
||||
.managerSettingsButtonButtons {
|
||||
background-color: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
border: 1px #40464d solid;
|
||||
cursor: pointer;
|
||||
}
|
||||
.managerSettingsButtonButtons:hover {
|
||||
color: #787878;
|
||||
}
|
||||
|
||||
/* окно свойств */
|
||||
#managerProperties {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
background-color: rgba(255, 255, 255, 0.97);
|
||||
border: 1px solid #000000;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
box-shadow: 0px 0px 5px #777;
|
||||
width: 600px;
|
||||
}
|
||||
#managerPropertiesMiddle {
|
||||
margin: 0px 20px 0px 20px;
|
||||
}
|
||||
|
||||
#managerPropertiesTop {
|
||||
border-bottom: 1px solid #000000;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
#managerPropertiesTopName {
|
||||
text-align: center;
|
||||
}
|
||||
#managerPropertiesTopClose {
|
||||
float: right;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-position: -159px -121px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#managerPropertiesWindow {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
}
|
||||
.managerPropertiesWindowDiv {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
padding: 9px;
|
||||
}
|
||||
.managerPropertiesWindowDiv:hover {
|
||||
color: #787878;
|
||||
}
|
||||
|
||||
#managerPropertiesDiv {
|
||||
border: 1px solid #000000;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.managerPropertiesDivDivs {
|
||||
padding: 6px 8px 6px 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#managerPropertiesDivButtons {
|
||||
padding: 9px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
#managerPropertiesDivButtons {
|
||||
text-align: center;
|
||||
}
|
||||
.managerPropertiesDivButton {
|
||||
margin: 3px 3px 3px 15px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
.managerPropertiesDivButton:hover {
|
||||
color: #787878;
|
||||
}
|
||||
|
||||
.editib {
|
||||
background-image: url(../../img/pict/b_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editimc {
|
||||
background-image: url(../../img/pict/mc_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editib:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editimc:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editf.active {
|
||||
background-image: url(../../img/pict/b_iconslyb.svg);
|
||||
background-color: #e7e7e7;
|
||||
}
|
||||
.editf.active:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
background-color: #e7e7e7;
|
||||
}
|
||||
524
main_plugin/manager/manager.js
Executable file
524
main_plugin/manager/manager.js
Executable file
@@ -0,0 +1,524 @@
|
||||
/**
|
||||
* @file manager.js
|
||||
* @brief Основной файл manager, отвечает за управление файлами
|
||||
*/
|
||||
|
||||
movementMenu("managerDiv");
|
||||
movementMenu("managerProperties");
|
||||
|
||||
/** @brief Элемент менеджера папок */
|
||||
let managerDiv = document.getElementById('managerDiv');
|
||||
managerData(currentPath);
|
||||
|
||||
/** @brief Основные кнопки и редактируемые поля в менеджере */
|
||||
function managerFun() {
|
||||
document.getElementById('managerCloseFun').onclick = function() {
|
||||
managerDiv.style.visibility = "hidden";
|
||||
};
|
||||
document.getElementById('managerHistoryBackFun').onclick = function() {
|
||||
if (managerHistoryIndex > 0) {
|
||||
managerHistoryIndex--;
|
||||
managerData(managerHistoryPaths[managerHistoryIndex]);
|
||||
}
|
||||
};
|
||||
document.getElementById('managerHistoryForwFun').onclick = function() {
|
||||
if (managerHistoryIndex < managerHistoryPaths.length - 1) {
|
||||
managerHistoryIndex++;
|
||||
managerData(managerHistoryPaths[managerHistoryIndex]);
|
||||
}
|
||||
};
|
||||
document.getElementById('managerBackFun').onclick = function() {
|
||||
managerData(removeLastSegment(currentPath));
|
||||
};
|
||||
|
||||
document.getElementById('managerSettingsCopy').onclick = function() {
|
||||
managerClipboard("copy", "{{copy}}");
|
||||
};
|
||||
document.getElementById('managerSettingsCut').onclick = function() {
|
||||
managerClipboard("cut", "{{cut}}");
|
||||
};
|
||||
/**
|
||||
* @brief Выполняет операции с буфером (копирование/вставка/вырезание)
|
||||
* @param action действие: copy/cut
|
||||
* @param messageText текст сообщения
|
||||
*/
|
||||
function managerClipboard(action, messageText) {
|
||||
if (managerTableDivFilePath) {
|
||||
let textToCopy = managerTableDivFilePath + '|' + action;
|
||||
navigator.clipboard.writeText(textToCopy).then(() => {
|
||||
document.getElementById('managerSettings').style.visibility = "hidden";
|
||||
}).catch(err => {
|
||||
console.error('Ошибка копирования в буфер:', err);
|
||||
});
|
||||
} else {
|
||||
messageFunction("{{right_click_to_select_file}}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let managerSettingsInsertId = document.getElementById('managerSettingsInsert');
|
||||
managerSettingsInsertId.onclick = function() {
|
||||
navigator.clipboard.readText().then(clipboardText => {
|
||||
let [clipboardPath, clipboardAction] = clipboardText.split('|');
|
||||
if (!clipboardPath || !clipboardAction) {
|
||||
messageFunction("{{right_click_to_select_file}}");
|
||||
return;
|
||||
}
|
||||
jsonrpcRequest("checkNameConflict", {
|
||||
name: clipboardPath,
|
||||
currentPath: currentPath
|
||||
}).then(response => {
|
||||
if (response == "true") {
|
||||
messageFunction("{{file_with_same_name_exists}}");
|
||||
} else {
|
||||
jsonrpcRequest("insertClipboard", {
|
||||
managerSettingsInsert: currentPath,
|
||||
clipboardPath: clipboardPath,
|
||||
clipboardAction: clipboardAction
|
||||
}).then(response => {
|
||||
if (response === "true") {
|
||||
messageFunction("{{file_pasted_successfully}}");
|
||||
} else {
|
||||
let errorMessage = response.error ? response.error : "{{file_paste_unknown_error}}";
|
||||
messageFunction("{{error}}: " + errorMessage);
|
||||
}
|
||||
managerData(currentPath);
|
||||
});
|
||||
}
|
||||
});
|
||||
}).catch(err => {
|
||||
console.error('Ошибка чтения из буфера:', err);
|
||||
});
|
||||
};
|
||||
|
||||
let managerSettingsRenameId = document.getElementById('managerSettingsRename');
|
||||
managerSettingsRenameId.onclick = async function() {
|
||||
if (managerTableDivFilePath) {
|
||||
let invalidCharacters = /[\/\\:*?"<>|]/;
|
||||
let managerTableDivFileName = managerTableDivFilePath.split(/[/\\]/).pop();
|
||||
messageQueue.push("{{enter_new_name}}");
|
||||
let title = await messageCreateInput(managerTableDivFileName || '');
|
||||
let isFolder = !/\./.test(managerTableDivFileName);
|
||||
if (title !== null) {
|
||||
if (title && !invalidCharacters.test(title) && (!isFolder || !title.includes('.'))) {
|
||||
messageQueue.push("{{rename_confirm}} " + title + "?");
|
||||
if (await messageCreateQuestion()) {
|
||||
let data = await jsonrpcRequest("checkNameConflict", { name: title, currentPath: currentPath });
|
||||
if (data == "true") {
|
||||
messageFunction("{{file_with_same_name_exists}}");
|
||||
} else {
|
||||
let res = await jsonrpcRequest("renameFile", { managerSettingsRename: managerTableDivFilePath, managerNamePath: title });
|
||||
if (res === "true") {
|
||||
messageFunction("{{rename_success}}");
|
||||
} else {
|
||||
messageFunction("{{rename_error}}");
|
||||
}
|
||||
managerData(currentPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messageFunction("{{invalid_name_error}}");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messageFunction("{{right_click_to_select_file}}");
|
||||
}
|
||||
};
|
||||
|
||||
let managerSettingsDeleteId = document.getElementById('managerSettingsDelete');
|
||||
managerSettingsDeleteId.onclick = async function() {
|
||||
if (managerTableDivFilePath) {
|
||||
messageQueue.push("{{delete_confirm}}");
|
||||
if (await messageCreateQuestion()) {
|
||||
let response = await jsonrpcRequest("deleteFile", { managerSettingsDelete: managerTableDivFilePath });
|
||||
if (response === "true") {
|
||||
messageFunction("{{delete_success}}");
|
||||
} else {
|
||||
messageFunction("{{delete_error}}");
|
||||
}
|
||||
managerData(currentPath);
|
||||
}
|
||||
} else {
|
||||
messageFunction("{{right_click_to_select_file}}");
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('managerSettingsButtonCreateFolder').onclick = async function() {
|
||||
await createManagerItem("папка", "{{enter_new_folder_name}}", "{{invalid_folder_name}}", "{{folder_created_successfully}}");
|
||||
};
|
||||
document.getElementById('managerSettingsButtonCreateFile').onclick = async function() {
|
||||
await createManagerItem("файл", "{{enter_new_file_name}}", "{{invalid_file_name}}", "{{file_created_successfully}}");
|
||||
};
|
||||
|
||||
async function createManagerItem(type, promptMessage, errorMessage, successMessage, nameSuffix = '') {
|
||||
messageQueue.push(promptMessage);
|
||||
let title = await messageCreateInput();
|
||||
if (title !== null) {
|
||||
let invalidCharacters = /[\/\\:*?"<>|]/;
|
||||
let isValidTitle = title && !invalidCharacters.test(title) && !title.startsWith('.') && !title.endsWith('.');
|
||||
if (type === "{{folder}}") {
|
||||
isValidTitle = isValidTitle && !title.includes('.');
|
||||
}
|
||||
if (isValidTitle) {
|
||||
title += nameSuffix;
|
||||
messageQueue.push("{{create}} " + type + " {{with_name}} " + title + "?");
|
||||
if (await messageCreateQuestion()) {
|
||||
let data = await jsonrpcRequest("checkNameConflict", { name: title, currentPath: currentPath });
|
||||
if (data == "true") {
|
||||
messageFunction("{{file_with_same_name_exists}}!");
|
||||
} else {
|
||||
let response = await jsonrpcRequest("createFile", { managerSettingsCreate: title, managerType: type, managerNamePath: currentPath });
|
||||
if (response === "true") {
|
||||
messageFunction(successMessage);
|
||||
} else if (response === "checkItemExists") {
|
||||
messageFunction(type + " {{item_already_exists}}");
|
||||
} else {
|
||||
messageFunction("{{create_error}} " + type + "!");
|
||||
}
|
||||
managerData(currentPath);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
messageFunction(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let managerSettingsPropertiesId = document.getElementById('managerSettingsProperties');
|
||||
managerSettingsPropertiesId.onclick = function() {
|
||||
document.getElementById('managerProperties').style.visibility = 'hidden';
|
||||
if (managerTableDivFilePath) {
|
||||
jsonrpcRequest("getFileProperties", { managerSettingsProperties: managerTableDivFilePath }).then(data => {
|
||||
let managerPropertiesId = document.getElementById('managerProperties');
|
||||
let managerPropertiesDivId = document.getElementById('managerPropertiesDiv');
|
||||
let managerPropertiesTopNameId = document.getElementById('managerPropertiesTopName');
|
||||
let managerPropertiesWindowPropertiesId = document.getElementById('managerPropertiesWindowProperties');
|
||||
let managerPropertiesWindowRightsId = document.getElementById('managerPropertiesWindowRights');
|
||||
|
||||
let tableProperties = document.createElement('table');
|
||||
tableProperties.style.width = "100%";
|
||||
|
||||
data.forEach(item => {
|
||||
let row = document.createElement('tr');
|
||||
let labelCell = document.createElement('td');
|
||||
let valueCell = document.createElement('td');
|
||||
labelCell.className = 'managerPropertiesDivDivs';
|
||||
valueCell.className = 'managerPropertiesDivDivs';
|
||||
labelCell.textContent = item.label;
|
||||
valueCell.textContent = item.value;
|
||||
row.appendChild(labelCell);
|
||||
row.appendChild(valueCell);
|
||||
tableProperties.appendChild(row);
|
||||
});
|
||||
|
||||
let tableRights = document.createElement('div');
|
||||
tableRights.innerHTML = "{{no_rights_yet}}";
|
||||
|
||||
managerPropertiesTopNameId.textContent = "{{properties}} " + data[0].value;
|
||||
managerPropertiesDivId.innerHTML = '';
|
||||
|
||||
managerPropertiesWindowPropertiesId.onclick = function() {
|
||||
managerPropertiesDivId.innerHTML = '';
|
||||
managerPropertiesDivId.appendChild(tableProperties);
|
||||
managerPropertiesWindowPropertiesId.style.backgroundColor = "#f3f3f3";
|
||||
managerPropertiesWindowRightsId.style.backgroundColor = "";
|
||||
};
|
||||
|
||||
managerPropertiesWindowRightsId.onclick = function() {
|
||||
managerPropertiesDivId.innerHTML = '';
|
||||
managerPropertiesDivId.appendChild(tableRights);
|
||||
managerPropertiesWindowPropertiesId.style.backgroundColor = "";
|
||||
managerPropertiesWindowRightsId.style.backgroundColor = "#f3f3f3";
|
||||
};
|
||||
|
||||
managerPropertiesWindowPropertiesId.click();
|
||||
|
||||
if (managerPropertiesId.style.visibility == 'hidden') {
|
||||
managerPropertiesId.style.visibility = 'visible';
|
||||
}
|
||||
|
||||
let managerPropertiesTopCloseId = document.getElementById('managerPropertiesTopClose');
|
||||
let managerPropertiesDivButtonOkId = document.getElementById('managerPropertiesDivButtonOk');
|
||||
let managerPropertiesDivButtonCancelId = document.getElementById('managerPropertiesDivButtonCancel');
|
||||
|
||||
managerPropertiesTopCloseId.onclick = function() {
|
||||
managerPropertiesId.style.visibility = 'hidden';
|
||||
};
|
||||
managerPropertiesDivButtonOkId.onclick = function() {
|
||||
managerPropertiesId.style.visibility = 'hidden';
|
||||
};
|
||||
managerPropertiesDivButtonCancelId.onclick = function() {
|
||||
managerPropertiesId.style.visibility = 'hidden';
|
||||
};
|
||||
});
|
||||
} else {
|
||||
messageFunction("{{right_click_to_select_file}}");
|
||||
}
|
||||
};
|
||||
|
||||
//загрузка файла для менеджера
|
||||
let managerSettingsLoadId = document.getElementById('managerSettingsLoad');
|
||||
managerSettingsLoadId.onclick = function() {
|
||||
let fileInput = document.createElement('input');
|
||||
fileInput.type = 'file';
|
||||
fileInput.addEventListener('change', function() {
|
||||
|
||||
jsonrpcRequest("checkNameConflict", { name: fileInput.files[0].name, currentPath: currentPath }).then(data => {
|
||||
if (data == "true") {
|
||||
messageFunction("{{file_with_same_name_exists}}")
|
||||
} else {
|
||||
const file = fileInput.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onload = function() {
|
||||
const base64Data = reader.result.split(',')[1];
|
||||
jsonrpcRequest("uploadFile", {
|
||||
fileName: file.name,
|
||||
fileData: base64Data,
|
||||
pathLoad: currentPath
|
||||
}).then(response => {
|
||||
messageFunction("{{file_uploaded_successfully}}")
|
||||
managerData(currentPath)
|
||||
}).catch(() => {
|
||||
messageFunction("{{file_with_same_name_exists}}")
|
||||
})
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
fileInput.click();
|
||||
};
|
||||
}
|
||||
|
||||
/** @brief Удаляет последний сегмент пути */
|
||||
function removeLastSegment(str) {
|
||||
const segments = str.split('/');
|
||||
if (segments.length > 1) {
|
||||
segments.pop();
|
||||
}
|
||||
return segments.join('/');
|
||||
}
|
||||
|
||||
/** @brief Инициализация окна настроек менеджера */
|
||||
function managerSettings() {
|
||||
let managerDiv = document.getElementById('managerDiv');
|
||||
let managerSettingsDiv = document.getElementById('managerSettings');
|
||||
|
||||
managerDiv.addEventListener('contextmenu', managerSettingsClick);
|
||||
touchLong(managerDiv, managerSettingsClick);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обрабатывает клик правой кнопкой мыши для показа настроек
|
||||
* @param event событие мыши
|
||||
*/
|
||||
function managerSettingsClick(event) {
|
||||
event.preventDefault();
|
||||
let managerSettingsDiv = document.getElementById('managerSettings');
|
||||
if (!isPhone) {
|
||||
managerSettingsDiv.style.left = `${touchX}px`;
|
||||
managerSettingsDiv.style.top = `${touchY}px`;
|
||||
} else {
|
||||
managerSettingsDiv.style.bottom = '15px';
|
||||
managerSettingsDiv.style.width = 'calc(100vw - 42px)';
|
||||
managerSettingsDiv.style.height = '42px';
|
||||
managerSettingsDiv.style.left = '15px';
|
||||
managerSettingsDiv.style.top = 'auto';
|
||||
managerSettingsDiv.style.boxShadow = 'none';
|
||||
}
|
||||
|
||||
let ids = [
|
||||
'managerSettingsCopy',
|
||||
'managerSettingsCut',
|
||||
'managerSettingsRename',
|
||||
'managerSettingsDelete',
|
||||
'managerSettingsProperties',
|
||||
'managerSettingsLoad',
|
||||
'managerSettingsInsert',
|
||||
'managerSettingsButtonCreateFolder',
|
||||
'managerSettingsButtonCreateFile'
|
||||
];
|
||||
ids.forEach(id => {
|
||||
let el = document.getElementById(id);
|
||||
if (el) {
|
||||
if (el.dataset.oldDisplay === undefined) {
|
||||
el.dataset.oldDisplay = getComputedStyle(el).display;
|
||||
}
|
||||
el.style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
if (event.target.closest('.managerTableDivFile')) {
|
||||
['managerSettingsCopy','managerSettingsCut','managerSettingsRename','managerSettingsDelete','managerSettingsProperties']
|
||||
.forEach(id => {
|
||||
let el = document.getElementById(id);
|
||||
el.style.display = el.dataset.oldDisplay;
|
||||
});
|
||||
} else {
|
||||
document.getElementById('managerSettingsLoad').style.display = document.getElementById('managerSettingsLoad').dataset.oldDisplay;
|
||||
navigator.clipboard.readText().then(text => {
|
||||
if (text) {
|
||||
let parts = text.split('|');
|
||||
if (parts.length === 2 && parts[0].trim() !== '' && parts[1].trim() !== '') {
|
||||
let el = document.getElementById('managerSettingsInsert');
|
||||
el.style.display = el.dataset.oldDisplay;
|
||||
}
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Ошибка чтения буфера:', err);
|
||||
});
|
||||
|
||||
|
||||
['managerSettingsButtonCreateFolder','managerSettingsButtonCreateFile']
|
||||
.forEach(id => {
|
||||
let el = document.getElementById(id);
|
||||
el.style.display = el.dataset.oldDisplay;
|
||||
});
|
||||
}
|
||||
|
||||
managerSettingsDiv.style.visibility = 'visible';
|
||||
document.addEventListener('pointerdown', function hideMenu(e) {
|
||||
if (!managerSettingsDiv.contains(e.target)) {
|
||||
managerSettingsDiv.style.visibility = 'hidden';
|
||||
document.removeEventListener('pointerdown', hideMenu);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Обрабатывает контекстное меню пути
|
||||
* @param event событие контекстного меню
|
||||
*/
|
||||
function managerPathContext(event){
|
||||
let targetElement=event.target.closest('[path]')
|
||||
if(targetElement){
|
||||
let pathValue=targetElement.getAttribute('path')
|
||||
managerTableDivFilePath=pathValue
|
||||
}
|
||||
}
|
||||
document.addEventListener('contextmenu',managerPathContext)
|
||||
touchLong(document, managerPathContext)
|
||||
|
||||
/** @brief Сохраняет файл через функцию "Сохранить как" */
|
||||
function saveHow() {
|
||||
let currentPathHow = currentPath;
|
||||
if (currentPathHow.startsWith('/')) {
|
||||
currentPathHow = currentPathHow.slice(1);
|
||||
}
|
||||
if (!currentPathHow.endsWith('/')) {
|
||||
currentPathHow += '/';
|
||||
}
|
||||
window.saveContentIdHow(currentPathHow);
|
||||
}
|
||||
|
||||
/** @brief Открывает выбранную страницу */
|
||||
function openPageBut() {
|
||||
if (openPageButPath != "no/Select") {
|
||||
getPage(openPageButPath);
|
||||
} else {
|
||||
messageFunction('{{select_file_ending_with_page_php}}');
|
||||
}
|
||||
}
|
||||
|
||||
/** @brief Обрабатывает выбор страницы через URL */
|
||||
function propertiesUrlFun() {
|
||||
let saveHowNameValue = document.getElementById('saveHowName').value;
|
||||
if (!saveHowNameValue.includes('.page.php')) return;
|
||||
let newValue = saveHowNameValue.replace(/\.page\.php/, "");
|
||||
let cp = currentPath;
|
||||
if (cp.charAt(0) === "/") {
|
||||
cp = cp.substring(1);
|
||||
}
|
||||
document.getElementById('treePropertiesDivUrlValue').innerHTML = cp + "/" + newValue;
|
||||
window.managerDataAction = "";
|
||||
managerDiv.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
/** @brief Обрабатывает выбор изображения для вставки */
|
||||
function selectImgFormButFun() {
|
||||
var rawPath = document.getElementById('managerPath').textContent;
|
||||
var cleanPath = rawPath.trim().replace(/\s*\/\s*/g, '/').replace(/\s+/g, '');
|
||||
var fileName = document.getElementById('saveHowName').value.trim();
|
||||
var fullPath = cleanPath + fileName;
|
||||
if (fullPath.startsWith('/')) {
|
||||
fullPath = fullPath.slice(1);
|
||||
}
|
||||
var img = document.createElement("img");
|
||||
img.src = fullPath;
|
||||
img.setAttribute("style", "float: left; margin: 10px; width: 250px; border: 0px solid rgb(0, 0, 0); overflow: hidden;");
|
||||
var sel = window.getSelection();
|
||||
if (sel.rangeCount) {
|
||||
var range = sel.getRangeAt(0);
|
||||
range.deleteContents();
|
||||
range.insertNode(img);
|
||||
}
|
||||
window.managerDataAction = "";
|
||||
managerDiv.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
/** @brief Обрабатывает выбор изображения для вставки в форму */
|
||||
function selectImgFormToFormButFun() {
|
||||
var rawPath = document.getElementById('managerPath').textContent;
|
||||
var cleanPath = rawPath.trim().replace(/\s*\/\s*/g, '/').replace(/\s+/g, '');
|
||||
var fileName = document.getElementById('saveHowName').value.trim();
|
||||
var fullPath = cleanPath + fileName;
|
||||
if (fullPath.startsWith('/')) fullPath = fullPath.slice(1);
|
||||
window.pendingImageSrc = fullPath;
|
||||
window.managerDataAction = "";
|
||||
managerDiv.style.visibility = "hidden";
|
||||
}
|
||||
|
||||
if (isPhone) document.getElementById('managerDiv').style.paddingBottom = "150px"
|
||||
if (!isPhone) {
|
||||
document.querySelectorAll('.managerSettingsButtons').forEach(btn=>{
|
||||
btn.style.backgroundColor='rgba(255, 255, 255, 1)'
|
||||
btn.style.borderRadius='5px'
|
||||
btn.style.padding='2px'
|
||||
btn.style.margin='3px'
|
||||
btn.style.cursor='pointer'
|
||||
btn.style.display='block'
|
||||
btn.addEventListener('mouseover',()=>btn.style.color='#787878')
|
||||
btn.addEventListener('mouseout',()=>btn.style.color='')
|
||||
})
|
||||
} else {
|
||||
document.querySelectorAll('.managerSettingsButtons').forEach(btn=>{
|
||||
btn.style.backgroundImage='url(../../img/pict/b_iconslyb.svg)'
|
||||
btn.style.height='42px'
|
||||
btn.style.minWidth='42px'
|
||||
btn.style.setProperty('background-size','calc(1122px* 1.5)','important')
|
||||
btn.style.display='inline-block'
|
||||
btn.style.borderRadius='5px'
|
||||
btn.style.cursor='pointer'
|
||||
btn.style.display='flex'
|
||||
btn.style.flexDirection='column'
|
||||
btn.style.alignItems='center'
|
||||
btn.style.fontSize='10px'
|
||||
btn.style.lineHeight='1'
|
||||
btn.style.justifyContent = 'flex-end'
|
||||
btn.style.top='2px'
|
||||
btn.style.position = 'relative'
|
||||
btn.style.width = 'auto'
|
||||
})
|
||||
let wrap = document.getElementById('managerSettings')
|
||||
wrap.style.display = 'flex'
|
||||
wrap.style.maxWidth = '-webkit-fill-available'
|
||||
wrap.style.overflowX = 'auto'
|
||||
wrap.style.overflowY = 'hidden'
|
||||
wrap.style.justifyContent = 'center'
|
||||
let div = document.getElementById('managerSettingsDiv')
|
||||
div.style.height = '-webkit-fill-available'
|
||||
div.style.display = 'inline-flex'
|
||||
div.style.whiteSpace = 'nowrap'
|
||||
div.style.width = 'max-content'
|
||||
div.style.gap = '7px'
|
||||
div.style.alignItems = 'center'
|
||||
}
|
||||
|
||||
window.managerSettings = managerSettings;
|
||||
window.managerFun = managerFun;
|
||||
window.saveHow = saveHow;
|
||||
window.openPageBut = openPageBut;
|
||||
window.propertiesUrlFun = propertiesUrlFun;
|
||||
window.selectImgFormButFun = selectImgFormButFun;
|
||||
window.selectImgFormToFormButFun = selectImgFormToFormButFun;
|
||||
45
main_plugin/manager/manager.php
Executable file
45
main_plugin/manager/manager.php
Executable file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* @file manager.php
|
||||
* @brief Содержит интерфейс для создания, удаления, копирования и редактирования файлов и папок
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php /** @brief Основной контейнер менеджера */ $managerDiv; ?>
|
||||
<div id="managerDiv" style="visibility: hidden; top: 20%; left: 50%; transform: translate(-50%, -20%);">
|
||||
</div>
|
||||
|
||||
<?php /** @brief Контейнер настроек менеджера */ $managerSettings; ?>
|
||||
<div id="managerSettings" style="visibility: hidden; top: 0px; left: 0px;">
|
||||
<div id="managerSettingsDiv">
|
||||
<span id="managerSettingsCopy" class="managerSettingsButtons" style="background-position: -890px -840px;">{{copy}}</span>
|
||||
<span id="managerSettingsCut" class="managerSettingsButtons" style="background-position: -1372px -419px;">{{cut}}</span>
|
||||
<span id="managerSettingsRename" class="managerSettingsButtons" style="background-position: -96px -359px;">{{rename}}</span>
|
||||
<span id="managerSettingsDelete" class="managerSettingsButtons" style="background-position: -654px -1018px;">{{delete}}</span>
|
||||
<span id="managerSettingsProperties" class="managerSettingsButtons" style="background-position: -1195px -1076px;">{{properties}}</span>
|
||||
<span id="managerSettingsLoad" class="managerSettingsButtons" style="background-position: -1059px -2px;">{{upload_file}}</span>
|
||||
<span id="managerSettingsInsert" class="managerSettingsButtons" style="background-position: -1435px -419px;">{{paste}}</span>
|
||||
<span id="managerSettingsButtonCreateFile" class="managerSettingsButtons" style="background-position: -642px -839px;">{{create_file}}</span>
|
||||
<span id="managerSettingsButtonCreateFolder" class="managerSettingsButtons" style="background-position: -1121px -840px;">{{create_folder}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Контейнер свойств выбранного элемента */ $managerProperties; ?>
|
||||
<div id="managerProperties" style="visibility: hidden; top: 20%; left: 50%; transform: translate(-50%, -20%);">
|
||||
<div id="managerPropertiesTop">
|
||||
<span id="managerPropertiesTopName" class="managerPropertiesTop"></span>
|
||||
<span id="managerPropertiesTopClose" class="editib"></span>
|
||||
</div>
|
||||
<div id="managerPropertiesMiddle">
|
||||
<div id="managerPropertiesWindow">
|
||||
<span id="managerPropertiesWindowProperties" class="managerPropertiesWindowDiv">{{properties}}</span>
|
||||
<span id="managerPropertiesWindowRights" class="managerPropertiesWindowDiv">{{rights}}</span>
|
||||
</div>
|
||||
<div id="managerPropertiesDiv">
|
||||
</div>
|
||||
<div id="managerPropertiesDivButtons">
|
||||
<div id="managerPropertiesDivButtonOk" class="managerPropertiesDivButton">{{ok}}</div>
|
||||
<div id="managerPropertiesDivButtonCancel" class="managerPropertiesDivButton">{{cancel}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
27
main_plugin/manager/plug.php
Executable file
27
main_plugin/manager/plug.php
Executable file
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* @file plug.php
|
||||
* @brief Подключает плагин manager для администраторов и выводит HTML с языковыми строками
|
||||
*/
|
||||
|
||||
global $path, $_SESSION, $configAdmins;
|
||||
|
||||
/** @brief Языковой массив для плагина manager */
|
||||
$lang = include $path . 'main_plugin/manager/lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_SESSION['lng'] ?? 'en';
|
||||
|
||||
if (in_array($_SESSION['username'], $configAdmins, true)) {
|
||||
include_once $path . 'main_plugin/manager/func.manager.php';
|
||||
|
||||
$Html = file_get_contents($path . 'main_plugin/manager/manager.php');
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$Html = str_replace('{{' . $key . '}}', $value, $Html);
|
||||
}
|
||||
echo $Html;
|
||||
|
||||
echo '<link rel="stylesheet" type="text/css" href="/main_plugin/manager/manager.css">';
|
||||
echo '<script type="text/javascript" src="/main_plugin/manager/lang.js.php?lng=' . $lng . '"></script>';
|
||||
}
|
||||
?>
|
||||
21
main_plugin/pickr/lang.js.php
Executable file
21
main_plugin/pickr/lang.js.php
Executable file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* @file lang.js.php
|
||||
* @brief Подготавливает языковые строки и подставляет их в JS-файл плагина pickr
|
||||
*/
|
||||
|
||||
/** @brief Языковой массив для pickr */
|
||||
$lang = include $path . 'lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_GET['lng'] ?? ($_SESSION['lng'] ?? 'en');
|
||||
|
||||
/** @brief Массив подстановок для шаблона JS */
|
||||
$placeholders = [];
|
||||
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$placeholders['{{' . $key . '}}'] = $value;
|
||||
}
|
||||
|
||||
$js = strtr(file_get_contents($path . 'pickr.js'), $placeholders);
|
||||
echo "window.pickrJs = (function() {\n" . $js . "\n return this;})();";
|
||||
511
main_plugin/pickr/pickr.css
Executable file
511
main_plugin/pickr/pickr.css
Executable file
@@ -0,0 +1,511 @@
|
||||
.pcr-app {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
.pcr-app.hide {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.theme-container button {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
font-weight: 500;
|
||||
font-size: 0.95em;
|
||||
color: #36425b;
|
||||
outline: none;
|
||||
background: #e4f1ff;
|
||||
border: none;
|
||||
border-bottom: 2px solid rgba(80, 139, 234, 0.67);
|
||||
padding: 0.6em 0.8em 0.5em;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
margin: 0 0.5em;
|
||||
opacity: 0.45;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.theme-container button.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.theme-container h3 {
|
||||
font-weight: 500;
|
||||
color: #36425b;
|
||||
}
|
||||
|
||||
.pickr-container {
|
||||
margin-top: 2em;
|
||||
display: none;
|
||||
}
|
||||
|
||||
main > p {
|
||||
margin-top: 0.35em;
|
||||
font-size: 0.75em;
|
||||
font-weight: 500;
|
||||
color: #42445a;
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 1000px) {
|
||||
body header {
|
||||
font-size: 0.6em;
|
||||
padding: 7vh 0;
|
||||
}
|
||||
|
||||
body header a {
|
||||
padding: 1em 2em;
|
||||
font-weight: 600;
|
||||
font-size: 1.05em;
|
||||
}
|
||||
|
||||
main > section {
|
||||
min-width: 90%;
|
||||
}
|
||||
|
||||
main > section h2 {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
main > section pre {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
main section.demo .hint svg {
|
||||
height: 1.2em;
|
||||
}
|
||||
|
||||
main section.demo .hint span {
|
||||
transform: translate3d(-3em, -1.4em, 0);
|
||||
font-size: 0.6em;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/* --------------------- */
|
||||
|
||||
|
||||
/*! Pickr 1.9.1 MIT | https://github.com/Simonwep/pickr */
|
||||
.pickr {
|
||||
position: relative;
|
||||
overflow: visible;
|
||||
transform: translateY(0)
|
||||
}
|
||||
|
||||
.pickr * {
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
border: none;
|
||||
-webkit-appearance: none
|
||||
}
|
||||
|
||||
.pickr .pcr-button {
|
||||
position: relative;
|
||||
height: 2em;
|
||||
width: 2em;
|
||||
padding: .5em;
|
||||
cursor: pointer;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;
|
||||
border-radius: .15em;
|
||||
background: url("data:image/svg+xml;utf8, <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 50 50\" stroke=\"%2342445A\" stroke-width=\"5px\" stroke-linecap=\"round\"><path d=\"M45,45L5,5\"></path><path d=\"M45,5L5,45\"></path></svg>") no-repeat center;
|
||||
background-size: 0;
|
||||
transition: all .3s
|
||||
}
|
||||
|
||||
.pickr .pcr-button::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("data:image/svg+xml;utf8, <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2 2\"><path fill=\"white\" d=\"M1,0H2V1H1V0ZM0,1H1V2H0V1Z\"/><path fill=\"gray\" d=\"M0,0H1V1H0V0ZM1,1H2V2H1V1Z\"/></svg>");
|
||||
background-size: .5em;
|
||||
border-radius: .15em;
|
||||
z-index: -1
|
||||
}
|
||||
|
||||
.pickr .pcr-button::before {
|
||||
z-index: initial
|
||||
}
|
||||
|
||||
.pickr .pcr-button::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
transition: background .3s;
|
||||
background: var(--pcr-color);
|
||||
border-radius: .15em
|
||||
}
|
||||
|
||||
.pickr .pcr-button.clear {
|
||||
background-size: 70%
|
||||
}
|
||||
|
||||
.pickr .pcr-button.clear::before {
|
||||
opacity: 0
|
||||
}
|
||||
|
||||
.pickr .pcr-button.clear:focus {
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)
|
||||
}
|
||||
|
||||
.pickr .pcr-button.disabled {
|
||||
cursor: not-allowed
|
||||
}
|
||||
|
||||
.pickr *,.pcr-app * {
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
border: none;
|
||||
-webkit-appearance: none
|
||||
}
|
||||
|
||||
.pickr input:focus,.pickr input.pcr-active,.pickr button:focus,.pickr button.pcr-active,.pcr-app input:focus,.pcr-app input.pcr-active,.pcr-app button:focus,.pcr-app button.pcr-active {
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,.85),0 0 0 3px var(--pcr-color)
|
||||
}
|
||||
|
||||
.pickr .pcr-palette,.pickr .pcr-slider,.pcr-app .pcr-palette,.pcr-app .pcr-slider {
|
||||
transition: box-shadow .3s
|
||||
}
|
||||
|
||||
.pickr .pcr-palette:focus,.pickr .pcr-slider:focus,.pcr-app .pcr-palette:focus,.pcr-app .pcr-slider:focus {
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(0,0,0,.25)
|
||||
}
|
||||
|
||||
.pcr-app {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
z-index: 10000;
|
||||
border-radius: .1em;
|
||||
background: #fff;
|
||||
transition: opacity .3s,visibility 0s .3s;
|
||||
font-family: -apple-system,BlinkMacSystemFont,"Segoe UI","Roboto","Helvetica Neue",Arial,sans-serif;
|
||||
box-shadow: 0 .15em 1.5em 0 rgba(0,0,0,.1),0 0 1em 0 rgba(0,0,0,.03);
|
||||
}
|
||||
|
||||
.pcr-app.visible {
|
||||
transition: opacity .3s;
|
||||
visibility: visible;
|
||||
opacity: 1
|
||||
}
|
||||
|
||||
.pcr-app .pcr-swatches {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
margin-top: .75em
|
||||
}
|
||||
|
||||
.pcr-app .pcr-swatches.pcr-last {
|
||||
margin: 0
|
||||
}
|
||||
|
||||
@supports(display: grid) {
|
||||
.pcr-app .pcr-swatches {
|
||||
display:grid;
|
||||
align-items: center;
|
||||
grid-template-columns: repeat(auto-fit, 1.75em)
|
||||
}
|
||||
}
|
||||
|
||||
.pcr-app .pcr-swatches>button {
|
||||
font-size: 1em;
|
||||
position: relative;
|
||||
width: calc(1.75em - 5px);
|
||||
height: calc(1.75em - 5px);
|
||||
border-radius: .15em;
|
||||
cursor: pointer;
|
||||
margin: 2.5px;
|
||||
flex-shrink: 0;
|
||||
justify-self: center;
|
||||
transition: all .15s;
|
||||
overflow: hidden;
|
||||
background: rgba(0,0,0,0);
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.pcr-app .pcr-swatches>button::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("data:image/svg+xml;utf8, <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2 2\"><path fill=\"white\" d=\"M1,0H2V1H1V0ZM0,1H1V2H0V1Z\"/><path fill=\"gray\" d=\"M0,0H1V1H0V0ZM1,1H2V2H1V1Z\"/></svg>");
|
||||
background-size: 6px;
|
||||
border-radius: .15em;
|
||||
z-index: -1
|
||||
}
|
||||
|
||||
.pcr-app .pcr-swatches>button::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--pcr-color);
|
||||
border: 1px solid rgba(0,0,0,.05);
|
||||
border-radius: .15em;
|
||||
box-sizing: border-box
|
||||
}
|
||||
|
||||
.pcr-app .pcr-swatches>button:hover {
|
||||
filter: brightness(1.05)
|
||||
}
|
||||
|
||||
.pcr-app .pcr-swatches>button:not(.pcr-active) {
|
||||
box-shadow: none
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
margin: 0 -0.2em 0 -0.2em
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction>* {
|
||||
/* margin: 0 .2em */
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction input {
|
||||
letter-spacing: .07em;
|
||||
font-size: .75em;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: #75797e;
|
||||
background: #f1f3f4;
|
||||
border-radius: .15em;
|
||||
transition: all .15s;
|
||||
padding: .45em .5em;
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction input:hover {
|
||||
filter: brightness(0.975)
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction input:focus {
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(66,133,244,.75)
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-result {
|
||||
color: #75797e;
|
||||
text-align: left;
|
||||
/* flex: 1 1 8em; */
|
||||
width: 6em;
|
||||
transition: all .2s;
|
||||
border-radius: .15em;
|
||||
background: #f1f3f4;
|
||||
cursor: text
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-result::-moz-selection {
|
||||
background: #4285f4;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-result::selection {
|
||||
background: #4285f4;
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-type.active {
|
||||
color: #fff;
|
||||
background: #4285f4
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear {
|
||||
color: #fff;
|
||||
width: auto
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-save,.pcr-app .pcr-interaction .pcr-cancel,.pcr-app .pcr-interaction .pcr-clear {
|
||||
color: #fff
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-save:hover,.pcr-app .pcr-interaction .pcr-cancel:hover,.pcr-app .pcr-interaction .pcr-clear:hover {
|
||||
filter: brightness(0.925)
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-save {
|
||||
background: #4285f4
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-clear,.pcr-app .pcr-interaction .pcr-cancel {
|
||||
background: #f44250
|
||||
}
|
||||
|
||||
.pcr-app .pcr-interaction .pcr-clear:focus,.pcr-app .pcr-interaction .pcr-cancel:focus {
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,.85),0 0 0 3px rgba(244,66,80,.75)
|
||||
}
|
||||
|
||||
.pcr-app .pcr-selection .pcr-picker {
|
||||
position: absolute;
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border: 2px solid #fff;
|
||||
border-radius: 100%;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none
|
||||
}
|
||||
|
||||
.pcr-app .pcr-selection .pcr-color-palette,.pcr-app .pcr-selection .pcr-color-chooser,.pcr-app .pcr-selection .pcr-color-opacity {
|
||||
position: relative;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
cursor: grab;
|
||||
cursor: -webkit-grab
|
||||
}
|
||||
|
||||
.pcr-app .pcr-selection .pcr-color-palette:active,.pcr-app .pcr-selection .pcr-color-chooser:active,.pcr-app .pcr-selection .pcr-color-opacity:active {
|
||||
cursor: grabbing;
|
||||
cursor: -webkit-grabbing
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] {
|
||||
width: 150px;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-swatches {
|
||||
margin-top: .6em;
|
||||
padding: 0 .6em
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-interaction {
|
||||
padding: 0 .6em .6em .6em
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection {
|
||||
display: grid;
|
||||
/* grid-gap: .6em;
|
||||
grid-template-columns: 1fr 4fr; */
|
||||
grid-template-rows: 5fr auto auto;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
align-self: flex-start
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-preview {
|
||||
grid-area: 2/1/4/1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: center;
|
||||
margin-left: .6em
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-preview .pcr-last-color {
|
||||
display: none
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-preview .pcr-current-color {
|
||||
position: relative;
|
||||
background: var(--pcr-color);
|
||||
width: 2em;
|
||||
height: 2em;
|
||||
border-radius: 50em;
|
||||
overflow: hidden
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-preview .pcr-current-color::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("data:image/svg+xml;utf8, <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2 2\"><path fill=\"white\" d=\"M1,0H2V1H1V0ZM0,1H1V2H0V1Z\"/><path fill=\"gray\" d=\"M0,0H1V1H0V0ZM1,1H2V2H1V1Z\"/></svg>");
|
||||
background-size: .5em;
|
||||
border-radius: .15em;
|
||||
z-index: -1
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-palette {
|
||||
grid-area: 1/1/2/3;
|
||||
width: 150px;
|
||||
height: 100px;
|
||||
z-index: 1
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-palette .pcr-palette {
|
||||
border-radius: .15em;
|
||||
width: 100%;
|
||||
height: 100%
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-palette .pcr-palette::before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: url("data:image/svg+xml;utf8, <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2 2\"><path fill=\"white\" d=\"M1,0H2V1H1V0ZM0,1H1V2H0V1Z\"/><path fill=\"gray\" d=\"M0,0H1V1H0V0ZM1,1H2V2H1V1Z\"/></svg>");
|
||||
background-size: .5em;
|
||||
border-radius: .15em;
|
||||
z-index: -1
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-chooser {
|
||||
grid-area: 2/2/2/2
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-opacity {
|
||||
grid-area: 3/2/3/2
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-selection .pcr-color-opacity {
|
||||
margin: 0 .6em
|
||||
}
|
||||
|
||||
.pcr-color-chooser {
|
||||
height: .5em;
|
||||
width: 140px;
|
||||
margin: 10px 5px 0px 5px;
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-chooser .pcr-picker,.pcr-app[data-theme=nano] .pcr-selection .pcr-color-opacity .pcr-picker {
|
||||
top: 50%;
|
||||
transform: translateY(-50%)
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-chooser .pcr-slider,.pcr-app[data-theme=nano] .pcr-selection .pcr-color-opacity .pcr-slider {
|
||||
flex-grow: 1;
|
||||
border-radius: 50em
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-chooser .pcr-slider {
|
||||
background: linear-gradient(to right, hsl(0, 100%, 50%), hsl(60, 100%, 50%), hsl(120, 100%, 50%), hsl(180, 100%, 50%), hsl(240, 100%, 50%), hsl(300, 100%, 50%), hsl(0, 100%, 50%))
|
||||
}
|
||||
|
||||
.pcr-app[data-theme=nano] .pcr-selection .pcr-color-opacity .pcr-slider {
|
||||
background: linear-gradient(to right, transparent, black),url("data:image/svg+xml;utf8, <svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 2 2\"><path fill=\"white\" d=\"M1,0H2V1H1V0ZM0,1H1V2H0V1Z\"/><path fill=\"gray\" d=\"M0,0H1V1H0V0ZM1,1H2V2H1V1Z\"/></svg>");
|
||||
background-size: 100%,.25em
|
||||
}
|
||||
4998
main_plugin/pickr/pickr.js
Executable file
4998
main_plugin/pickr/pickr.js
Executable file
File diff suppressed because it is too large
Load Diff
13
main_plugin/pickr/plug.php
Executable file
13
main_plugin/pickr/plug.php
Executable file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* @file plug.php
|
||||
* @brief Подключает плагин pickr для администраторов и подключает JS с языковыми строками
|
||||
*/
|
||||
|
||||
global $path, $_SESSION, $configAdmins;
|
||||
|
||||
if (in_array($_SESSION['username'], $configAdmins, true)) {
|
||||
echo '<script type="text/javascript" src="/main_plugin/pickr/lang.js.php?lng=' . $lng . '"></script>';
|
||||
echo '<link rel="stylesheet" type="text/css" href="/main_plugin/pickr/pickr.css">';
|
||||
}
|
||||
?>
|
||||
22
main_plugin/siteSettings/lang.js.php
Executable file
22
main_plugin/siteSettings/lang.js.php
Executable file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
/**
|
||||
* @file lang.js.php
|
||||
* @brief Подготавливает языковые строки и подставляет их в JS-файл плагина siteSettings
|
||||
*/
|
||||
|
||||
global $path, $_SESSION;
|
||||
|
||||
/** @brief Языковой массив для siteSettings */
|
||||
$lang = include $path . 'lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_GET['lng'] ?? ($_SESSION['lng'] ?? 'en');
|
||||
|
||||
/** @brief Массив подстановок для шаблона JS */
|
||||
$placeholders = [];
|
||||
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$placeholders['{{' . $key . '}}'] = $value;
|
||||
}
|
||||
|
||||
echo strtr(file_get_contents($path . 'siteSettings.js'), $placeholders);
|
||||
77
main_plugin/siteSettings/lang.php
Executable file
77
main_plugin/siteSettings/lang.php
Executable file
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
$lang = [
|
||||
'ru' => [
|
||||
'authorization' => 'Авторизация',
|
||||
'login_label' => 'Логин',
|
||||
'password_label' => 'Пароль',
|
||||
'incorrect_login_password' => 'Неверный логин или пароль!',
|
||||
'repeat_password_label' => 'Повторите пароль',
|
||||
'email_label' => 'Почта',
|
||||
'fill_all_fields' => 'Вы должны заполнить все поля!',
|
||||
'passwords_do_not_match' => 'Пароли не совпадают!',
|
||||
'account_creation_request_sent' => 'Запрос на создание аккаунта отправлен!',
|
||||
'user_exists' => 'Пользователь с таким логином уже существует!',
|
||||
'account_creation_error' => 'Ошибка при создании аккаунта!',
|
||||
'incorrect_email' => 'Вы ввели неправильную почту!',
|
||||
'account_creation_request_error' => 'Ошибка при отправке запроса на создание аккаунта!',
|
||||
'account_authorization' => 'Авторизация аккаунта',
|
||||
'login' => 'Войти',
|
||||
'register' => 'Зарегистрироваться',
|
||||
'logoff' => 'Выйти',
|
||||
'editor_page' => 'Редактор страницы',
|
||||
'file_manager' => 'Файловый менеджер',
|
||||
'site_tree' => 'Дерево сайта',
|
||||
'save_data_question' => 'Сохранить данные?',
|
||||
'save_new_page' => 'Сохраните новую страницу!',
|
||||
],
|
||||
'en' => [
|
||||
'authorization' => 'Authorization',
|
||||
'login_label' => 'Login',
|
||||
'password_label' => 'Password',
|
||||
'incorrect_login_password' => 'Incorrect login or password!',
|
||||
'repeat_password_label' => 'Repeat password',
|
||||
'email_label' => 'Email',
|
||||
'fill_all_fields' => 'You must fill out all fields!',
|
||||
'passwords_do_not_match' => 'Passwords do not match!',
|
||||
'account_creation_request_sent' => 'Account creation request sent!',
|
||||
'user_exists' => 'User with this login already exists!',
|
||||
'account_creation_error' => 'Error while creating the account!',
|
||||
'incorrect_email' => 'Incorrect email!',
|
||||
'account_creation_request_error' => 'Error sending account creation request!',
|
||||
'account_authorization' => 'Account authorization',
|
||||
'login' => 'Login',
|
||||
'register' => 'Register',
|
||||
'logoff' => 'Log out',
|
||||
'editor_page' => 'Page editor',
|
||||
'file_manager' => 'File manager',
|
||||
'site_tree' => 'Site tree',
|
||||
'save_data_question' => 'Save changes?',
|
||||
'save_new_page' => 'Save the new page!',
|
||||
],
|
||||
'lv' => [
|
||||
'authorization' => 'Autentifikācija',
|
||||
'login_label' => 'Lietotājvārds',
|
||||
'password_label' => 'Parole',
|
||||
'incorrect_login_password' => 'Nepareizs lietotājvārds vai parole!',
|
||||
'repeat_password_label' => 'Atkārtot paroli',
|
||||
'email_label' => 'E-pasts',
|
||||
'fill_all_fields' => 'Jums jāaizpilda visi lauki!',
|
||||
'passwords_do_not_match' => 'Paroles nesakrīt!',
|
||||
'account_creation_request_sent' => 'Pieprasījums par konta izveidi nosūtīts!',
|
||||
'user_exists' => 'Lietotājs ar šo lietotājvārdu jau pastāv!',
|
||||
'account_creation_error' => 'Kļūda, izveidojot kontu!',
|
||||
'incorrect_email' => 'Nepareizs e-pasts!',
|
||||
'account_creation_request_error' => 'Kļūda, nosūtot pieprasījumu par konta izveidi!',
|
||||
'account_authorization' => 'Konta autentifikācija',
|
||||
'login' => 'Ieiet',
|
||||
'register' => 'Reģistrēties',
|
||||
'logoff' => 'Iziet',
|
||||
'editor_page' => 'Lapas redaktors',
|
||||
'file_manager' => 'Failu pārvaldnieks',
|
||||
'site_tree' => 'Saites koks',
|
||||
'save_data_question' => 'Saglabāt datus?',
|
||||
'save_new_page' => 'Saglabājiet jauno lapu!',
|
||||
],
|
||||
];
|
||||
|
||||
return $lang;
|
||||
28
main_plugin/siteSettings/plug.php
Executable file
28
main_plugin/siteSettings/plug.php
Executable file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* @file plug.php
|
||||
* @brief Подключает плагин siteSettings для администраторов, подставляет языковые строки и выводит HTML-код страницы настроек сайта
|
||||
*/
|
||||
|
||||
global $path, $_SESSION, $configAdmins;
|
||||
|
||||
/** @brief Языковой массив для плагина siteSettings */
|
||||
$lang = include $path . 'main_plugin/siteSettings/lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_SESSION['lng'] ?? 'en';
|
||||
|
||||
if (in_array($_SESSION['username'], $configAdmins, true)) {
|
||||
|
||||
$Html = file_get_contents($path . 'main_plugin/siteSettings/siteSettings.php');
|
||||
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$Html = str_replace('{{' . $key . '}}', $value, $Html);
|
||||
}
|
||||
|
||||
echo $Html;
|
||||
|
||||
echo '<link rel="stylesheet" type="text/css" href="/main_plugin/siteSettings/siteSettings.css">';
|
||||
echo '<script type="text/javascript" src="/main_plugin/siteSettings/lang.js.php?lng=' . $lng . '"></script>';
|
||||
}
|
||||
?>
|
||||
33
main_plugin/siteSettings/siteSettings.css
Executable file
33
main_plugin/siteSettings/siteSettings.css
Executable file
@@ -0,0 +1,33 @@
|
||||
#siteSettingsButton {
|
||||
background: url(../../img/pict/mc_iconslyb.svg) -1840px 1664px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
float: right;
|
||||
margin: 0px 5px;
|
||||
}
|
||||
#siteSettingsButton:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
}
|
||||
#siteSettings {
|
||||
margin: -2px -63px 0px 0px;
|
||||
position: absolute;
|
||||
top: 30px;
|
||||
right: 80px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
||||
z-index: 1001;
|
||||
font-family: Arial, sans-serif;
|
||||
padding: 2px;
|
||||
}
|
||||
.siteSettingsOption {
|
||||
padding: 2px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
.siteSettingsOption:hover {
|
||||
color: rgb(153, 153, 153);
|
||||
text-shadow: -1px -1px #666, 1px 1px #FFF;
|
||||
}
|
||||
88
main_plugin/siteSettings/siteSettings.js
Executable file
88
main_plugin/siteSettings/siteSettings.js
Executable file
@@ -0,0 +1,88 @@
|
||||
/**
|
||||
* @file siteSettings.js
|
||||
* @brief Основной файл siteSettings, открытие плагинов
|
||||
*/
|
||||
|
||||
addEventListener("load", function() {
|
||||
|
||||
let menu = document.getElementById('siteSettings');
|
||||
let button = document.getElementById('siteSettingsButton');
|
||||
|
||||
menu.onclick = function() {
|
||||
menu.style.display = 'none';
|
||||
};
|
||||
|
||||
function hbodyHref() {
|
||||
let hbody = document.getElementById('hbody');
|
||||
let links = hbody.querySelectorAll('a[href]');
|
||||
links.forEach(link => {
|
||||
let originalHref = link.getAttribute('href');
|
||||
link.removeAttribute('href');
|
||||
link.onclick = function(event) {
|
||||
event.preventDefault();
|
||||
hbodyHrefFunction(originalHref);
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function hbodyHrefFunction(href) {
|
||||
if (document.getElementById('basis3')?.style.visibility === "visible" && window.contentIsEdit == true) {
|
||||
messageQueue.push("{{save_data_question}}");
|
||||
try {
|
||||
const userConfirmed = await messageCreateQuestion();
|
||||
if (userConfirmed) {
|
||||
if (window.newPageFunValue == "newPage") {
|
||||
document.getElementById("saveHow").click();
|
||||
messageFunction("{{save_new_page}}");
|
||||
return;
|
||||
} else {
|
||||
await saveChanges();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log("Ошибка: ", error);
|
||||
}
|
||||
}
|
||||
window.location.href = href;
|
||||
}
|
||||
|
||||
hbodyHref();
|
||||
|
||||
});
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @brief Подключает или отключает плагин по ID
|
||||
* @param id ID элемента DOM
|
||||
* @param plugin Название плагина
|
||||
* @param fnName Имя функции инициализации
|
||||
*/
|
||||
async function togglePlugin(id, plugin, fnName) {
|
||||
const exists = !!document.getElementById(id);
|
||||
if (exists) {
|
||||
if (plugin === 'editor') await removePluginDom('pickr');
|
||||
if (typeof window[fnName] === 'function') window[fnName]();
|
||||
await removePluginDom(plugin);
|
||||
} else {
|
||||
await includePlugin(plugin);
|
||||
await new Promise(res => {
|
||||
const i = setInterval(() => {
|
||||
if (typeof window[fnName] === 'function') {
|
||||
clearInterval(i);
|
||||
window[fnName]();
|
||||
res();
|
||||
}
|
||||
});
|
||||
});
|
||||
if (plugin === 'editor') await includePlugin('pickr');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/** @brief Переключает редактор */
|
||||
function toggleEditor() { return togglePlugin("basis3", "editor", "basisVis"); }
|
||||
/** @brief Переключает дерево сайта */
|
||||
function toggleSiteTree() { return togglePlugin("treeDiv", "site_tree", "basisVisSiteTree"); }
|
||||
/** @brief Переключает менеджер */
|
||||
function toggleManager() { return togglePlugin("managerDiv", "manager", "basisVisManager"); }
|
||||
35
main_plugin/siteSettings/siteSettings.php
Executable file
35
main_plugin/siteSettings/siteSettings.php
Executable file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
/**
|
||||
* @file siteSettings.php
|
||||
* @brief Контейнер настроек сайта: кнопка открытия, переключение между редактором страниц, деревом сайта и файловым менеджером
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php /** @brief Кнопка открытия панели настроек сайта */ $siteSettingsButton; ?>
|
||||
<span id="siteSettingsButton" onclick="toggleMenu()"></span>
|
||||
|
||||
<?php /** @brief Панель настроек сайта с опциями */ $siteSettings; ?>
|
||||
<div id="siteSettings" class="borderStyle" style="display: none;">
|
||||
<div id="editor" class="siteSettingsOption" onclick="toggleEditor()">{{editor_page}}</div>
|
||||
<div id="siteTree" class="siteSettingsOption" onclick="toggleSiteTree()">{{site_tree}}</div>
|
||||
<div id="manager" class="siteSettingsOption" onclick="toggleManager()">{{file_manager}}</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.addEventListener("load", function() {
|
||||
try {
|
||||
var siteSettingsButton = document.getElementById("siteSettingsButton").outerHTML;
|
||||
var siteSettings = document.getElementById("siteSettings").outerHTML;
|
||||
|
||||
document.getElementById("siteSettingsButton").remove();
|
||||
document.getElementById("siteSettings").remove();
|
||||
|
||||
var container = document.getElementById("hbody");
|
||||
if (!container) throw new Error("#hbody не найден для siteSettings");
|
||||
container.insertAdjacentHTML("beforeend", siteSettingsButton);
|
||||
container.insertAdjacentHTML("beforeend", siteSettings);
|
||||
} catch (e) {
|
||||
console.error("Ошибка в блоке siteSettings:", e);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
203
main_plugin/site_tree/func.site_tree.php
Executable file
203
main_plugin/site_tree/func.site_tree.php
Executable file
@@ -0,0 +1,203 @@
|
||||
<?php
|
||||
/**
|
||||
* @file func.site_tree.php
|
||||
* @brief Функции для работы с деревом сайта: получение, сохранение и обработка структуры, а также получение списка папок
|
||||
*/
|
||||
|
||||
/**
|
||||
* @brief Получает структуру дерева сайта из XML-файла для текущего языка
|
||||
* @param array $params Не используется, но требуется для совместимости
|
||||
* @return array Массив с элементами дерева сайта, включая дочерние элементы и атрибуты
|
||||
* @throws Exception Если не удалось прочитать или разобрать XML-файл, или структура не сгенерирована
|
||||
*/
|
||||
function getSiteTree($params) {
|
||||
global $path, $_SESSION;
|
||||
|
||||
$file = $path . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'filepath.' . $_SESSION['lng'] . '.php';
|
||||
$content = file_get_contents($file);
|
||||
$xml = simplexml_load_string($content);
|
||||
if (!$xml) {
|
||||
throw new Exception("Problem with file", -32004);
|
||||
}
|
||||
|
||||
function generateArray($xml, $urlParts = []) {
|
||||
$result = [];
|
||||
$anyOpen = false;
|
||||
|
||||
foreach ($xml->children() as $child) {
|
||||
$item = [
|
||||
'name' => (string) $child->attributes()->name,
|
||||
'tag' => $child->getName(),
|
||||
'children' => generateArray($child, $urlParts),
|
||||
'isOpen' => false,
|
||||
|
||||
'url' => (string) $child->attributes()->url,
|
||||
'title' => (string) $child->attributes()->title,
|
||||
'template' => (string) $child->attributes()->template,
|
||||
'PageMenu' => (string) $child->attributes()->PageMenu,
|
||||
'users' => (string) $child->attributes()->users,
|
||||
'group' => (string) $child->attributes()->group,
|
||||
'news' => (string) $child->attributes()->news,
|
||||
'plugins' => (string) $child->attributes()->plugins,
|
||||
|
||||
'content' => (string)$child
|
||||
];
|
||||
|
||||
if (!empty($urlParts) && $urlParts[0] == $item['tag']) {
|
||||
array_shift($urlParts);
|
||||
if (empty($urlParts)) {
|
||||
if ($item['tag'] !== 'index') {
|
||||
$item['isOpen'] = true;
|
||||
$anyOpen = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($item['children'] as $childItem) {
|
||||
if (!empty($childItem['isOpen'])) {
|
||||
$item['isOpen'] = true;
|
||||
$anyOpen = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = $item;
|
||||
}
|
||||
|
||||
if (!$anyOpen) {
|
||||
foreach ($result as &$item) {
|
||||
if ($item['tag'] === 'index') {
|
||||
$item['isOpen'] = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
foreach ($result as &$item) {
|
||||
if ($item['tag'] === 'index') {
|
||||
$item['isOpen'] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
$url = $_SERVER['REQUEST_URI'];
|
||||
$urlParts = array_filter(explode('/', trim($url, '/')));
|
||||
$urlParts = array_map(function($part) {
|
||||
return pathinfo($part, PATHINFO_FILENAME);
|
||||
}, $urlParts);
|
||||
|
||||
$result = generateArray($xml, $urlParts);
|
||||
if (!is_array($result)) {
|
||||
throw new Exception("Failed to generate structure", -32003);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Сохраняет структуру дерева сайта в XML-файл для текущего языка
|
||||
* @param array $params Массив с ключом 'data', содержащий структуру сайта
|
||||
* @return string "true" при успешном сохранении
|
||||
* @throws Exception Если не удалось сохранить файл или файл пустой
|
||||
*/
|
||||
function saveSiteTree($params) {
|
||||
global $path, $_SESSION;
|
||||
|
||||
$file = $path . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'filepath.' . $_SESSION['lng'] . '.php';
|
||||
|
||||
$treeData = isset($params['data']) ? $params['data'] : [];
|
||||
$siteName = isset($treeData['sitename']) ? htmlspecialchars($treeData['sitename'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : '';
|
||||
$slogan = isset($treeData['slogan']) ? htmlspecialchars($treeData['slogan'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : '';
|
||||
|
||||
$xmlContent = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n".
|
||||
"<site>\n".
|
||||
" <sitename>$siteName</sitename>\n".
|
||||
" <slogan>$slogan</slogan>\n".
|
||||
buildSiteTreeXml($treeData['children']).
|
||||
"</site>\n";
|
||||
|
||||
$ok = file_put_contents($file, $xmlContent);
|
||||
|
||||
if ($ok === false || !is_file($file) || !filesize($file)) {
|
||||
throw new Exception("Failed to save file", -32004);
|
||||
}
|
||||
|
||||
return 'true';
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Рекурсивно строит XML для структуры сайта
|
||||
* @param array $data Массив с элементами дерева сайта
|
||||
* @param int $level Уровень вложенности для отступов
|
||||
* @return string Строка XML, представляющая элементы дерева
|
||||
*/
|
||||
function buildSiteTreeXml($data, $level = 1) {
|
||||
$xml = "";
|
||||
foreach ($data as $item) {
|
||||
if (!isset($item['name']) || empty($item['name'])) continue;
|
||||
|
||||
$tag = htmlspecialchars(trim(explode(' ', $item['name'])[0]), ENT_XML1 | ENT_QUOTES, 'UTF-8');
|
||||
$nameAttr = htmlspecialchars(trim($tag), ENT_XML1 | ENT_QUOTES, 'UTF-8');
|
||||
|
||||
$attributes = [
|
||||
'url' => isset($item['url']) ? htmlspecialchars($item['url'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : '',
|
||||
'title' => isset($item['title']) ? htmlspecialchars($item['title'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : '',
|
||||
'name' => $nameAttr,
|
||||
'template' => isset($item['template']) ? htmlspecialchars($item['template'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : '',
|
||||
'PageMenu' => isset($item['PageMenu']) ? htmlspecialchars($item['PageMenu'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : '',
|
||||
'users' => isset($item['users']) ? htmlspecialchars($item['users'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : '',
|
||||
'group' => isset($item['group']) ? htmlspecialchars($item['group'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : '',
|
||||
'news' => isset($item['news']) ? htmlspecialchars($item['news'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : '',
|
||||
'plugins' => isset($item['plugins']) ? htmlspecialchars($item['plugins'], ENT_XML1 | ENT_QUOTES, 'UTF-8') : ''
|
||||
];
|
||||
|
||||
$attrString = "";
|
||||
foreach ($attributes as $key => $value) {
|
||||
$attrString .= " $key='$value'";
|
||||
}
|
||||
|
||||
$xml .= str_repeat(" ", $level) . "<$tag$attrString>\n";
|
||||
if (!empty($item['children'])) {
|
||||
$xml .= buildSiteTreeXml($item['children'], $level + 1);
|
||||
}
|
||||
$xml .= str_repeat(" ", $level) . "</$tag>\n";
|
||||
}
|
||||
return $xml;
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Получает список папок внутри указанной директории
|
||||
* @param array $params Массив с ключом 'folder', указывающим путь к директории
|
||||
* @return array Массив с именами папок
|
||||
* @throws Exception Если директория недействительна или невозможно прочитать содержимое
|
||||
*/
|
||||
function getFolders($params) {
|
||||
global $path;
|
||||
$folder = $path . DIRECTORY_SEPARATOR . $params['folder'];
|
||||
$folders = [];
|
||||
$ok = true;
|
||||
|
||||
if (!is_dir($folder)) {
|
||||
$ok = false;
|
||||
} else {
|
||||
$files = scandir($folder);
|
||||
if ($files === false) {
|
||||
$ok = false;
|
||||
} else {
|
||||
foreach ($files as $file) {
|
||||
if ($file != '.' && $file != '..' && is_dir($folder . DIRECTORY_SEPARATOR . $file)) {
|
||||
$folders[] = $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$ok) {
|
||||
throw new Exception("Failed to read folder list", -32004);
|
||||
}
|
||||
return $folders;
|
||||
}
|
||||
|
||||
?>
|
||||
21
main_plugin/site_tree/lang.js.php
Executable file
21
main_plugin/site_tree/lang.js.php
Executable file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
/**
|
||||
* @file lang.js.php
|
||||
* @brief Подготавливает языковые строки и подставляет их в JS-файл плагина site_tree
|
||||
*/
|
||||
|
||||
/** @brief Языковой массив для site_tree */
|
||||
$lang = include $path . 'lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_GET['lng'] ?? ($_SESSION['lng'] ?? 'en');
|
||||
|
||||
/** @brief Массив подстановок для шаблона JS */
|
||||
$placeholders = [];
|
||||
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$placeholders['{{' . $key . '}}'] = $value;
|
||||
}
|
||||
|
||||
$js = strtr(file_get_contents($path . 'site_tree.js'), $placeholders);
|
||||
echo "window.site_treeJs = (function() {\n" . $js . "\n})();";
|
||||
83
main_plugin/site_tree/lang.php
Executable file
83
main_plugin/site_tree/lang.php
Executable file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
$lang = [
|
||||
'ru' => [
|
||||
'tree_site_title' => 'Дерево сайта',
|
||||
'save' => 'Сохранить',
|
||||
'add' => 'Добавить',
|
||||
'paste' => 'Вставить',
|
||||
'copy' => 'Копировать',
|
||||
'rename' => 'Переименовать',
|
||||
'properties' => 'Свойства',
|
||||
'delete' => 'Удалить',
|
||||
'choose' => 'Выбрать',
|
||||
'enter_new_name' => 'Введите новое имя',
|
||||
'name_only_english_letters' => 'Имя должно содержать только английские буквы без пробелов и символов.',
|
||||
'no_item_selected' => 'Нет выбранного элемента.',
|
||||
'delete_all_subpages' => 'Удалить все подстраницы страницы',
|
||||
'save_new_page' => 'Сохраните новую страницу!',
|
||||
'enter_tag_for_new_page' => 'Введите тег для новой страницы:',
|
||||
'tag_only_english_letters' => 'Тег должен содержать только английские буквы без пробелов и символов.',
|
||||
'tag_already_exists' => 'Элемент с таким тегом уже существует.',
|
||||
'select_page_for_link' => 'Выбор страницы для ссылки',
|
||||
'rights' => 'Права',
|
||||
'ok' => 'ОК',
|
||||
'cancel' => 'Отмена',
|
||||
'select' => 'Выбрать',
|
||||
'no_rights' => 'Прав пока что нету',
|
||||
'properties' => 'Свойства',
|
||||
],
|
||||
'en' => [
|
||||
'tree_site_title' => 'Site Tree',
|
||||
'save' => 'Save',
|
||||
'add' => 'Add',
|
||||
'paste' => 'Paste',
|
||||
'copy' => 'Copy',
|
||||
'rename' => 'Rename',
|
||||
'properties' => 'Properties',
|
||||
'delete' => 'Delete',
|
||||
'choose' => 'Choose',
|
||||
'enter_new_name' => 'Enter new name',
|
||||
'name_only_english_letters' => 'Name must contain only English letters without spaces and symbols.',
|
||||
'no_item_selected' => 'No item selected.',
|
||||
'delete_all_subpages' => 'Delete all subpages of the page',
|
||||
'save_new_page' => 'Save the new page!',
|
||||
'enter_tag_for_new_page' => 'Enter tag for the new page:',
|
||||
'tag_only_english_letters' => 'Tag must contain only English letters without spaces and symbols.',
|
||||
'tag_already_exists' => 'An element with this tag already exists.',
|
||||
'select_page_for_link' => 'Select page for the link',
|
||||
'rights' => 'Rights',
|
||||
'ok' => 'OK',
|
||||
'cancel' => 'Cancel',
|
||||
'select' => 'Select',
|
||||
'no_rights' => 'No rights yet',
|
||||
'properties' => 'Properties',
|
||||
],
|
||||
'lv' => [
|
||||
'tree_site_title' => 'Saites koks',
|
||||
'save' => 'Saglabāt',
|
||||
'add' => 'Pievienot',
|
||||
'paste' => 'Ielīmēt',
|
||||
'copy' => 'Kopēt',
|
||||
'rename' => 'Pārdēvēt',
|
||||
'properties' => 'Īpašības',
|
||||
'delete' => 'Dzēst',
|
||||
'choose' => 'Izvēlēties',
|
||||
'enter_new_name' => 'Ievadiet jaunu nosaukumu',
|
||||
'name_only_english_letters' => 'Nosaukumam jābūt tikai angļu burtiem bez atstarpēm un simboliem.',
|
||||
'no_item_selected' => 'Nav izvēlēts neviens elements.',
|
||||
'delete_all_subpages' => 'Dzēst visas apakšlapas',
|
||||
'save_new_page' => 'Saglabājiet jauno lapu!',
|
||||
'enter_tag_for_new_page' => 'Ievadiet taga nosaukumu jaunai lapai:',
|
||||
'tag_only_english_letters' => 'Tagam jābūt tikai angļu burtiem bez atstarpēm un simboliem.',
|
||||
'tag_already_exists' => 'Tādā pašā tagā jau pastāv elements.',
|
||||
'select_page_for_link' => 'Izvēlieties lapu saitei',
|
||||
'rights' => 'Tiesības',
|
||||
'ok' => 'Labi',
|
||||
'cancel' => 'Atcelt',
|
||||
'select' => 'Izvēlēties',
|
||||
'no_rights' => 'Nav tiesību vēl',
|
||||
'properties' => 'Īpašības',
|
||||
],
|
||||
];
|
||||
|
||||
return $lang;
|
||||
28
main_plugin/site_tree/plug.php
Executable file
28
main_plugin/site_tree/plug.php
Executable file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
/**
|
||||
* @file plug.php
|
||||
* @brief Подключает плагин site_tree для администраторов, подставляет языковые строки и выводит HTML-код дерева сайта
|
||||
*/
|
||||
|
||||
global $path, $_SESSION, $configAdmins;
|
||||
|
||||
/** @brief Языковой массив для плагина site_tree */
|
||||
$lang = include $path . 'main_plugin/site_tree/lang.php';
|
||||
|
||||
/** @brief Текущий язык пользователя, по умолчанию 'en' */
|
||||
$lng = $_SESSION['lng'] ?? 'en';
|
||||
|
||||
if (in_array($_SESSION['username'], $configAdmins, true)) {
|
||||
include_once $path . 'main_plugin/site_tree/func.site_tree.php';
|
||||
|
||||
$Html = file_get_contents($path . 'main_plugin/site_tree/site_tree.php');
|
||||
foreach ($lang[$lng] as $key => $value) {
|
||||
$Html = str_replace('{{' . $key . '}}', $value, $Html);
|
||||
}
|
||||
|
||||
echo $Html;
|
||||
|
||||
echo '<link rel="stylesheet" type="text/css" href="/main_plugin/site_tree/site_tree.css">';
|
||||
echo '<script type="text/javascript" src="/main_plugin/site_tree/lang.js.php?lng=' . $lng . '"></script>';
|
||||
}
|
||||
?>
|
||||
213
main_plugin/site_tree/site_tree.css
Executable file
213
main_plugin/site_tree/site_tree.css
Executable file
@@ -0,0 +1,213 @@
|
||||
/* основной div */
|
||||
#treeDiv {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid #000000;
|
||||
width: 800px;
|
||||
border-radius: 5px;
|
||||
height: 600px;
|
||||
font-size: 1em;
|
||||
box-shadow: 0px 0px 5px #777;
|
||||
}
|
||||
|
||||
/* верхний див */
|
||||
#treeTop {
|
||||
text-align: center;
|
||||
border-bottom: 1px #40464d solid;
|
||||
padding: 5px;
|
||||
background-color: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
#treeTopTitle {
|
||||
text-align: center;
|
||||
}
|
||||
#treeCloseFun {
|
||||
float: right;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-position: -159px -121px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* основное древо сайта */
|
||||
|
||||
#treeTableDiv {
|
||||
top: 36px;
|
||||
height: 480px;
|
||||
position: relative;
|
||||
margin: 0px 20px 0px 20px;
|
||||
border: 1px #40464d solid;
|
||||
overflow-y: overlay;
|
||||
border-radius: 5px;
|
||||
width: calc(100% - 40px);
|
||||
}
|
||||
|
||||
li.has-children::marker {
|
||||
content: "► ";
|
||||
}
|
||||
li.has-children.open::marker {
|
||||
content: "▼ ";
|
||||
}
|
||||
li.no-children::marker {
|
||||
content: "□ ";
|
||||
}
|
||||
|
||||
/* окно страницы */
|
||||
|
||||
.tree-details {
|
||||
display: none;
|
||||
background: white;
|
||||
border: 1px solid black;
|
||||
padding: 3px;
|
||||
margin: 3px 0px 10px 0px;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
width: max-content;
|
||||
}
|
||||
.tree-details div {
|
||||
margin: 3px;
|
||||
}
|
||||
|
||||
/* окно настроек */
|
||||
#treeSettings {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
background-color: rgba(255, 255, 255, 0.92);
|
||||
border: 1px solid #000000;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
box-shadow: 0px 0px 5px #777;
|
||||
padding: 5px;
|
||||
animation: fadeIn 0.5s ease-in forwards;
|
||||
}
|
||||
|
||||
.treeSettingsButtons {
|
||||
background-color: rgba(255, 255, 255, 1);
|
||||
border-radius: 5px;
|
||||
padding: 2px;
|
||||
margin: 3px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
}
|
||||
.treeSettingsButtons:hover {
|
||||
color: #787878;
|
||||
}
|
||||
|
||||
#treeSettingsSave {
|
||||
border: 1px #40464d solid;
|
||||
border-radius: 5px;
|
||||
padding: 5px;
|
||||
margin: 44px 20px 0px 10px;
|
||||
cursor: pointer;
|
||||
display: block;
|
||||
width: 91px;
|
||||
float: right;
|
||||
}
|
||||
#treeSettingsSave:hover {
|
||||
color: #787878;
|
||||
}
|
||||
|
||||
/* окно свойств */
|
||||
#treeProperties {
|
||||
display: inline-block;
|
||||
position: fixed;
|
||||
z-index: 100;
|
||||
user-select: none;
|
||||
background-color: rgba(255, 255, 255, 0.97);
|
||||
border: 1px solid #000000;
|
||||
border-radius: 5px;
|
||||
font-size: 1em;
|
||||
box-shadow: 0px 0px 5px #777;
|
||||
width: 600px;
|
||||
max-width: calc(100% - 20px);
|
||||
}
|
||||
#treePropertiesMiddle {
|
||||
margin: 0px 20px 0px 20px;
|
||||
}
|
||||
|
||||
#treePropertiesTop {
|
||||
border-bottom: 1px solid #000000;
|
||||
padding: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
#treePropertiesTopName {
|
||||
text-align: center;
|
||||
}
|
||||
#treePropertiesTopClose {
|
||||
float: right;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
background-position: -159px -121px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
#treePropertiesWindow {
|
||||
text-align: center;
|
||||
display: flex;
|
||||
}
|
||||
.treePropertiesWindowDiv {
|
||||
cursor: pointer;
|
||||
text-align: center;
|
||||
width: 50%;
|
||||
display: inline-block;
|
||||
padding: 9px;
|
||||
}
|
||||
.treePropertiesWindowDiv:hover {
|
||||
color: #787878;
|
||||
}
|
||||
|
||||
#treePropertiesDiv {
|
||||
border: 1px solid #000000;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.treePropertiesDivDivs {
|
||||
padding: 6px 8px 6px 8px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
#treePropertiesDivButtons {
|
||||
padding: 9px;
|
||||
display: flex;
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
}
|
||||
#treePropertiesDivButtons {
|
||||
text-align: center;
|
||||
}
|
||||
.treePropertiesDivButton {
|
||||
margin: 3px 3px 3px 15px;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
}
|
||||
.treePropertiesDivButton:hover {
|
||||
color: #787878;
|
||||
}
|
||||
|
||||
.editib {
|
||||
background-image: url(../../img/pict/b_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editimc {
|
||||
background-image: url(../../img/pict/mc_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editib:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editimc:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
}
|
||||
|
||||
.editf.active {
|
||||
background-image: url(../../img/pict/b_iconslyb.svg);
|
||||
background-color: #e7e7e7;
|
||||
}
|
||||
.editf.active:hover {
|
||||
background-image: url(../../img/pict/g_iconslyb.svg);
|
||||
background-color: #e7e7e7;
|
||||
}
|
||||
1025
main_plugin/site_tree/site_tree.js
Executable file
1025
main_plugin/site_tree/site_tree.js
Executable file
File diff suppressed because it is too large
Load Diff
49
main_plugin/site_tree/site_tree.php
Executable file
49
main_plugin/site_tree/site_tree.php
Executable file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* @file site_tree.php
|
||||
* @brief Контейнеры и элементы дерева сайта: основное дерево, панель настроек узлов, свойства узлов с кнопками действий
|
||||
*/
|
||||
?>
|
||||
|
||||
<?php /** @brief Основной контейнер дерева сайта */ $treeDiv; ?>
|
||||
<div id="treeDiv" style="visibility: hidden; top: 20%; left: 50%; transform: translate(-50%, -20%);">
|
||||
<div id="treeTop">
|
||||
<span id="treeTopTitle">{{tree_site_title}}</span>
|
||||
<span id="treeCloseFun" class="editib"></span>
|
||||
</div>
|
||||
<div id="treeTableDiv"></div>
|
||||
<span id="treeSettingsSave">{{save}}</span>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Панель настроек элементов дерева */ $treeSettings; ?>
|
||||
<div id="treeSettings" style="visibility: hidden; top: 0px; left: 0px;">
|
||||
<div id="treeSettingsDiv">
|
||||
<span id="treeSettingsAdd" class="treeSettingsButtons">{{add}}</span>
|
||||
<span id="treeSettingsPaste" class="treeSettingsButtons">{{paste}}</span>
|
||||
<span id="treeSettingsCopy" class="treeSettingsButtons">{{copy}}</span>
|
||||
<span id="treeSettingsRename" class="treeSettingsButtons">{{rename}}</span>
|
||||
<span id="treeSettingsProperties" class="treeSettingsButtons">{{properties}}</span>
|
||||
<span id="treeSettingsDelete" class="treeSettingsButtons">{{delete}}</span>
|
||||
<span id="treeSettingsChoose" class="treeSettingsButtons">{{choose}}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php /** @brief Панель свойств выбранного узла дерева с кнопками подтверждения */ $treeProperties; ?>
|
||||
<div id="treeProperties" style="visibility: hidden; top: 20%; left: 50%; transform: translate(-50%, -20%);">
|
||||
<div id="treePropertiesTop">
|
||||
<span id="treePropertiesTopName" class="treePropertiesTop"></span>
|
||||
<span id="treePropertiesTopClose" class="editib"></span>
|
||||
</div>
|
||||
<div id="treePropertiesMiddle">
|
||||
<div id="treePropertiesWindow">
|
||||
<span id="treePropertiesWindowProperties" class="treePropertiesWindowDiv">{{properties}}</span>
|
||||
<span id="treePropertiesWindowRights" class="treePropertiesWindowDiv">{{rights}}</span>
|
||||
</div>
|
||||
<div id="treePropertiesDiv">
|
||||
</div>
|
||||
<div id="treePropertiesDivButtons">
|
||||
<div id="treePropertiesDivButtonOk" class="treePropertiesDivButton">{{ok}}</div>
|
||||
<div id="treePropertiesDivButtonCancel" class="treePropertiesDivButton">{{cancel}}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user