mirror of
https://github.com/muerwre/markdown-home-tab.git
synced 2025-04-25 08:56:41 +07:00
added new editor, locking, props persistance
This commit is contained in:
parent
0940d6abf8
commit
601eda17de
22 changed files with 2860 additions and 123 deletions
|
@ -1,5 +1,5 @@
|
|||
import { DockviewApi, DockviewReadyEvent } from "dockview";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { createDefaultLayout } from "../utils/createDefaultLayout";
|
||||
|
||||
export const useGridLayoutPersistance = () => {
|
||||
|
@ -24,12 +24,22 @@ export const useGridLayoutPersistance = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const persistLayout = useCallback(() => {
|
||||
if (!api.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const layout = api.current.toJSON();
|
||||
|
||||
localStorage.setItem("dockview_persistance_layout", JSON.stringify(layout));
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!api.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const disposable = api.current.onDidLayoutChange(() => {
|
||||
const onLayoutChange = api.current.onDidLayoutChange(() => {
|
||||
if (!api.current) {
|
||||
return;
|
||||
}
|
||||
|
@ -38,18 +48,19 @@ export const useGridLayoutPersistance = () => {
|
|||
createDefaultLayout(api.current);
|
||||
}
|
||||
|
||||
const layout = api.current.toJSON();
|
||||
persistLayout();
|
||||
});
|
||||
|
||||
localStorage.setItem(
|
||||
"dockview_persistance_layout",
|
||||
JSON.stringify(layout)
|
||||
);
|
||||
const onPanelChange = api.current.onDidActivePanelChange((event) => {
|
||||
console.log(event);
|
||||
});
|
||||
|
||||
return () => {
|
||||
disposable.dispose();
|
||||
onLayoutChange.dispose();
|
||||
onPanelChange.dispose();
|
||||
};
|
||||
}, []);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [persistLayout, api.current]);
|
||||
|
||||
return { api, onReady };
|
||||
return { api, onReady, persistLayout };
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { DockviewReact, IDockviewPanelProps } from "dockview";
|
||||
import { useGridLayoutPersistance } from "./hooks/useGridLayoutPersistance";
|
||||
import { FC, createElement, useCallback, useMemo } from "react";
|
||||
import { FC, createElement, useCallback, useEffect, useMemo } from "react";
|
||||
import { GridLayoutComponentProps } from "../../types";
|
||||
import { GridLayoutItemWrapper } from "../GridLayoutItemWrapper";
|
||||
import { splitLayoutVertical } from "./utils/splitLayoutVertical";
|
||||
|
@ -12,10 +12,16 @@ export interface GridLayoutProps {
|
|||
}
|
||||
|
||||
interface DefaultLayoutProps {
|
||||
panelProps: IDockviewPanelProps<{ title: string }>;
|
||||
panelProps: IDockviewPanelProps<{ title: string; locked?: boolean }>;
|
||||
component: FC<GridLayoutComponentProps>;
|
||||
persistLayout: () => void;
|
||||
}
|
||||
const DefaultLayout = ({ component, panelProps }: DefaultLayoutProps) => {
|
||||
|
||||
const DefaultLayout = ({
|
||||
component,
|
||||
panelProps,
|
||||
persistLayout,
|
||||
}: DefaultLayoutProps) => {
|
||||
const splitVertical = useCallback(() => {
|
||||
splitLayoutVertical(panelProps.api.id, panelProps.containerApi);
|
||||
}, [panelProps.api.id, panelProps.containerApi]);
|
||||
|
@ -28,27 +34,47 @@ const DefaultLayout = ({ component, panelProps }: DefaultLayoutProps) => {
|
|||
panelProps.api.close();
|
||||
}, [panelProps.api]);
|
||||
|
||||
const locked = Boolean(panelProps.params.locked);
|
||||
|
||||
const lock = useCallback(() => {
|
||||
panelProps.api.updateParameters({
|
||||
...panelProps.params,
|
||||
locked: !locked,
|
||||
});
|
||||
}, [locked, panelProps.api, panelProps.params]);
|
||||
|
||||
useEffect(() => {
|
||||
persistLayout();
|
||||
}, [locked, persistLayout]);
|
||||
|
||||
return (
|
||||
<GridLayoutItemWrapper
|
||||
splitVertical={splitVertical}
|
||||
splitHorizontal={splitHorizontal}
|
||||
remove={remove}
|
||||
locked={locked}
|
||||
lock={lock}
|
||||
>
|
||||
{createElement(component, {
|
||||
id: panelProps.api.id,
|
||||
title: panelProps.params.title,
|
||||
locked,
|
||||
})}
|
||||
</GridLayoutItemWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export const GridLayout: FC<GridLayoutProps> = ({ component }) => {
|
||||
const { onReady } = useGridLayoutPersistance();
|
||||
const { onReady, persistLayout } = useGridLayoutPersistance();
|
||||
|
||||
const components = useMemo(
|
||||
() => ({
|
||||
default: (props: IDockviewPanelProps<{ title: string }>) => (
|
||||
<DefaultLayout panelProps={props} component={component} />
|
||||
<DefaultLayout
|
||||
panelProps={props}
|
||||
component={component}
|
||||
persistLayout={persistLayout}
|
||||
/>
|
||||
),
|
||||
}),
|
||||
[component]
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
.layout {
|
||||
:global {
|
||||
.vertical .sash {
|
||||
border-top: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.horizontal .sash {
|
||||
border-right: 1px solid var(--color-border);
|
||||
.sash::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 1.5px;
|
||||
background: var(--color-border);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ export const createDefaultLayout = (api: DockviewApi) => {
|
|||
title: "",
|
||||
params: {
|
||||
title: "",
|
||||
locked: false,
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ export const splitLayoutHorizontal = (
|
|||
title: "",
|
||||
params: {
|
||||
title: "",
|
||||
locked: false,
|
||||
},
|
||||
position: {
|
||||
referencePanel,
|
||||
|
|
|
@ -11,6 +11,7 @@ export const splitLayoutVertical = (
|
|||
title: "",
|
||||
params: {
|
||||
title: "",
|
||||
locked: false,
|
||||
},
|
||||
position: {
|
||||
referencePanel,
|
||||
|
|
|
@ -5,13 +5,15 @@ import { IconButton } from "~/components/buttons/IconButton";
|
|||
import DeleteIcon from "~/assets/images/delete.svg";
|
||||
import SplitVertical from "~/assets/images/split-vertical.svg";
|
||||
import SplitHorizontal from "~/assets/images/split-horizontal.svg";
|
||||
|
||||
console.log(DeleteIcon);
|
||||
import Locked from "~/assets/images/locked.svg";
|
||||
import Unlocked from "~/assets/images/unlocked.svg";
|
||||
|
||||
type GridLayoutItemWrapperProps = PropsWithChildren & {
|
||||
splitVertical: () => void;
|
||||
splitHorizontal: () => void;
|
||||
remove: () => void;
|
||||
locked: boolean;
|
||||
lock: () => void;
|
||||
};
|
||||
|
||||
const GridLayoutItemWrapper: FC<GridLayoutItemWrapperProps> = ({
|
||||
|
@ -19,25 +21,34 @@ const GridLayoutItemWrapper: FC<GridLayoutItemWrapperProps> = ({
|
|||
splitVertical,
|
||||
splitHorizontal,
|
||||
remove,
|
||||
locked,
|
||||
lock,
|
||||
}) => (
|
||||
<div className={styles.wrapper}>
|
||||
<div className={styles.menu}>
|
||||
<IconButton
|
||||
onClick={splitVertical}
|
||||
role="button"
|
||||
className={styles.button}
|
||||
>
|
||||
<SplitVertical />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={splitHorizontal}
|
||||
role="button"
|
||||
className={styles.button}
|
||||
>
|
||||
<SplitHorizontal />
|
||||
</IconButton>
|
||||
<IconButton onClick={remove} role="button" className={styles.button}>
|
||||
<DeleteIcon />
|
||||
{!locked && (
|
||||
<>
|
||||
<IconButton
|
||||
onClick={splitVertical}
|
||||
role="button"
|
||||
className={styles.button}
|
||||
>
|
||||
<SplitVertical />
|
||||
</IconButton>
|
||||
<IconButton
|
||||
onClick={splitHorizontal}
|
||||
role="button"
|
||||
className={styles.button}
|
||||
>
|
||||
<SplitHorizontal />
|
||||
</IconButton>
|
||||
<IconButton onClick={remove} role="button" className={styles.button}>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</>
|
||||
)}
|
||||
<IconButton onClick={lock} role="button" className={styles.button}>
|
||||
{locked ? <Locked /> : <Unlocked />}
|
||||
</IconButton>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -2,12 +2,21 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
|
||||
.menu {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover .menu {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.menu {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
top: 2px;
|
||||
right: 4px;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
.button {
|
||||
|
|
|
@ -1,47 +0,0 @@
|
|||
import { FC } from "react";
|
||||
import {
|
||||
Editor,
|
||||
rootCtx,
|
||||
defaultValueCtx,
|
||||
editorViewOptionsCtx,
|
||||
} from "@milkdown/core";
|
||||
import { Milkdown, useEditor } from "@milkdown/react";
|
||||
import { commonmark } from "@milkdown/preset-commonmark";
|
||||
import { listener, listenerCtx } from "@milkdown/plugin-listener";
|
||||
import { clipboard } from "@milkdown/plugin-clipboard";
|
||||
|
||||
import styles from "./styles.module.scss";
|
||||
|
||||
interface MarkdownEditorProps {
|
||||
value?: string;
|
||||
onChange?: (val: string) => void;
|
||||
}
|
||||
|
||||
export const MarkdownEditor: FC<MarkdownEditorProps> = ({
|
||||
value = "",
|
||||
onChange,
|
||||
}) => {
|
||||
useEditor((root) =>
|
||||
Editor.make()
|
||||
.config((ctx) => {
|
||||
ctx.set(rootCtx, root);
|
||||
ctx.set(defaultValueCtx, value);
|
||||
ctx.get(listenerCtx).markdownUpdated((_, markdown) => {
|
||||
onChange?.(markdown);
|
||||
});
|
||||
|
||||
ctx.update(editorViewOptionsCtx, (prev) => ({
|
||||
...prev,
|
||||
attributes: {
|
||||
class: styles.editor,
|
||||
spellcheck: "false",
|
||||
},
|
||||
}));
|
||||
})
|
||||
.use(commonmark)
|
||||
.use(listener)
|
||||
.use(clipboard)
|
||||
);
|
||||
|
||||
return <Milkdown />;
|
||||
};
|
|
@ -1,22 +0,0 @@
|
|||
.editor {
|
||||
outline: none;
|
||||
height: 100%;
|
||||
|
||||
& > :first-child {
|
||||
margin-top: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
}
|
||||
|
||||
& > :last-child {
|
||||
margin-bottom: 0 !important;
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
div[data-milkdown-root="true"] {
|
||||
height: 100%;
|
||||
|
||||
:global(.milkdown) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
const safelyGetStringValue = (key: string) => {
|
||||
try {
|
||||
return localStorage.getItem(key) ?? "";
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
return "";
|
||||
}
|
||||
};
|
||||
const safelySetStringValue = (key: string, value: string) => {
|
||||
try {
|
||||
return localStorage.setItem(key, value);
|
||||
} catch (error) {
|
||||
console.warn(error);
|
||||
}
|
||||
};
|
||||
|
||||
export const usePersistedValue = (
|
||||
id: string,
|
||||
prefix: string
|
||||
): [string, (val: string) => void] => {
|
||||
const key = `${prefix}${id}`;
|
||||
const [value, setValue] = useState(safelyGetStringValue(key));
|
||||
|
||||
useEffect(() => {
|
||||
setValue(safelyGetStringValue(key));
|
||||
}, [id, key]);
|
||||
|
||||
useEffect(() => {
|
||||
safelySetStringValue(key, value);
|
||||
}, [key, value]);
|
||||
|
||||
return [value, setValue];
|
||||
};
|
|
@ -1,23 +0,0 @@
|
|||
import { MilkdownProvider } from "@milkdown/react";
|
||||
import { FC } from "react";
|
||||
import { MarkdownEditor } from "../../components/MarkdownEditor";
|
||||
import styles from "./styles.module.scss";
|
||||
import { usePersistedValue } from "./hooks/usePersistedValue";
|
||||
|
||||
interface MarkdownEditorContainerProps {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export const MarkdownEditorContainer: FC<MarkdownEditorContainerProps> = ({
|
||||
id,
|
||||
}) => {
|
||||
const [value, setValue] = usePersistedValue(id, "MarkdownEditorContainer");
|
||||
|
||||
return (
|
||||
<div className={styles.editor}>
|
||||
<MilkdownProvider>
|
||||
<MarkdownEditor value={value} onChange={setValue} />
|
||||
</MilkdownProvider>
|
||||
</div>
|
||||
);
|
||||
};
|
|
@ -1,6 +0,0 @@
|
|||
.editor {
|
||||
height: 100%;
|
||||
padding: 16px;
|
||||
overflow: scroll;
|
||||
box-sizing: border-box;
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
export interface GridLayoutComponentProps {
|
||||
id: string;
|
||||
title: string;
|
||||
locked: boolean;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue