@valbuild/ui 0.18.0 → 0.19.0
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/dist/valbuild-ui.cjs.d.ts +3 -3
- package/dist/valbuild-ui.cjs.js +7217 -2477
- package/dist/valbuild-ui.esm.js +7083 -2343
- package/jest.config.js +4 -0
- package/package.json +6 -3
- package/src/assets/icons/Bold.tsx +2 -2
- package/src/assets/icons/Chevron.tsx +2 -2
- package/src/assets/icons/ImageIcon.tsx +2 -2
- package/src/assets/icons/Italic.tsx +2 -2
- package/src/assets/icons/Strikethrough.tsx +2 -2
- package/src/assets/icons/Underline.tsx +2 -2
- package/src/components/Button.tsx +2 -2
- package/src/components/Dropdown.tsx +2 -2
- package/src/components/RichTextEditor/Nodes/ImageNode.tsx +50 -59
- package/src/components/RichTextEditor/Plugins/ImagePlugin.tsx +1 -2
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +32 -54
- package/src/components/RichTextEditor/RichTextEditor.tsx +22 -118
- package/src/components/RichTextEditor/conversion.test.ts +132 -0
- package/src/components/RichTextEditor/conversion.ts +389 -0
- package/src/components/ValOverlay.tsx +333 -88
- package/src/components/ValWindow.stories.tsx +55 -49
- package/src/components/ValWindow.tsx +80 -78
- package/src/components/forms/Form.tsx +6 -2
- package/src/components/forms/TextArea.tsx +1 -1
- package/src/stories/RichTextEditor.stories.tsx +30 -278
- package/src/utils/readImage.ts +78 -0
- package/tailwind.config.js +4 -4
- package/tsconfig.json +2 -1
package/jest.config.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valbuild/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
@@ -16,17 +16,19 @@
|
|
|
16
16
|
"@lexical/utils": "^0.10.0",
|
|
17
17
|
"@types/express": "^4.17.17",
|
|
18
18
|
"@types/react": "^18.0.26",
|
|
19
|
+
"@valbuild/core": "~0.19.0",
|
|
19
20
|
"classnames": "^2.3.2",
|
|
20
21
|
"esbuild": "^0.17.19",
|
|
21
22
|
"lexical": "^0.10.0",
|
|
22
23
|
"react-feather": "^2.0.10",
|
|
24
|
+
"react-resizable": "^3.0.5",
|
|
23
25
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
24
26
|
"rollup-plugin-postcss": "^4.0.2",
|
|
25
27
|
"rollup-plugin-typescript2": "^0.34.1",
|
|
26
|
-
"zod": "^3.22.2"
|
|
27
|
-
"@valbuild/core": "~0.18.0"
|
|
28
|
+
"zod": "^3.22.2"
|
|
28
29
|
},
|
|
29
30
|
"devDependencies": {
|
|
31
|
+
"@lexical/headless": "^0.10.0",
|
|
30
32
|
"@storybook/addon-essentials": "^7.0.12",
|
|
31
33
|
"@storybook/addon-interactions": "^7.0.12",
|
|
32
34
|
"@storybook/addon-links": "^7.0.12",
|
|
@@ -36,6 +38,7 @@
|
|
|
36
38
|
"@storybook/react": "^7.0.12",
|
|
37
39
|
"@storybook/react-vite": "^7.0.12",
|
|
38
40
|
"@storybook/testing-library": "^0.0.14-next.2",
|
|
41
|
+
"@types/react-resizable": "^3.0.5",
|
|
39
42
|
"autoprefixer": "^10.4.13",
|
|
40
43
|
"postcss": "^8.4.21",
|
|
41
44
|
"prop-types": "^15.8.1",
|
|
@@ -3,6 +3,8 @@ import { FC } from "react";
|
|
|
3
3
|
const Bold: FC<{ className?: string }> = ({ className }) => {
|
|
4
4
|
return (
|
|
5
5
|
<svg
|
|
6
|
+
height={12}
|
|
7
|
+
width={12}
|
|
6
8
|
xmlns="http://www.w3.org/2000/svg"
|
|
7
9
|
viewBox="0 0 24 24"
|
|
8
10
|
fill="none"
|
|
@@ -11,8 +13,6 @@ const Bold: FC<{ className?: string }> = ({ className }) => {
|
|
|
11
13
|
strokeLinecap="round"
|
|
12
14
|
strokeLinejoin="round"
|
|
13
15
|
className={className}
|
|
14
|
-
width={25}
|
|
15
|
-
height={25}
|
|
16
16
|
>
|
|
17
17
|
<path d="M6 4h8a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
|
|
18
18
|
<path d="M6 12h9a4 4 0 0 1 4 4 4 4 0 0 1-4 4H6z"></path>
|
|
@@ -11,8 +11,8 @@ const Italic: FC<{ className?: string }> = ({ className }) => {
|
|
|
11
11
|
strokeLinecap="round"
|
|
12
12
|
strokeLinejoin="round"
|
|
13
13
|
className={className}
|
|
14
|
-
width={
|
|
15
|
-
height={
|
|
14
|
+
width={12}
|
|
15
|
+
height={12}
|
|
16
16
|
>
|
|
17
17
|
<line x1="19" y1="4" x2="10" y2="4"></line>
|
|
18
18
|
<line x1="14" y1="20" x2="5" y2="20"></line>
|
|
@@ -11,8 +11,8 @@ const Underline: FC<{ className?: string }> = ({ className }) => {
|
|
|
11
11
|
strokeLinecap="round"
|
|
12
12
|
strokeLinejoin="round"
|
|
13
13
|
className={className}
|
|
14
|
-
width={
|
|
15
|
-
height={
|
|
14
|
+
width={12}
|
|
15
|
+
height={12}
|
|
16
16
|
>
|
|
17
17
|
<path d="M18 4V11C18 14.3137 15.3137 17 12 17C8.68629 17 6 14.3137 6 11V4M4 21H20" />
|
|
18
18
|
</svg>
|
|
@@ -40,7 +40,7 @@ const Button: FC<ButtonProps> = ({
|
|
|
40
40
|
<button
|
|
41
41
|
disabled={disabled}
|
|
42
42
|
className={classNames(
|
|
43
|
-
"font-sans font-[12px] tracking-[0.04em] py-
|
|
43
|
+
"font-sans font-[12px] tracking-[0.04em] py-1 px-2 rounded whitespace-nowrap group relative text-primary",
|
|
44
44
|
{
|
|
45
45
|
"font-bold": variant === "primary",
|
|
46
46
|
"bg-base hover:bg-base text-fill disabled:bg-fill disabled:text-base":
|
|
@@ -53,7 +53,7 @@ const Button: FC<ButtonProps> = ({
|
|
|
53
53
|
>
|
|
54
54
|
{tooltip && (
|
|
55
55
|
<div
|
|
56
|
-
className={`absolute bottom-[-75%] left-0 z-20 bg-black w-fit h-fit text-
|
|
56
|
+
className={`absolute bottom-[-75%] left-0 z-20 bg-black w-fit h-fit text-primary hidden group-hover:block`}
|
|
57
57
|
>
|
|
58
58
|
<div>{tooltip}</div>
|
|
59
59
|
</div>
|
|
@@ -48,7 +48,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
|
|
48
48
|
}, []);
|
|
49
49
|
|
|
50
50
|
return (
|
|
51
|
-
<div ref={dropdownRef}>
|
|
51
|
+
<div className="text-[12px]" ref={dropdownRef}>
|
|
52
52
|
<Button
|
|
53
53
|
onClick={(ev) => {
|
|
54
54
|
ev.preventDefault();
|
|
@@ -68,7 +68,7 @@ const Dropdown: React.FC<DropdownProps> = ({
|
|
|
68
68
|
</span>
|
|
69
69
|
</Button>
|
|
70
70
|
{isOpen && (
|
|
71
|
-
<div className="absolute left-0 mt-2 w-48
|
|
71
|
+
<div className="absolute left-0 mt-2 w-48 shadow-lg font-mono text-[14px] text-primary bg-border z-overlay">
|
|
72
72
|
<div className="py-1 rounded-md">
|
|
73
73
|
{options?.map((option, idx) => (
|
|
74
74
|
<button
|
|
@@ -6,80 +6,65 @@ import {
|
|
|
6
6
|
Spread,
|
|
7
7
|
} from "lexical";
|
|
8
8
|
|
|
9
|
-
export
|
|
10
|
-
altText: string;
|
|
11
|
-
height?: number;
|
|
12
|
-
key?: NodeKey;
|
|
13
|
-
maxWidth?: number;
|
|
9
|
+
export type ImagePayload = {
|
|
14
10
|
src: string;
|
|
11
|
+
sha256?: string;
|
|
12
|
+
fileExt?: string;
|
|
13
|
+
altText?: string;
|
|
14
|
+
height?: number;
|
|
15
15
|
width?: number;
|
|
16
|
-
}
|
|
16
|
+
};
|
|
17
17
|
|
|
18
|
-
export type SerializedImageNode = Spread<
|
|
19
|
-
{
|
|
20
|
-
altText: string;
|
|
21
|
-
width?: number;
|
|
22
|
-
maxWidth: number;
|
|
23
|
-
height?: number;
|
|
24
|
-
src: string;
|
|
25
|
-
},
|
|
26
|
-
SerializedLexicalNode
|
|
27
|
-
>;
|
|
18
|
+
export type SerializedImageNode = Spread<ImagePayload, SerializedLexicalNode>;
|
|
28
19
|
|
|
29
20
|
export class ImageNode extends DecoratorNode<JSX.Element> {
|
|
30
21
|
__src: string;
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
22
|
+
__sha256?: string;
|
|
23
|
+
__imageFileExt?: string;
|
|
24
|
+
__altText?: string;
|
|
25
|
+
__width?: number;
|
|
26
|
+
__height?: number;
|
|
35
27
|
|
|
36
28
|
static getType(): string {
|
|
37
29
|
return "image";
|
|
38
30
|
}
|
|
39
31
|
|
|
40
32
|
static clone(node: ImageNode): ImageNode {
|
|
41
|
-
return new ImageNode(
|
|
42
|
-
node.__src,
|
|
43
|
-
node.
|
|
44
|
-
node.
|
|
45
|
-
node.
|
|
46
|
-
node.
|
|
47
|
-
node.
|
|
48
|
-
);
|
|
33
|
+
return new ImageNode({
|
|
34
|
+
src: node.__src,
|
|
35
|
+
sha256: node.__sha256,
|
|
36
|
+
altText: node.__altText,
|
|
37
|
+
width: node.__width,
|
|
38
|
+
height: node.__height,
|
|
39
|
+
fileExt: node.__fileExt,
|
|
40
|
+
});
|
|
49
41
|
}
|
|
50
42
|
|
|
51
|
-
constructor(
|
|
52
|
-
src: string,
|
|
53
|
-
altText?: string,
|
|
54
|
-
width?: "inherit" | number,
|
|
55
|
-
height?: "inherit" | number,
|
|
56
|
-
maxWidth?: number,
|
|
57
|
-
key?: NodeKey
|
|
58
|
-
) {
|
|
43
|
+
constructor(payload: ImagePayload, key?: NodeKey) {
|
|
59
44
|
super(key);
|
|
60
|
-
this.__src = src;
|
|
61
|
-
this.__altText = altText
|
|
62
|
-
this.__width = width
|
|
63
|
-
this.__height = height
|
|
64
|
-
this.
|
|
45
|
+
this.__src = payload.src;
|
|
46
|
+
this.__altText = payload.altText;
|
|
47
|
+
this.__width = payload.width;
|
|
48
|
+
this.__height = payload.height;
|
|
49
|
+
this.__imageFileExt = payload.fileExt;
|
|
50
|
+
this.__sha256 = payload.sha256;
|
|
65
51
|
}
|
|
66
52
|
|
|
67
53
|
exportJSON(): SerializedImageNode {
|
|
68
54
|
return {
|
|
69
55
|
altText: this.__altText,
|
|
70
|
-
height: this.
|
|
71
|
-
maxWidth: this.__maxWidth,
|
|
56
|
+
height: this.__width,
|
|
72
57
|
src: this.__src,
|
|
73
58
|
type: "image",
|
|
74
59
|
version: 1,
|
|
75
|
-
|
|
60
|
+
fileExt: this.__imageFileExt,
|
|
61
|
+
width: this.__width,
|
|
62
|
+
sha256: this.__sha256,
|
|
76
63
|
};
|
|
77
64
|
}
|
|
78
65
|
|
|
79
66
|
static importJSON(serializedNode: SerializedImageNode): ImageNode {
|
|
80
|
-
|
|
81
|
-
const node = $createImageNode(src);
|
|
82
|
-
return node;
|
|
67
|
+
return $createImageNode(serializedNode);
|
|
83
68
|
}
|
|
84
69
|
|
|
85
70
|
createDOM(): HTMLElement {
|
|
@@ -92,24 +77,30 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
|
|
|
92
77
|
|
|
93
78
|
decorate(): JSX.Element {
|
|
94
79
|
return (
|
|
95
|
-
<
|
|
96
|
-
key={this.__key}
|
|
80
|
+
<ImageComponent
|
|
97
81
|
src={this.__src}
|
|
98
|
-
|
|
99
|
-
|
|
82
|
+
altText={this.__altText}
|
|
83
|
+
empty={this.__empty}
|
|
100
84
|
height={this.__height}
|
|
85
|
+
nodeKey={this.getKey()}
|
|
101
86
|
/>
|
|
102
87
|
);
|
|
103
88
|
}
|
|
104
89
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
height?:
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
90
|
+
|
|
91
|
+
function ImageComponent(props: {
|
|
92
|
+
empty: boolean;
|
|
93
|
+
altText?: string;
|
|
94
|
+
height?: number;
|
|
95
|
+
nodeKey?: NodeKey;
|
|
96
|
+
src: string;
|
|
97
|
+
width?: number;
|
|
98
|
+
}): JSX.Element {
|
|
99
|
+
return <img src={props.src}></img>;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function $createImageNode(payload: ImagePayload): ImageNode {
|
|
103
|
+
return new ImageNode(payload);
|
|
113
104
|
}
|
|
114
105
|
|
|
115
106
|
export function $isImageNode(node: LexicalNode | null): boolean {
|
|
@@ -29,12 +29,11 @@ export default function ImagesPlugin(): JSX.Element | null {
|
|
|
29
29
|
editor.registerCommand<InsertImagePayload>(
|
|
30
30
|
INSERT_IMAGE_COMMAND,
|
|
31
31
|
(payload) => {
|
|
32
|
-
const imageNode = $createImageNode(payload
|
|
32
|
+
const imageNode = $createImageNode(payload);
|
|
33
33
|
$insertNodes([imageNode]);
|
|
34
34
|
if ($isRootOrShadowRoot(imageNode.getParentOrThrow())) {
|
|
35
35
|
$wrapNodeInElement(imageNode, $createParagraphNode).selectEnd();
|
|
36
36
|
}
|
|
37
|
-
|
|
38
37
|
return true;
|
|
39
38
|
},
|
|
40
39
|
COMMAND_PRIORITY_EDITOR
|
|
@@ -52,6 +52,7 @@ import Button from "../../Button";
|
|
|
52
52
|
import Dropdown from "../../Dropdown";
|
|
53
53
|
import UploadModal from "../../UploadModal";
|
|
54
54
|
import { INSERT_IMAGE_COMMAND } from "./ImagePlugin";
|
|
55
|
+
import { readImage } from "../../../utils/readImage";
|
|
55
56
|
|
|
56
57
|
export interface ToolbarSettingsProps {
|
|
57
58
|
fontsFamilies?: string[];
|
|
@@ -152,15 +153,15 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
|
|
|
152
153
|
}
|
|
153
154
|
}
|
|
154
155
|
|
|
155
|
-
setFontSize(
|
|
156
|
-
|
|
157
|
-
);
|
|
158
|
-
setFontColor(
|
|
159
|
-
|
|
160
|
-
);
|
|
161
|
-
setFontFamily(
|
|
162
|
-
|
|
163
|
-
);
|
|
156
|
+
// setFontSize(
|
|
157
|
+
// $getSelectionStyleValueForProperty(selection, "font-size", "15px")
|
|
158
|
+
// );
|
|
159
|
+
// setFontColor(
|
|
160
|
+
// $getSelectionStyleValueForProperty(selection, "color", "#000")
|
|
161
|
+
// );
|
|
162
|
+
// setFontFamily(
|
|
163
|
+
// $getSelectionStyleValueForProperty(selection, "font-family", "Arial")
|
|
164
|
+
// );
|
|
164
165
|
}
|
|
165
166
|
}, [activeEditor]);
|
|
166
167
|
|
|
@@ -276,16 +277,9 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
|
|
|
276
277
|
});
|
|
277
278
|
};
|
|
278
279
|
|
|
279
|
-
const uploadImage = (url: string, alt?: string) => {
|
|
280
|
-
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
|
|
281
|
-
altText: "URL image",
|
|
282
|
-
src: url,
|
|
283
|
-
});
|
|
284
|
-
};
|
|
285
|
-
|
|
286
280
|
return (
|
|
287
|
-
<div className="
|
|
288
|
-
<div className="flex flex-row gap-
|
|
281
|
+
<div className="sticky top-0 border-b bg-base border-highlight">
|
|
282
|
+
<div className="flex flex-row gap-1">
|
|
289
283
|
<Dropdown
|
|
290
284
|
options={Object.values(blockTypes)}
|
|
291
285
|
label={
|
|
@@ -295,28 +289,12 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
|
|
|
295
289
|
formatText(blockTypesLookup[selectedOption]);
|
|
296
290
|
}}
|
|
297
291
|
/>
|
|
298
|
-
<Dropdown
|
|
299
|
-
onChange={changeFontFamily}
|
|
300
|
-
options={fontsFamilies ?? ["sans", "serif", "solina"]}
|
|
301
|
-
label={fontFamily}
|
|
302
|
-
/>
|
|
303
|
-
<Dropdown
|
|
304
|
-
onChange={changeFontSize}
|
|
305
|
-
options={
|
|
306
|
-
fontSizes ??
|
|
307
|
-
[11, 12, 13, 14, 15, 16, 17, 18, 19, 20].map((size) => `${size}px`)
|
|
308
|
-
}
|
|
309
|
-
label={fontSize}
|
|
310
|
-
/>
|
|
311
|
-
</div>
|
|
312
|
-
<div className="flex flex-row gap-2">
|
|
313
292
|
<Button
|
|
314
293
|
variant="primary"
|
|
315
294
|
onClick={(ev) => {
|
|
316
295
|
ev.preventDefault();
|
|
317
296
|
editor.dispatchCommand(FORMAT_TEXT_COMMAND, "bold");
|
|
318
297
|
}}
|
|
319
|
-
tooltip="Format text as bold"
|
|
320
298
|
active={isBold}
|
|
321
299
|
icon={<Bold className={`${isBold && "stroke-[3px]"}`} />}
|
|
322
300
|
/>
|
|
@@ -338,27 +316,27 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
|
|
|
338
316
|
}}
|
|
339
317
|
icon={<Italic className={`${isItalic && "stroke-[3px]"}`} />}
|
|
340
318
|
/>
|
|
341
|
-
<
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
319
|
+
<label className="flex items-center justify-center">
|
|
320
|
+
<ImageIcon />
|
|
321
|
+
<input
|
|
322
|
+
type="file"
|
|
323
|
+
hidden={true}
|
|
324
|
+
onChange={(ev) => {
|
|
325
|
+
ev.preventDefault();
|
|
326
|
+
|
|
327
|
+
readImage(ev)
|
|
328
|
+
.then((res) => {
|
|
329
|
+
editor.dispatchCommand(INSERT_IMAGE_COMMAND, {
|
|
330
|
+
...res,
|
|
331
|
+
});
|
|
332
|
+
})
|
|
333
|
+
.catch((err) => {
|
|
334
|
+
console.error("Error reading image", err);
|
|
335
|
+
});
|
|
336
|
+
}}
|
|
337
|
+
/>
|
|
338
|
+
</label>
|
|
349
339
|
</div>
|
|
350
|
-
<Button
|
|
351
|
-
icon={<ImageIcon />}
|
|
352
|
-
onClick={(ev) => {
|
|
353
|
-
ev.preventDefault();
|
|
354
|
-
setShowModal(true);
|
|
355
|
-
}}
|
|
356
|
-
></Button>
|
|
357
|
-
<UploadModal
|
|
358
|
-
setShowModal={setShowModal}
|
|
359
|
-
showModal={showModal}
|
|
360
|
-
uploadImage={uploadImage}
|
|
361
|
-
/>
|
|
362
340
|
</div>
|
|
363
341
|
);
|
|
364
342
|
};
|
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
$createTextNode,
|
|
5
|
-
$getRoot,
|
|
6
|
-
LexicalEditor,
|
|
7
|
-
LexicalNode,
|
|
8
|
-
} from "lexical";
|
|
9
|
-
import {
|
|
10
|
-
ListItemNode,
|
|
11
|
-
ListNode,
|
|
12
|
-
$createListNode,
|
|
13
|
-
$createListItemNode,
|
|
14
|
-
} from "@lexical/list";
|
|
2
|
+
import { LexicalEditor } from "lexical";
|
|
3
|
+
import { ListItemNode, ListNode } from "@lexical/list";
|
|
15
4
|
import { LexicalComposer } from "@lexical/react/LexicalComposer";
|
|
16
5
|
import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
|
17
6
|
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
|
@@ -23,18 +12,12 @@ import { ImageNode } from "./Nodes/ImageNode";
|
|
|
23
12
|
import { AutoFocus } from "./Plugins/AutoFocus";
|
|
24
13
|
import ImagesPlugin from "./Plugins/ImagePlugin";
|
|
25
14
|
import Toolbar from "./Plugins/Toolbar";
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
HeadingNode as ValHeadingNode,
|
|
30
|
-
ListItemNode as ValListItemNode,
|
|
31
|
-
ParagraphNode as ValParagraphNode,
|
|
32
|
-
ListNode as ValListNode,
|
|
33
|
-
} from "@valbuild/core";
|
|
34
|
-
import { $createHeadingNode, HeadingNode } from "@lexical/rich-text";
|
|
15
|
+
import { AnyRichTextOptions, RichText } from "@valbuild/core";
|
|
16
|
+
import { HeadingNode } from "@lexical/rich-text";
|
|
17
|
+
import { toLexical } from "./conversion";
|
|
35
18
|
|
|
36
19
|
export interface RichTextEditorProps {
|
|
37
|
-
richtext: RichText
|
|
20
|
+
richtext: RichText<AnyRichTextOptions>;
|
|
38
21
|
onEditor?: (editor: LexicalEditor) => void; // Not the ideal way of passing the editor to the upper context, we need it to be able to save
|
|
39
22
|
}
|
|
40
23
|
|
|
@@ -42,92 +25,21 @@ function onError(error: any) {
|
|
|
42
25
|
console.error(error);
|
|
43
26
|
}
|
|
44
27
|
|
|
45
|
-
type ValNode =
|
|
46
|
-
| ValTextNode
|
|
47
|
-
| ValHeadingNode
|
|
48
|
-
| ValListItemNode
|
|
49
|
-
| ValParagraphNode
|
|
50
|
-
| ValListNode;
|
|
51
|
-
function toLexicalNode(node: ValNode): LexicalNode {
|
|
52
|
-
switch (node.type) {
|
|
53
|
-
case "heading":
|
|
54
|
-
return toLexicalHeadingNode(node);
|
|
55
|
-
case "listitem":
|
|
56
|
-
return toLexicalListItemNode(node);
|
|
57
|
-
case "paragraph":
|
|
58
|
-
return toLexicalParagraphNode(node);
|
|
59
|
-
case "list":
|
|
60
|
-
return toLexicalListNode(node);
|
|
61
|
-
case "text":
|
|
62
|
-
return toLexicalTextNode(node);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function toLexicalHeadingNode(heading: ValHeadingNode): LexicalNode {
|
|
67
|
-
const node = $createHeadingNode(heading.tag);
|
|
68
|
-
node.setFormat(heading.format || "");
|
|
69
|
-
node.setIndent(heading.indent || 0);
|
|
70
|
-
node.setDirection(heading.direction || "ltr");
|
|
71
|
-
node.append(...heading.children.map((child) => toLexicalNode(child)));
|
|
72
|
-
return node;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function toLexicalParagraphNode(paragraph: ValParagraphNode): LexicalNode {
|
|
76
|
-
const node = $createParagraphNode();
|
|
77
|
-
node.setFormat(paragraph.format || "");
|
|
78
|
-
node.setIndent(paragraph.indent || 0);
|
|
79
|
-
node.setDirection(paragraph.direction || "ltr");
|
|
80
|
-
node.append(...paragraph.children.map((child) => toLexicalNode(child)));
|
|
81
|
-
return node;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function toLexicalListItemNode(listItem: ValListItemNode): LexicalNode {
|
|
85
|
-
const node = $createListItemNode();
|
|
86
|
-
node.setFormat(listItem.format || "");
|
|
87
|
-
node.setIndent(listItem.indent || 0);
|
|
88
|
-
node.setDirection(listItem.direction || "ltr");
|
|
89
|
-
node.setValue(listItem.value);
|
|
90
|
-
node.setChecked(listItem.checked);
|
|
91
|
-
node.append(...listItem.children.map((child) => toLexicalNode(child)));
|
|
92
|
-
return node;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function toLexicalListNode(list: ValListNode): LexicalNode {
|
|
96
|
-
const node = $createListNode(list.listType, list.start);
|
|
97
|
-
node.setFormat(list.format || "");
|
|
98
|
-
node.setIndent(list.indent || 0);
|
|
99
|
-
node.setDirection(list.direction || "ltr");
|
|
100
|
-
node.append(...list.children.map((child) => toLexicalNode(child)));
|
|
101
|
-
return node;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function toLexicalTextNode(text: ValTextNode): LexicalNode {
|
|
105
|
-
const node = $createTextNode(text.text);
|
|
106
|
-
node.setFormat(text.format as any); // TODO: why is text.format numbers when we are trying it out?
|
|
107
|
-
text.indent && node.setIndent(text.indent);
|
|
108
|
-
text.direction && node.setDirection(text.direction);
|
|
109
|
-
node.setStyle(text.style || "");
|
|
110
|
-
node.setDetail(text.detail || 0);
|
|
111
|
-
return node;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
28
|
export const RichTextEditor: FC<RichTextEditorProps> = ({
|
|
115
29
|
richtext,
|
|
116
30
|
onEditor,
|
|
117
31
|
}) => {
|
|
118
|
-
const prePopulatedState = () => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
...richtext.children.map((child) => toLexicalNode(child))
|
|
32
|
+
const prePopulatedState = (editor: LexicalEditor) => {
|
|
33
|
+
editor.setEditorState(
|
|
34
|
+
editor.parseEditorState({ root: toLexical(richtext) })
|
|
122
35
|
);
|
|
123
|
-
root.selectEnd();
|
|
124
36
|
};
|
|
125
37
|
const initialConfig = {
|
|
126
38
|
namespace: "val",
|
|
127
39
|
editorState: prePopulatedState,
|
|
128
40
|
nodes: [HeadingNode, ImageNode, ListNode, ListItemNode],
|
|
129
41
|
theme: {
|
|
130
|
-
root: "
|
|
42
|
+
root: "p-4 bg-fill text-white font-roboto border-b border-highlight",
|
|
131
43
|
text: {
|
|
132
44
|
bold: "font-semibold",
|
|
133
45
|
underline: "underline",
|
|
@@ -152,25 +64,17 @@ export const RichTextEditor: FC<RichTextEditorProps> = ({
|
|
|
152
64
|
onError,
|
|
153
65
|
};
|
|
154
66
|
return (
|
|
155
|
-
<
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
}
|
|
168
|
-
ErrorBoundary={LexicalErrorBoundary}
|
|
169
|
-
/>
|
|
170
|
-
<ListPlugin />
|
|
171
|
-
<AutoFocus />
|
|
172
|
-
<HistoryPlugin />
|
|
173
|
-
</LexicalComposer>
|
|
174
|
-
</div>
|
|
67
|
+
<LexicalComposer initialConfig={initialConfig}>
|
|
68
|
+
<AutoFocus />
|
|
69
|
+
<Toolbar onEditor={onEditor} />
|
|
70
|
+
<RichTextPlugin
|
|
71
|
+
contentEditable={<LexicalContentEditable className="outline-none" />}
|
|
72
|
+
placeholder={<div className="">Enter some text...</div>}
|
|
73
|
+
ErrorBoundary={LexicalErrorBoundary}
|
|
74
|
+
/>
|
|
75
|
+
<ListPlugin />
|
|
76
|
+
<ImagesPlugin />
|
|
77
|
+
<HistoryPlugin />
|
|
78
|
+
</LexicalComposer>
|
|
175
79
|
);
|
|
176
80
|
};
|