Skip to content

Mind map view #3

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion components/Block/BlockContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ export const BlockContent: React.FC<PropsType> = ({
const isEditing = editingBlockId === shallowBlock.id;

if (!isEditing) {
return <Preview className={className} blockId={shallowBlock.id} />;
return (
<Preview
className={className + " whitespace-pre-wrap"}
blockId={shallowBlock.id}
/>
);
}

return (
Expand Down
26 changes: 18 additions & 8 deletions components/Block/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,26 @@ import clsx from "clsx";
import deepEqual from "fast-deep-equal/es6/react";
import { useAtomValue, useUpdateAtom } from "jotai/utils";
import { nanoid } from "nanoid";
import { memo as ReactMemo, useCallback } from "react";
import {
memo as ReactMemo,
useCallback,
forwardRef,
CSSProperties,
RefObject,
} from "react";
import { useAdapter } from "../Editor/adapters/AdapterContext";
import { Token, tokenizer } from "../Editor/parser";
import { anchorOffsetAtom, editingBlockIdAtom } from "../Editor/store";

export type PropsType = {
blockId: string;
className: string;
style?: CSSProperties;
};
const PreviewImpl: React.FC<PropsType> = ({ blockId, className }) => {
const PreviewImpl: React.ForwardRefRenderFunction<HTMLElement, PropsType> = (
{ blockId, className, style },
ref
) => {
const { blockFamily, pageIdAtom } = useAdapter();
const setAnchorOffset = useUpdateAtom(anchorOffsetAtom);
const setEditingBlockId = useUpdateAtom(editingBlockIdAtom);
Expand Down Expand Up @@ -45,11 +55,10 @@ const PreviewImpl: React.FC<PropsType> = ({ blockId, className }) => {
}
`}</style>
<div
className={clsx(
"preview flex-1 cursor-text whitespace-pre-wrap",
className
)}
className={clsx("preview flex-1 cursor-text", className)}
onClick={focusCallback}
ref={ref as RefObject<HTMLDivElement>}
style={style}
>
{parsed.map((token) => {
return (
Expand All @@ -66,6 +75,7 @@ const PreviewImpl: React.FC<PropsType> = ({ blockId, className }) => {
);
};

export const Preview = ReactMemo(PreviewImpl, (prevProps, nextProps) =>
deepEqual(prevProps, nextProps)
export const Preview = ReactMemo(
forwardRef(PreviewImpl),
(prevProps, nextProps) => deepEqual(prevProps, nextProps)
);
2 changes: 1 addition & 1 deletion components/Editor/adapters/memory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ const isStaleAtom = atom(false);
const defaultPageIdFromRoute = () => {
if (process.browser) {
const matches = window?.location?.pathname?.match(
/\/note\/([^\/]+)?\/?/
/\/(?:note|graph)\/([^\/]+)?\/?/
) ?? [""];
return matches![1] as string;
}
Expand Down
11 changes: 8 additions & 3 deletions components/Editor/blocks/Code.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,21 @@
import { MouseEventHandler } from "react";
import { MouseEventHandler, forwardRef } from "react";

export type PropType = {
content: string;
focusTextHelper: (offset?: number) => MouseEventHandler<HTMLElement>;
};
export const Code: React.FC<PropType> = ({ content, focusTextHelper }) => {

export const Code = forwardRef<HTMLElement, PropType>(function Code(
{ content, focusTextHelper },
ref
) {
return (
<code
ref={ref}
className="bg-gray-100 box-border px-1"
onClickCapture={focusTextHelper(1)}
>
{content}
</code>
);
};
});
27 changes: 17 additions & 10 deletions components/Editor/blocks/HyperLink.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { forwardRef } from "react";
import Link from "next/link";

export type PropsType = {
Expand All @@ -8,14 +9,20 @@ export type PropsType = {
const stopPropagation = (ev) => {
ev.stopPropagation();
};
export const HyperLink: React.FC<PropsType> = ({ url, alt }) => (
<Link href={url}>
<a
className="text-blue-600 hover:underline"
href={url}
onClickCapture={stopPropagation}
>
{alt}
</a>
</Link>

export const HyperLink = forwardRef<HTMLAnchorElement, PropsType>(
function HyperLink({ url, alt }, ref) {
return (
<Link href={url}>
<a
ref={ref}
className="text-blue-600 hover:underline"
href={url}
onClickCapture={stopPropagation}
>
{alt}
</a>
</Link>
);
}
);
12 changes: 9 additions & 3 deletions components/Editor/blocks/Image.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
export const Image: React.FC<{ url: string; alt: string }> = ({ url, alt }) => (
// eslint-disable-next-line @next/next/no-img-element
<img src={url} alt={alt} className="max-w-full" />
import { forwardRef } from "react";

export const Image = forwardRef<HTMLImageElement, { url: string; alt: string }>(
function Image({ url, alt }, ref) {
return (
// eslint-disable-next-line @next/next/no-img-element
<img ref={ref} src={url} alt={alt} className="max-w-full" />
);
}
);
18 changes: 15 additions & 3 deletions components/Editor/blocks/Text.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,15 @@
export function Text({ children, focusTextHelper }) {
return <span onClickCapture={focusTextHelper(0)}>{children}</span>;
}
import { forwardRef, ReactNode, MouseEventHandler } from "react";

export const Text = forwardRef<
HTMLElement,
{
children?: ReactNode;
focusTextHelper: (offset: number) => MouseEventHandler<HTMLElement>;
}
>(function Text({ children, focusTextHelper }, ref) {
return (
<span ref={ref} onClickCapture={focusTextHelper(0)}>
{children}
</span>
);
});
4 changes: 3 additions & 1 deletion components/Editor/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ export const Editor: React.FC<PropsType> = ({
setPage(page);
router.beforePopState((nextState) => {
if (nextState.url === "/note/[pageId]") {
const pageId = nextState?.as?.match?.(/note\/([^\/]+)(?:\/.+)*/)?.[1];
const pageId = nextState?.as?.match?.(
/(?:note|graph)\/([^\/]+)(?:\/.+)*/
)?.[1];
if (pageId) {
setPageId(pageId);
}
Expand Down
174 changes: 174 additions & 0 deletions components/MindMap/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import React, { useMemo, Suspense, useEffect, useState, useRef } from "react";
import { Group } from "@visx/group";
import { Tree, Cluster, Pack, hierarchy } from "@visx/hierarchy";
import { HierarchyPointNode } from "@visx/hierarchy/lib/types";
import { LinkHorizontalStep, LinkHorizontal } from "@visx/shape";
import { Preview } from "../Block/Preview";
import { ParentSize } from "@visx/responsive";
import { Zoom } from "@visx/zoom";

const peach = "#fd9b93";
const pink = "#fe6e9e";
const blue = "#03c0dc";
const green = "#26deb0";
const plum = "#71248e";
const lightpurple = "#374469";
const white = "#ffffff";
export const background = "#272b4d";

export interface TreeNode {
id: string;
title?: string;
children?: this[];
}

type HierarchyNode = HierarchyPointNode<TreeNode>;

/** Handles rendering Root, Parent, and other Nodes. */
function Node({ node }: { node: HierarchyNode }) {
const width = 5000;
const height = 5000;

return (
<Group top={node.x} left={node.y}>
<DynamicPreview width={width} height={height} node={node} />
</Group>
);
}

function DynamicPreview(props: {
height: number;
width: number;
node: HierarchyNode;
}) {
const { node } = props;
const [height, setHeight] = useState(props.height);
const [width, setWidth] = useState(props.width);

const ref = useRef<HTMLElement | HTMLImageElement>(null);
const syncSize = () => {
const rect = ref.current?.getBoundingClientRect();
const img = ref.current?.querySelector("img");
if (img) {
img.addEventListener("load", (ev) => {
setHeight(Math.min(img.height, 320));
setWidth(Math.min(img.width, 320));
setTimeout(() => {
const rect = ref.current!.getBoundingClientRect();
setHeight(rect.height);
setWidth(rect.width);
}, 0);
});
} else {
setHeight(Math.min(rect?.height ?? props.height, 320));
setWidth(Math.max(Math.min(rect?.width ?? props.width, 320), 120));
}
};
useEffect(() => {
syncSize();
}, [width, height, ref.current]);

const translate = useMemo(() => {
if (ref.current?.tagName === "IMG") {
return "";
}
return `translate(8, ${height * (node.children ? 0.25 : -0.95)})`;
}, [height, ref.current, node.children, node]);
return (
<foreignObject height={height + 4} width={width} transform={translate}>
<Suspense fallback={"Loading"}>
<Preview
ref={ref}
blockId={node.data.id}
className="inline-block break-words max-w-md bg-gray-100 p-2 shadow-md border border-gray-200"
/>
</Suspense>
</foreignObject>
);
}

const defaultMargin = { top: 48, left: 80, right: 80, bottom: 16 };

export type TreeProps = {
width: number;
height: number;
margin?: { top: number; right: number; bottom: number; left: number };
rawTree: TreeNode;
};

function MindMapTree({
width,
height,
margin = defaultMargin,
rawTree,
}: TreeProps) {
const data = useMemo(() => hierarchy(rawTree), []);
const yMax = height - margin.top - margin.bottom;
const xMax = width - margin.left - margin.right;
const scale = 1;
return width < 10 ? null : (
<Zoom<SVGSVGElement>
width={width}
height={height}
scaleXMin={0.5 / scale}
scaleYMin={0.5 / scale}
>
{(zoom) => (
<div className="relative">
<svg
width={width}
height={height}
ref={zoom.containerRef}
style={{
cursor: zoom.isDragging ? "grabbing" : "grab",
touchAction: "none",
}}
>
<rect fill={"#fcfcfc"} width={width} height={height} />
<Tree<TreeNode>
root={data}
size={[yMax * scale, xMax * scale]}
separation={(a, b) => {
return (a.parent == b.parent ? 0.2 : 0.1) * a.depth;
}}
>
{(tree) => {
return (
<Group transform={zoom.toString()}>
{tree.links().map((link, i) => (
<LinkHorizontalStep
key={`link-${i}`}
data={link}
stroke={"#333"}
strokeWidth="2"
fill="none"
percent={0.9}
/>
))}
{tree.descendants().map((node, i) => (
<Node key={`node-${i}`} node={node} />
))}
</Group>
);
}}
</Tree>
</svg>
</div>
)}
</Zoom>
);
}

export const MindMap: React.FC<{ tree: TreeNode }> = ({ tree }) => {
return (
<ParentSize>
{(parent) => (
<MindMapTree
width={parent.width}
rawTree={tree}
height={parent.height}
/>
)}
</ParentSize>
);
};
Loading