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

1
.tool-versions Normal file
View file

@ -0,0 +1 @@
nodejs 18.0.0

View file

@ -21,3 +21,6 @@ yarn
yarn dev
```
## TO-DO
- Use Remirror as editor

View file

@ -21,10 +21,14 @@
"@milkdown/react": "^7.2.1",
"@milkdown/theme-nord": "^7.2.1",
"@milkdown/transformer": "^7.2.1",
"@remirror/pm": "^2.0.4",
"@remirror/react": "^2.0.27",
"@remirror/react-editors": "^1.0.27",
"classnames": "^2.3.2",
"dockview": "^1.7.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"remirror": "^2.0.26",
"sass": "^1.62.0",
"uuid": "^9.0.0",
"vite-plugin-svgr": "^2.4.0",

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48">
<path d="M220 976q-24.75 0-42.375-17.625T160 916V482q0-24.75 17.625-42.375T220 422h70v-96q0-78.85 55.606-134.425Q401.212 136 480.106 136T614.5 191.575Q670 247.15 670 326v96h70q24.75 0 42.375 17.625T800 482v434q0 24.75-17.625 42.375T740 976H220Zm0-60h520V482H220v434Zm260.168-140Q512 776 534.5 753.969T557 701q0-30-22.668-54.5t-54.5-24.5Q448 622 425.5 646.5t-22.5 55q0 30.5 22.668 52.5t54.5 22ZM350 422h260v-96q0-54.167-37.882-92.083-37.883-37.917-92-37.917Q426 196 388 233.917 350 271.833 350 326v96ZM220 916V482v434Z" />
</svg>

After

Width:  |  Height:  |  Size: 617 B

View file

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 96 960 960" width="48">
<path d="M220 422h390v-96q0-54.167-37.882-92.083-37.883-37.917-92-37.917Q426 196 388 233.917 350 271.833 350 326h-60q0-79 55.606-134.5t134.5-55.5Q559 136 614.5 191.575T670 326v96h70q24.75 0 42.375 17.625T800 482v434q0 24.75-17.625 42.375T740 976H220q-24.75 0-42.375-17.625T160 916V482q0-24.75 17.625-42.375T220 422Zm0 494h520V482H220v434Zm260.168-140Q512 776 534.5 753.969T557 701q0-30-22.668-54.5t-54.5-24.5Q448 622 425.5 646.5t-22.5 55q0 30.5 22.668 52.5t54.5 22ZM220 916V482v434Z" />
</svg>

After

Width:  |  Height:  |  Size: 582 B

View file

@ -0,0 +1,109 @@
import { FC, useCallback } from "react";
import {
FloatingToolbar,
Remirror,
useRemirror,
EditorComponent,
HeadingLevelButtonGroup,
FormattingButtonGroup,
} from "@remirror/react";
import jsx from "refractor/lang/jsx.js";
import typescript from "refractor/lang/typescript.js";
import { Extension, ExtensionPriority, RemirrorEventListener } from "remirror";
import {
BlockquoteExtension,
UnderlineExtension,
BoldExtension,
BulletListExtension,
CodeBlockExtension,
CodeExtension,
HardBreakExtension,
HeadingExtension,
ItalicExtension,
LinkExtension,
ListItemExtension,
MarkdownExtension,
OrderedListExtension,
StrikeExtension,
TableExtension,
TrailingNodeExtension,
GapCursorExtension,
} from "remirror/extensions";
import styles from "./styles.module.scss";
interface RemirrorEditorProps {
locked: boolean;
value?: string;
onChange?: (val: string) => void;
}
const RemirrorEditor: FC<RemirrorEditorProps> = ({
value,
locked,
onChange,
}) => {
const { manager, state, setState } = useRemirror({
extensions,
builtin: {
exitMarksOnArrowPress: false,
},
content: value,
stringHandler: "markdown",
});
const onStateChange = useCallback<RemirrorEventListener<Extension>>(
({ state, helpers }) => {
if (helpers && onChange) {
onChange(helpers.getMarkdown(state));
}
setState(state);
},
[onChange, setState]
);
return (
<Remirror
placeholder="Start typing..."
manager={manager}
classNames={[styles.editor]}
editable={!locked}
onChange={onStateChange}
state={state}
>
<EditorComponent />
{!locked && (
<FloatingToolbar>
<FormattingButtonGroup />
<HeadingLevelButtonGroup />
</FloatingToolbar>
)}
</Remirror>
);
};
const extensions = (): Extension[] => [
new LinkExtension({ autoLink: true }),
new BoldExtension(),
new UnderlineExtension(),
new StrikeExtension(),
new ItalicExtension(),
new HeadingExtension(),
new BlockquoteExtension(),
new BulletListExtension({ enableSpine: false }),
new OrderedListExtension(),
new ListItemExtension({
priority: ExtensionPriority.High,
// enableCollapsible: true,
}),
new CodeExtension(),
new CodeBlockExtension({ supportedLanguages: [jsx, typescript] }),
new TrailingNodeExtension(),
new TableExtension(),
new MarkdownExtension({ copyAsMarkdown: true }),
new GapCursorExtension(),
new HardBreakExtension(),
];
export { RemirrorEditor };

View file

@ -12,11 +12,3 @@
margin-right: 0 !important;
}
}
div[data-milkdown-root="true"] {
height: 100%;
:global(.milkdown) {
height: 100%;
}
}

View file

@ -1,23 +1,22 @@
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";
import { RemirrorEditor } from "../../components/RemirrorEditor";
interface MarkdownEditorContainerProps {
id: string;
locked: boolean;
}
export const MarkdownEditorContainer: FC<MarkdownEditorContainerProps> = ({
id,
locked,
}) => {
const [value, setValue] = usePersistedValue(id, "MarkdownEditorContainer");
return (
<div className={styles.editor}>
<MilkdownProvider>
<MarkdownEditor value={value} onChange={setValue} />
</MilkdownProvider>
<RemirrorEditor value={value} onChange={setValue} locked={locked} />
</div>
);
};

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,9 +21,13 @@ const GridLayoutItemWrapper: FC<GridLayoutItemWrapperProps> = ({
splitVertical,
splitHorizontal,
remove,
locked,
lock,
}) => (
<div className={styles.wrapper}>
<div className={styles.menu}>
{!locked && (
<>
<IconButton
onClick={splitVertical}
role="button"
@ -39,6 +45,11 @@ const GridLayoutItemWrapper: FC<GridLayoutItemWrapperProps> = ({
<IconButton onClick={remove} role="button" className={styles.button}>
<DeleteIcon />
</IconButton>
</>
)}
<IconButton onClick={lock} role="button" className={styles.button}>
{locked ? <Locked /> : <Unlocked />}
</IconButton>
</div>
{children}

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,4 +1,5 @@
export interface GridLayoutComponentProps {
id: string;
title: string;
locked: boolean;
}

View file

@ -1,6 +1,6 @@
import { FC } from "react";
import { GridLayout } from "~/modules/layout/components/GridLayout";
import { MarkdownEditorContainer } from "~/modules/layout/editor/containers/MarkdownEditorContainer/index";
import { MarkdownEditorContainer } from "~/modules/editor/containers/MarkdownEditorContainer/index";
const Editor: FC = () => <GridLayout component={MarkdownEditorContainer} />;

2650
yarn.lock

File diff suppressed because it is too large Load diff