@valbuild/ui 0.20.2 → 0.21.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 +4 -2
- package/dist/valbuild-ui.cjs.js +10915 -9677
- package/dist/valbuild-ui.esm.js +8191 -6953
- package/package.json +4 -2
- package/src/components/RichTextEditor/Nodes/ImageNode.tsx +0 -8
- package/src/components/RichTextEditor/Plugins/LinkEditorPlugin.tsx +58 -0
- package/src/components/RichTextEditor/Plugins/Toolbar.tsx +70 -2
- package/src/components/RichTextEditor/RichTextEditor.tsx +15 -4
- package/src/components/ValOverlay.tsx +39 -27
- package/src/components/ValWindow.stories.tsx +5 -54
- package/src/components/dashboard/ValDashboard.tsx +2 -0
- package/src/exports.ts +1 -0
- package/src/richtext/conversion/conversion.test.ts +146 -0
- package/src/richtext/conversion/lexicalToRichTextSource.test.ts +89 -0
- package/src/richtext/conversion/lexicalToRichTextSource.ts +286 -0
- package/src/richtext/conversion/parseRichTextSource.test.ts +424 -0
- package/src/richtext/conversion/parseRichTextSource.ts +228 -0
- package/src/richtext/conversion/richTextSourceToLexical.test.ts +381 -0
- package/src/richtext/conversion/richTextSourceToLexical.ts +293 -0
- package/src/stories/RichTextEditor.stories.tsx +3 -47
- package/src/utils/imageMimeType.ts +23 -0
- package/src/utils/readImage.ts +1 -25
- package/src/components/RichTextEditor/conversion.test.ts +0 -132
- package/src/components/RichTextEditor/conversion.ts +0 -389
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@valbuild/ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"sideEffects": false,
|
|
5
5
|
"scripts": {
|
|
6
6
|
"typecheck": "tsc --noEmit",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"build-storybook": "storybook build"
|
|
10
10
|
},
|
|
11
11
|
"dependencies": {
|
|
12
|
+
"@lexical/link": "^0.10.0",
|
|
12
13
|
"@lexical/list": "^0.10.0",
|
|
13
14
|
"@lexical/react": "^0.10.0",
|
|
14
15
|
"@lexical/rich-text": "^0.10.0",
|
|
@@ -16,10 +17,11 @@
|
|
|
16
17
|
"@lexical/utils": "^0.10.0",
|
|
17
18
|
"@types/express": "^4.17.17",
|
|
18
19
|
"@types/react": "^18.0.26",
|
|
19
|
-
"@valbuild/core": "~0.
|
|
20
|
+
"@valbuild/core": "~0.21.0",
|
|
20
21
|
"classnames": "^2.3.2",
|
|
21
22
|
"esbuild": "^0.17.19",
|
|
22
23
|
"lexical": "^0.10.0",
|
|
24
|
+
"marked": "^9.0.3",
|
|
23
25
|
"react-feather": "^2.0.10",
|
|
24
26
|
"react-resizable": "^3.0.5",
|
|
25
27
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
@@ -8,8 +8,6 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export type ImagePayload = {
|
|
10
10
|
src: string;
|
|
11
|
-
sha256?: string;
|
|
12
|
-
fileExt?: string;
|
|
13
11
|
altText?: string;
|
|
14
12
|
height?: number;
|
|
15
13
|
width?: number;
|
|
@@ -32,11 +30,9 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
|
|
|
32
30
|
static clone(node: ImageNode): ImageNode {
|
|
33
31
|
return new ImageNode({
|
|
34
32
|
src: node.__src,
|
|
35
|
-
sha256: node.__sha256,
|
|
36
33
|
altText: node.__altText,
|
|
37
34
|
width: node.__width,
|
|
38
35
|
height: node.__height,
|
|
39
|
-
fileExt: node.__fileExt,
|
|
40
36
|
});
|
|
41
37
|
}
|
|
42
38
|
|
|
@@ -46,8 +42,6 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
|
|
|
46
42
|
this.__altText = payload.altText;
|
|
47
43
|
this.__width = payload.width;
|
|
48
44
|
this.__height = payload.height;
|
|
49
|
-
this.__imageFileExt = payload.fileExt;
|
|
50
|
-
this.__sha256 = payload.sha256;
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
exportJSON(): SerializedImageNode {
|
|
@@ -57,9 +51,7 @@ export class ImageNode extends DecoratorNode<JSX.Element> {
|
|
|
57
51
|
src: this.__src,
|
|
58
52
|
type: "image",
|
|
59
53
|
version: 1,
|
|
60
|
-
fileExt: this.__imageFileExt,
|
|
61
54
|
width: this.__width,
|
|
62
|
-
sha256: this.__sha256,
|
|
63
55
|
};
|
|
64
56
|
}
|
|
65
57
|
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { $isLinkNode } from "@lexical/link";
|
|
2
|
+
import { useLexicalComposerContext } from "@lexical/react/LexicalComposerContext";
|
|
3
|
+
import { $isAtNodeEnd } from "@lexical/selection";
|
|
4
|
+
import { $findMatchingParent, mergeRegister } from "@lexical/utils";
|
|
5
|
+
import {
|
|
6
|
+
$getSelection,
|
|
7
|
+
$isRangeSelection,
|
|
8
|
+
CLICK_COMMAND,
|
|
9
|
+
COMMAND_PRIORITY_LOW,
|
|
10
|
+
ElementNode,
|
|
11
|
+
RangeSelection,
|
|
12
|
+
TextNode,
|
|
13
|
+
} from "lexical";
|
|
14
|
+
import { useEffect } from "react";
|
|
15
|
+
|
|
16
|
+
export function getSelectedNode(
|
|
17
|
+
selection: RangeSelection
|
|
18
|
+
): TextNode | ElementNode {
|
|
19
|
+
const anchor = selection.anchor;
|
|
20
|
+
const focus = selection.focus;
|
|
21
|
+
const anchorNode = selection.anchor.getNode();
|
|
22
|
+
const focusNode = selection.focus.getNode();
|
|
23
|
+
if (anchorNode === focusNode) {
|
|
24
|
+
return anchorNode;
|
|
25
|
+
}
|
|
26
|
+
const isBackward = selection.isBackward();
|
|
27
|
+
if (isBackward) {
|
|
28
|
+
return $isAtNodeEnd(focus) ? anchorNode : focusNode;
|
|
29
|
+
} else {
|
|
30
|
+
return $isAtNodeEnd(anchor) ? anchorNode : focusNode;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export default function LinkEditorPlugin() {
|
|
35
|
+
const [editor] = useLexicalComposerContext();
|
|
36
|
+
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
return mergeRegister(
|
|
39
|
+
editor.registerCommand(
|
|
40
|
+
CLICK_COMMAND,
|
|
41
|
+
(payload) => {
|
|
42
|
+
const selection = $getSelection();
|
|
43
|
+
if ($isRangeSelection(selection)) {
|
|
44
|
+
const node = getSelectedNode(selection);
|
|
45
|
+
const linkNode = $findMatchingParent(node, $isLinkNode);
|
|
46
|
+
if ($isLinkNode(linkNode) && (payload.metaKey || payload.ctrlKey)) {
|
|
47
|
+
window.open(linkNode.getURL(), "_blank");
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
},
|
|
53
|
+
COMMAND_PRIORITY_LOW
|
|
54
|
+
)
|
|
55
|
+
);
|
|
56
|
+
}, []);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
@@ -53,6 +53,9 @@ import Dropdown from "../../Dropdown";
|
|
|
53
53
|
import UploadModal from "../../UploadModal";
|
|
54
54
|
import { INSERT_IMAGE_COMMAND } from "./ImagePlugin";
|
|
55
55
|
import { readImage } from "../../../utils/readImage";
|
|
56
|
+
import { $isLinkNode, LinkNode, TOGGLE_LINK_COMMAND } from "@lexical/link";
|
|
57
|
+
import { getSelectedNode } from "./LinkEditorPlugin";
|
|
58
|
+
import { Check, Link, X } from "react-feather";
|
|
56
59
|
|
|
57
60
|
export interface ToolbarSettingsProps {
|
|
58
61
|
fontsFamilies?: string[];
|
|
@@ -91,7 +94,12 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
|
|
|
91
94
|
const [inputUrl, setInputUrl] = useState<boolean>(false);
|
|
92
95
|
const [uploadMode, setUploadMode] = useState<"url" | "file">("url");
|
|
93
96
|
const [file, setFile] = useState<File | null>(null);
|
|
94
|
-
|
|
97
|
+
|
|
98
|
+
const [url, setUrl] = useState<string | null>(null);
|
|
99
|
+
|
|
100
|
+
const dispatchLinkChange = (url: string | null) => {
|
|
101
|
+
editor.dispatchCommand(TOGGLE_LINK_COMMAND, url);
|
|
102
|
+
};
|
|
95
103
|
|
|
96
104
|
const blockTypes: { [key: string]: string } = {
|
|
97
105
|
paragraph: "Normal",
|
|
@@ -126,6 +134,20 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
|
|
|
126
134
|
element = anchorNode.getTopLevelElementOrThrow();
|
|
127
135
|
}
|
|
128
136
|
|
|
137
|
+
// LINK STUFF
|
|
138
|
+
const node = getSelectedNode(selection);
|
|
139
|
+
const linkParent = $findMatchingParent(
|
|
140
|
+
node,
|
|
141
|
+
$isLinkNode
|
|
142
|
+
) as LinkNode | null;
|
|
143
|
+
if (linkParent !== null) {
|
|
144
|
+
const href = linkParent.getURL();
|
|
145
|
+
setUrl(href);
|
|
146
|
+
} else {
|
|
147
|
+
setUrl(null);
|
|
148
|
+
}
|
|
149
|
+
// ====================
|
|
150
|
+
|
|
129
151
|
const elementKey = element.getKey();
|
|
130
152
|
const elementDOM = activeEditor.getElementByKey(elementKey);
|
|
131
153
|
setIsBold(selection.hasFormat("bold"));
|
|
@@ -278,7 +300,7 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
|
|
|
278
300
|
};
|
|
279
301
|
|
|
280
302
|
return (
|
|
281
|
-
<div className="sticky top-0 border-b bg-base border-highlight">
|
|
303
|
+
<div className="sticky top-0 border-b bg-base border-highlight flex flex-col">
|
|
282
304
|
<div className="flex flex-row gap-1">
|
|
283
305
|
<Dropdown
|
|
284
306
|
options={Object.values(blockTypes)}
|
|
@@ -316,6 +338,21 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
|
|
|
316
338
|
}}
|
|
317
339
|
icon={<Italic className={`${isItalic && "stroke-[3px]"}`} />}
|
|
318
340
|
/>
|
|
341
|
+
<Button
|
|
342
|
+
active={url !== null}
|
|
343
|
+
onClick={(ev) => {
|
|
344
|
+
ev.preventDefault();
|
|
345
|
+
setUrl("");
|
|
346
|
+
dispatchLinkChange("");
|
|
347
|
+
}}
|
|
348
|
+
icon={
|
|
349
|
+
<Link
|
|
350
|
+
width={12}
|
|
351
|
+
height={12}
|
|
352
|
+
className={`${url !== null && "stroke-[3px]"}`}
|
|
353
|
+
/>
|
|
354
|
+
}
|
|
355
|
+
/>
|
|
319
356
|
<label className="flex items-center justify-center">
|
|
320
357
|
<ImageIcon />
|
|
321
358
|
<input
|
|
@@ -337,6 +374,37 @@ const Toolbar: FC<ToolbarSettingsProps> = ({
|
|
|
337
374
|
/>
|
|
338
375
|
</label>
|
|
339
376
|
</div>
|
|
377
|
+
{url !== null && (
|
|
378
|
+
<div className="flex flex-row p-2">
|
|
379
|
+
<input
|
|
380
|
+
type="text"
|
|
381
|
+
placeholder="Enter URL"
|
|
382
|
+
className="w-1/3 text-primary bg-base px-2"
|
|
383
|
+
value={url}
|
|
384
|
+
onChange={(ev) => {
|
|
385
|
+
ev.preventDefault();
|
|
386
|
+
setUrl(ev.target.value);
|
|
387
|
+
}}
|
|
388
|
+
></input>
|
|
389
|
+
<Button
|
|
390
|
+
variant="primary"
|
|
391
|
+
onClick={(ev) => {
|
|
392
|
+
ev.preventDefault();
|
|
393
|
+
// empty url will remove link
|
|
394
|
+
dispatchLinkChange(url || null);
|
|
395
|
+
}}
|
|
396
|
+
icon={<Check size={14} />}
|
|
397
|
+
/>
|
|
398
|
+
<Button
|
|
399
|
+
variant="primary"
|
|
400
|
+
onClick={(ev) => {
|
|
401
|
+
ev.preventDefault();
|
|
402
|
+
dispatchLinkChange(null);
|
|
403
|
+
}}
|
|
404
|
+
icon={<X size={14} />}
|
|
405
|
+
/>
|
|
406
|
+
</div>
|
|
407
|
+
)}
|
|
340
408
|
</div>
|
|
341
409
|
);
|
|
342
410
|
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
4
|
import { LexicalEditor } from "lexical";
|
|
3
5
|
import { ListItemNode, ListNode } from "@lexical/list";
|
|
@@ -6,6 +8,7 @@ import LexicalErrorBoundary from "@lexical/react/LexicalErrorBoundary";
|
|
|
6
8
|
import { HistoryPlugin } from "@lexical/react/LexicalHistoryPlugin";
|
|
7
9
|
import { RichTextPlugin } from "@lexical/react/LexicalRichTextPlugin";
|
|
8
10
|
import { ListPlugin } from "@lexical/react/LexicalListPlugin";
|
|
11
|
+
import { LinkPlugin } from "@lexical/react/LexicalLinkPlugin";
|
|
9
12
|
import { FC } from "react";
|
|
10
13
|
import LexicalContentEditable from "./ContentEditable";
|
|
11
14
|
import { ImageNode } from "./Nodes/ImageNode";
|
|
@@ -14,8 +17,11 @@ import ImagesPlugin from "./Plugins/ImagePlugin";
|
|
|
14
17
|
import Toolbar from "./Plugins/Toolbar";
|
|
15
18
|
import { AnyRichTextOptions, RichTextSource } from "@valbuild/core";
|
|
16
19
|
import { HeadingNode } from "@lexical/rich-text";
|
|
17
|
-
import {
|
|
20
|
+
import { richTextSourceToLexical } from "../../richtext/conversion/richTextSourceToLexical";
|
|
18
21
|
import { useValOverlayContext } from "../ValOverlayContext";
|
|
22
|
+
import { parseRichTextSource } from "../../exports";
|
|
23
|
+
import { LinkNode } from "@lexical/link";
|
|
24
|
+
import LinkEditorPlugin from "./Plugins/LinkEditorPlugin";
|
|
19
25
|
|
|
20
26
|
export interface RichTextEditorProps {
|
|
21
27
|
richtext: RichTextSource<AnyRichTextOptions>;
|
|
@@ -35,13 +41,15 @@ export const RichTextEditor: FC<RichTextEditorProps> = ({
|
|
|
35
41
|
const { windowSize } = useValOverlayContext();
|
|
36
42
|
const prePopulatedState = (editor: LexicalEditor) => {
|
|
37
43
|
editor.setEditorState(
|
|
38
|
-
editor.parseEditorState({
|
|
44
|
+
editor.parseEditorState({
|
|
45
|
+
root: richTextSourceToLexical(parseRichTextSource(richtext)),
|
|
46
|
+
})
|
|
39
47
|
);
|
|
40
48
|
};
|
|
41
49
|
const initialConfig = {
|
|
42
50
|
namespace: "val",
|
|
43
51
|
editorState: prePopulatedState,
|
|
44
|
-
nodes: [HeadingNode, ImageNode, ListNode, ListItemNode],
|
|
52
|
+
nodes: [HeadingNode, ImageNode, ListNode, ListItemNode, LinkNode],
|
|
45
53
|
theme: {
|
|
46
54
|
text: {
|
|
47
55
|
bold: "font-semibold",
|
|
@@ -61,8 +69,9 @@ export const RichTextEditor: FC<RichTextEditorProps> = ({
|
|
|
61
69
|
h3: "text-2xl font-bold",
|
|
62
70
|
h4: "text-xl font-bold",
|
|
63
71
|
h5: "text-lg font-bold",
|
|
64
|
-
h6: "text-
|
|
72
|
+
h6: "text-md font-bold",
|
|
65
73
|
},
|
|
74
|
+
link: "text-highlight underline",
|
|
66
75
|
},
|
|
67
76
|
onError,
|
|
68
77
|
};
|
|
@@ -86,6 +95,8 @@ export const RichTextEditor: FC<RichTextEditorProps> = ({
|
|
|
86
95
|
placeholder={<div className="">Enter some text...</div>}
|
|
87
96
|
ErrorBoundary={LexicalErrorBoundary}
|
|
88
97
|
/>
|
|
98
|
+
<LinkPlugin />
|
|
99
|
+
<LinkEditorPlugin />
|
|
89
100
|
<ListPlugin />
|
|
90
101
|
<ImagesPlugin />
|
|
91
102
|
<HistoryPlugin />
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
Dispatch,
|
|
3
5
|
SetStateAction,
|
|
@@ -21,7 +23,6 @@ import {
|
|
|
21
23
|
AnyRichTextOptions,
|
|
22
24
|
FileSource,
|
|
23
25
|
Internal,
|
|
24
|
-
RichText,
|
|
25
26
|
RichTextSource,
|
|
26
27
|
SerializedSchema,
|
|
27
28
|
SourcePath,
|
|
@@ -31,9 +32,10 @@ import { Modules, resolvePath } from "../utils/resolvePath";
|
|
|
31
32
|
import { ValApi } from "@valbuild/core";
|
|
32
33
|
import { RichTextEditor } from "../exports";
|
|
33
34
|
import { LexicalEditor } from "lexical";
|
|
34
|
-
import { LexicalRootNode
|
|
35
|
+
import { LexicalRootNode } from "../richtext/conversion/richTextSourceToLexical";
|
|
35
36
|
import { PatchJSON } from "@valbuild/core/patch";
|
|
36
37
|
import { readImage } from "../utils/readImage";
|
|
38
|
+
import { lexicalToRichTextSource } from "../richtext/conversion/lexicalToRichTextSource";
|
|
37
39
|
|
|
38
40
|
export type ValOverlayProps = {
|
|
39
41
|
defaultTheme?: "dark" | "light";
|
|
@@ -60,7 +62,7 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
60
62
|
);
|
|
61
63
|
|
|
62
64
|
const [state, setState] = useState<{
|
|
63
|
-
[path: SourcePath]: () => PatchJSON
|
|
65
|
+
[path: SourcePath]: () => Promise<PatchJSON>;
|
|
64
66
|
}>({});
|
|
65
67
|
const initPatchCallback = useCallback((currentPath: SourcePath | null) => {
|
|
66
68
|
return (callback: PatchCallback) => {
|
|
@@ -169,16 +171,22 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
169
171
|
const [moduleId] = Internal.splitModuleIdAndModulePath(
|
|
170
172
|
windowTarget.path
|
|
171
173
|
);
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
174
|
+
const res = state[windowTarget.path]();
|
|
175
|
+
if (res) {
|
|
176
|
+
res
|
|
177
|
+
.then((patch) => {
|
|
178
|
+
console.log("Submitting", patch);
|
|
179
|
+
return api.postPatches(moduleId, patch);
|
|
180
|
+
})
|
|
181
|
+
.then((res) => {
|
|
182
|
+
console.log(res);
|
|
183
|
+
})
|
|
184
|
+
.finally(() => {
|
|
185
|
+
console.log("done");
|
|
186
|
+
});
|
|
187
|
+
} else {
|
|
188
|
+
console.error("No patch");
|
|
189
|
+
}
|
|
182
190
|
}
|
|
183
191
|
}}
|
|
184
192
|
/>
|
|
@@ -190,7 +198,7 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
190
198
|
);
|
|
191
199
|
}
|
|
192
200
|
|
|
193
|
-
type PatchCallback = (modulePath: string) => PatchJSON
|
|
201
|
+
type PatchCallback = (modulePath: string) => Promise<PatchJSON>;
|
|
194
202
|
|
|
195
203
|
function ImageField({
|
|
196
204
|
defaultValue,
|
|
@@ -207,7 +215,7 @@ function ImageField({
|
|
|
207
215
|
} | null>(null);
|
|
208
216
|
const url = defaultValue && Internal.convertFileSource(defaultValue).url;
|
|
209
217
|
useEffect(() => {
|
|
210
|
-
registerPatchCallback((path) => {
|
|
218
|
+
registerPatchCallback(async (path) => {
|
|
211
219
|
const pathParts = path.split("/");
|
|
212
220
|
if (!data) {
|
|
213
221
|
return [];
|
|
@@ -273,22 +281,26 @@ function RichTextField({
|
|
|
273
281
|
const [editor, setEditor] = useState<LexicalEditor | null>(null);
|
|
274
282
|
useEffect(() => {
|
|
275
283
|
if (editor) {
|
|
276
|
-
registerPatchCallback((path) => {
|
|
277
|
-
const {
|
|
278
|
-
?
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
+
registerPatchCallback(async (path) => {
|
|
285
|
+
const { templateStrings, exprs, files } = editor?.toJSON()?.editorState
|
|
286
|
+
? await lexicalToRichTextSource(
|
|
287
|
+
editor?.toJSON()?.editorState.root as LexicalRootNode
|
|
288
|
+
)
|
|
289
|
+
: ({
|
|
290
|
+
[VAL_EXTENSION]: "richtext",
|
|
291
|
+
templateStrings: [""],
|
|
292
|
+
exprs: [],
|
|
284
293
|
files: {},
|
|
285
|
-
}
|
|
294
|
+
} as RichTextSource<AnyRichTextOptions> & {
|
|
295
|
+
files: Record<string, string>;
|
|
296
|
+
});
|
|
286
297
|
return [
|
|
287
298
|
{
|
|
288
|
-
op: "replace",
|
|
299
|
+
op: "replace" as const,
|
|
289
300
|
path,
|
|
290
301
|
value: {
|
|
291
|
-
|
|
302
|
+
templateStrings,
|
|
303
|
+
exprs,
|
|
292
304
|
[VAL_EXTENSION]: "richtext",
|
|
293
305
|
},
|
|
294
306
|
},
|
|
@@ -334,7 +346,7 @@ function TextField({
|
|
|
334
346
|
// to avoid registering a new callback every time the value changes
|
|
335
347
|
const ref = useRef<HTMLTextAreaElement>(null);
|
|
336
348
|
useEffect(() => {
|
|
337
|
-
registerPatchCallback((path) => {
|
|
349
|
+
registerPatchCallback(async (path) => {
|
|
338
350
|
return [
|
|
339
351
|
{
|
|
340
352
|
op: "replace",
|
|
@@ -1,8 +1,4 @@
|
|
|
1
1
|
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import {
|
|
3
|
-
AnyRichTextOptions,
|
|
4
|
-
RichTextSource as RichTextSourceType,
|
|
5
|
-
} from "@valbuild/core";
|
|
6
2
|
import { RichTextEditor } from "../exports";
|
|
7
3
|
import { FormContainer } from "./forms/FormContainer";
|
|
8
4
|
import { ImageForm } from "./forms/ImageForm";
|
|
@@ -78,56 +74,11 @@ export const RichText: Story = {
|
|
|
78
74
|
}}
|
|
79
75
|
>
|
|
80
76
|
<RichTextEditor
|
|
81
|
-
richtext={
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
{ tag: "h2", children: ["Title 2"] },
|
|
87
|
-
{ tag: "h3", children: ["Title 3"] },
|
|
88
|
-
{ tag: "h4", children: ["Title 4"] },
|
|
89
|
-
{ tag: "h5", children: ["Title 5"] },
|
|
90
|
-
{ tag: "h6", children: ["Title 6"] },
|
|
91
|
-
{
|
|
92
|
-
tag: "p",
|
|
93
|
-
children: [
|
|
94
|
-
{
|
|
95
|
-
tag: "span",
|
|
96
|
-
classes: ["bold", "italic", "line-through"],
|
|
97
|
-
children: ["Formatted span"],
|
|
98
|
-
},
|
|
99
|
-
],
|
|
100
|
-
},
|
|
101
|
-
{
|
|
102
|
-
tag: "ul",
|
|
103
|
-
children: [
|
|
104
|
-
{
|
|
105
|
-
tag: "li",
|
|
106
|
-
children: [
|
|
107
|
-
{
|
|
108
|
-
tag: "ol",
|
|
109
|
-
dir: "rtl",
|
|
110
|
-
children: [
|
|
111
|
-
{
|
|
112
|
-
tag: "li",
|
|
113
|
-
children: [
|
|
114
|
-
{
|
|
115
|
-
tag: "span",
|
|
116
|
-
classes: ["italic"],
|
|
117
|
-
children: ["number 1.1"],
|
|
118
|
-
},
|
|
119
|
-
],
|
|
120
|
-
},
|
|
121
|
-
{ tag: "li", children: ["number 1.2"] },
|
|
122
|
-
],
|
|
123
|
-
},
|
|
124
|
-
],
|
|
125
|
-
},
|
|
126
|
-
],
|
|
127
|
-
},
|
|
128
|
-
],
|
|
129
|
-
} as RichTextSourceType<AnyRichTextOptions>
|
|
130
|
-
}
|
|
77
|
+
richtext={{
|
|
78
|
+
_type: "richtext",
|
|
79
|
+
templateStrings: ["# Title 1"],
|
|
80
|
+
exprs: [],
|
|
81
|
+
}}
|
|
131
82
|
/>
|
|
132
83
|
</FormContainer>
|
|
133
84
|
),
|
package/src/exports.ts
CHANGED
|
@@ -2,3 +2,4 @@ export { RichTextEditor } from "./components/RichTextEditor/RichTextEditor";
|
|
|
2
2
|
export { ValOverlay } from "./components/ValOverlay";
|
|
3
3
|
export { type Inputs } from "./components/forms/Form";
|
|
4
4
|
export { ValDashboard } from "./components/dashboard/ValDashboard";
|
|
5
|
+
export { parseRichTextSource } from "./richtext/conversion/parseRichTextSource";
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initVal,
|
|
3
|
+
RichTextSource,
|
|
4
|
+
AnyRichTextOptions,
|
|
5
|
+
FILE_REF_PROP,
|
|
6
|
+
} from "@valbuild/core";
|
|
7
|
+
import { richTextSourceToLexical } from "./richTextSourceToLexical";
|
|
8
|
+
import { lexicalToRichTextSource } from "./lexicalToRichTextSource";
|
|
9
|
+
import { parseRichTextSource } from "./parseRichTextSource";
|
|
10
|
+
|
|
11
|
+
const { val } = initVal();
|
|
12
|
+
const cases: {
|
|
13
|
+
description: string;
|
|
14
|
+
input: RichTextSource<AnyRichTextOptions>;
|
|
15
|
+
}[] = [
|
|
16
|
+
{
|
|
17
|
+
description: "basic",
|
|
18
|
+
input: val.richtext`
|
|
19
|
+
# Title 1
|
|
20
|
+
|
|
21
|
+
## Title 2
|
|
22
|
+
|
|
23
|
+
### Title 3
|
|
24
|
+
|
|
25
|
+
#### Title 4
|
|
26
|
+
|
|
27
|
+
##### Title 5
|
|
28
|
+
|
|
29
|
+
###### Title 6
|
|
30
|
+
|
|
31
|
+
Some paragraph. Another sentence.
|
|
32
|
+
|
|
33
|
+
Another paragraph.
|
|
34
|
+
|
|
35
|
+
Formatting: **bold**, _italic_, ~~line-through~~, ***bold and italic***.
|
|
36
|
+
|
|
37
|
+
- List 1
|
|
38
|
+
1. List 1.1
|
|
39
|
+
1. List 1.2
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
description: "all features",
|
|
44
|
+
input: val.richtext`
|
|
45
|
+
# Title 1
|
|
46
|
+
|
|
47
|
+
Title 1 content.
|
|
48
|
+
|
|
49
|
+
## Title 2
|
|
50
|
+
|
|
51
|
+
Title 2 content.
|
|
52
|
+
|
|
53
|
+
### Title 3
|
|
54
|
+
|
|
55
|
+
Title 3 content.
|
|
56
|
+
|
|
57
|
+
#### Title 4
|
|
58
|
+
|
|
59
|
+
Title 4 content.
|
|
60
|
+
|
|
61
|
+
##### Title 5
|
|
62
|
+
|
|
63
|
+
###### Title 6
|
|
64
|
+
|
|
65
|
+
Some paragraph. Another sentence.
|
|
66
|
+
|
|
67
|
+
Another paragraph.
|
|
68
|
+
|
|
69
|
+
Formatting: **bold**, _italic_, ~~line-through~~, ***bold and italic***.
|
|
70
|
+
|
|
71
|
+
- List 1
|
|
72
|
+
1. List 1.1
|
|
73
|
+
1. List 1.2
|
|
74
|
+
|
|
75
|
+
Inline link: ${val.link("**link**", { href: "https://link.com" })}
|
|
76
|
+
|
|
77
|
+
<br />
|
|
78
|
+
|
|
79
|
+
Block link:
|
|
80
|
+
|
|
81
|
+
${val.link("**link**", { href: "https://link.com" })}
|
|
82
|
+
|
|
83
|
+
<br />
|
|
84
|
+
|
|
85
|
+
Block Image:
|
|
86
|
+
|
|
87
|
+
${val.file("/public/test.jpg", {
|
|
88
|
+
width: 100,
|
|
89
|
+
height: 100,
|
|
90
|
+
sha256: "123",
|
|
91
|
+
})}
|
|
92
|
+
|
|
93
|
+
<br />
|
|
94
|
+
|
|
95
|
+
<br />
|
|
96
|
+
|
|
97
|
+
- List 1
|
|
98
|
+
1. List 1.1
|
|
99
|
+
1. List 1.2
|
|
100
|
+
- List 2
|
|
101
|
+
- List 3
|
|
102
|
+
1. Formatted **list**
|
|
103
|
+
Test 123
|
|
104
|
+
`,
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
|
|
108
|
+
describe("isomorphic richtext <-> conversion", () => {
|
|
109
|
+
test.each(cases)("$description", async ({ input }) => {
|
|
110
|
+
const inputSource = input;
|
|
111
|
+
|
|
112
|
+
const res = await lexicalToRichTextSource(
|
|
113
|
+
richTextSourceToLexical(parseRichTextSource(inputSource))
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
const output = stringifyRichTextSource(res);
|
|
117
|
+
// console.log("EOF>>" + output + "<<EOF");
|
|
118
|
+
expect(stringifyRichTextSource(inputSource)).toStrictEqual(output);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
function stringifyRichTextSource({
|
|
123
|
+
templateStrings,
|
|
124
|
+
exprs,
|
|
125
|
+
}: RichTextSource<AnyRichTextOptions>): string {
|
|
126
|
+
let lines = "";
|
|
127
|
+
for (let i = 0; i < templateStrings.length; i++) {
|
|
128
|
+
const line = templateStrings[i];
|
|
129
|
+
const expr = exprs[i];
|
|
130
|
+
lines += line;
|
|
131
|
+
if (expr) {
|
|
132
|
+
if (expr._type === "file") {
|
|
133
|
+
lines += `\${val.file("${expr[FILE_REF_PROP]}", ${JSON.stringify(
|
|
134
|
+
expr.metadata
|
|
135
|
+
)})}`;
|
|
136
|
+
} else if (expr._type === "link") {
|
|
137
|
+
lines += `\${val.link("${expr.children[0]}", ${JSON.stringify({
|
|
138
|
+
href: expr.href,
|
|
139
|
+
})})}`;
|
|
140
|
+
} else {
|
|
141
|
+
throw Error("Unknown expr: " + JSON.stringify(expr, null, 2));
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return lines;
|
|
146
|
+
}
|