@eventuras/scribo 0.4.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +31 -0
- package/README.md +31 -0
- package/eslint.config.js +4 -0
- package/index.html +13 -0
- package/package.json +41 -0
- package/src/App.css +35 -0
- package/src/App.tsx +42 -0
- package/src/MarkdownEditor.css +1127 -0
- package/src/MarkdownEditor.tsx +105 -0
- package/src/context/SharedHistoryContext.tsx +36 -0
- package/src/index.tsx +12 -0
- package/src/main.tsx +4 -0
- package/src/nodes/EditorNodes.ts +39 -0
- package/src/plugins/AutoLinkPlugin.tsx +34 -0
- package/src/plugins/FloatingLinkEditorPlugin.css +41 -0
- package/src/plugins/FloatingLinkEditorPlugin.tsx +469 -0
- package/src/plugins/HistoryPlugin.tsx +506 -0
- package/src/plugins/LinkPlugin.tsx +17 -0
- package/src/plugins/ToolbarPlugin.tsx +528 -0
- package/src/themes/EditorTheme.css +297 -0
- package/src/themes/EditorTheme.ts +118 -0
- package/src/ui/ContentEditable.css +23 -0
- package/src/ui/ContentEditable.tsx +19 -0
- package/src/ui/DropDown.tsx +262 -0
- package/src/ui/Placeholder.css +28 -0
- package/src/ui/Placeholder.tsx +21 -0
- package/src/utils/environment.ts +54 -0
- package/src/utils/getSelectedNode.ts +28 -0
- package/src/utils/setFloatingElemPosition.ts +51 -0
- package/src/utils/setFloatingElemPositionForLinkEditor.ts +46 -0
- package/src/utils/url.ts +39 -0
- package/src/vite-env.d.ts +8 -0
- package/tsconfig.node.json +11 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import {
|
|
2
|
+
$convertFromMarkdownString,
|
|
3
|
+
$convertToMarkdownString,
|
|
4
|
+
TRANSFORMERS,
|
|
5
|
+
} from "@lexical/markdown";
|
|
6
|
+
import {
|
|
7
|
+
$getRoot,
|
|
8
|
+
} from 'lexical';
|
|
9
|
+
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
|
10
|
+
import { LexicalErrorBoundary } from "@lexical/react/LexicalErrorBoundary";
|
|
11
|
+
import { OnChangePlugin } from "@lexical/react/LexicalOnChangePlugin";
|
|
12
|
+
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
13
|
+
import { EditorState } from "lexical";
|
|
14
|
+
import React, { useState } from "react";
|
|
15
|
+
|
|
16
|
+
import EditorNodes from "./nodes/EditorNodes";
|
|
17
|
+
import ToolbarPlugin from "./plugins/ToolbarPlugin";
|
|
18
|
+
import EditorTheme from "./themes/EditorTheme";
|
|
19
|
+
import ContentEditable from "./ui/ContentEditable";
|
|
20
|
+
import Placeholder from "./ui/Placeholder";
|
|
21
|
+
import {HistoryPlugin} from '@lexical/react/LexicalHistoryPlugin';
|
|
22
|
+
import "./MarkdownEditor.css";
|
|
23
|
+
import LinkPlugin from "./plugins/LinkPlugin";
|
|
24
|
+
import FloatingLinkEditorPlugin from "./plugins/FloatingLinkEditorPlugin";
|
|
25
|
+
import AutoLinkPlugin from "./plugins/AutoLinkPlugin";
|
|
26
|
+
import {useSharedHistoryContext} from './context/SharedHistoryContext';
|
|
27
|
+
|
|
28
|
+
export type onChangeMisc = {
|
|
29
|
+
plainText: string
|
|
30
|
+
}
|
|
31
|
+
export interface MarkdownEditorProps {
|
|
32
|
+
initialMarkdown?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
onChange?: (markdown: string, misc: onChangeMisc) => void;
|
|
35
|
+
onBlur?: () => void;
|
|
36
|
+
placeholder?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const MarkdownEditor: React.FC<MarkdownEditorProps> = (props) => {
|
|
40
|
+
const {historyState} = useSharedHistoryContext();
|
|
41
|
+
const [isLinkEditMode, setIsLinkEditMode] = useState<boolean>(false);
|
|
42
|
+
|
|
43
|
+
const [floatingAnchorElem, setFloatingAnchorElem] =
|
|
44
|
+
useState<HTMLDivElement | undefined>(undefined);
|
|
45
|
+
|
|
46
|
+
const onRef = (_floatingAnchorElem: HTMLDivElement) => {
|
|
47
|
+
if (_floatingAnchorElem !== null) {
|
|
48
|
+
setFloatingAnchorElem(_floatingAnchorElem);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const internalOnChange = (editorState: EditorState) => {
|
|
53
|
+
editorState.read(() => {
|
|
54
|
+
const markdown = $convertToMarkdownString(TRANSFORMERS);
|
|
55
|
+
const plainText = $getRoot().getTextContent()
|
|
56
|
+
props.onChange && props.onChange(markdown, { plainText });
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const initialConfig = {
|
|
61
|
+
namespace: "ScriboMarkdown",
|
|
62
|
+
nodes: [...EditorNodes],
|
|
63
|
+
onError: (error: Error) => {
|
|
64
|
+
throw error;
|
|
65
|
+
},
|
|
66
|
+
theme: EditorTheme,
|
|
67
|
+
editorState: () => {
|
|
68
|
+
$convertFromMarkdownString(props.initialMarkdown ?? "", TRANSFORMERS);
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<div className={props.className}>
|
|
74
|
+
<LexicalComposer initialConfig={initialConfig}>
|
|
75
|
+
<div className="editor-shell" onBlur={props.onBlur}>
|
|
76
|
+
<ToolbarPlugin setIsLinkEditMode={setIsLinkEditMode} />
|
|
77
|
+
<div className="editor-container rich-text">
|
|
78
|
+
<HistoryPlugin externalHistoryState={historyState} />
|
|
79
|
+
<AutoLinkPlugin />
|
|
80
|
+
<LinkPlugin />
|
|
81
|
+
<FloatingLinkEditorPlugin
|
|
82
|
+
anchorElem={floatingAnchorElem}
|
|
83
|
+
isLinkEditMode={isLinkEditMode}
|
|
84
|
+
setIsLinkEditMode={setIsLinkEditMode}
|
|
85
|
+
/>
|
|
86
|
+
<RichTextPlugin
|
|
87
|
+
contentEditable={
|
|
88
|
+
<div className="editor-scroller">
|
|
89
|
+
<div className="editor" ref={onRef}>
|
|
90
|
+
<ContentEditable />
|
|
91
|
+
</div>
|
|
92
|
+
</div>
|
|
93
|
+
}
|
|
94
|
+
placeholder={<Placeholder>{props.placeholder}</Placeholder>}
|
|
95
|
+
ErrorBoundary={LexicalErrorBoundary}
|
|
96
|
+
/>
|
|
97
|
+
</div>
|
|
98
|
+
{props.onChange && <OnChangePlugin onChange={internalOnChange} />}
|
|
99
|
+
</div>
|
|
100
|
+
</LexicalComposer>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default MarkdownEditor;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {HistoryState} from '@lexical/react/LexicalHistoryPlugin';
|
|
10
|
+
import type {JSX} from 'react';
|
|
11
|
+
|
|
12
|
+
import {createEmptyHistoryState} from '@lexical/react/LexicalHistoryPlugin';
|
|
13
|
+
import * as React from 'react';
|
|
14
|
+
import {createContext, ReactNode, useContext, useMemo} from 'react';
|
|
15
|
+
|
|
16
|
+
type ContextShape = {
|
|
17
|
+
historyState?: HistoryState;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const Context: React.Context<ContextShape> = createContext({});
|
|
21
|
+
|
|
22
|
+
export const SharedHistoryContext = ({
|
|
23
|
+
children,
|
|
24
|
+
}: {
|
|
25
|
+
children: ReactNode;
|
|
26
|
+
}): JSX.Element => {
|
|
27
|
+
const historyContext = useMemo(
|
|
28
|
+
() => ({historyState: createEmptyHistoryState()}),
|
|
29
|
+
[],
|
|
30
|
+
);
|
|
31
|
+
return <Context.Provider value={historyContext}>{children}</Context.Provider>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export const useSharedHistoryContext = (): ContextShape => {
|
|
35
|
+
return useContext(Context);
|
|
36
|
+
};
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import ReactDOM from 'react-dom/client'
|
|
3
|
+
import { App } from './App'
|
|
4
|
+
import './App.css'
|
|
5
|
+
|
|
6
|
+
import readmeRaw from '../README.md?raw'
|
|
7
|
+
|
|
8
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
9
|
+
<React.StrictMode>
|
|
10
|
+
<App initialMarkdown={readmeRaw} />
|
|
11
|
+
</React.StrictMode>,
|
|
12
|
+
)
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
* Modified from: https://github.com/facebook/lexical/blob/main/packages/lexical-playground/src/nodes/PlaygroundNodes.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { CodeHighlightNode, CodeNode } from "@lexical/code";
|
|
11
|
+
import { HashtagNode } from "@lexical/hashtag";
|
|
12
|
+
import { AutoLinkNode, LinkNode } from "@lexical/link";
|
|
13
|
+
import { ListItemNode, ListNode } from "@lexical/list";
|
|
14
|
+
import { MarkNode } from "@lexical/mark";
|
|
15
|
+
import { OverflowNode } from "@lexical/overflow";
|
|
16
|
+
import { HorizontalRuleNode } from "@lexical/react/LexicalHorizontalRuleNode";
|
|
17
|
+
import { HeadingNode, QuoteNode } from "@lexical/rich-text";
|
|
18
|
+
import { TableCellNode, TableNode, TableRowNode } from "@lexical/table";
|
|
19
|
+
import type { Klass, LexicalNode } from "lexical";
|
|
20
|
+
|
|
21
|
+
const EditorNodes: Array<Klass<LexicalNode>> = [
|
|
22
|
+
HeadingNode,
|
|
23
|
+
ListNode,
|
|
24
|
+
ListItemNode,
|
|
25
|
+
QuoteNode,
|
|
26
|
+
CodeNode,
|
|
27
|
+
TableNode,
|
|
28
|
+
TableCellNode,
|
|
29
|
+
TableRowNode,
|
|
30
|
+
HashtagNode,
|
|
31
|
+
CodeHighlightNode,
|
|
32
|
+
AutoLinkNode,
|
|
33
|
+
LinkNode,
|
|
34
|
+
OverflowNode,
|
|
35
|
+
HorizontalRuleNode,
|
|
36
|
+
MarkNode,
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export default EditorNodes;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type {JSX} from 'react';
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
AutoLinkPlugin,
|
|
13
|
+
createLinkMatcherWithRegExp,
|
|
14
|
+
} from '@lexical/react/LexicalAutoLinkPlugin';
|
|
15
|
+
import * as React from 'react';
|
|
16
|
+
|
|
17
|
+
const URL_REGEX =
|
|
18
|
+
/((https?:\/\/(www\.)?)|(www\.))[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)(?<![-.+():%])/;
|
|
19
|
+
|
|
20
|
+
const EMAIL_REGEX =
|
|
21
|
+
/(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/;
|
|
22
|
+
|
|
23
|
+
const MATCHERS = [
|
|
24
|
+
createLinkMatcherWithRegExp(URL_REGEX, (text) => {
|
|
25
|
+
return text.startsWith('http') ? text : `https://${text}`;
|
|
26
|
+
}),
|
|
27
|
+
createLinkMatcherWithRegExp(EMAIL_REGEX, (text) => {
|
|
28
|
+
return `mailto:${text}`;
|
|
29
|
+
}),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
export default function LexicalAutoLinkPlugin(): JSX.Element {
|
|
33
|
+
return <AutoLinkPlugin matchers={MATCHERS} />;
|
|
34
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
.link-editor {
|
|
2
|
+
display: flex;
|
|
3
|
+
position: absolute;
|
|
4
|
+
top: 0;
|
|
5
|
+
left: 0;
|
|
6
|
+
z-index: 10;
|
|
7
|
+
max-width: 400px;
|
|
8
|
+
width: 100%;
|
|
9
|
+
opacity: 0;
|
|
10
|
+
background-color: #fff;
|
|
11
|
+
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3);
|
|
12
|
+
border-radius: 0 0 8px 8px;
|
|
13
|
+
transition: opacity 0.5s;
|
|
14
|
+
will-change: transform;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.link-editor .button {
|
|
18
|
+
width: 20px;
|
|
19
|
+
height: 20px;
|
|
20
|
+
display: inline-block;
|
|
21
|
+
padding: 6px;
|
|
22
|
+
border-radius: 8px;
|
|
23
|
+
cursor: pointer;
|
|
24
|
+
margin: 0 2px;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.link-editor .button.hovered {
|
|
28
|
+
width: 20px;
|
|
29
|
+
height: 20px;
|
|
30
|
+
display: inline-block;
|
|
31
|
+
background-color: #eee;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.link-editor .button i,
|
|
35
|
+
.actions i {
|
|
36
|
+
background-size: contain;
|
|
37
|
+
display: inline-block;
|
|
38
|
+
height: 20px;
|
|
39
|
+
width: 20px;
|
|
40
|
+
vertical-align: -0.25em;
|
|
41
|
+
}
|