diff --git a/src/main.ts b/src/main.ts index 00d6590..9930ec1 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,7 +11,7 @@ import { MM_VIEW_TYPE } from './constants'; export default class MarkMap extends Plugin { vault: Vault; workspace: Workspace; - markMapView: MindmapView; + mindmapView: MindmapView; async onload() { console.log("Loading Mind Map plugin"); @@ -21,7 +21,7 @@ import { MM_VIEW_TYPE } from './constants'; this.registerView( MM_VIEW_TYPE, (leaf: WorkspaceLeaf) => - (this.markMapView = new MindmapView(leaf, {path:this.activeLeafPath(this.workspace), basename: this.activeLeafName(this.workspace)})) + (this.mindmapView = new MindmapView(leaf, {path:this.activeLeafPath(this.workspace), basename: this.activeLeafName(this.workspace)})) ); this.addCommand({ diff --git a/src/mindmap-view.ts b/src/mindmap-view.ts new file mode 100644 index 0000000..31e3d8b --- /dev/null +++ b/src/mindmap-view.ts @@ -0,0 +1,173 @@ +import { EventRef, ItemView, Menu, Vault, Workspace, WorkspaceLeaf } from 'obsidian'; +import { transform } from 'markmap-lib'; +import { Markmap } from 'markmap-view'; +import { INode } from 'markmap-common'; +import { MD_VIEW_TYPE, MM_VIEW_TYPE } from './constants'; +import ObsidianMarkmap from './obsidian-markmap-plugin'; +import { createSVG, getComputedCss, removeExistingSVG } from './markmap-svg'; +import { copyImageToClipboard } from './copy-image'; + +export default class MindmapView extends ItemView { + filePath: string; + fileName: string; + linkedLeaf: WorkspaceLeaf; + displayText: string; + currentMd: string; + vault: Vault; + workspace: Workspace; + listeners: EventRef[]; + emptyDiv: HTMLDivElement; + svg: SVGElement; + obsMarkmap: ObsidianMarkmap; + + getViewType(): string { + return MM_VIEW_TYPE; + } + + getDisplayText(): string { + return this.displayText ?? 'Mind Map'; + } + + getIcon() { + return "dot-network"; + } + + onMoreOptionsMenu(menu: Menu) { + menu.addItem((item) => + item + .setIcon('image-file') + .setTitle('Copy screenshot') + .onClick(() => copyImageToClipboard(this.svg)) + + ); + menu.showAtPosition({x: 0, y: 0}); + } + + constructor(leaf: WorkspaceLeaf, initialFileInfo: {path:string, basename:string}){ + super(leaf); + this.filePath = initialFileInfo.path; + this.fileName = initialFileInfo.basename; + this.vault = this.app.vault; + this.workspace = this.app.workspace; + } + + async onOpen() { + this.obsMarkmap = new ObsidianMarkmap(this.vault); + this.registerActiveLeafUpdate(); + this.listeners = [ + this.workspace.on('layout-ready', () => this.update()), + this.workspace.on('resize', () => this.update()), + this.workspace.on('css-change', () => this.update()), + ]; + // this.leaf.on('group-change', (group) => this.updateLinkedLeaf(group, this)); + } + + async onClose() { + this.listeners.forEach(listener => this.workspace.offref(listener)); + } + + registerActiveLeafUpdate() { + this.registerInterval( + window.setInterval(() => this.checkAndUpdate(), 1000) + ); + } + + async checkAndUpdate() { + try { + if(await this.checkActiveLeaf()) { + this.update(); + } + } catch (error) { + console.log(error) + } + } + + updateLinkedLeaf(group: string, mmView: MindmapView) { + if(group === null) { + mmView.linkedLeaf = undefined; + return; + } + const mdLinkedLeaf = mmView.workspace.getGroupLeaves(group).filter(l => l.view.getViewType() === MM_VIEW_TYPE)[0]; + mmView.linkedLeaf = mdLinkedLeaf; + this.checkAndUpdate(); + } + + async update(){ + if(this.filePath) { + await this.readMarkDown(); + if(this.currentMd.length === 0 || this.getLeafTarget().view.getViewType() != MD_VIEW_TYPE){ + this.displayEmpty(true); + removeExistingSVG(); + } else { + const { root, features } = await this.transformMarkdown(); + this.displayEmpty(false); + this.svg = createSVG(this.containerEl); + this.renderMarkmap(root, this.svg); + } + } + this.displayText = this.fileName != undefined ? `Mind Map of ${this.fileName}` : 'Mind Map'; + this.load(); + } + + async checkActiveLeaf() { + if(this.app.workspace.activeLeaf.view.getViewType() === MM_VIEW_TYPE){ + return false; + } + const pathHasChanged = this.readFilePath(); + const markDownHasChanged = await this.readMarkDown(); + const updateRequired = pathHasChanged || markDownHasChanged; + return updateRequired; + } + + readFilePath() { + const fileInfo = (this.getLeafTarget().view as any).file; + const pathHasChanged = this.filePath != fileInfo.path; + this.filePath = fileInfo.path; + this.fileName = fileInfo.basename; + return pathHasChanged; + } + + getLeafTarget() {; + return this.linkedLeaf != undefined ? this.linkedLeaf : this.app.workspace.activeLeaf; + } + + async readMarkDown() { + const md = await this.app.vault.adapter.read(this.filePath); + const markDownHasChanged = this.currentMd != md; + this.currentMd = md; + return markDownHasChanged; + } + + async transformMarkdown() { + const { root, features } = transform(this.currentMd); + this.obsMarkmap.updateInternalLinks(root); + return { root, features }; + } + + async renderMarkmap(root: INode, svg: SVGElement) { + const { font } = getComputedCss(this.containerEl); + const options = { + autoFit: false, + duration: 10, + nodeFont: font + }; + try { + const markmapSVG = Markmap.create(svg, options, root); + } catch (error) { + console.error(error); + } + } + + displayEmpty(display: boolean) { + if(this.emptyDiv === undefined) { + const div = document.createElement('div') + div.className = 'pane-empty'; + div.innerText = 'No content found'; + removeExistingSVG(); + this.containerEl.children[1].appendChild(div); + this.emptyDiv = div; + } + const style = display ? 'display: block' : 'display: none'; + this.emptyDiv.setAttr('style', style); + } +} \ No newline at end of file