verdere refactoring
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
"use strict";
|
||||
|
||||
/** Own creation, loading and destruction of the wiki's active CMap editor. */
|
||||
export class CmapEditorHost {
|
||||
constructor({ canvas, factory, createOptions = null, getModel = null, onCreated = null, onDestroyed = null }) {
|
||||
this.canvas = canvas;
|
||||
this.factory = factory;
|
||||
this.createOptions = createOptions;
|
||||
this.getModel = getModel;
|
||||
this.onCreated = onCreated;
|
||||
this.onDestroyed = onDestroyed;
|
||||
this.editor = null;
|
||||
}
|
||||
|
||||
create(options = null, model = null) {
|
||||
this.destroy();
|
||||
if (!this.factory) return null;
|
||||
const editorOptions = options ?? this.createOptions?.() ?? {};
|
||||
this.editor = this.factory(this.canvas, editorOptions);
|
||||
const loadedModel = model || this.getModel?.();
|
||||
if (loadedModel) this.editor.loadModel(loadedModel);
|
||||
if (this.onCreated) this.onCreated(this.editor);
|
||||
return this.editor;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (!this.editor) return;
|
||||
this.editor.destroy();
|
||||
if (this.onDestroyed) this.onDestroyed(this.editor);
|
||||
this.editor = null;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
"use strict";
|
||||
|
||||
import { escapeHtml } from "../../cmap/cmap-utils.js";
|
||||
import { splitPageReference } from "../reference.js";
|
||||
|
||||
/** Render wiki-specific HTML content for CMap editor items. */
|
||||
export class CmapEditorPresentation {
|
||||
@@ -37,7 +38,7 @@ export class CmapEditorPresentation {
|
||||
`<span class="cmap-card-usage" title="${escapeHtml(usageLabel)}">(${usageCount})</span>`;
|
||||
const descriptionReference = this.canonicalPageReference(record.descriptionPageSlug || "");
|
||||
const descriptionExists = Boolean(descriptionReference &&
|
||||
this.state.pages.some((page) => page.slug === descriptionReference));
|
||||
this.state.pages.some((page) => this.pageMatchesReference(page, descriptionReference)));
|
||||
const descriptionButton = record.descriptionPageSlug ?
|
||||
`<button type="button" class="rw-cmap-view-description ${descriptionExists ? "is-filled" : "is-empty"}" aria-label="${escapeHtml(this.translate("view-concept-description", "View concept description"))}">I</button>` : "";
|
||||
const linkedTarget = record.pageSlug || record.cmapSlug || record.parentCmapLink;
|
||||
@@ -63,4 +64,13 @@ export class CmapEditorPresentation {
|
||||
const current = typeof entry === "number" ? entry : Number(entry?.count) || 0;
|
||||
return Math.max(1, storedTotal - current + (Number(record.usageCount) || 1));
|
||||
}
|
||||
|
||||
pageMatchesReference(page, reference) {
|
||||
if (page.slug === reference) return true;
|
||||
const parts = splitPageReference(reference);
|
||||
return String(page.namespace || "").toLocaleLowerCase() ===
|
||||
parts.namespace.toLocaleLowerCase() &&
|
||||
String(page.pageSlug || page.slug || "").toLocaleLowerCase() ===
|
||||
parts.slug.toLocaleLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
"use strict";
|
||||
|
||||
/** Connect CMap toolbar actions to an editor and application callbacks. */
|
||||
export class CmapEditorUiController {
|
||||
constructor({ root, getEditor, actions = {} }) {
|
||||
this.root = root;
|
||||
this.getEditor = getEditor;
|
||||
this.actions = actions;
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
bind(selector, event, handler) {
|
||||
const element = this.root.querySelector(selector);
|
||||
if (!element) return;
|
||||
const listener = (eventObject) => handler(eventObject, this.getEditor());
|
||||
element.addEventListener(event, listener);
|
||||
this.listeners.push(() => element.removeEventListener(event, listener));
|
||||
}
|
||||
|
||||
initialize() {
|
||||
this.bind("#cmap-save-map", "click", () => this.actions.save?.());
|
||||
this.bind("#cmap-undo", "click", (_event, editor) => editor?.undo());
|
||||
this.bind("#cmap-redo", "click", (_event, editor) => editor?.redo());
|
||||
this.bind("#cmap-select-all", "click", (_event, editor) => editor?.selectAll());
|
||||
this.bind("#cmap-delete-selected", "click", (_event, editor) => editor?.deleteSelection());
|
||||
this.bind("#cmap-edit-selected", "click", () => this.actions.edit?.());
|
||||
this.bind("#cmap-copy-selected", "click", (_event, editor) => editor?.copySelectionReferences());
|
||||
this.bind("#cmap-cut-selected", "click", (_event, editor) => editor?.cutSelectionReferences());
|
||||
this.bind("#cmap-paste-concepts", "click", (_event, editor) => editor?.pasteConceptReferences());
|
||||
this.bind("#cmap-group-selected", "click", () => this.actions.group?.());
|
||||
this.bind("#cmap-ungroup-selected", "click", (_event, editor) => editor?.ungroupSelection());
|
||||
this.bind("#cmap-hide-selected", "click", (_event, editor) => editor?.hideSelectionInCurrentContext());
|
||||
this.bind("#cmap-select-all", "click", (_event, editor) => editor?.selectAll());
|
||||
this.bind("#cmap-zoom-out", "click", () => this.actions.zoom?.(-10));
|
||||
this.bind("#cmap-zoom-in", "click", () => this.actions.zoom?.(10));
|
||||
this.bind("#cmap-zoom-reset", "click", () => this.actions.zoom?.(100, true));
|
||||
this.bind("#cmap-zoom-percent", "change", (event) => this.actions.zoom?.(event.target.value, true));
|
||||
this.bind("#cmap-toggle-page-guides", "click", () => this.actions.togglePageGuides?.());
|
||||
this.bind("#cmap-selection-toolbar", "click", (event, editor) => {
|
||||
const button = event.target.closest("button");
|
||||
if (!button || button.disabled || !editor) return;
|
||||
if (button.dataset.cmapLayout) {
|
||||
editor.applySelectionLayout(button.dataset.cmapLayout);
|
||||
} else {
|
||||
this.actions.selection?.(button.dataset.cmapSelectionAction, editor);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
for (const remove of this.listeners) remove();
|
||||
this.listeners = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
"use strict";
|
||||
|
||||
/** Coordinate CMap routes, parent contexts and browser-history state. */
|
||||
export class CmapNavigationController {
|
||||
constructor({ navigateToHash, cmapRoute, getEditor, getCurrentMap,
|
||||
getParentMap, storage, historyObject = window.history, locationObject = window.location }) {
|
||||
this.navigateToHash = navigateToHash;
|
||||
this.cmapRoute = cmapRoute;
|
||||
this.getEditor = getEditor;
|
||||
this.getCurrentMap = getCurrentMap;
|
||||
this.getParentMap = getParentMap;
|
||||
this.storage = storage;
|
||||
this.history = historyObject;
|
||||
this.location = locationObject;
|
||||
}
|
||||
|
||||
open(slug) {
|
||||
return this.navigateToHash(this.cmapRoute(slug));
|
||||
}
|
||||
|
||||
openLinked(record) {
|
||||
if (!record?.cmapSlug) return false;
|
||||
this.rememberContext();
|
||||
this.open(record.cmapSlug).catch((error) => console.error(error));
|
||||
return true;
|
||||
}
|
||||
|
||||
openParent() {
|
||||
const source = this.getParentMap();
|
||||
if (source?.slug) {
|
||||
this.open(source.slug).catch((error) => console.error(error));
|
||||
return true;
|
||||
}
|
||||
const editor = this.getEditor();
|
||||
return Boolean(editor?.openParentMap());
|
||||
}
|
||||
|
||||
requestTransition(action) {
|
||||
return this.storage.requestTransition(action);
|
||||
}
|
||||
|
||||
rememberContext() {
|
||||
const editor = this.getEditor();
|
||||
const current = this.getCurrentMap();
|
||||
const root = editor?.activeMapRoot;
|
||||
if (!current?.slug || !root) return;
|
||||
this.history.replaceState({ ...this.history.state, cmapContext: {
|
||||
mapSlug: current.slug,
|
||||
rootItemId: Number(root.id)
|
||||
} }, "", this.location.href);
|
||||
}
|
||||
|
||||
async restoreContext(slug) {
|
||||
const context = this.history.state?.cmapContext;
|
||||
if (!context || context.mapSlug !== slug) return false;
|
||||
const editor = this.getEditor();
|
||||
const root = editor?.itemRecord(context.rootItemId);
|
||||
if (root?.kind === "submap" && root.separateMap) editor.openSubmapMap(root);
|
||||
this.history.replaceState({ ...this.history.state, cmapContext: null }, "", this.location.href);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
"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;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
"use strict";
|
||||
|
||||
/** Coordinate CMap import and export workflows without owning their formats. */
|
||||
export class CmapTransferController {
|
||||
constructor({ repository, jsonImporter, jsonExporter, markdownExporter,
|
||||
getCurrentMap, getPages, getConceptMaps, loadPages, loadConceptMaps,
|
||||
navigateToMap, saveBeforeTransfer, confirm, status, translate }) {
|
||||
Object.assign(this, {
|
||||
repository, jsonImporter, jsonExporter, markdownExporter, getCurrentMap,
|
||||
getPages, getConceptMaps, loadPages, loadConceptMaps, navigateToMap,
|
||||
saveBeforeTransfer, confirm, status, translate
|
||||
});
|
||||
}
|
||||
|
||||
async exportMarkdown(options) {
|
||||
const map = this.getCurrentMap();
|
||||
if (!map) throw new Error(this.translate("cmap-export-unavailable", "CMap export is unavailable."));
|
||||
await this.saveBeforeTransfer();
|
||||
return this.markdownExporter.export(map, options);
|
||||
}
|
||||
|
||||
async exportJson(depth) {
|
||||
const map = this.getCurrentMap();
|
||||
if (!map) throw new Error(this.translate("cmap-json-unavailable", "CMap JSON export is unavailable."));
|
||||
await this.saveBeforeTransfer();
|
||||
return this.jsonExporter.export(map, depth);
|
||||
}
|
||||
|
||||
async importFile(file) {
|
||||
const bundle = await this.jsonImporter.read(file);
|
||||
await this.saveBeforeTransfer();
|
||||
const conflicts = this.jsonImporter.conflicts(bundle, this.getPages(), this.getConceptMaps());
|
||||
const replaceExisting = (conflicts.pages.length || conflicts.conceptMaps.length) && this.confirm(
|
||||
this.translate("cmap-import-conflicts", "The import contains existing CMaps or pages. Replace them?"));
|
||||
const result = await this.jsonImporter.import(bundle, {
|
||||
pages: this.getPages(), conceptMaps: this.getConceptMaps(), replaceExisting,
|
||||
summary: this.translate("imported-from-cmap-json", "Imported from CMap JSON")
|
||||
});
|
||||
await this.loadPages();
|
||||
await this.loadConceptMaps();
|
||||
await this.navigateToMap(bundle.rootCmapSlug);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,11 @@ import { CmapHistoryDialog } from "./dialogs/history-dialog.js";
|
||||
import { CmapMetadataDialog } from "./dialogs/metadata-dialog.js";
|
||||
import { CmapPeopleDialog } from "./dialogs/people-dialog.js";
|
||||
import { CmapUnsavedDialog } from "./dialogs/unsaved-dialog.js";
|
||||
import { CmapStorageController } from "./cmap-storage-controller.js";
|
||||
import { CmapNavigationController } from "./cmap-navigation-controller.js";
|
||||
import { CmapTransferController } from "./cmap-transfer-controller.js";
|
||||
import { CmapEditorUiController } from "./cmap-editor-ui-controller.js";
|
||||
import { CmapEditorHost } from "./cmap-editor-host.js";
|
||||
import { CmapEmbedView } from "./cmap-embed-view.js";
|
||||
import { CmapEditorPresentation } from "./cmap-editor-presentation.js";
|
||||
|
||||
@@ -117,10 +122,84 @@ export class CmapWorkspaceController {
|
||||
const exportDialog = new CmapExportDialog(
|
||||
$("cmap-export-dialog"), tr, buildCurrentCmapMarkdownExport, buildCurrentCmapJsonExport);
|
||||
const unsavedDialog = new CmapUnsavedDialog($("cmap-unsaved-dialog"));
|
||||
const storage = new CmapStorageController({
|
||||
repository: cmapRepository,
|
||||
getEditor: () => cmapPrototypeState().editor,
|
||||
getCurrentMap: currentCmapStorageMap,
|
||||
getViewVisible: () => !$("cmap-view").classList.contains("hidden"),
|
||||
renderSvg: currentCmapRenderedSvg,
|
||||
reloadMaps: loadConceptMaps,
|
||||
status: showCmapStatus,
|
||||
translate: tr,
|
||||
unsavedDialog,
|
||||
canEdit: () => can("editor"),
|
||||
onMapSaved: (saved) => {
|
||||
if (!state.currentConceptMap && !state.currentConceptMapSource) {
|
||||
state.currentConceptMap = saved;
|
||||
const savedRoute = cmapRoute(saved.slug);
|
||||
history.replaceState(history.state, "", `${location.pathname}${location.search}${savedRoute}`);
|
||||
state.cmapGuardHash = savedRoute;
|
||||
} else if (state.currentConceptMapSource?.slug === saved.slug) {
|
||||
state.currentConceptMapSource = saved;
|
||||
} else if (state.currentConceptMap?.slug === saved.slug) {
|
||||
state.currentConceptMap = saved;
|
||||
}
|
||||
updateStoredConceptMapSummary(saved);
|
||||
}
|
||||
});
|
||||
const navigation = new CmapNavigationController({
|
||||
navigateToHash,
|
||||
cmapRoute,
|
||||
getEditor: () => cmapPrototypeState().editor,
|
||||
getCurrentMap: currentCmapStorageMap,
|
||||
getParentMap: () => state.currentConceptMapSource,
|
||||
storage
|
||||
});
|
||||
const transfer = new CmapTransferController({
|
||||
repository: cmapRepository,
|
||||
jsonImporter,
|
||||
jsonExporter,
|
||||
markdownExporter,
|
||||
getCurrentMap: () => state.currentConceptMap,
|
||||
getPages: () => state.pages,
|
||||
getConceptMaps: () => state.conceptMaps,
|
||||
loadPages,
|
||||
loadConceptMaps,
|
||||
navigateToMap: (slug) => navigation.open(slug),
|
||||
saveBeforeTransfer: () => cmapHasUnsavedChanges() ?
|
||||
saveStoredConceptMap({ automatic: true, force: true, historyMode: "autosave" }) : true,
|
||||
confirm: (message) => window.confirm(message),
|
||||
status: showCmapStatus,
|
||||
translate: tr
|
||||
});
|
||||
const editorUi = new CmapEditorUiController({
|
||||
root: document,
|
||||
getEditor: () => cmapPrototypeState().editor,
|
||||
actions: {
|
||||
save: () => saveStoredConceptMap(),
|
||||
edit: () => editSelectedCmapNode(),
|
||||
group: () => groupSelectedCmapItems(),
|
||||
zoom: (value, absolute = false) => setCmapZoom(
|
||||
absolute ? Number(value) : Number($("cmap-zoom-percent").value) + value),
|
||||
togglePageGuides: () => {
|
||||
const visible = $("cmap-toggle-page-guides").getAttribute("aria-checked") !== "true";
|
||||
setCmapPageGuides(visible);
|
||||
settingsRepository.setPageGuidesVisible(visible).catch((error) => showCmapStatus(error.message));
|
||||
},
|
||||
selection: (action, editor) => {
|
||||
if (action === "edit") editSelectedCmapNode();
|
||||
if (action === "group") groupSelectedCmapItems();
|
||||
if (action === "ungroup") editor.ungroupSelection();
|
||||
if (action === "hide") editor.hideSelectionInCurrentContext();
|
||||
}
|
||||
}
|
||||
});
|
||||
const editorHost = new CmapEditorHost({
|
||||
canvas: $("cmap-canvas"),
|
||||
factory: (canvas, options) => window.RacketWikiCmap.createEditor(canvas, options),
|
||||
createOptions: () => ({})
|
||||
});
|
||||
|
||||
let pendingCmapTransition = null;
|
||||
let cmapAutosaveTimer = null;
|
||||
let cmapSavePromise = null;
|
||||
let cmapEmbedHydrationTimer = null;
|
||||
const CMAP_AUTOSAVE_DELAY = 1500;
|
||||
|
||||
@@ -211,45 +290,19 @@ export class CmapWorkspaceController {
|
||||
}
|
||||
|
||||
function openLinkedCmap(record) {
|
||||
if (!record || !record.cmapSlug) return false;
|
||||
rememberActiveCmapContext();
|
||||
navigateToHash(cmapRoute(record.cmapSlug)).catch((error) => console.error(error));
|
||||
return true;
|
||||
return navigation.openLinked(record);
|
||||
}
|
||||
|
||||
function rememberActiveCmapContext() {
|
||||
const prototype = cmapPrototypeState();
|
||||
const editor = prototype.editor;
|
||||
const mapSlug = state.currentConceptMap?.slug;
|
||||
const root = editor?.activeMapRoot;
|
||||
if (!mapSlug || !root) return;
|
||||
const context = {
|
||||
mapSlug,
|
||||
rootItemId: Number(root.id)
|
||||
};
|
||||
history.replaceState({ ...history.state, cmapContext: context }, "", location.href);
|
||||
return navigation.rememberContext();
|
||||
}
|
||||
|
||||
async function restoreActiveCmapContext(slug) {
|
||||
const context = history.state?.cmapContext;
|
||||
if (!context || context.mapSlug !== slug) return false;
|
||||
const prototype = cmapPrototypeState();
|
||||
const root = prototype.editor?.itemRecord(context.rootItemId);
|
||||
if (root && root.kind === "submap" && root.separateMap) {
|
||||
prototype.editor.openSubmapMap(root);
|
||||
}
|
||||
history.replaceState({ ...history.state, cmapContext: null }, "", location.href);
|
||||
return true;
|
||||
return navigation.restoreContext(slug);
|
||||
}
|
||||
|
||||
function openParentCmap() {
|
||||
const source = state.currentConceptMapSource;
|
||||
if (source && source.slug) {
|
||||
navigateToHash(cmapRoute(source.slug)).catch((error) => console.error(error));
|
||||
return true;
|
||||
}
|
||||
const prototype = cmapPrototypeState();
|
||||
return Boolean(prototype.editor && prototype.editor.openParentMap());
|
||||
return navigation.openParent();
|
||||
}
|
||||
|
||||
function titledCmapComboboxEntry(record) {
|
||||
@@ -441,6 +494,68 @@ export class CmapWorkspaceController {
|
||||
actionButtons.get("group").disabled = !editor || !editor.canGroupSelection();
|
||||
actionButtons.get("ungroup").disabled = !editor || !editor.canUngroupSelection();
|
||||
actionButtons.get("hide").disabled = !editor || !editor.canHideSelectionInCurrentContext();
|
||||
const namespaceButton = $("cmap-move-selected-to-namespace");
|
||||
namespaceButton.disabled = !editor || !diagramNamespace() ||
|
||||
!selected.some((item) => item.kind !== "phrase");
|
||||
}
|
||||
|
||||
function diagramNamespace() {
|
||||
const model = state.currentConceptMapSource?.model || state.currentConceptMap?.model;
|
||||
return model?.metadata()?.namespace || "";
|
||||
}
|
||||
|
||||
function pageMatchesReference(page, reference) {
|
||||
if (page.slug === reference) return true;
|
||||
const parts = splitPageReference(reference);
|
||||
return String(page.namespace || "").toLocaleLowerCase() === parts.namespace.toLocaleLowerCase() &&
|
||||
String(page.pageSlug || splitPageReference(page.slug).slug).toLocaleLowerCase() ===
|
||||
parts.slug.toLocaleLowerCase();
|
||||
}
|
||||
|
||||
async function moveSelectedDescriptionsToNamespace() {
|
||||
const editor = cmapPrototypeState().editor;
|
||||
const namespace = diagramNamespace();
|
||||
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 = splitPageReference(current);
|
||||
const target = pageReference(namespace, parts.slug);
|
||||
if (target === current) continue;
|
||||
const existing = state.pages.find((page) => pageMatchesReference(page, current));
|
||||
const conflict = state.pages.find((page) => pageMatchesReference(page, target));
|
||||
if (existing && conflict && existing !== conflict) {
|
||||
showCmapStatus(tr("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 api(`/api/pages/${encodeURIComponent(change.current)}/rename`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
title: change.existing.title,
|
||||
namespace,
|
||||
slug: splitPageReference(change.current).slug,
|
||||
summary: tr("move-description-page-summary", "Moved description page to CMap namespace")
|
||||
})
|
||||
});
|
||||
change.target = page.slug;
|
||||
}
|
||||
editor.updateItem(change.record, { descriptionPageSlug: change.target });
|
||||
}
|
||||
await loadPages();
|
||||
editor.commitHistory();
|
||||
if (currentCmapStorageMap()) await saveStoredConceptMap({ automatic: true });
|
||||
showCmapStatus(tr("descriptions-moved-to-namespace", "Selected descriptions moved to {namespace}.")
|
||||
.replace("{namespace}", namespace), true);
|
||||
return true;
|
||||
}
|
||||
|
||||
function openCmapContextMenu(clientX, clientY, createContext = {}) {
|
||||
@@ -623,10 +738,7 @@ export class CmapWorkspaceController {
|
||||
closeCmapContextMenu();
|
||||
$("cmap-map-navigation").classList.add("hidden");
|
||||
$("cmap-active-map-title").textContent = "";
|
||||
if (state.cmapPrototype && state.cmapPrototype.editor &&
|
||||
typeof state.cmapPrototype.editor.destroy === "function") {
|
||||
state.cmapPrototype.editor.destroy();
|
||||
}
|
||||
editorHost.destroy();
|
||||
canvas.replaceChildren();
|
||||
state.cmapPrototype = null;
|
||||
updateCmapSelectionToolbar(null, [], null);
|
||||
@@ -648,7 +760,7 @@ export class CmapWorkspaceController {
|
||||
}
|
||||
|
||||
const prototype = cmapPrototypeState();
|
||||
prototype.editor = window.RacketWikiCmap.createEditor(canvas, {
|
||||
prototype.editor = editorHost.create({
|
||||
renderItem: (record) => cmapNodeHtml(record),
|
||||
boundaryReferenceMapTitle: state.currentConceptMapSource?.title ||
|
||||
state.currentConceptMap?.title || "",
|
||||
@@ -658,7 +770,9 @@ export class CmapWorkspaceController {
|
||||
state.currentConceptMap?.model?.metadata()?.namespace || "";
|
||||
const isDescriptionReference = record.descriptionPageSlug === record.pageSlug;
|
||||
const page = isDescriptionReference ? splitPageReference(record.pageSlug) : null;
|
||||
const target = namespace && page?.namespace === "cmap" ?
|
||||
const existingPage = state.pages.some((pageRecord) =>
|
||||
pageRecord.slug === record.pageSlug);
|
||||
const target = !existingPage && namespace && page?.namespace === "cmap" ?
|
||||
pageReference(namespace, page.slug) : record.pageSlug;
|
||||
if (isDescriptionReference && target !== record.pageSlug) {
|
||||
record.pageSlug = target;
|
||||
@@ -855,9 +969,7 @@ export class CmapWorkspaceController {
|
||||
}
|
||||
|
||||
function currentCmapSnapshot() {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (!prototype.editor) return null;
|
||||
return JSON.stringify(prototype.editor.toDocument());
|
||||
return storage.currentSnapshot();
|
||||
}
|
||||
|
||||
function currentCmapRenderedSvg() {
|
||||
@@ -876,37 +988,20 @@ export class CmapWorkspaceController {
|
||||
}
|
||||
|
||||
function markCurrentCmapSaved(snapshot = currentCmapSnapshot()) {
|
||||
storage.markSaved(snapshot);
|
||||
state.cmapSavedSnapshot = snapshot;
|
||||
}
|
||||
|
||||
function cmapHasUnsavedChanges() {
|
||||
if ($("cmap-view").classList.contains("hidden")) return false;
|
||||
const currentSnapshot = currentCmapSnapshot();
|
||||
if (currentSnapshot === null || state.cmapSavedSnapshot === null) return false;
|
||||
if (currentSnapshot === state.cmapSavedSnapshot) return false;
|
||||
const editor = cmapPrototypeState().editor;
|
||||
if (editor && !editor.canUndo() && !editor.history.hasPendingCommit()) {
|
||||
state.cmapSavedSnapshot = currentSnapshot;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
return storage.hasUnsavedChanges();
|
||||
}
|
||||
|
||||
function cancelCmapAutosave() {
|
||||
if (cmapAutosaveTimer !== null) {
|
||||
window.clearTimeout(cmapAutosaveTimer);
|
||||
cmapAutosaveTimer = null;
|
||||
}
|
||||
storage.cancelAutosave();
|
||||
}
|
||||
|
||||
function scheduleCmapAutosave() {
|
||||
cancelCmapAutosave();
|
||||
if (!can("editor") || !state.currentConceptMap || !cmapHasUnsavedChanges()) return;
|
||||
showCmapStatus(tr("autosave-pending", "Changes waiting to be saved"));
|
||||
cmapAutosaveTimer = window.setTimeout(() => {
|
||||
cmapAutosaveTimer = null;
|
||||
saveStoredConceptMap({ automatic: true }).catch((error) => console.error(error));
|
||||
}, CMAP_AUTOSAVE_DELAY);
|
||||
storage.scheduleAutosave();
|
||||
}
|
||||
|
||||
function updateStoredConceptMapSummary(conceptMap) {
|
||||
@@ -922,29 +1017,7 @@ export class CmapWorkspaceController {
|
||||
}
|
||||
|
||||
async function requestCmapTransition(action) {
|
||||
if (!cmapHasUnsavedChanges()) {
|
||||
await action();
|
||||
return true;
|
||||
}
|
||||
if (pendingCmapTransition) return false;
|
||||
|
||||
const transition = (async () => {
|
||||
const choice = await unsavedDialog.choose();
|
||||
if (choice === "save" && !await saveStoredConceptMap()) return false;
|
||||
if (choice === "discard") markCurrentCmapSaved();
|
||||
if (choice === "cancel") {
|
||||
renderConceptMapSelector();
|
||||
return false;
|
||||
}
|
||||
await action();
|
||||
return true;
|
||||
})();
|
||||
pendingCmapTransition = transition;
|
||||
try {
|
||||
return await transition;
|
||||
} finally {
|
||||
if (pendingCmapTransition === transition) pendingCmapTransition = null;
|
||||
}
|
||||
return navigation.requestTransition(action);
|
||||
}
|
||||
|
||||
async function openStoredConceptMap(slug) {
|
||||
@@ -1192,14 +1265,7 @@ export class CmapWorkspaceController {
|
||||
}
|
||||
|
||||
async function buildCurrentCmapMarkdownExport({ depth, includeWikiPages }) {
|
||||
if (!state.currentConceptMap) {
|
||||
throw new Error(tr("cmap-export-unavailable", "CMap export is unavailable."));
|
||||
}
|
||||
if (cmapHasUnsavedChanges() && can("editor")) {
|
||||
const saved = await saveStoredConceptMap({ automatic: true, force: true, historyMode: "autosave" });
|
||||
if (!saved) throw new Error(tr("save-before-export-failed", "The CMap could not be saved before export."));
|
||||
}
|
||||
return markdownExporter.export(state.currentConceptMap, {
|
||||
return transfer.exportMarkdown({
|
||||
maxDepth: depth,
|
||||
includeWikiPages,
|
||||
language: state.language
|
||||
@@ -1207,43 +1273,11 @@ export class CmapWorkspaceController {
|
||||
}
|
||||
|
||||
async function buildCurrentCmapJsonExport(depth) {
|
||||
if (!state.currentConceptMap) {
|
||||
throw new Error(tr("cmap-json-unavailable", "CMap JSON export is unavailable."));
|
||||
}
|
||||
if (cmapHasUnsavedChanges() && can("editor")) {
|
||||
const saved = await saveStoredConceptMap({ automatic: true, force: true, historyMode: "autosave" });
|
||||
if (!saved) throw new Error(tr("save-before-export-failed", "The CMap could not be saved before export."));
|
||||
}
|
||||
return jsonExporter.export(state.currentConceptMap, depth);
|
||||
return transfer.exportJson(depth);
|
||||
}
|
||||
|
||||
async function importCmapBundleFile(file) {
|
||||
const bundle = await jsonImporter.read(file);
|
||||
if (cmapHasUnsavedChanges()) {
|
||||
const saved = await saveStoredConceptMap({ automatic: true, force: true, historyMode: "autosave" });
|
||||
if (!saved) throw new Error(tr("save-before-import-failed", "The current CMap could not be saved before import."));
|
||||
}
|
||||
|
||||
const conflicts = jsonImporter.conflicts(bundle, state.pages, state.conceptMaps);
|
||||
let replaceExisting = false;
|
||||
if (conflicts.pages.length || conflicts.conceptMaps.length) {
|
||||
replaceExisting = window.confirm(tr(
|
||||
"cmap-import-conflicts",
|
||||
"The import contains {maps} existing CMaps and {pages} existing pages. Choose OK to replace them with the imported content, or Cancel to keep them and import only new records.")
|
||||
.replace("{maps}", conflicts.conceptMaps.length)
|
||||
.replace("{pages}", conflicts.pages.length));
|
||||
}
|
||||
|
||||
const result = await jsonImporter.import(bundle, {
|
||||
pages: state.pages,
|
||||
conceptMaps: state.conceptMaps,
|
||||
replaceExisting,
|
||||
summary: tr("imported-from-cmap-json", "Imported from CMap JSON")
|
||||
});
|
||||
await loadPages();
|
||||
await loadConceptMaps();
|
||||
await navigateToHash(cmapRoute(bundle.rootCmapSlug));
|
||||
return result;
|
||||
return transfer.importFile(file);
|
||||
}
|
||||
|
||||
async function handleCmapImportFile(file) {
|
||||
@@ -1266,7 +1300,7 @@ export class CmapWorkspaceController {
|
||||
|
||||
async function deleteStoredConceptMap() {
|
||||
cancelCmapAutosave();
|
||||
if (cmapSavePromise) await cmapSavePromise;
|
||||
await storage.waitForSave();
|
||||
const conceptMap = state.currentConceptMap;
|
||||
if (!conceptMap) return false;
|
||||
const question = tr(
|
||||
@@ -1304,85 +1338,7 @@ export class CmapWorkspaceController {
|
||||
async function saveStoredConceptMap({ automatic = false, force = false,
|
||||
summary = null, snapshotVersion = false,
|
||||
historyMode = null } = {}) {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (!prototype.editor) return false;
|
||||
cancelCmapAutosave();
|
||||
if (automatic && !currentCmapStorageMap()) return false;
|
||||
const effectiveHistoryMode = historyMode ||
|
||||
(snapshotVersion ? "snapshot" : (automatic ? "autosave" : "manual"));
|
||||
if (cmapSavePromise) {
|
||||
const firstSaveSucceeded = await cmapSavePromise;
|
||||
if (!firstSaveSucceeded) return false;
|
||||
if (force || cmapHasUnsavedChanges() || effectiveHistoryMode !== "autosave") {
|
||||
return saveStoredConceptMap({ automatic, force, summary, snapshotVersion, historyMode });
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
const snapshot = currentCmapSnapshot();
|
||||
if (snapshot === null) return false;
|
||||
if (!force && snapshot === state.cmapSavedSnapshot && effectiveHistoryMode === "autosave") {
|
||||
return true;
|
||||
}
|
||||
|
||||
const model = prototype.editor.currentModel();
|
||||
const renderedSvg = currentCmapRenderedSvg();
|
||||
const conceptMapAtStart = currentCmapStorageMap();
|
||||
let saveSucceeded = false;
|
||||
showCmapStatus(snapshotVersion ? tr("creating-snapshot", "Creating snapshot…") :
|
||||
(automatic ? tr("autosaving", "Saving automatically…") : tr("saving", "Saving…")));
|
||||
cmapSavePromise = (async () => {
|
||||
try {
|
||||
if (!conceptMapAtStart) {
|
||||
const title = window.prompt(tr("concept-map-name", "Concept map name"), "");
|
||||
if (!title || !title.trim()) {
|
||||
showCmapStatus("");
|
||||
return false;
|
||||
}
|
||||
state.currentConceptMap = await cmapRepository.create(title.trim(), model, null, { renderedSvg });
|
||||
const savedRoute = cmapRoute(state.currentConceptMap.slug);
|
||||
history.replaceState(history.state, "", `${location.pathname}${location.search}${savedRoute}`);
|
||||
state.cmapGuardHash = savedRoute;
|
||||
await loadConceptMaps();
|
||||
} else {
|
||||
const savedConceptMap = await cmapRepository.save(
|
||||
conceptMapAtStart,
|
||||
model,
|
||||
{
|
||||
summary: summary || (automatic ?
|
||||
tr("automatic-save", "Automatic save") :
|
||||
tr("manual-save", "Manual save")),
|
||||
snapshot: snapshotVersion,
|
||||
saveKind: effectiveHistoryMode,
|
||||
renderedSvg
|
||||
});
|
||||
if (state.currentConceptMapSource &&
|
||||
state.currentConceptMapSource.slug === conceptMapAtStart.slug) {
|
||||
state.currentConceptMapSource = savedConceptMap;
|
||||
updateStoredConceptMapSummary(savedConceptMap);
|
||||
} else if (state.currentConceptMap &&
|
||||
state.currentConceptMap.slug === conceptMapAtStart.slug) {
|
||||
state.currentConceptMap = savedConceptMap;
|
||||
updateStoredConceptMapSummary(savedConceptMap);
|
||||
}
|
||||
}
|
||||
markCurrentCmapSaved(snapshot);
|
||||
await loadConceptMaps();
|
||||
showCmapStatus(
|
||||
snapshotVersion ? tr("snapshot-created", "Snapshot created") :
|
||||
(automatic ? tr("concept-map-autosaved", "CMap saved automatically") : tr("concept-map-saved", "CMap saved")),
|
||||
true);
|
||||
saveSucceeded = true;
|
||||
return true;
|
||||
} catch (error) {
|
||||
showCmapStatus(error.message);
|
||||
return false;
|
||||
}
|
||||
})().finally(() => {
|
||||
cmapSavePromise = null;
|
||||
if (saveSucceeded && cmapHasUnsavedChanges()) scheduleCmapAutosave();
|
||||
});
|
||||
return cmapSavePromise;
|
||||
return storage.save({ automatic, force, summary, snapshotVersion, historyMode });
|
||||
}
|
||||
|
||||
async function createConceptMapSnapshot() {
|
||||
@@ -1479,7 +1435,6 @@ export class CmapWorkspaceController {
|
||||
showCmapStatus(error.message);
|
||||
});
|
||||
});
|
||||
$("cmap-save-map").addEventListener("click", () => saveStoredConceptMap());
|
||||
$("cmap-rename-map").addEventListener("click", () => renameStoredConceptMap());
|
||||
$("cmap-edit-metadata").addEventListener("click", openCmapMetadataDialog);
|
||||
$("cmap-export-markdown").addEventListener("click", openCmapExportDialog);
|
||||
@@ -1491,6 +1446,10 @@ export class CmapWorkspaceController {
|
||||
$("cmap-manage-people").addEventListener("click", () => {
|
||||
peopleDialog.open().catch((error) => showCmapStatus(error.message));
|
||||
});
|
||||
$("cmap-move-selected-to-namespace").addEventListener("click", () => {
|
||||
moveSelectedDescriptionsToNamespace()
|
||||
.catch((error) => showCmapStatus(error.message));
|
||||
});
|
||||
$("cmap-delete-map").addEventListener("click", () => deleteStoredConceptMap());
|
||||
$("cmap-create-snapshot").addEventListener("click", () => {
|
||||
createConceptMapSnapshot().catch((error) => showCmapStatus(error.message));
|
||||
@@ -1521,83 +1480,10 @@ export class CmapWorkspaceController {
|
||||
});
|
||||
});
|
||||
}
|
||||
$("cmap-edit-selected").addEventListener("click", () => editSelectedCmapNode());
|
||||
$("cmap-cut-selected").addEventListener("click", () => {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (prototype.editor) prototype.editor.cutSelectionReferences();
|
||||
});
|
||||
$("cmap-copy-selected").addEventListener("click", () => {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (prototype.editor) prototype.editor.copySelectionReferences();
|
||||
});
|
||||
$("cmap-paste-concepts").addEventListener("click", () => {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (prototype.editor) prototype.editor.pasteConceptReferences();
|
||||
});
|
||||
$("cmap-undo").addEventListener("click", () => {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (prototype.editor) prototype.editor.undo();
|
||||
});
|
||||
$("cmap-redo").addEventListener("click", () => {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (prototype.editor) prototype.editor.redo();
|
||||
});
|
||||
$("cmap-select-all").addEventListener("click", () => {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (prototype.editor) prototype.editor.selectAll();
|
||||
});
|
||||
$("cmap-group-selected").addEventListener("click", () => {
|
||||
groupSelectedCmapItems();
|
||||
});
|
||||
$("cmap-ungroup-selected").addEventListener("click", () => {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (prototype.editor) prototype.editor.ungroupSelection();
|
||||
});
|
||||
$("cmap-hide-selected").addEventListener("click", () => {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (prototype.editor) prototype.editor.hideSelectionInCurrentContext();
|
||||
});
|
||||
$("cmap-delete-selected").addEventListener("click", () => {
|
||||
const prototype = cmapPrototypeState();
|
||||
if (prototype.editor) prototype.editor.deleteSelection();
|
||||
});
|
||||
$("cmap-selection-toolbar").addEventListener("click", (event) => {
|
||||
const button = event.target.closest("button");
|
||||
if (!button || button.disabled) return;
|
||||
const prototype = cmapPrototypeState();
|
||||
const editor = prototype.editor;
|
||||
if (!editor) return;
|
||||
const layoutCommand = button.dataset.cmapLayout;
|
||||
if (layoutCommand) {
|
||||
editor.applySelectionLayout(layoutCommand);
|
||||
return;
|
||||
}
|
||||
switch (button.dataset.cmapSelectionAction) {
|
||||
case "edit":
|
||||
editSelectedCmapNode();
|
||||
break;
|
||||
case "group":
|
||||
groupSelectedCmapItems();
|
||||
break;
|
||||
case "ungroup":
|
||||
editor.ungroupSelection();
|
||||
break;
|
||||
case "hide":
|
||||
editor.hideSelectionInCurrentContext();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
});
|
||||
$("cmap-toggle-page-guides").addEventListener("click", () => {
|
||||
const visible = $("cmap-toggle-page-guides").getAttribute("aria-checked") !== "true";
|
||||
setCmapPageGuides(visible);
|
||||
settingsRepository.setPageGuidesVisible(visible)
|
||||
.catch((error) => {
|
||||
setCmapPageGuides(settingsRepository.pageGuidesVisible);
|
||||
showCmapStatus(error.message);
|
||||
});
|
||||
});
|
||||
$("cmap-reset").addEventListener("click", () => {
|
||||
if (state.currentConceptMap) {
|
||||
requestCmapTransition(() => openStoredConceptMap(state.currentConceptMap.slug))
|
||||
@@ -1649,10 +1535,6 @@ export class CmapWorkspaceController {
|
||||
parentSubmap: prototype.editor.submapAtPoint(point)
|
||||
});
|
||||
});
|
||||
$("cmap-zoom-out").addEventListener("click", () => setCmapZoom(Number($("cmap-zoom-percent").value) - 10));
|
||||
$("cmap-zoom-in").addEventListener("click", () => setCmapZoom(Number($("cmap-zoom-percent").value) + 10));
|
||||
$("cmap-zoom-reset").addEventListener("click", () => setCmapZoom(100));
|
||||
$("cmap-zoom-percent").addEventListener("change", (event) => setCmapZoom(event.target.value));
|
||||
$("cmap-import-file").addEventListener("change", (event) => {
|
||||
const file = event.currentTarget.files && event.currentTarget.files[0];
|
||||
if (file) handleCmapImportFile(file);
|
||||
@@ -1662,7 +1544,7 @@ export class CmapWorkspaceController {
|
||||
!$("cmap-view").classList.contains("hidden")) {
|
||||
event.preventDefault();
|
||||
if (can("editor")) {
|
||||
if (pendingCmapTransition) {
|
||||
if (storage.hasPendingTransition()) {
|
||||
unsavedDialog.chooseSave();
|
||||
return;
|
||||
}
|
||||
@@ -1674,6 +1556,8 @@ export class CmapWorkspaceController {
|
||||
handleCmapKeyboardShortcut(event);
|
||||
});
|
||||
|
||||
editorUi.initialize();
|
||||
|
||||
/**
|
||||
* Load persistent CMap preferences and the data needed by the workspace.
|
||||
*/
|
||||
@@ -1702,6 +1586,9 @@ export class CmapWorkspaceController {
|
||||
this.normalizeExternalUrl = normalizedCmapExternalUrl;
|
||||
this.clearDescriptionPreviews = () => cmapDescriptionPreview.clear();
|
||||
this.requestTransition = requestCmapTransition;
|
||||
this.navigation = navigation;
|
||||
this.transfer = transfer;
|
||||
this.storage = storage;
|
||||
this.show = showCmapPrototype;
|
||||
this.hasUnsavedChanges = cmapHasUnsavedChanges;
|
||||
this.startSlug = startCmapSlug;
|
||||
|
||||
Reference in New Issue
Block a user