added new editor, locking, props persistance

This commit is contained in:
Fedor Katurov 2023-04-25 18:11:59 +06:00
parent 0940d6abf8
commit 601eda17de
22 changed files with 2860 additions and 123 deletions

View file

@ -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 };
};

View file

@ -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]

View file

@ -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);
}
}
}

View file

@ -8,6 +8,7 @@ export const createDefaultLayout = (api: DockviewApi) => {
title: "",
params: {
title: "",
locked: false,
},
});

View file

@ -11,6 +11,7 @@ export const splitLayoutHorizontal = (
title: "",
params: {
title: "",
locked: false,
},
position: {
referencePanel,

View file

@ -11,6 +11,7 @@ export const splitLayoutVertical = (
title: "",
params: {
title: "",
locked: false,
},
position: {
referencePanel,

View file

@ -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>

View file

@ -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 {

View file

@ -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 />;
};

View file

@ -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%;
}
}

View file

@ -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];
};

View file

@ -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>
);
};

View file

@ -1,6 +0,0 @@
.editor {
height: 100%;
padding: 16px;
overflow: scroll;
box-sizing: border-box;
}

View file

@ -1,4 +1,5 @@
export interface GridLayoutComponentProps {
id: string;
title: string;
locked: boolean;
}