169 lines
5.7 KiB
JavaScript
169 lines
5.7 KiB
JavaScript
"use strict";
|
|
|
|
/** Coordinate CMap persistence, dirty state, autosave and transitions. */
|
|
export class CmapStorageController {
|
|
constructor({ repository, getEditor, getCurrentMap, getViewVisible, renderSvg,
|
|
reloadMaps, status, translate, unsavedDialog, canEdit, onMapSaved }) {
|
|
this.repository = repository;
|
|
this.getEditor = getEditor;
|
|
this.getCurrentMap = getCurrentMap;
|
|
this.getViewVisible = getViewVisible;
|
|
this.renderSvg = renderSvg;
|
|
this.reloadMaps = reloadMaps;
|
|
this.status = status;
|
|
this.translate = translate;
|
|
this.unsavedDialog = unsavedDialog;
|
|
this.canEdit = canEdit;
|
|
this.onMapSaved = onMapSaved;
|
|
this.savedSnapshot = null;
|
|
this.autosaveTimer = null;
|
|
this.savePromise = null;
|
|
this.pendingTransition = null;
|
|
}
|
|
|
|
currentSnapshot() {
|
|
const editor = this.getEditor();
|
|
return editor ? JSON.stringify(editor.toDocument()) : null;
|
|
}
|
|
|
|
markSaved(snapshot = this.currentSnapshot()) {
|
|
this.savedSnapshot = snapshot;
|
|
}
|
|
|
|
hasUnsavedChanges() {
|
|
if (!this.getViewVisible()) return false;
|
|
const current = this.currentSnapshot();
|
|
if (current === null || this.savedSnapshot === null || current === this.savedSnapshot) return false;
|
|
const editor = this.getEditor();
|
|
if (editor && !editor.canUndo() && !editor.history.hasPendingCommit()) {
|
|
this.savedSnapshot = current;
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
cancelAutosave() {
|
|
if (this.autosaveTimer !== null) {
|
|
window.clearTimeout(this.autosaveTimer);
|
|
this.autosaveTimer = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
this.status(this.translate("autosave-pending", "Changes waiting to be saved"));
|
|
this.autosaveTimer = window.setTimeout(() => {
|
|
this.autosaveTimer = null;
|
|
this.save({ automatic: true }).catch((error) => console.error(error));
|
|
}, 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();
|
|
return true;
|
|
}
|
|
if (this.pendingTransition) return false;
|
|
const transition = (async () => {
|
|
const choice = await this.unsavedDialog.choose();
|
|
if (choice === "save" && !await this.save()) return false;
|
|
if (choice === "discard") this.markSaved();
|
|
if (choice === "cancel") return false;
|
|
await action();
|
|
return true;
|
|
})();
|
|
this.pendingTransition = transition;
|
|
try {
|
|
return await transition;
|
|
} finally {
|
|
if (this.pendingTransition === transition) this.pendingTransition = null;
|
|
}
|
|
}
|
|
|
|
hasPendingTransition() {
|
|
return this.pendingTransition !== null;
|
|
}
|
|
|
|
hasSaveInProgress() {
|
|
return this.savePromise !== null;
|
|
}
|
|
|
|
async waitForSave() {
|
|
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();
|
|
if (!editor) return false;
|
|
this.cancelAutosave();
|
|
if (automatic && !this.getCurrentMap()) return false;
|
|
const effectiveHistoryMode = historyMode ||
|
|
(snapshotVersion ? "snapshot" : (automatic ? "autosave" : "manual"));
|
|
if (this.savePromise) {
|
|
const succeeded = await this.savePromise;
|
|
if (!succeeded) return false;
|
|
if (force || this.hasUnsavedChanges() || effectiveHistoryMode !== "autosave") {
|
|
return this.save({ automatic, force, summary, snapshotVersion, historyMode });
|
|
}
|
|
return true;
|
|
}
|
|
const snapshot = this.currentSnapshot();
|
|
if (snapshot === null) return false;
|
|
if (!force && snapshot === this.savedSnapshot && effectiveHistoryMode === "autosave") return true;
|
|
|
|
const model = editor.currentModel();
|
|
const renderedSvg = typeof this.renderSvg === "function" ? this.renderSvg() : "";
|
|
const storedMap = this.getCurrentMap();
|
|
let succeeded = false;
|
|
this.status(this.translate("saving", "Saving…"));
|
|
this.savePromise = (async () => {
|
|
try {
|
|
if (!storedMap) {
|
|
const title = window.prompt(this.translate("concept-map-name", "Concept map name"), "");
|
|
if (!title || !title.trim()) return false;
|
|
const created = await this.repository.create(title.trim(), model, null, { renderedSvg });
|
|
if (this.onMapSaved) this.onMapSaved(created);
|
|
} else {
|
|
const saved = await this.repository.save(storedMap, model, {
|
|
summary: summary || this.translate("manual-save", "Manual save"),
|
|
snapshot: snapshotVersion,
|
|
saveKind: effectiveHistoryMode,
|
|
renderedSvg
|
|
});
|
|
if (this.onMapSaved) this.onMapSaved(saved);
|
|
}
|
|
this.markSaved(snapshot);
|
|
await this.reloadMaps();
|
|
succeeded = true;
|
|
return true;
|
|
} catch (error) {
|
|
this.status(error.message);
|
|
return false;
|
|
}
|
|
})().finally(() => {
|
|
this.savePromise = null;
|
|
if (succeeded && this.hasUnsavedChanges()) this.scheduleAutosave();
|
|
});
|
|
return this.savePromise;
|
|
}
|
|
|
|
destroy() {
|
|
this.cancelAutosave();
|
|
}
|
|
}
|