documentatie

This commit is contained in:
2026-09-03 17:24:56 +02:00
parent e971dfe942
commit 42fcea9af8
14 changed files with 135 additions and 16 deletions
@@ -48,6 +48,12 @@ export class CmapConceptController {
this.getDialog()?.openNew(context, this.dialogResources());
}
/**
* goal : Apply one validated concept-dialog submission as one editor history step.
* pre : record identifies an existing placement, or createContext describes a new placement.
* post : Shared concept content and placement presentation are updated and autosave is scheduled.
* result : False when the editor or required create context is unavailable.
*/
async saveConcept(record, createContext, values) {
const editor = this.getEditor();
if ((!record && !createContext) || !editor) return false;
@@ -101,6 +107,7 @@ export class CmapConceptController {
return true;
}
/** Update selection-dependent layout and concept action availability in the toolbar. */
updateSelectionToolbar(editor, selectedRecords = [], connector = null) {
const toolbar = document.getElementById("cmap-selection-toolbar");
const selected = Array.isArray(selectedRecords) ? selectedRecords : [];
@@ -127,6 +134,12 @@ export class CmapConceptController {
parts.slug.toLocaleLowerCase();
}
/**
* goal : Move selected concept description pages into the active CMap namespace.
* pre : An active CMap namespace exists and selected records may have description references.
* post : Existing pages are renamed, editor references are changed and the map is autosaved.
* result : False when no applicable selection exists or a target-page conflict is found.
*/
async moveSelectedDescriptionsToNamespace() {
const editor = this.getEditor();
const namespace = this.getNamespace();
@@ -187,6 +200,7 @@ export class CmapConceptController {
return options;
}
/** Open inline phrase editing or the concept dialog for the selected editor record. */
editSelected(record = null) {
const editor = this.getEditor();
const selectedRecord = record || editor?.selected();
@@ -201,6 +215,7 @@ export class CmapConceptController {
this.openDialog(selectedRecord);
}
/** Group the current editor selection into a named inline submap. */
groupSelection() {
const editor = this.getEditor();
if (!editor || !editor.canGroupSelection()) return false;
@@ -216,6 +231,7 @@ export class CmapConceptController {
});
}
/** Populate a newly opened sample submap with its initial child records. */
populateSubmap(record, editor) {
const baseX = Number(record.node.attr("x"));
const baseY = Number(record.node.attr("y"));
+6
View File
@@ -12,6 +12,11 @@ export class CmapEditorHost {
this.editor = null;
}
/**
* goal : Replace any active editor with a newly configured editor instance.
* post : The previous editor is destroyed; an optional model is loaded before onCreated runs.
* result : The active editor, or null when no factory is configured.
*/
create(options = null, model = null) {
this.destroy();
if (!this.factory) return null;
@@ -23,6 +28,7 @@ export class CmapEditorHost {
return this.editor;
}
/** Destroy the active editor once and release the host's reference to it. */
destroy() {
if (!this.editor) return;
this.editor.destroy();
@@ -17,6 +17,10 @@ export class CmapEditorUiController {
this.listeners.push(() => element.removeEventListener(event, listener));
}
/**
* Bind every standard CMap toolbar action once.
* result : This controller, so the host may retain the initialized instance.
*/
initialize() {
this.bind("#cmap-save-map", "click", () => this.actions.save?.());
this.bind("#cmap-undo", "click", (_event, editor) => editor?.undo());
@@ -47,6 +51,7 @@ export class CmapEditorUiController {
return this;
}
/** Remove every listener installed by initialize before the host is disposed. */
destroy() {
for (const remove of this.listeners) remove();
this.listeners = [];
@@ -29,6 +29,11 @@ export class CmapMapController {
this.refreshPageConnections = refreshPageConnections;
}
/**
* goal : Refresh the CMap catalogue and its lightweight global reference index.
* post : Workspace CMap lists, selector and usage indicators reflect the repository.
* result : The current list of stored CMaps.
*/
async loadMaps() {
const [conceptMaps, placements] = await Promise.all([
this.repository.list(),
@@ -65,6 +70,11 @@ export class CmapMapController {
this.renderSelector();
}
/**
* goal : Open a stored CMap or its source map when it is a derived submap view.
* pre : slug identifies a stored CMap; a later request supersedes this load safely.
* post : Active map state, editor document and saved snapshot are synchronized.
*/
async open(slug) {
if (!slug) return;
const loadSequence = ++this.state.cmapLoadSequence;
@@ -103,6 +113,7 @@ export class CmapMapController {
this.status(this.translate("concept-map-loaded", "CMap loaded"), true);
}
/** Restore the source submap context for a stored derived CMap. */
openDerivedView(conceptMap, derivedView) {
const editor = this.getEditor();
const root = editor.itemRecord(derivedView.rootItemId);
@@ -124,6 +135,10 @@ export class CmapMapController {
editor.openSubmapMap(root);
}
/**
* goal : Load one immutable CMap version for review or subsequent explicit save.
* post : The editor shows the historical model while the current map remains dirty.
*/
async loadHistoricalVersion(version) {
const conceptMap = this.getCurrentStorageMap();
if (!conceptMap) return;
@@ -145,6 +160,11 @@ export class CmapMapController {
location.hash = this.cmapRoute(conceptMap.slug);
}
/**
* goal : Extract the selected inline submap into a complete stored CMap.
* post : Parent and child map are saved consistently; cross-boundary relations remain on the parent.
* result : Whether extraction completed or was deliberately cancelled.
*/
async promoteSelectedSubmap() {
const editor = this.getEditor();
const record = editor ? editor.selected() : null;
@@ -263,6 +283,11 @@ export class CmapMapController {
{ tags: [], summary: "", explanationPageSlug: "" };
}
/**
* goal : Persist map metadata through either the source editor or a derived map model.
* post : Namespace, tags, summary and explanation-page reference are stored together.
* result : False when there is no editable map or saving fails.
*/
async saveMetadata(metadata) {
const conceptMap = this.state.currentConceptMap;
const editor = this.getEditor();
@@ -290,6 +315,11 @@ export class CmapMapController {
return true;
}
/**
* goal : Archive the active CMap after explicit title confirmation.
* post : Active map state is cleared and navigation continues to another CMap or overview.
* result : False when confirmation is cancelled, mismatched or persistence fails.
*/
async archive() {
this.cancelAutosave();
const conceptMap = this.state.currentConceptMap;
@@ -324,6 +354,7 @@ export class CmapMapController {
}
}
/** Create an explicit, named immutable snapshot of the active stored CMap. */
async createSnapshot() {
if (!this.getCurrentStorageMap()) return false;
const description = window.prompt(this.translate("snapshot-description", "Snapshot description"), "");
@@ -14,10 +14,12 @@ export class CmapNavigationController {
this.location = locationObject;
}
/** Open a stored CMap through the central hash-navigation contract. */
open(slug) {
return this.navigateToHash(this.cmapRoute(slug));
}
/** Remember the active separate-submap root before following a linked CMap. */
openLinked(record) {
if (!record?.cmapSlug) return false;
this.rememberContext();
@@ -25,6 +27,7 @@ export class CmapNavigationController {
return true;
}
/** Return to a source CMap when present, otherwise leave one inline submap level. */
openParent() {
const source = this.getParentMap();
if (source?.slug) {
@@ -39,6 +42,7 @@ export class CmapNavigationController {
return this.storage.requestTransition(action);
}
/** Store the active separate-submap context in the current browser history entry. */
rememberContext() {
const editor = this.getEditor();
const current = this.getCurrentMap();
@@ -50,6 +54,10 @@ export class CmapNavigationController {
} }, "", this.location.href);
}
/**
* Restore a remembered separate-submap root after browser navigation.
* post : A consumed context is removed from history state to prevent repeated restoration.
*/
async restoreContext(slug) {
const context = this.history.state?.cmapContext;
if (!context || context.mapSlug !== slug) return false;
@@ -49,6 +49,10 @@ export class CmapStorageController {
}
}
/**
* goal : Queue one delayed autosave for the current dirty CMap.
* post : Any preceding autosave timer is replaced; non-editable or clean maps are not queued.
*/
scheduleAutosave() {
this.cancelAutosave();
if (!this.canEdit() || !this.getCurrentMap() || !this.hasUnsavedChanges()) return;
@@ -59,6 +63,10 @@ export class CmapStorageController {
}, 1500);
}
/**
* goal : Guard a route or view transition while unsaved CMap changes exist.
* result : True only when the action ran after saving, discarding or finding no changes.
*/
async requestTransition(action) {
if (!this.hasUnsavedChanges()) {
await action();
@@ -93,6 +101,11 @@ export class CmapStorageController {
if (this.savePromise) await this.savePromise;
}
/**
* goal : Persist the active editor model and its derived SVG render in one ordered workflow.
* post : A successful save updates the current map, saved snapshot and CMap catalogue.
* result : False for cancellation, unavailable editor or persistence failure.
*/
async save({ automatic = false, force = false, summary = null,
snapshotVersion = false, historyMode = null } = {}) {
const editor = this.getEditor();
@@ -12,6 +12,7 @@ export class CmapTransferController {
});
}
/** Save pending changes, then create the requested readable Markdown export. */
async exportMarkdown(options) {
const map = this.getCurrentMap();
if (!map) throw new Error(this.translate("cmap-export-unavailable", "CMap export is unavailable."));
@@ -19,6 +20,7 @@ export class CmapTransferController {
return this.markdownExporter.export(map, options);
}
/** Save pending changes, then create the requested portable JSON bundle. */
async exportJson(depth) {
const map = this.getCurrentMap();
if (!map) throw new Error(this.translate("cmap-json-unavailable", "CMap JSON export is unavailable."));
@@ -26,6 +28,11 @@ export class CmapTransferController {
return this.jsonExporter.export(map, depth);
}
/**
* goal : Import a validated CMap bundle after presenting replacement conflicts.
* post : Page and CMap catalogues are refreshed and navigation opens the imported root CMap.
* result : The importer summary.
*/
async importFile(file) {
const bundle = await this.jsonImporter.read(file);
await this.saveBeforeTransfer();
@@ -292,6 +292,19 @@ export class CmapWorkspaceController {
return text.length > 150 ? `${text.slice(0, 147)}` : text;
}
function showCmapDescriptionTooltip(button) {
const item = button.closest("[data-rw-cmap-item-id]");
const editor = cmapPrototypeState().editor;
const record = item && editor ? editor.itemRecord(Number(item.dataset.rwCmapItemId)) : null;
if (record?.descriptionPageSlug) {
cmapDescriptionPreview.show(button, record.descriptionPageSlug, tr("loading", "Loading…"));
}
}
function hideCmapDescriptionTooltip() {
cmapDescriptionPreview.hide();
}
function addCmapPrototypeNode(options = {}) {
return conceptController.addNode(options);
}