chore(react/type_widget): get mindmap to render

This commit is contained in:
Elian Doran
2025-09-20 21:52:57 +03:00
parent cc19a217ad
commit 02259c55f3
4 changed files with 203 additions and 173 deletions

View File

@@ -17,6 +17,7 @@ import File from "./type_widgets/File";
import Image from "./type_widgets/Image"; import Image from "./type_widgets/Image";
import { ReadOnlyCode, EditableCode } from "./type_widgets/code/Code"; import { ReadOnlyCode, EditableCode } from "./type_widgets/code/Code";
import Mermaid from "./type_widgets/Mermaid"; import Mermaid from "./type_widgets/Mermaid";
import MindMap from "./type_widgets/MindMap";
/** /**
* A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one, * A `NoteType` altered by the note detail widget, taking into consideration whether the note is editable or not and adding special note types such as an empty one,
@@ -96,6 +97,7 @@ function getCorrespondingWidget(noteType: ExtendedNoteType | undefined, props: T
case "readOnlyCode": return <ReadOnlyCode {...props} /> case "readOnlyCode": return <ReadOnlyCode {...props} />
case "editableCode": return <EditableCode {...props} /> case "editableCode": return <EditableCode {...props} />
case "mermaid": return <Mermaid {...props} /> case "mermaid": return <Mermaid {...props} />
case "mindMap": return <MindMap {...props} />
default: break; default: break;
} }
} }

View File

@@ -0,0 +1,124 @@
.note-detail-mind-map {
height: 100%;
overflow: hidden !important;
}
.note-detail-mind-map .mind-map-container {
height: 100%;
}
.map-container .node-menu {
position: absolute;
top: 60px;
right: 20px;
bottom: 80px;
overflow: auto;
background: var(--panel-bgcolor);
color: var(--main-color);
border-radius: 5px;
box-shadow: 0 1px 2px #0003;
width: 240px;
box-sizing: border-box;
padding: 0 15px 15px;
transition: .3s all
}
.map-container .node-menu.close {
height: 29px;
width: 46px;
overflow: hidden
}
.map-container .node-menu .button-container {
padding: 3px 0;
direction: rtl
}
.map-container .node-menu #nm-tag {
margin-top: 20px
}
.map-container .node-menu .nm-fontsize-container {
display: flex;
justify-content: space-around;
margin-bottom: 20px
}
.map-container .node-menu .nm-fontsize-container div {
height: 36px;
width: 36px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 2px #0003;
background-color: #fff;
color: tomato;
border-radius: 100%
}
.map-container .node-menu .nm-fontcolor-container {
margin-bottom: 10px
}
.map-container .node-menu input,
.map-container .node-menu textarea {
background: var(--input-background-color);
border: 1px solid var(--panel-border-color);
border-radius: var(--bs-border-radius);
color: var(--main-color);
padding: 5px;
margin: 10px 0;
width: 100%;
box-sizing: border-box;
}
.map-container .node-menu textarea {
resize: none
}
.map-container .node-menu .split6 {
display: inline-block;
width: 16.66%;
margin-bottom: 5px
}
.map-container .node-menu .palette {
border-radius: 100%;
width: 21px;
height: 21px;
border: 1px solid #edf1f2;
margin: auto
}
.map-container .node-menu .nmenu-selected,
.map-container .node-menu .palette:hover {
box-shadow: tomato 0 0 0 2px;
background-color: #c7e9fa
}
.map-container .node-menu .size-selected {
background-color: tomato !important;
border-color: tomato;
fill: #fff;
color: #fff
}
.map-container .node-menu .size-selected svg {
color: #fff
}
.map-container .node-menu .bof {
text-align: center
}
.map-container .node-menu .bof span {
display: inline-block;
font-size: 14px;
border-radius: 4px;
padding: 2px 5px
}
.map-container .node-menu .bof .selected {
background-color: tomato;
color: #fff
}

View File

@@ -0,0 +1,77 @@
import { useCallback, useEffect, useRef } from "preact/hooks";
import { TypeWidgetProps } from "./type_widget";
import { MindElixirData, default as VanillaMindElixir } from "mind-elixir";
import { HTMLAttributes } from "preact";
// allow node-menu plugin css to be bundled by webpack
import nodeMenu from "@mind-elixir/node-menu";
import "mind-elixir/style";
import "@mind-elixir/node-menu/dist/style.css";
import "./MindMap.css";
const NEW_TOPIC_NAME = "";
interface MindmapModel extends MindElixirData {
direction: number;
}
interface MindElixirProps {
direction: number;
containerProps?: Omit<HTMLAttributes<HTMLDivElement>, "ref">;
content: MindElixirData;
}
export default function MindMap({ }: TypeWidgetProps) {
const content = VanillaMindElixir.new(NEW_TOPIC_NAME);
const onKeyDown = useCallback((e: KeyboardEvent) => {
/*
* Some global shortcuts interfere with the default shortcuts of the mind map,
* as defined here: https://mind-elixir.com/docs/guides/shortcuts
*/
if (e.key === "F1") {
e.stopPropagation();
}
// Zoom controls
const isCtrl = e.ctrlKey && !e.altKey && !e.metaKey;
if (isCtrl && (e.key == "-" || e.key == "=" || e.key == "0")) {
e.stopPropagation();
}
}, []);
return (
<div className="note-detail-mind-map note-detail-printable">
<MindElixir
content={content}
containerProps={{
className: "mind-map-container",
onKeyDown
}}
/>
</div>
)
}
function MindElixir({ content, containerProps, direction }: MindElixirProps) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const mind = new VanillaMindElixir({
el: containerRef.current,
direction
});
mind.install(nodeMenu);
mind.init(content);
return () => mind.destroy();
}, []);
return (
<div ref={containerRef} {...containerProps}>
</div>
)
}

View File

@@ -1,154 +1,9 @@
import TypeWidget from "./type_widget.js"; import TypeWidget from "./type_widget.js";
import utils from "../../services/utils.js"; import utils from "../../services/utils.js";
import type { MindElixirInstance } from "mind-elixir"; import type { MindElixirInstance } from "mind-elixir";
import nodeMenu from "@mind-elixir/node-menu";
import type FNote from "../../entities/fnote.js"; import type FNote from "../../entities/fnote.js";
import type { EventData } from "../../components/app_context.js"; import type { EventData } from "../../components/app_context.js";
// allow node-menu plugin css to be bundled by webpack
import "mind-elixir/style";
import "@mind-elixir/node-menu/dist/style.css";
const NEW_TOPIC_NAME = "";
const TPL = /*html*/`
<div class="note-detail-mind-map note-detail-printable">
<div class="mind-map-container">
</div>
<style>
.note-detail-mind-map {
height: 100%;
overflow: hidden !important;
}
.note-detail-mind-map .mind-map-container {
height: 100%;
}
.map-container .node-menu {
position: absolute;
top: 60px;
right: 20px;
bottom: 80px;
overflow: auto;
background: var(--panel-bgcolor);
color: var(--main-color);
border-radius: 5px;
box-shadow: 0 1px 2px #0003;
width: 240px;
box-sizing: border-box;
padding: 0 15px 15px;
transition: .3s all
}
.map-container .node-menu.close {
height: 29px;
width: 46px;
overflow: hidden
}
.map-container .node-menu .button-container {
padding: 3px 0;
direction: rtl
}
.map-container .node-menu #nm-tag {
margin-top: 20px
}
.map-container .node-menu .nm-fontsize-container {
display: flex;
justify-content: space-around;
margin-bottom: 20px
}
.map-container .node-menu .nm-fontsize-container div {
height: 36px;
width: 36px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 1px 2px #0003;
background-color: #fff;
color: tomato;
border-radius: 100%
}
.map-container .node-menu .nm-fontcolor-container {
margin-bottom: 10px
}
.map-container .node-menu input,
.map-container .node-menu textarea {
background: var(--input-background-color);
border: 1px solid var(--panel-border-color);
border-radius: var(--bs-border-radius);
color: var(--main-color);
padding: 5px;
margin: 10px 0;
width: 100%;
box-sizing: border-box;
}
.map-container .node-menu textarea {
resize: none
}
.map-container .node-menu .split6 {
display: inline-block;
width: 16.66%;
margin-bottom: 5px
}
.map-container .node-menu .palette {
border-radius: 100%;
width: 21px;
height: 21px;
border: 1px solid #edf1f2;
margin: auto
}
.map-container .node-menu .nmenu-selected,
.map-container .node-menu .palette:hover {
box-shadow: tomato 0 0 0 2px;
background-color: #c7e9fa
}
.map-container .node-menu .size-selected {
background-color: tomato !important;
border-color: tomato;
fill: #fff;
color: #fff
}
.map-container .node-menu .size-selected svg {
color: #fff
}
.map-container .node-menu .bof {
text-align: center
}
.map-container .node-menu .bof span {
display: inline-block;
font-size: 14px;
border-radius: 4px;
padding: 2px 5px
}
.map-container .node-menu .bof .selected {
background-color: tomato;
color: #fff
}
</style>
</div>
`;
interface MindmapModel {
direction: number;
}
export default class MindMapWidget extends TypeWidget { export default class MindMapWidget extends TypeWidget {
private $content!: JQuery<HTMLElement>; private $content!: JQuery<HTMLElement>;
@@ -161,24 +16,6 @@ export default class MindMapWidget extends TypeWidget {
} }
doRender() { doRender() {
this.$widget = $(TPL);
this.$content = this.$widget.find(".mind-map-container");
this.$content.on("keydown", (e) => {
/*
* Some global shortcuts interfere with the default shortcuts of the mind map,
* as defined here: https://mind-elixir.com/docs/guides/shortcuts
*/
if (e.key === "F1") {
e.stopPropagation();
}
// Zoom controls
const isCtrl = e.ctrlKey && !e.altKey && !e.metaKey;
if (isCtrl && (e.key == "-" || e.key == "=" || e.key == "0")) {
e.stopPropagation();
}
});
// Save the mind map if the user changes the layout direction. // Save the mind map if the user changes the layout direction.
this.$content.on("click", ".mind-elixir-toolbar.lt", () => { this.$content.on("click", ".mind-elixir-toolbar.lt", () => {
this.spacedUpdate.scheduleUpdate(); this.spacedUpdate.scheduleUpdate();
@@ -213,13 +50,9 @@ export default class MindMapWidget extends TypeWidget {
} }
async #initLibrary(direction?: number) { async #initLibrary(direction?: number) {
this.MindElixir = (await import("mind-elixir")).default;
const mind = new this.MindElixir({ const mind = new this.MindElixir({
el: this.$content[0],
direction: direction ?? this.MindElixir.LEFT direction: direction ?? this.MindElixir.LEFT
}); });
mind.install(nodeMenu);
this.mind = mind; this.mind = mind;
mind.init(this.MindElixir.new(NEW_TOPIC_NAME)); mind.init(this.MindElixir.new(NEW_TOPIC_NAME));
@@ -230,12 +63,6 @@ export default class MindMapWidget extends TypeWidget {
this.spacedUpdate.scheduleUpdate(); this.spacedUpdate.scheduleUpdate();
} }
}); });
// If the note is displayed directly after a refresh, the scroll ends up at (0,0), making it difficult for the user to see.
// Adding an arbitrary wait until the element is attached to the DOM seems to do the trick for now.
setTimeout(() => {
mind.toCenter();
}, 200);
} }
async getData() { async getData() {