Добавляем все файлы
This commit is contained in:
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';
|
||||
}
|
||||
Reference in New Issue
Block a user