148 lines
5.3 KiB
JavaScript
148 lines
5.3 KiB
JavaScript
"use strict";
|
|
|
|
import { ConceptMapConnector } from "../model/concept-map.js";
|
|
import { debug, numberOr } from "../cmap-utils.js";
|
|
|
|
/** Own map-local connector mutations and linking-phrase membership. */
|
|
export class CmapRelationController {
|
|
constructor(editor) {
|
|
this.editor = editor;
|
|
}
|
|
|
|
get items() { return this.editor.items; }
|
|
get connectors() { return this.editor.connectors; }
|
|
|
|
connectWithPhrase(source, target, label = "?????", editImmediately = true) {
|
|
const a = this.editor.itemCenter(source);
|
|
const b = this.editor.itemCenter(target);
|
|
const parentSubmap = this.commonSubmapParent([source, target]);
|
|
const phrase = this.editor.addItem({
|
|
kind: "phrase",
|
|
label,
|
|
parentSubmap,
|
|
submapDepth: parentSubmap ? parentSubmap.submapDepth + 1 : 0,
|
|
x: ((a.x + b.x) / 2) - 72,
|
|
y: ((a.y + b.y) / 2) - 18,
|
|
backgroundColor: "#fbfbf8",
|
|
borderColor: "transparent"
|
|
});
|
|
this.editor.addConnector(source, phrase, false);
|
|
this.editor.addConnector(phrase, target, true);
|
|
this.reconcilePhraseMembership(phrase);
|
|
this.editor.selectItem(phrase);
|
|
if (editImmediately) this.editor.editPhraseInline(phrase);
|
|
return phrase;
|
|
}
|
|
|
|
submapChain(record) {
|
|
const result = [];
|
|
if (record.kind === "submap") result.push(record);
|
|
let parent = record.parentSubmap;
|
|
while (parent) {
|
|
result.push(parent);
|
|
parent = parent.parentSubmap;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
commonSubmapParent(records) {
|
|
if (!records.length) return null;
|
|
const chains = records.map((record) => this.submapChain(record));
|
|
return chains[0]
|
|
.filter((candidate) => chains.every((chain) => chain.includes(candidate)))
|
|
.sort((a, b) => b.submapDepth - a.submapDepth)[0] || null;
|
|
}
|
|
|
|
reconcilePhraseMembership(phrase = null) {
|
|
const phrases = phrase ? [phrase] : this.items.filter((item) => item.kind === "phrase");
|
|
for (const item of phrases) {
|
|
const endpoints = this.connectors
|
|
.filter((connector) => connector.source === item || connector.target === item)
|
|
.map((connector) => connector.source === item ? connector.target : connector.source)
|
|
.filter((endpoint) => endpoint.kind !== "phrase");
|
|
if (!endpoints.length) continue;
|
|
const parent = this.commonSubmapParent(endpoints);
|
|
item.parentSubmap = parent;
|
|
item.submapDepth = parent ? parent.submapDepth + 1 : 0;
|
|
}
|
|
}
|
|
|
|
addConnector(source, target, hasArrow = true, options = {}) {
|
|
const sourceCenter = this.editor.itemCenter(source);
|
|
const targetCenter = this.editor.itemCenter(target);
|
|
const link = this.editor.view.createConnector({
|
|
content: "",
|
|
width: 1,
|
|
height: 1,
|
|
backgroundColor: "transparent",
|
|
borderColor: "transparent",
|
|
borderWidth: 0,
|
|
lineColor: options.lineColor || "#333",
|
|
lineWidth: numberOr(Number(options.lineWidth), 2),
|
|
hasArrow,
|
|
cx: (sourceCenter.x + targetCenter.x) / 2,
|
|
cy: (sourceCenter.y + targetCenter.y) / 2,
|
|
sourceX: sourceCenter.x,
|
|
sourceY: sourceCenter.y,
|
|
targetX: targetCenter.x,
|
|
targetY: targetCenter.y
|
|
});
|
|
link.sourceNode(source.node).targetNode(target.node);
|
|
link.straighten();
|
|
link.draggable(true);
|
|
|
|
const record = {
|
|
id: Number.isInteger(Number(options.id)) && Number(options.id) > 0 ?
|
|
Number(options.id) : this.editor.nextConnectorId,
|
|
link,
|
|
source,
|
|
target,
|
|
visualSource: source,
|
|
visualTarget: target,
|
|
hasArrow,
|
|
lineColor: options.lineColor || "#333",
|
|
lineWidth: numberOr(Number(options.lineWidth), 2)
|
|
};
|
|
this.editor.nextConnectorId = Math.max(this.editor.nextConnectorId, record.id + 1);
|
|
this.editor.documents.attachConnectorToModel(record);
|
|
this.connectors.push(record);
|
|
link.onRendered((_renderedLink, element) => this.editor.decorateConnector(record, element));
|
|
link.onConnectionChange((_changedLink, type, node) =>
|
|
this.editor.handleConnectorConnectionChange(record, type, node));
|
|
link.visible(this.editor.isItemVisible(source) && this.editor.isItemVisible(target));
|
|
this.editor.scheduleHistoryCommit();
|
|
return record;
|
|
}
|
|
|
|
handleConnectorConnectionChange(connector, type, node) {
|
|
if (!connector || (type !== "source" && type !== "target")) return false;
|
|
const endpoint = this.items.find((item) => item.node === node) || null;
|
|
const otherType = type === "source" ? "target" : "source";
|
|
if (!endpoint || endpoint === connector[otherType]) {
|
|
connector[`visual${type === "source" ? "Source" : "Target"}`] = null;
|
|
this.editor.applyConnectorVisualEndpoints(connector,
|
|
this.editor.connectorEndpoint(connector.source),
|
|
this.editor.connectorEndpoint(connector.target));
|
|
return false;
|
|
}
|
|
if (connector[type] === endpoint) {
|
|
connector[`visual${type === "source" ? "Source" : "Target"}`] = endpoint;
|
|
return false;
|
|
}
|
|
const previous = connector[type];
|
|
connector[type] = endpoint;
|
|
connector[`visual${type === "source" ? "Source" : "Target"}`] = endpoint;
|
|
this.reconcilePhraseMembership();
|
|
this.editor.refreshSubmapVisibility();
|
|
this.editor.refreshConnectorGeometry();
|
|
debug("connector endpoint changed", {
|
|
connectorId: connector.id,
|
|
type,
|
|
previousItemId: previous ? previous.id : null,
|
|
itemId: endpoint.id
|
|
});
|
|
this.editor.scheduleHistoryCommit();
|
|
return true;
|
|
}
|
|
}
|