refactoring van de cmap structuren bijna compleet
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
"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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user