compleet ontvlochten
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
"use strict";
|
||||
|
||||
/** Manage concept dialogs, selection actions and CMap concept page metadata. */
|
||||
export class CmapConceptController {
|
||||
constructor({ state, api, translate, splitPageReference, pageReference,
|
||||
getEditor, getCurrentStorageMap, save, loadPages, getDialog, status,
|
||||
conceptIdForLabel, getNamespace, getPlacementContext }) {
|
||||
this.state = state;
|
||||
this.api = api;
|
||||
this.translate = translate;
|
||||
this.splitPageReference = splitPageReference;
|
||||
this.pageReference = pageReference;
|
||||
this.getEditor = getEditor;
|
||||
this.getCurrentStorageMap = getCurrentStorageMap;
|
||||
this.save = save;
|
||||
this.loadPages = loadPages;
|
||||
this.getDialog = getDialog;
|
||||
this.status = status;
|
||||
this.conceptIdForLabel = conceptIdForLabel;
|
||||
this.getNamespace = getNamespace;
|
||||
this.getPlacementContext = getPlacementContext;
|
||||
}
|
||||
|
||||
addNode(options = {}) {
|
||||
const editor = this.getEditor();
|
||||
if (!editor) return null;
|
||||
const conceptId = options.conceptId || this.conceptIdForLabel(options.label);
|
||||
return editor.addItem(conceptId ? { ...options, conceptId } : options);
|
||||
}
|
||||
|
||||
dialogResources() {
|
||||
const editor = this.getEditor();
|
||||
const namespaceModel = this.state.currentConceptMapSource?.model ||
|
||||
this.state.currentConceptMap?.model;
|
||||
return {
|
||||
cmapNamespace: namespaceModel?.metadata()?.namespace || "",
|
||||
pages: this.state.pages,
|
||||
conceptMaps: this.state.conceptMaps,
|
||||
parentMapAvailable: Boolean(editor && editor.activeMapRoot)
|
||||
};
|
||||
}
|
||||
|
||||
openDialog(record) {
|
||||
this.getDialog()?.open(record, this.dialogResources());
|
||||
}
|
||||
|
||||
openNewDialog(context = {}) {
|
||||
this.getDialog()?.openNew(context, this.dialogResources());
|
||||
}
|
||||
|
||||
async saveConcept(record, createContext, values) {
|
||||
const editor = this.getEditor();
|
||||
if ((!record && !createContext) || !editor) return false;
|
||||
const existingNonPersonTags = record && Array.isArray(record.tags) ?
|
||||
record.tags.filter((tag) => !(tag && typeof tag === "object" && tag.type === "person")) : [];
|
||||
const personTags = values.personNames.map((name) => ({ type: "person", value: name }));
|
||||
const changes = {
|
||||
kind: record && record.kind === "submap" ? "submap" :
|
||||
(values.pageSlug ? "page" : "concept"),
|
||||
label: values.label,
|
||||
synopsis: values.synopsis,
|
||||
aspects: values.aspects,
|
||||
tags: [...existingNonPersonTags, ...personTags],
|
||||
descriptionPageSlug: values.descriptionPageSlug,
|
||||
pageSlug: values.pageSlug,
|
||||
cmapSlug: values.cmapSlug,
|
||||
externalUrl: values.externalUrl,
|
||||
parentCmapLink: values.parentCmapLink,
|
||||
imageSource: values.imageSource,
|
||||
...values.appearance
|
||||
};
|
||||
const existingConceptId = this.conceptIdForLabel(values.label);
|
||||
if (!record || record.kind !== "submap") {
|
||||
changes.borderColor = values.pageSlug ? "#4479a1" :
|
||||
((values.cmapSlug || values.parentCmapLink) ? "#57834a" : "#a97c00");
|
||||
}
|
||||
if (record) {
|
||||
if (existingConceptId && record.conceptId !== existingConceptId) {
|
||||
const previousConceptId = record.conceptId;
|
||||
record.conceptId = existingConceptId;
|
||||
editor.refreshConceptUsageIndicators([previousConceptId, existingConceptId].filter(Boolean));
|
||||
}
|
||||
editor.updateItem(record, changes);
|
||||
editor.selectItem(record);
|
||||
} else {
|
||||
if (existingConceptId) changes.conceptId = existingConceptId;
|
||||
if (createContext.point) {
|
||||
changes.x = Math.max(0, createContext.point.x - 70);
|
||||
changes.y = Math.max(0, createContext.point.y - 30);
|
||||
}
|
||||
if (createContext.parentSubmap) {
|
||||
changes.parentSubmap = createContext.parentSubmap;
|
||||
changes.submapDepth = createContext.parentSubmap.submapDepth + 1;
|
||||
}
|
||||
const newRecord = this.addNode(changes);
|
||||
if (createContext.source && newRecord) editor.finishRelation(createContext.source, newRecord);
|
||||
else if (newRecord) editor.selectItem(newRecord);
|
||||
}
|
||||
editor.commitHistory();
|
||||
if (this.getCurrentStorageMap()) await this.save({ automatic: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
updateSelectionToolbar(editor, selectedRecords = [], connector = null) {
|
||||
const toolbar = document.getElementById("cmap-selection-toolbar");
|
||||
const selected = Array.isArray(selectedRecords) ? selectedRecords : [];
|
||||
toolbar.dataset.selectionCount = String(selected.length);
|
||||
for (const button of toolbar.querySelectorAll("[data-cmap-layout]")) {
|
||||
button.disabled = !editor || !editor.canLayoutSelection(button.dataset.cmapLayout);
|
||||
}
|
||||
const actionButtons = new Map(Array.from(
|
||||
toolbar.querySelectorAll("[data-cmap-selection-action]"),
|
||||
(button) => [button.dataset.cmapSelectionAction, button]));
|
||||
actionButtons.get("edit").disabled = !editor || selected.length !== 1 || Boolean(connector);
|
||||
actionButtons.get("group").disabled = !editor || !editor.canGroupSelection();
|
||||
actionButtons.get("ungroup").disabled = !editor || !editor.canUngroupSelection();
|
||||
actionButtons.get("hide").disabled = !editor || !editor.canHideSelectionInCurrentContext();
|
||||
document.getElementById("cmap-move-selected-to-namespace").disabled = !editor ||
|
||||
!this.getNamespace() || !selected.some((item) => item.kind !== "phrase");
|
||||
}
|
||||
|
||||
pageMatchesReference(page, reference) {
|
||||
if (page.slug === reference) return true;
|
||||
const parts = this.splitPageReference(reference);
|
||||
return String(page.namespace || "").toLocaleLowerCase() === parts.namespace.toLocaleLowerCase() &&
|
||||
String(page.pageSlug || this.splitPageReference(page.slug).slug).toLocaleLowerCase() ===
|
||||
parts.slug.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
async moveSelectedDescriptionsToNamespace() {
|
||||
const editor = this.getEditor();
|
||||
const namespace = this.getNamespace();
|
||||
if (!editor || !namespace) return false;
|
||||
const records = editor.selectedAll().filter((record) => record.kind !== "phrase");
|
||||
if (!records.length) return false;
|
||||
const changes = [];
|
||||
for (const record of records) {
|
||||
const current = record.descriptionPageSlug;
|
||||
if (!current) continue;
|
||||
const parts = this.splitPageReference(current);
|
||||
const target = this.pageReference(namespace, parts.slug);
|
||||
if (target === current) continue;
|
||||
const existing = this.state.pages.find((page) => this.pageMatchesReference(page, current));
|
||||
const conflict = this.state.pages.find((page) => this.pageMatchesReference(page, target));
|
||||
if (existing && conflict && existing !== conflict) {
|
||||
this.status(this.translate("namespace-move-conflict", "Cannot move {page}: the target page already exists.")
|
||||
.replace("{page}", target));
|
||||
return false;
|
||||
}
|
||||
changes.push({ record, current, target, existing });
|
||||
}
|
||||
if (!changes.length) return false;
|
||||
for (const change of changes) {
|
||||
if (change.existing) {
|
||||
const page = await this.api(`/api/pages/${encodeURIComponent(change.current)}/rename`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title: change.existing.title,
|
||||
namespace,
|
||||
slug: this.splitPageReference(change.current).slug,
|
||||
summary: this.translate("move-description-page-summary", "Moved description page to CMap namespace")
|
||||
})
|
||||
});
|
||||
change.target = page.slug;
|
||||
}
|
||||
editor.updateItem(change.record, { descriptionPageSlug: change.target });
|
||||
}
|
||||
await this.loadPages();
|
||||
editor.commitHistory();
|
||||
if (this.getCurrentStorageMap()) await this.save({ automatic: true });
|
||||
this.status(this.translate("descriptions-moved-to-namespace", "Selected descriptions moved to {namespace}.")
|
||||
.replace("{namespace}", namespace), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
placementOptions() {
|
||||
const context = this.getPlacementContext() || {};
|
||||
const options = {};
|
||||
if (context.point) {
|
||||
options.x = Math.max(0, context.point.x - 70);
|
||||
options.y = Math.max(0, context.point.y - 30);
|
||||
}
|
||||
if (context.parentSubmap) {
|
||||
options.parentSubmap = context.parentSubmap;
|
||||
options.submapDepth = context.parentSubmap.submapDepth + 1;
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
editSelected(record = null) {
|
||||
const editor = this.getEditor();
|
||||
const selectedRecord = record || editor?.selected();
|
||||
if (!selectedRecord) {
|
||||
window.alert(this.translate("select-one-concept", "Select one concept first."));
|
||||
return;
|
||||
}
|
||||
if (selectedRecord.kind === "phrase") {
|
||||
editor.editPhraseInline(selectedRecord);
|
||||
return;
|
||||
}
|
||||
this.openDialog(selectedRecord);
|
||||
}
|
||||
|
||||
groupSelection() {
|
||||
const editor = this.getEditor();
|
||||
if (!editor || !editor.canGroupSelection()) return false;
|
||||
const label = window.prompt(
|
||||
this.translate("group-submap-name", "Name of the main concept for the new sub-CMap"),
|
||||
this.translate("sub-concept-map", "Sub concept map"));
|
||||
if (!label || !label.trim()) return false;
|
||||
return editor.groupSelection({
|
||||
label: label.trim(),
|
||||
conceptId: this.conceptIdForLabel(label),
|
||||
childMap: label.trim(),
|
||||
synopsis: this.translate("grouped-submap-synopsis", "Grouped sub-concept map.")
|
||||
});
|
||||
}
|
||||
|
||||
populateSubmap(record, editor) {
|
||||
const baseX = Number(record.node.attr("x"));
|
||||
const baseY = Number(record.node.attr("y"));
|
||||
const detail = editor.addSubmapItem(record, {
|
||||
label: "Detail concept", synopsis: "Concept inside the expanded submap.",
|
||||
x: baseX + 315, y: baseY + 170, backgroundColor: "#fff4cf", borderColor: "#a97c00"
|
||||
});
|
||||
editor.connectWithPhrase(record, detail, "contains", false);
|
||||
if (record.submapDepth < 2) {
|
||||
const nested = editor.addSubmapItem(record, {
|
||||
label: "Nested submap", synopsis: "This submap can also be expanded.", kind: "submap",
|
||||
childMap: `${record.childMap || record.label}/nested`, x: baseX + 60, y: baseY + 310,
|
||||
backgroundColor: "#edf7e8", borderColor: "#57834a"
|
||||
});
|
||||
editor.connectWithPhrase(detail, nested, "contains", false);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user