@use-kona/editor 0.1.2-rc.3 → 0.1.2-rc.5
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/package.json +5 -4
- package/src/core/serialize.ts +8 -5
- package/src/examples/{LanguageSelector.module.css → CodeBlock.module.css} +16 -3
- package/src/examples/{LanguageSelector.tsx → CodeBlock.tsx} +45 -35
- package/src/examples/Editor.tsx +24 -9
- package/src/examples/getPlugins.tsx +3 -3
- package/src/examples/text.tsx +1 -1
- package/src/plugins/BasicFormattingPlugin/BasicFormattingPlugin.tsx +22 -1
- package/src/plugins/CodeBlockPlugin/CodeBlock.tsx +0 -5
- package/src/plugins/CodeBlockPlugin/CodeBlockPlugin.tsx +18 -17
- package/src/plugins/CodeBlockPlugin/styles.module.css +0 -3
- package/src/plugins/CommandsPlugin/Menu.tsx +8 -2
- package/src/plugins/CommandsPlugin/styles.module.css +21 -15
- package/src/plugins/HeadingsPlugin/HeadingsPlugin.tsx +25 -1
- package/src/plugins/LinksPlugin/LinksPlugin.tsx +8 -0
- package/src/plugins/ListsPlugin/ListsPlugin.tsx +15 -0
- package/src/types.ts +7 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@use-kona/editor",
|
|
3
|
-
"version": "0.1.2-rc.
|
|
3
|
+
"version": "0.1.2-rc.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"exports": {
|
|
6
6
|
".": "./src/index.ts"
|
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
"@storybook/test": "^9.0.0-alpha.2",
|
|
36
36
|
"@testing-library/jest-dom": "^6.6.3",
|
|
37
37
|
"@testing-library/react": "^16.3.0",
|
|
38
|
+
"@types/escape-html": "^1.0.4",
|
|
38
39
|
"@types/is-url": "^1.2.32",
|
|
39
40
|
"@types/react": "^19.1.8",
|
|
40
41
|
"@types/react-dom": "18",
|
|
@@ -57,15 +58,15 @@
|
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
60
|
"@nanostores/react": "^1.0.0",
|
|
61
|
+
"clsx": "^2.1.1",
|
|
60
62
|
"escape-html": "^1.0.3",
|
|
61
63
|
"is-hotkey": "^0.2.0",
|
|
64
|
+
"is-url": "^1.2.4",
|
|
62
65
|
"nanostores": "^1.0.1",
|
|
63
66
|
"prismjs": "^1.30.0",
|
|
64
67
|
"slate": "^0.117.2",
|
|
65
68
|
"slate-history": "^0.113.1",
|
|
66
69
|
"slate-hyperscript": "^0.100.0",
|
|
67
|
-
"slate-react": "^0.117.3"
|
|
68
|
-
"clsx": "^2.1.1",
|
|
69
|
-
"is-url": "^1.2.4"
|
|
70
|
+
"slate-react": "^0.117.3"
|
|
70
71
|
}
|
|
71
72
|
}
|
package/src/core/serialize.ts
CHANGED
|
@@ -5,11 +5,14 @@ import type { IPlugin } from '../types';
|
|
|
5
5
|
|
|
6
6
|
export const serialize =
|
|
7
7
|
(plugins: IPlugin[]) =>
|
|
8
|
-
(
|
|
9
|
-
node: CustomElement | CustomElement[] | CustomText | CustomText[],
|
|
10
|
-
): string => {
|
|
8
|
+
(node: CustomElement | CustomText): string => {
|
|
11
9
|
const serializers = plugins
|
|
12
10
|
.flatMap((plugin) => plugin.blocks?.map((element) => element.serialize))
|
|
11
|
+
.concat(
|
|
12
|
+
plugins.flatMap((plugin) =>
|
|
13
|
+
plugin.leafs?.map((element) => element.serialize),
|
|
14
|
+
),
|
|
15
|
+
)
|
|
13
16
|
.filter(Boolean);
|
|
14
17
|
|
|
15
18
|
if (Array.isArray(node)) {
|
|
@@ -21,11 +24,11 @@ export const serialize =
|
|
|
21
24
|
return current?.(node) || prev;
|
|
22
25
|
}, escapeHtml(node.text));
|
|
23
26
|
} else {
|
|
24
|
-
const children: string = node
|
|
27
|
+
const children: string = node?.children
|
|
25
28
|
?.map((n) => serialize(plugins)(n))
|
|
26
29
|
.join('');
|
|
27
30
|
|
|
28
|
-
if (node
|
|
31
|
+
if (node?.type === 'paragraph') {
|
|
29
32
|
return `<p>${children}</p>`;
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
.root {
|
|
2
2
|
display: flex;
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
background-color: var(--kona-editor-alt-background-color);
|
|
5
|
+
border-radius: 8px;
|
|
5
6
|
padding: 4px;
|
|
6
|
-
border-bottom: 1px solid var(--kona-editor-border-color);
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
.button {
|
|
@@ -24,6 +24,19 @@
|
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
.menu {
|
|
28
|
+
display: flex;
|
|
29
|
+
justify-content: space-between;
|
|
30
|
+
column-gap: 4px;
|
|
31
|
+
padding: 4px 0;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.content {
|
|
35
|
+
background-color: var(--kona-editor-background-color);
|
|
36
|
+
border-radius: 6px;
|
|
37
|
+
padding: 8px 4px;
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
.customMenu {
|
|
28
41
|
max-height: 250px;
|
|
29
42
|
width: 200px;
|
|
@@ -1,21 +1,13 @@
|
|
|
1
|
-
import { forwardRef, useMemo } from 'react';
|
|
1
|
+
import { forwardRef, type ReactNode, useMemo } from 'react';
|
|
2
2
|
import { Node } from 'slate';
|
|
3
3
|
import type { CustomElement } from '../../types';
|
|
4
|
+
import styles from './CodeBlock.module.css';
|
|
4
5
|
import { CheckIcon } from './icons/check';
|
|
5
6
|
import { CopyIcon } from './icons/copy';
|
|
6
|
-
import styles from './LanguageSelector.module.css';
|
|
7
7
|
import { Button } from './ui/Button';
|
|
8
8
|
import { Dropdown } from './ui/Dropdown';
|
|
9
9
|
import { Menu as MenuBase, type MenuConfig } from './ui/Menu';
|
|
10
10
|
|
|
11
|
-
type Props = {
|
|
12
|
-
value: string;
|
|
13
|
-
onChange: (value: string) => void;
|
|
14
|
-
params: {
|
|
15
|
-
element: CustomElement;
|
|
16
|
-
};
|
|
17
|
-
};
|
|
18
|
-
|
|
19
11
|
const languages = [
|
|
20
12
|
{ value: 'javascript', label: 'JavaScript' },
|
|
21
13
|
{ value: 'typescript', label: 'TypeScript' },
|
|
@@ -40,11 +32,24 @@ const languages = [
|
|
|
40
32
|
{ value: 'plaintext', label: 'Plain Text' },
|
|
41
33
|
];
|
|
42
34
|
|
|
43
|
-
|
|
44
|
-
|
|
35
|
+
type Props = {
|
|
36
|
+
value: string;
|
|
37
|
+
onChange: (value: string) => void;
|
|
38
|
+
params: {
|
|
39
|
+
element: CustomElement;
|
|
40
|
+
Content: () => ReactNode;
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const CodeBlock = (props: Props) => {
|
|
45
|
+
const {
|
|
46
|
+
value: language,
|
|
47
|
+
onChange,
|
|
48
|
+
params: { element, Content },
|
|
49
|
+
} = props;
|
|
45
50
|
|
|
46
51
|
const handleCopyClick = () => {
|
|
47
|
-
const text = Array.from(Node.texts(
|
|
52
|
+
const text = Array.from(Node.texts(element))
|
|
48
53
|
.map((nodeEntry) => nodeEntry[0].text)
|
|
49
54
|
.join('\n');
|
|
50
55
|
|
|
@@ -72,28 +77,33 @@ export const LanguageSelector = (props: Props) => {
|
|
|
72
77
|
);
|
|
73
78
|
return (
|
|
74
79
|
<div className={styles.root}>
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
{
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
<
|
|
96
|
-
|
|
80
|
+
<div className={styles.menu}>
|
|
81
|
+
<Dropdown
|
|
82
|
+
config={menuConfig}
|
|
83
|
+
Menu={forwardRef<HTMLDivElement, { className: string }>(
|
|
84
|
+
(props, ref) => (
|
|
85
|
+
<div
|
|
86
|
+
{...props}
|
|
87
|
+
ref={ref}
|
|
88
|
+
className={[styles.customMenu, props.className].join(' ')}
|
|
89
|
+
/>
|
|
90
|
+
),
|
|
91
|
+
)}
|
|
92
|
+
>
|
|
93
|
+
{({ ref, onClick }) => (
|
|
94
|
+
<Button ref={ref} size="sm" onClick={onClick}>
|
|
95
|
+
{languages.find((l) => l.value === language)?.label ||
|
|
96
|
+
'Select language'}
|
|
97
|
+
</Button>
|
|
98
|
+
)}
|
|
99
|
+
</Dropdown>
|
|
100
|
+
<Button size="sm" type="button" onClick={handleCopyClick}>
|
|
101
|
+
<CopyIcon size={16} />
|
|
102
|
+
</Button>
|
|
103
|
+
</div>
|
|
104
|
+
<div className={styles.content}>
|
|
105
|
+
<Content />
|
|
106
|
+
</div>
|
|
97
107
|
</div>
|
|
98
108
|
);
|
|
99
109
|
};
|
package/src/examples/Editor.tsx
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
forwardRef,
|
|
3
|
+
useEffect,
|
|
4
|
+
useImperativeHandle,
|
|
5
|
+
useRef,
|
|
6
|
+
useState,
|
|
7
|
+
} from 'react';
|
|
2
8
|
import { DndProvider } from 'react-dnd';
|
|
3
9
|
import { HTML5Backend } from 'react-dnd-html5-backend';
|
|
4
10
|
import type { Descendant } from 'slate';
|
|
5
11
|
import type { CustomElement } from '../../types';
|
|
6
12
|
import { deserialize } from '../core/deserialize';
|
|
13
|
+
import { serialize } from '../core/serialize';
|
|
7
14
|
import { KonaEditor } from '../editor';
|
|
8
15
|
import type { EditorRef } from '../types';
|
|
9
16
|
import styles from './Editor.module.css';
|
|
@@ -18,21 +25,29 @@ type Props = {
|
|
|
18
25
|
onChange?: (value: Descendant[]) => void;
|
|
19
26
|
};
|
|
20
27
|
|
|
21
|
-
export const ExampleEditor = (props: Props) => {
|
|
22
|
-
const { initialValueType = 'kona-editor' } =
|
|
28
|
+
export const ExampleEditor = forwardRef((props: Props, ref) => {
|
|
29
|
+
const { value: defaultValue = text, initialValueType = 'kona-editor' } =
|
|
30
|
+
props;
|
|
23
31
|
const [plugins] = useState(getPlugins());
|
|
24
32
|
const [value, setValue] = useState<Descendant[] | null>(null);
|
|
25
33
|
|
|
26
|
-
const
|
|
34
|
+
const editorRef = useRef<EditorRef>(null);
|
|
35
|
+
|
|
36
|
+
useImperativeHandle(
|
|
37
|
+
ref,
|
|
38
|
+
() => ({
|
|
39
|
+
serialize: serialize(plugins),
|
|
40
|
+
}),
|
|
41
|
+
[plugins],
|
|
42
|
+
);
|
|
27
43
|
|
|
28
44
|
// biome-ignore lint/correctness/useExhaustiveDependencies: only on init
|
|
29
45
|
useEffect(() => {
|
|
30
46
|
if (initialValueType === 'kona-editor') {
|
|
31
|
-
setValue(
|
|
47
|
+
setValue(defaultValue);
|
|
32
48
|
} else {
|
|
33
|
-
const parsed = deserialize(plugins)(
|
|
49
|
+
const parsed = deserialize(plugins)(defaultValue);
|
|
34
50
|
parsed && setValue(parsed as Descendant[]);
|
|
35
|
-
console.log(parsed);
|
|
36
51
|
}
|
|
37
52
|
}, []);
|
|
38
53
|
|
|
@@ -41,7 +56,7 @@ export const ExampleEditor = (props: Props) => {
|
|
|
41
56
|
<div className={[styles.root].join(' ')}>
|
|
42
57
|
{value && (
|
|
43
58
|
<KonaEditor
|
|
44
|
-
ref={
|
|
59
|
+
ref={editorRef}
|
|
45
60
|
initialValue={value || (initialValue as CustomElement[])}
|
|
46
61
|
plugins={plugins}
|
|
47
62
|
onChange={props.onChange || console.log}
|
|
@@ -50,4 +65,4 @@ export const ExampleEditor = (props: Props) => {
|
|
|
50
65
|
</div>
|
|
51
66
|
</DndProvider>
|
|
52
67
|
);
|
|
53
|
-
};
|
|
68
|
+
});
|
|
@@ -19,12 +19,12 @@ import {
|
|
|
19
19
|
} from '../plugins';
|
|
20
20
|
import type { CodeElement } from '../plugins/CodeBlockPlugin/types';
|
|
21
21
|
import { Backdrop } from './Backdrop';
|
|
22
|
+
import { CodeBlock } from './CodeBlock';
|
|
22
23
|
import { colors } from './colors';
|
|
23
24
|
import { DragBlock } from './DragBlock';
|
|
24
25
|
import { FloatingMenu } from './FloatingMenu';
|
|
25
26
|
import { getCommands } from './getCommands';
|
|
26
27
|
import { getShortcuts } from './getShortcuts';
|
|
27
|
-
import { LanguageSelector } from './LanguageSelector';
|
|
28
28
|
import { LinksHint } from './LinksHint';
|
|
29
29
|
import { Menu } from './Menu';
|
|
30
30
|
import { $store } from './store';
|
|
@@ -126,9 +126,9 @@ export const getPlugins = () => {
|
|
|
126
126
|
}),
|
|
127
127
|
new ListsPlugin({}),
|
|
128
128
|
new CodeBlockPlugin({
|
|
129
|
-
|
|
129
|
+
renderCodeBlock: (value, onChange, params) => {
|
|
130
130
|
return (
|
|
131
|
-
<
|
|
131
|
+
<CodeBlock
|
|
132
132
|
value={value}
|
|
133
133
|
onChange={onChange}
|
|
134
134
|
params={{
|
package/src/examples/text.tsx
CHANGED
|
@@ -40,7 +40,7 @@ export const text = (
|
|
|
40
40
|
Kona Editor
|
|
41
41
|
</htext>{' '}
|
|
42
42
|
is a text editor based on Slate.js that I use in{' '}
|
|
43
|
-
<hlink url="https://kona.to">Kona
|
|
43
|
+
<hlink url="https://kona.to">Kona calendar</hlink> for notes and event
|
|
44
44
|
descriptions. I decided to open-source the editor for a few reasons:
|
|
45
45
|
</paragraph>
|
|
46
46
|
<numberedList>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import escapeHtml from 'escape-html';
|
|
2
|
+
import { Editor, Text } from 'slate';
|
|
2
3
|
import { jsx } from 'slate-hyperscript';
|
|
3
4
|
import type { CustomElement, CustomText } from '../../../types';
|
|
4
5
|
import type { IPlugin } from '../../types';
|
|
@@ -64,6 +65,26 @@ export class BasicFormattingPlugin
|
|
|
64
65
|
|
|
65
66
|
return <span {...attributes}>{content}</span>;
|
|
66
67
|
},
|
|
68
|
+
serialize: (node: CustomElement | CustomLeaf) => {
|
|
69
|
+
if (Text.isText(node)) {
|
|
70
|
+
let text = escapeHtml(node.text);
|
|
71
|
+
|
|
72
|
+
if (node.bold) {
|
|
73
|
+
text = `<strong>${text}</strong>`;
|
|
74
|
+
}
|
|
75
|
+
if (node.italic) {
|
|
76
|
+
text = `<em>${text}</em>`;
|
|
77
|
+
}
|
|
78
|
+
if (node.underline) {
|
|
79
|
+
text = `<u>${text}</u>`;
|
|
80
|
+
}
|
|
81
|
+
if (node.strikethrough) {
|
|
82
|
+
text = `<s>${text}</s>`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return text;
|
|
86
|
+
}
|
|
87
|
+
},
|
|
67
88
|
deserialize: (element: HTMLElement, children) => {
|
|
68
89
|
const { nodeName } = element;
|
|
69
90
|
|
|
@@ -1,19 +1,14 @@
|
|
|
1
|
-
import type { ReactNode } from 'react';
|
|
2
1
|
import type { RenderElementProps } from 'slate-react';
|
|
3
2
|
import styles from './styles.module.css';
|
|
4
3
|
import type { CodeElement } from './types';
|
|
5
4
|
|
|
6
5
|
type Props = RenderElementProps & {
|
|
7
6
|
element: CodeElement;
|
|
8
|
-
renderLanguageSelector: (element: CodeElement) => ReactNode;
|
|
9
7
|
};
|
|
10
8
|
|
|
11
9
|
export const CodeBlock = (props: Props) => {
|
|
12
10
|
return (
|
|
13
11
|
<div {...props.attributes} className={styles.root} spellCheck={false}>
|
|
14
|
-
<div contentEditable={false}>
|
|
15
|
-
{props.renderLanguageSelector(props.element)}
|
|
16
|
-
</div>
|
|
17
12
|
<div className={styles.content}>
|
|
18
13
|
<div className={styles.code}>{props.children}</div>
|
|
19
14
|
</div>
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Prism from 'prismjs';
|
|
2
|
-
import type { KeyboardEvent } from 'react';
|
|
2
|
+
import type { KeyboardEvent, ReactNode } from 'react';
|
|
3
3
|
import {
|
|
4
4
|
type DecoratedRange,
|
|
5
5
|
Editor,
|
|
@@ -44,10 +44,11 @@ import 'prismjs/components/prism-yaml';
|
|
|
44
44
|
import 'prismjs/components/prism-markdown';
|
|
45
45
|
|
|
46
46
|
type Options = {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
onChange: (
|
|
47
|
+
renderCodeBlock: (
|
|
48
|
+
language: string,
|
|
49
|
+
onChange: (language: string) => void,
|
|
50
50
|
params: {
|
|
51
|
+
Content: () => ReactNode;
|
|
51
52
|
element: CodeElement;
|
|
52
53
|
},
|
|
53
54
|
) => React.ReactNode;
|
|
@@ -189,25 +190,25 @@ export class CodeBlockPlugin implements IPlugin {
|
|
|
189
190
|
{
|
|
190
191
|
type: CodeBlockPlugin.CODE_ELEMENT,
|
|
191
192
|
render: (props: RenderElementProps, editor: Editor) => {
|
|
193
|
+
const { language } = props.element as CodeElement;
|
|
192
194
|
const onChange = (language: string) => {
|
|
193
195
|
const path = ReactEditor.findPath(editor, props.element);
|
|
194
196
|
Transforms.setNodes<CodeElement>(editor, { language }, { at: path });
|
|
195
197
|
};
|
|
196
198
|
|
|
199
|
+
const Content = (): ReactNode => {
|
|
200
|
+
return (
|
|
201
|
+
<CodeBlock {...props} element={props.element as CodeElement} />
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
197
205
|
return (
|
|
198
|
-
|
|
199
|
-
{
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
onChange,
|
|
205
|
-
{
|
|
206
|
-
element: element as CodeElement,
|
|
207
|
-
},
|
|
208
|
-
)
|
|
209
|
-
}
|
|
210
|
-
/>
|
|
206
|
+
<>
|
|
207
|
+
{this.options.renderCodeBlock(language, onChange, {
|
|
208
|
+
element: props.element as CodeElement,
|
|
209
|
+
Content,
|
|
210
|
+
})}
|
|
211
|
+
</>
|
|
211
212
|
);
|
|
212
213
|
},
|
|
213
214
|
},
|
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
.root {
|
|
2
|
-
border: 1px solid var(--kona-editor-border-color, #eee);
|
|
3
|
-
border-radius: 4px;
|
|
4
2
|
overflow: hidden;
|
|
5
3
|
font-family: monospace;
|
|
6
4
|
display: flex;
|
|
@@ -9,7 +7,6 @@
|
|
|
9
7
|
|
|
10
8
|
.content {
|
|
11
9
|
display: flex;
|
|
12
|
-
background-color: var(--kona-editor-background-color, #fff);
|
|
13
10
|
}
|
|
14
11
|
|
|
15
12
|
.line {
|
|
@@ -111,7 +111,6 @@ export const Menu = (props: Props) => {
|
|
|
111
111
|
case 'Escape': {
|
|
112
112
|
event.stopPropagation();
|
|
113
113
|
$store.setKey('isOpen', false);
|
|
114
|
-
close();
|
|
115
114
|
break;
|
|
116
115
|
}
|
|
117
116
|
}
|
|
@@ -166,7 +165,14 @@ export const Menu = (props: Props) => {
|
|
|
166
165
|
return createPortal(
|
|
167
166
|
renderMenu(
|
|
168
167
|
<>
|
|
169
|
-
{store.isOpen &&
|
|
168
|
+
{store.isOpen && (
|
|
169
|
+
<div
|
|
170
|
+
className={styles.backdrop}
|
|
171
|
+
onClick={() => {
|
|
172
|
+
$store.setKey('isOpen', false);
|
|
173
|
+
}}
|
|
174
|
+
/>
|
|
175
|
+
)}
|
|
170
176
|
<div
|
|
171
177
|
ref={handleMenuLayout}
|
|
172
178
|
style={style}
|
|
@@ -1,22 +1,15 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
--menu-background-color: #fff;
|
|
3
|
-
--menu-background-color-hover: #f9f9f9;
|
|
4
|
-
--menu-text-color: #444;
|
|
5
|
-
--menu-border-color: #ddd;
|
|
6
|
-
}
|
|
7
|
-
|
|
8
1
|
.menu {
|
|
9
2
|
position: absolute;
|
|
10
|
-
color: var(--
|
|
3
|
+
color: var(--kona-editor-text-color);
|
|
11
4
|
z-index: 31;
|
|
12
5
|
top: -100000px;
|
|
13
6
|
left: -100000px;
|
|
14
7
|
opacity: 0;
|
|
15
8
|
transform: scale(0.9);
|
|
16
9
|
margin-top: -6px;
|
|
17
|
-
background-color: var(--
|
|
10
|
+
background-color: var(--kona-editor-background-color, #fff);
|
|
18
11
|
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.025);
|
|
19
|
-
border: 1px solid var(--
|
|
12
|
+
border: 1px solid var(--kona-editor-border-color, #ddd);
|
|
20
13
|
border-radius: 4px;
|
|
21
14
|
transition: transform 0.25s;
|
|
22
15
|
display: flex;
|
|
@@ -24,6 +17,19 @@
|
|
|
24
17
|
max-height: 200px;
|
|
25
18
|
overflow-y: auto;
|
|
26
19
|
font-size: 12px;
|
|
20
|
+
|
|
21
|
+
&::-webkit-scrollbar {
|
|
22
|
+
width: 6px;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
&::-webkit-scrollbar-track {
|
|
26
|
+
background: transparent;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
&::-webkit-scrollbar-thumb {
|
|
30
|
+
background-color: rgba(0, 0, 0, 0.25);
|
|
31
|
+
border-radius: 10px;
|
|
32
|
+
}
|
|
27
33
|
}
|
|
28
34
|
|
|
29
35
|
.button {
|
|
@@ -36,15 +42,15 @@
|
|
|
36
42
|
height: 40px;
|
|
37
43
|
box-sizing: border-box;
|
|
38
44
|
cursor: pointer;
|
|
39
|
-
color: var(--
|
|
45
|
+
color: var(--kona-editor-text-color, #444);
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
.button:hover {
|
|
43
|
-
background-color: var(--
|
|
49
|
+
background-color: var(--kona-editor-alt-background-color, #f9f9f9);
|
|
44
50
|
}
|
|
45
51
|
|
|
46
52
|
.active {
|
|
47
|
-
background-color: var(--
|
|
53
|
+
background-color: var(--kona-editor-alt-background-color, #f9f9f9);
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
.icon {
|
|
@@ -53,8 +59,8 @@
|
|
|
53
59
|
opacity: 0.5;
|
|
54
60
|
padding: 2px;
|
|
55
61
|
font-size: 12px;
|
|
56
|
-
background: var(--
|
|
57
|
-
color: var(--
|
|
62
|
+
background: var(--kona-editor-alt-background-color, #f9f9f9);
|
|
63
|
+
color: var(--kona-editor-text-color, #444);
|
|
58
64
|
border-radius: 4px;
|
|
59
65
|
display: inline-flex;
|
|
60
66
|
align-items: center;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Editor, Element, Transforms } from 'slate';
|
|
1
|
+
import { Descendant, Editor, Element, Transforms } from 'slate';
|
|
2
2
|
import { jsx } from 'slate-hyperscript';
|
|
3
3
|
import type { RenderElementProps } from 'slate-react';
|
|
4
4
|
import type { IPlugin } from '../../types';
|
|
@@ -25,6 +25,14 @@ export class HeadingsPlugin implements IPlugin {
|
|
|
25
25
|
);
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
|
+
serialize: (node: Descendant, children) => {
|
|
29
|
+
if (
|
|
30
|
+
Element.isElement(node) &&
|
|
31
|
+
node.type === HeadingsPlugin.HeadingLevel1
|
|
32
|
+
) {
|
|
33
|
+
return `<h1>${children}</h1>`;
|
|
34
|
+
}
|
|
35
|
+
},
|
|
28
36
|
},
|
|
29
37
|
{
|
|
30
38
|
type: HeadingsPlugin.HeadingLevel2,
|
|
@@ -42,6 +50,14 @@ export class HeadingsPlugin implements IPlugin {
|
|
|
42
50
|
);
|
|
43
51
|
}
|
|
44
52
|
},
|
|
53
|
+
serialize: (node: Descendant, children) => {
|
|
54
|
+
if (
|
|
55
|
+
Element.isElement(node) &&
|
|
56
|
+
node.type === HeadingsPlugin.HeadingLevel2
|
|
57
|
+
) {
|
|
58
|
+
return `<h2>${children}</h2>`;
|
|
59
|
+
}
|
|
60
|
+
},
|
|
45
61
|
},
|
|
46
62
|
{
|
|
47
63
|
type: HeadingsPlugin.HeadingLevel3,
|
|
@@ -59,6 +75,14 @@ export class HeadingsPlugin implements IPlugin {
|
|
|
59
75
|
);
|
|
60
76
|
}
|
|
61
77
|
},
|
|
78
|
+
serialize: (node: Descendant, children) => {
|
|
79
|
+
if (
|
|
80
|
+
Element.isElement(node) &&
|
|
81
|
+
node.type === HeadingsPlugin.HeadingLevel3
|
|
82
|
+
) {
|
|
83
|
+
return `<h3>${children}</h3>`;
|
|
84
|
+
}
|
|
85
|
+
},
|
|
62
86
|
},
|
|
63
87
|
];
|
|
64
88
|
|
|
@@ -56,6 +56,14 @@ export class LinksPlugin implements IPlugin {
|
|
|
56
56
|
/>
|
|
57
57
|
);
|
|
58
58
|
},
|
|
59
|
+
serialize: (element, children) => {
|
|
60
|
+
if (
|
|
61
|
+
Element.isElement(element) &&
|
|
62
|
+
element.type === LinksPlugin.LINK_TYPE
|
|
63
|
+
) {
|
|
64
|
+
return `<a href="${(element as LinkElement).url}">${children}</a>`;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
59
67
|
deserialize: (element: HTMLElement, children) => {
|
|
60
68
|
if (element.tagName === 'A') {
|
|
61
69
|
const url = element.getAttribute('href') || '';
|
|
@@ -134,6 +134,11 @@ export class ListsPlugin implements IPlugin {
|
|
|
134
134
|
</ul>
|
|
135
135
|
);
|
|
136
136
|
},
|
|
137
|
+
serialize: (element, children) => {
|
|
138
|
+
if (element.type === ListsPlugin.BULLETED_LIST_ELEMENT) {
|
|
139
|
+
return `<ul>${children}</ul>`;
|
|
140
|
+
}
|
|
141
|
+
},
|
|
137
142
|
deserialize: (element: HTMLElement, children) => {
|
|
138
143
|
const { nodeName } = element;
|
|
139
144
|
if (nodeName === 'UL') {
|
|
@@ -154,6 +159,11 @@ export class ListsPlugin implements IPlugin {
|
|
|
154
159
|
</ol>
|
|
155
160
|
);
|
|
156
161
|
},
|
|
162
|
+
serialize: (element, children) => {
|
|
163
|
+
if (element.type === ListsPlugin.NUMBERED_LIST_ELEMENT) {
|
|
164
|
+
return `<ol>${children}</ol>`;
|
|
165
|
+
}
|
|
166
|
+
},
|
|
157
167
|
deserialize: (element: HTMLElement, children) => {
|
|
158
168
|
const { nodeName } = element;
|
|
159
169
|
if (nodeName === 'OL') {
|
|
@@ -174,6 +184,11 @@ export class ListsPlugin implements IPlugin {
|
|
|
174
184
|
</li>
|
|
175
185
|
);
|
|
176
186
|
},
|
|
187
|
+
serialize: (element, children) => {
|
|
188
|
+
if (element.type === ListsPlugin.BULLETED_LIST_ELEMENT) {
|
|
189
|
+
return `<li>${children}</li>`;
|
|
190
|
+
}
|
|
191
|
+
},
|
|
177
192
|
deserialize: (element: HTMLElement, children) => {
|
|
178
193
|
const { nodeName } = element;
|
|
179
194
|
if (nodeName === 'LI') {
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import type { KeyboardEvent, ReactElement, ReactNode } from 'react';
|
|
2
|
-
import type {
|
|
3
|
-
DecoratedRange,
|
|
4
|
-
Descendant,
|
|
5
|
-
Editor,
|
|
6
|
-
Node,
|
|
7
|
-
NodeEntry,
|
|
8
|
-
} from 'slate';
|
|
2
|
+
import type { DecoratedRange, Descendant, Editor, NodeEntry } from 'slate';
|
|
9
3
|
import type { RenderElementProps, RenderLeafProps } from 'slate-react';
|
|
10
4
|
import type { CustomElement, CustomText } from '../types';
|
|
11
5
|
|
|
@@ -60,6 +54,7 @@ export type Leaf<T extends Editor, TLeaf extends CustomText = CustomText> = {
|
|
|
60
54
|
editor: T,
|
|
61
55
|
) => ReactElement | null;
|
|
62
56
|
isVoid?: boolean;
|
|
57
|
+
serialize?: Serialize;
|
|
63
58
|
deserialize?: Deserialize;
|
|
64
59
|
};
|
|
65
60
|
|
|
@@ -72,15 +67,16 @@ export type UiParams = {
|
|
|
72
67
|
};
|
|
73
68
|
|
|
74
69
|
export type EditorRef = {
|
|
75
|
-
serialize: (
|
|
76
|
-
node: CustomElement | CustomElement[] | CustomText | CustomText[],
|
|
77
|
-
) => string;
|
|
70
|
+
serialize: (node: CustomElement | CustomText) => string;
|
|
78
71
|
deserialize: (
|
|
79
72
|
element: HTMLElement,
|
|
80
73
|
) => (Descendant | string)[] | string | Descendant | null;
|
|
81
74
|
};
|
|
82
75
|
|
|
83
|
-
export type Serialize = (
|
|
76
|
+
export type Serialize = (
|
|
77
|
+
node: CustomElement | CustomText,
|
|
78
|
+
children?: string,
|
|
79
|
+
) => string | undefined;
|
|
84
80
|
|
|
85
81
|
export type Deserialize = (
|
|
86
82
|
element: HTMLElement,
|