big cmap refactoring
This commit is contained in:
@@ -0,0 +1,546 @@
|
||||
"use strict";
|
||||
|
||||
import { debug, elementDescription, selectionStyle, escapeHtml } from "../cmap-utils.js";
|
||||
|
||||
export class CmapItemDecorator {
|
||||
constructor(editor) {
|
||||
this.editor = editor;
|
||||
}
|
||||
|
||||
get items() { return this.editor.items; }
|
||||
get connectors() { return this.editor.connectors; }
|
||||
get canvas() { return this.editor.canvas; }
|
||||
get zoomFactor() { return this.editor.zoomFactor; }
|
||||
|
||||
itemHtml(record) {
|
||||
if (record.kind === "phrase") {
|
||||
return `<div class="rw-cmap-phrase-label">${escapeHtml(record.label || "?????")}</div>`;
|
||||
}
|
||||
if (this.editor.renderItem) return this.editor.renderItem(record);
|
||||
return `<div>${escapeHtml(record.label)}</div>`;
|
||||
}
|
||||
|
||||
editPhraseInline(record) {
|
||||
if (!record || record.kind !== "phrase") return;
|
||||
const value = record.label || "?????";
|
||||
record.node.attr("content",
|
||||
`<input class="rw-cmap-phrase-input" type="text" value="${escapeHtml(value)}" aria-label="${escapeHtml(this.editor.labels.relation)}">`);
|
||||
record.node.redraw();
|
||||
const element = record.node.element();
|
||||
const input = element ? element.querySelector(".rw-cmap-phrase-input") : null;
|
||||
if (!input) {
|
||||
record.editWhenRendered = true;
|
||||
return;
|
||||
}
|
||||
|
||||
const commit = () => {
|
||||
this.editor.scheduleHistoryCommit();
|
||||
const text = input.value.trim() || "?????";
|
||||
record.label = text;
|
||||
record.node.attr("content", this.itemHtml(record));
|
||||
record.node.redraw();
|
||||
this.editor.selectItem(record);
|
||||
};
|
||||
|
||||
input.addEventListener("pointerdown", (event) => event.stopPropagation());
|
||||
input.addEventListener("keydown", (event) => {
|
||||
if (event.key === "Enter") {
|
||||
event.preventDefault();
|
||||
input.blur();
|
||||
}
|
||||
if (event.key === "Escape") {
|
||||
event.preventDefault();
|
||||
input.value = value;
|
||||
input.blur();
|
||||
}
|
||||
});
|
||||
input.addEventListener("blur", commit, { once: true });
|
||||
input.focus();
|
||||
input.select();
|
||||
}
|
||||
|
||||
applyItemTypography(record, element) {
|
||||
const title = element.querySelector(".cmap-card-title");
|
||||
if (title) {
|
||||
title.style.color = record.textColor;
|
||||
title.style.fontFamily = record.fontFamily;
|
||||
title.style.fontSize = record.fontSize;
|
||||
title.style.fontWeight = record.fontWeight;
|
||||
title.style.fontStyle = record.fontStyle;
|
||||
}
|
||||
const synopsis = element.querySelector(".cmap-card-synopsis");
|
||||
if (synopsis) {
|
||||
synopsis.style.color = record.synopsisTextColor;
|
||||
synopsis.style.fontFamily = record.synopsisFontFamily;
|
||||
synopsis.style.fontSize = record.synopsisFontSize;
|
||||
synopsis.style.fontWeight = record.synopsisFontWeight;
|
||||
synopsis.style.fontStyle = record.synopsisFontStyle;
|
||||
}
|
||||
}
|
||||
|
||||
decorateItem(record, renderedElement = null) {
|
||||
const element = renderedElement || record.node.element();
|
||||
if (!element) return;
|
||||
element.classList.remove("rw-cmap-item-concept", "rw-cmap-item-page", "rw-cmap-item-submap", "rw-cmap-item-phrase");
|
||||
element.classList.add("cmap-prototype-node", "rw-cmap-item", `rw-cmap-item-${record.kind}`);
|
||||
element.dataset.rwCmapItemId = String(record.id);
|
||||
element.style.fontFamily = record.fontFamily;
|
||||
element.style.fontSize = record.fontSize;
|
||||
element.style.fontWeight = record.fontWeight;
|
||||
element.style.fontStyle = record.fontStyle;
|
||||
element.style.overflow = "visible";
|
||||
this.applyItemTypography(record, element);
|
||||
|
||||
if (record.fitContentPending || record.kind !== "phrase") {
|
||||
this.fitItemToContent(record, element);
|
||||
}
|
||||
|
||||
const image = element.querySelector(".cmap-card-image");
|
||||
if (image && image.dataset.rwCmapFitBound !== "1") {
|
||||
image.dataset.rwCmapFitBound = "1";
|
||||
image.addEventListener("load", () => {
|
||||
if (record.kind === "phrase" && !record.autoWidth && !record.autoHeight) return;
|
||||
record.fitContentPending = true;
|
||||
this.fitItemToContent(record, element);
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
const descriptionButton = element.querySelector(".rw-cmap-view-description");
|
||||
if (descriptionButton && descriptionButton.dataset.rwCmapBound !== "1") {
|
||||
descriptionButton.dataset.rwCmapBound = "1";
|
||||
descriptionButton.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
descriptionButton.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (record.descriptionPageSlug && this.editor.onOpenPage) {
|
||||
this.editor.onOpenPage({ ...record, pageSlug: record.descriptionPageSlug });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const linkedButton = element.querySelector(".rw-cmap-open-linked");
|
||||
if (linkedButton && linkedButton.dataset.rwCmapBound !== "1") {
|
||||
linkedButton.dataset.rwCmapBound = "1";
|
||||
linkedButton.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
linkedButton.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (record.parentCmapLink) {
|
||||
if (this.editor.onOpenParentCmap) this.editor.onOpenParentCmap(record);
|
||||
else this.editor.openParentMap();
|
||||
} else if (record.cmapSlug && this.editor.onOpenCmap) {
|
||||
this.editor.onOpenCmap(record);
|
||||
} else if (record.pageSlug && this.editor.onOpenPage) {
|
||||
this.editor.onOpenPage(record);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const externalButton = element.querySelector(".rw-cmap-open-external");
|
||||
if (externalButton && externalButton.dataset.rwCmapBound !== "1") {
|
||||
externalButton.dataset.rwCmapBound = "1";
|
||||
externalButton.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
externalButton.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
if (record.externalUrl && this.editor.onOpenExternalUrl) this.editor.onOpenExternalUrl(record);
|
||||
});
|
||||
}
|
||||
|
||||
if (element.dataset.rwCmapBound !== "1") {
|
||||
element.dataset.rwCmapBound = "1";
|
||||
debug("item pointer handlers attached", {
|
||||
id: record.id,
|
||||
kind: record.kind,
|
||||
element: elementDescription(element),
|
||||
style: selectionStyle(element)
|
||||
});
|
||||
}
|
||||
|
||||
if (this.editor.selectedItems.has(record)) {
|
||||
element.classList.add("rw-cmap-selected");
|
||||
element.classList.toggle("rw-cmap-selected-primary", this.editor.selectedItem === record);
|
||||
element.setAttribute("aria-selected", "true");
|
||||
if (this.editor.selectedItem === record) this.ensureHandles(record, element);
|
||||
}
|
||||
this.ensureSubmapToggle(record, element);
|
||||
debug("cmap node rendered and decorated", {
|
||||
id: record.id,
|
||||
kind: record.kind,
|
||||
selected: this.editor.selectedItems.has(record),
|
||||
element: elementDescription(element),
|
||||
style: selectionStyle(element)
|
||||
});
|
||||
if (record.editWhenRendered) {
|
||||
record.editWhenRendered = false;
|
||||
queueMicrotask(() => this.editPhraseInline(record));
|
||||
}
|
||||
this.editor.ensureCanvasExtent(
|
||||
Number(record.node.attr("x")) + Number(record.node.attr("width")),
|
||||
Number(record.node.attr("y")) + Number(record.node.attr("height"))
|
||||
);
|
||||
if (!record.moveMembership) {
|
||||
let parent = record.parentSubmap;
|
||||
while (parent) {
|
||||
this.editor.updateSubmapFrame(parent);
|
||||
parent = parent.parentSubmap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fitItemToContent(record, element) {
|
||||
record.fitContentPending = false;
|
||||
if (record.kind === "phrase" && !record.autoWidth && !record.autoHeight) return;
|
||||
|
||||
const fixedWidth = !record.autoWidth && record.kind !== "phrase";
|
||||
|
||||
const probe = document.createElement("div");
|
||||
probe.className = element.className;
|
||||
probe.innerHTML = this.itemHtml(record);
|
||||
Object.assign(probe.style, {
|
||||
position: "fixed",
|
||||
left: "-10000px",
|
||||
top: "0",
|
||||
width: fixedWidth ? `${record.width}px` : "max-content",
|
||||
height: "auto",
|
||||
maxWidth: fixedWidth ? "none" : (record.kind === "phrase" ? "280px" : "380px"),
|
||||
boxSizing: "border-box",
|
||||
fontFamily: record.fontFamily,
|
||||
fontSize: record.fontSize,
|
||||
fontWeight: record.fontWeight,
|
||||
fontStyle: record.fontStyle,
|
||||
lineHeight: "1.25",
|
||||
overflow: "visible",
|
||||
pointerEvents: "none",
|
||||
transform: "none",
|
||||
visibility: "hidden",
|
||||
whiteSpace: "normal"
|
||||
});
|
||||
this.applyItemTypography(record, probe);
|
||||
|
||||
const content = probe.firstElementChild;
|
||||
if (content) {
|
||||
Object.assign(content.style, {
|
||||
width: fixedWidth ? "100%" : "max-content",
|
||||
height: "auto",
|
||||
maxWidth: fixedWidth ? "none" : (record.kind === "phrase" ? "276px" : "376px"),
|
||||
overflow: "visible",
|
||||
whiteSpace: "normal"
|
||||
});
|
||||
}
|
||||
|
||||
document.body.append(probe);
|
||||
const bounds = probe.getBoundingClientRect();
|
||||
probe.remove();
|
||||
|
||||
const minimumWidth = record.kind === "phrase" ? 50 : 100;
|
||||
const minimumHeight = record.kind === "phrase" ? 24 : 40;
|
||||
const measuredWidth = Math.ceil(bounds.width) + 4;
|
||||
const measuredHeight = Math.ceil(bounds.height) + 4;
|
||||
const nextWidth = record.autoWidth ? Math.max(minimumWidth, measuredWidth) : record.width;
|
||||
const nextHeight = record.autoHeight ? Math.max(minimumHeight, measuredHeight) :
|
||||
(record.kind === "phrase" ? record.height : Math.max(record.height, measuredHeight));
|
||||
|
||||
if (nextWidth === record.width && nextHeight === record.height) return;
|
||||
const beforeAutomaticLayout = this.editor.onAutomaticLayoutChange ? this.editor.historySnapshot() : null;
|
||||
const previousWidth = record.width;
|
||||
const previousHeight = record.height;
|
||||
const attributes = { width: nextWidth, height: nextHeight };
|
||||
if (record.kind === "phrase") {
|
||||
attributes.x = Number(record.node.attr("x")) + ((previousWidth - nextWidth) / 2);
|
||||
attributes.y = Number(record.node.attr("y")) + ((previousHeight - nextHeight) / 2);
|
||||
}
|
||||
record.width = nextWidth;
|
||||
record.height = nextHeight;
|
||||
record.node.attr(attributes);
|
||||
record.node.redraw();
|
||||
this.editor.redrawConnectorsFor(record);
|
||||
debug("automatic item size applied", {
|
||||
id: record.id,
|
||||
kind: record.kind,
|
||||
width: nextWidth,
|
||||
height: nextHeight
|
||||
});
|
||||
this.editor.refreshHistorySnapshot();
|
||||
if (this.editor.onAutomaticLayoutChange) {
|
||||
const afterAutomaticLayout = this.editor.historySnapshot();
|
||||
if (beforeAutomaticLayout !== afterAutomaticLayout) {
|
||||
this.editor.onAutomaticLayoutChange({
|
||||
beforeSnapshot: beforeAutomaticLayout,
|
||||
afterSnapshot: afterAutomaticLayout,
|
||||
itemId: record.id
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
decorateConnector(record, renderedElement = null) {
|
||||
const element = renderedElement || record.link.element();
|
||||
if (!element) return;
|
||||
element.classList.add("rw-cmap-connector");
|
||||
element.dataset.rwCmapConnectorId = String(record.id);
|
||||
}
|
||||
|
||||
ensureSubmapToggle(record, element) {
|
||||
let toggle = element.querySelector(":scope > .rw-cmap-submap-toggle");
|
||||
let open = element.querySelector(":scope > .rw-cmap-submap-open");
|
||||
if (record.kind !== "submap") {
|
||||
if (toggle) toggle.remove();
|
||||
if (open) open.remove();
|
||||
return;
|
||||
}
|
||||
if (record === this.editor.activeMapRoot) {
|
||||
if (toggle) toggle.remove();
|
||||
if (open) open.remove();
|
||||
return;
|
||||
}
|
||||
const legacySeparateMap = record.separateMap && !record.cmapSlug;
|
||||
if (record.expanded && !legacySeparateMap) {
|
||||
if (toggle) toggle.remove();
|
||||
toggle = null;
|
||||
}
|
||||
if (!toggle) {
|
||||
if (!record.expanded || legacySeparateMap) {
|
||||
toggle = document.createElement("button");
|
||||
toggle.type = "button";
|
||||
toggle.className = "rw-cmap-submap-toggle";
|
||||
toggle.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
toggle.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.editor.selectItem(record);
|
||||
this.editor.toggleSubmap(record);
|
||||
});
|
||||
element.append(toggle);
|
||||
}
|
||||
}
|
||||
if (toggle) {
|
||||
toggle.textContent = legacySeparateMap ? "↗" : "+";
|
||||
toggle.title = legacySeparateMap ? "Open concept map" : "Expand submap";
|
||||
toggle.setAttribute("aria-label", toggle.title);
|
||||
toggle.setAttribute("aria-expanded", String(record.expanded));
|
||||
}
|
||||
if (record.cmapSlug && this.editor.onOpenStoredSubMap) {
|
||||
if (!open) {
|
||||
open = document.createElement("button");
|
||||
open.type = "button";
|
||||
open.className = "rw-cmap-submap-open";
|
||||
open.textContent = "↗";
|
||||
open.title = "Open as separate concept map";
|
||||
open.setAttribute("aria-label", open.title);
|
||||
open.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
open.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.editor.selectItem(record);
|
||||
this.editor.onOpenStoredSubMap(record);
|
||||
});
|
||||
element.append(open);
|
||||
}
|
||||
} else if (open) {
|
||||
open.remove();
|
||||
}
|
||||
}
|
||||
|
||||
ensureHandles(record, element) {
|
||||
if (!element.querySelector(":scope > .rw-cmap-relation-handle")) {
|
||||
const relation = document.createElement("button");
|
||||
relation.type = "button";
|
||||
relation.className = "rw-cmap-handle rw-cmap-relation-handle";
|
||||
relation.title = this.editor.labels.createRelation;
|
||||
relation.setAttribute("aria-label", this.editor.labels.createRelation);
|
||||
relation.setAttribute("aria-hidden", "false");
|
||||
relation.addEventListener("pointerdown", (event) => this.editor.startRelationDrag(event, record));
|
||||
element.append(relation);
|
||||
}
|
||||
|
||||
if (record.kind !== "phrase" &&
|
||||
!element.querySelector(":scope > .rw-cmap-edit-handle")) {
|
||||
const edit = document.createElement("button");
|
||||
edit.type = "button";
|
||||
edit.className = "rw-cmap-handle rw-cmap-edit-handle";
|
||||
edit.title = this.editor.labels.editConcept;
|
||||
edit.setAttribute("aria-label", this.editor.labels.editConcept);
|
||||
edit.setAttribute("aria-hidden", "false");
|
||||
edit.addEventListener("pointerdown", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
});
|
||||
edit.addEventListener("mousedown", (event) => event.stopPropagation());
|
||||
edit.addEventListener("click", (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.editor.selectItem(record);
|
||||
if (this.editor.onEditItem) this.editor.onEditItem(record);
|
||||
});
|
||||
element.append(edit);
|
||||
}
|
||||
|
||||
if (record.kind !== "phrase" &&
|
||||
!element.querySelector(":scope > .rw-cmap-resize-handle")) {
|
||||
const resize = document.createElement("button");
|
||||
resize.type = "button";
|
||||
resize.className = "rw-cmap-handle rw-cmap-resize-handle";
|
||||
resize.title = this.editor.labels.resizeConcept;
|
||||
resize.setAttribute("aria-label", this.editor.labels.resizeConcept);
|
||||
resize.setAttribute("aria-hidden", "false");
|
||||
resize.addEventListener("pointerdown", (event) => this.editor.startResize(event, record));
|
||||
element.append(resize);
|
||||
}
|
||||
}
|
||||
|
||||
removeHandles(element) {
|
||||
for (const handle of element.querySelectorAll(":scope > .rw-cmap-handle")) handle.remove();
|
||||
}
|
||||
|
||||
boundaryConceptFor(record, crossedConnector, inside) {
|
||||
if (!record || record.kind !== "phrase") return record;
|
||||
for (const connector of this.connectors) {
|
||||
if (connector === crossedConnector) continue;
|
||||
if (connector.source !== record && connector.target !== record) continue;
|
||||
const neighbour = connector.source === record ? connector.target : connector.source;
|
||||
if (neighbour.kind === "phrase") continue;
|
||||
if (this.editor.itemInsideActiveMap(neighbour) === inside) return neighbour;
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
refreshBoundaryReferences() {
|
||||
if (this.editor.boundaryLayer) {
|
||||
this.editor.boundaryLayer.remove();
|
||||
this.editor.boundaryLayer = null;
|
||||
}
|
||||
if (!this.editor.activeMapRoot) return;
|
||||
const surface = this.editor.surfaceElement();
|
||||
if (!surface) return;
|
||||
const crossings = [];
|
||||
for (const connector of this.connectors) {
|
||||
const sourceInside = this.editor.itemInsideActiveMap(connector.source);
|
||||
const targetInside = this.editor.itemInsideActiveMap(connector.target);
|
||||
if (sourceInside === targetInside) continue;
|
||||
const insideRecord = sourceInside ? connector.source : connector.target;
|
||||
const outsideRecord = sourceInside ? connector.target : connector.source;
|
||||
const insideConcept = this.boundaryConceptFor(insideRecord, connector, true);
|
||||
const outsideConcept = this.boundaryConceptFor(outsideRecord, connector, false);
|
||||
if (!insideConcept || !outsideConcept || !this.editor.isItemVisible(insideConcept)) continue;
|
||||
crossings.push({ connector, sourceInside, insideConcept, outsideConcept });
|
||||
}
|
||||
if (!crossings.length) return;
|
||||
|
||||
const layer = document.createElement("div");
|
||||
layer.className = "rw-cmap-boundary-layer";
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.classList.add("rw-cmap-boundary-lines");
|
||||
const definitions = document.createElementNS("http://www.w3.org/2000/svg", "defs");
|
||||
const marker = document.createElementNS("http://www.w3.org/2000/svg", "marker");
|
||||
marker.setAttribute("id", "rw-cmap-boundary-arrow");
|
||||
marker.setAttribute("viewBox", "0 0 10 10");
|
||||
marker.setAttribute("refX", "9");
|
||||
marker.setAttribute("refY", "5");
|
||||
marker.setAttribute("markerWidth", "7");
|
||||
marker.setAttribute("markerHeight", "7");
|
||||
marker.setAttribute("orient", "auto-start-reverse");
|
||||
const arrow = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
arrow.setAttribute("d", "M 0 0 L 10 5 L 0 10 z");
|
||||
arrow.setAttribute("fill", "#4a5560");
|
||||
marker.append(arrow);
|
||||
definitions.append(marker);
|
||||
svg.append(definitions);
|
||||
layer.append(svg);
|
||||
surface.append(layer);
|
||||
this.editor.boundaryLayer = layer;
|
||||
|
||||
const viewLeft = this.canvas.scrollLeft / this.zoomFactor;
|
||||
const viewTop = this.canvas.scrollTop / this.zoomFactor;
|
||||
const viewWidth = this.canvas.clientWidth / this.zoomFactor;
|
||||
const viewHeight = this.canvas.clientHeight / this.zoomFactor;
|
||||
const buttonWidth = 190;
|
||||
const occupied = { left: [], right: [] };
|
||||
const reserveY = (side, desired) => {
|
||||
let y = Math.max(viewTop + 12, Math.min(desired, viewTop + viewHeight - 40));
|
||||
while (occupied[side].some((used) => Math.abs(used - y) < 34)) y += 34;
|
||||
if (y > viewTop + viewHeight - 40) y = viewTop + 12;
|
||||
occupied[side].push(y);
|
||||
return y;
|
||||
};
|
||||
|
||||
for (const crossing of crossings) {
|
||||
const insideX = Number(crossing.insideConcept.node.attr("x")) +
|
||||
(Number(crossing.insideConcept.node.attr("width")) / 2);
|
||||
const insideY = Number(crossing.insideConcept.node.attr("y")) +
|
||||
(Number(crossing.insideConcept.node.attr("height")) / 2);
|
||||
const outsideX = Number(crossing.outsideConcept.node.attr("x")) +
|
||||
(Number(crossing.outsideConcept.node.attr("width")) / 2);
|
||||
const side = outsideX < insideX ? "left" : "right";
|
||||
const x = side === "left" ? viewLeft + 12 : viewLeft + viewWidth - buttonWidth - 12;
|
||||
const y = reserveY(side, insideY - 15);
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = `rw-cmap-boundary-reference rw-cmap-boundary-reference-${side}`;
|
||||
button.style.transform = `translate(${x}px, ${y}px)`;
|
||||
button.style.width = `${buttonWidth}px`;
|
||||
button.textContent = crossing.outsideConcept.label || "External concept";
|
||||
button.title = "Open the concept map containing this connection";
|
||||
button.addEventListener("click", () => {
|
||||
if (this.editor.onOpenBoundaryReference) {
|
||||
this.editor.onOpenBoundaryReference(crossing.outsideConcept, crossing.connector);
|
||||
}
|
||||
});
|
||||
layer.append(button);
|
||||
|
||||
const boundaryX = side === "left" ? x + buttonWidth : x;
|
||||
const boundaryY = y + 15;
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
const startX = crossing.sourceInside ? insideX : boundaryX;
|
||||
const startY = crossing.sourceInside ? insideY : boundaryY;
|
||||
const endX = crossing.sourceInside ? boundaryX : insideX;
|
||||
const endY = crossing.sourceInside ? boundaryY : insideY;
|
||||
path.setAttribute("d", `M ${startX} ${startY} L ${endX} ${endY}`);
|
||||
path.setAttribute("fill", "none");
|
||||
path.setAttribute("stroke", crossing.connector.lineColor || "#4a5560");
|
||||
path.setAttribute("stroke-width", String(crossing.connector.lineWidth || 2));
|
||||
if (crossing.connector.hasArrow) path.setAttribute("marker-end", "url(#rw-cmap-boundary-arrow)");
|
||||
svg.append(path);
|
||||
}
|
||||
}
|
||||
|
||||
updateSubmapAnchorLine(record, bounds, surface = this.editor.surfaceElement()) {
|
||||
if (!surface || !bounds || record === this.editor.activeMapRoot || !record.expanded) {
|
||||
if (record.submapAnchorLineElement) {
|
||||
record.submapAnchorLineElement.remove();
|
||||
record.submapAnchorLineElement = null;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!record.submapAnchorLineElement) {
|
||||
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
svg.classList.add("rw-cmap-submap-anchor-line");
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
svg.append(path);
|
||||
surface.prepend(svg);
|
||||
record.submapAnchorLineElement = svg;
|
||||
}
|
||||
const anchor = this.editor.itemCenter(record);
|
||||
const target = {
|
||||
x: Math.max(bounds.left, Math.min(anchor.x, bounds.right)),
|
||||
y: Math.max(bounds.top, Math.min(anchor.y, bounds.bottom))
|
||||
};
|
||||
record.submapAnchorLineElement.querySelector("path")
|
||||
.setAttribute("d", `M ${anchor.x} ${anchor.y} L ${target.x} ${target.y}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user