"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; } }