mirror of
https://github.com/muerwre/markdown-home-tab.git
synced 2025-04-24 16:36: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
.tool-versions
Normal file
1
.tool-versions
Normal file
|
@ -0,0 +1 @@
|
|||
nodejs 18.0.0
|
|
@ -21,3 +21,6 @@ yarn
|
|||
yarn dev
|
||||
```
|
||||
|
||||
## TO-DO
|
||||
|
||||
- Use Remirror as editor
|
||||
|
|
|
@ -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",
|
||||
|
|
3
src/assets/images/locked.svg
Normal file
3
src/assets/images/locked.svg
Normal 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 |
3
src/assets/images/unlocked.svg
Normal file
3
src/assets/images/unlocked.svg
Normal 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 |
109
src/modules/editor/components/RemirrorEditor/index.tsx
Normal file
109
src/modules/editor/components/RemirrorEditor/index.tsx
Normal 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 };
|
|
@ -12,11 +12,3 @@
|
|||
margin-right: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
div[data-milkdown-root="true"] {
|
||||
height: 100%;
|
||||
|
||||
:global(.milkdown) {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -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,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}
|
||||
|
|
|
@ -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,4 +1,5 @@
|
|||
export interface GridLayoutComponentProps {
|
||||
id: string;
|
||||
title: string;
|
||||
locked: boolean;
|
||||
}
|
||||
|
|
|
@ -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} />;
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue