@valbuild/ui 0.20.0 → 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 +6 -4
- package/dist/valbuild-ui.cjs.js +13528 -12255
- package/dist/valbuild-ui.esm.js +8313 -7040
- package/package.json +4 -2
- package/src/components/Button.tsx +0 -9
- package/src/components/Dropdown.tsx +32 -25
- 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 +33 -8
- package/src/components/ValOverlay.tsx +55 -31
- package/src/components/ValOverlayContext.tsx +17 -0
- package/src/components/ValWindow.stories.tsx +5 -51
- package/src/components/ValWindow.tsx +27 -12
- package/src/components/dashboard/ValDashboard.tsx +2 -0
- package/src/components/forms/Form.tsx +2 -2
- 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",
|
|
@@ -7,7 +7,6 @@ export interface ButtonProps
|
|
|
7
7
|
icon?: React.ReactElement<SVGProps<SVGSVGElement>>;
|
|
8
8
|
active?: boolean;
|
|
9
9
|
disabled?: boolean;
|
|
10
|
-
tooltip?: string;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export function PrimaryButton({
|
|
@@ -34,7 +33,6 @@ const Button: FC<ButtonProps> = ({
|
|
|
34
33
|
icon,
|
|
35
34
|
// active = false,
|
|
36
35
|
disabled = false,
|
|
37
|
-
tooltip,
|
|
38
36
|
}) => {
|
|
39
37
|
return (
|
|
40
38
|
<button
|
|
@@ -51,13 +49,6 @@ const Button: FC<ButtonProps> = ({
|
|
|
51
49
|
)}
|
|
52
50
|
onClick={onClick}
|
|
53
51
|
>
|
|
54
|
-
{tooltip && (
|
|
55
|
-
<div
|
|
56
|
-
className={`absolute bottom-[-75%] left-0 z-20 bg-black w-fit h-fit text-primary hidden group-hover:block`}
|
|
57
|
-
>
|
|
58
|
-
<div>{tooltip}</div>
|
|
59
|
-
</div>
|
|
60
|
-
)}
|
|
61
52
|
<span className="flex flex-row items-center justify-center gap-2">
|
|
62
53
|
{icon && icon}
|
|
63
54
|
{children}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React, { SVGProps, useEffect, useRef, useState } from "react";
|
|
2
2
|
import Chevron from "../assets/icons/Chevron";
|
|
3
3
|
import Button from "./Button";
|
|
4
|
+
import { useValOverlayContext } from "./ValOverlayContext";
|
|
4
5
|
|
|
5
6
|
export interface DropdownProps {
|
|
6
7
|
options: string[];
|
|
@@ -15,11 +16,11 @@ const Dropdown: React.FC<DropdownProps> = ({
|
|
|
15
16
|
onChange,
|
|
16
17
|
label,
|
|
17
18
|
icon,
|
|
18
|
-
// variant = "primary",
|
|
19
19
|
}) => {
|
|
20
20
|
const [isOpen, setIsOpen] = useState<boolean>(false);
|
|
21
21
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
22
22
|
const [selectedOption, setSelectedOption] = useState<number>(0);
|
|
23
|
+
const { windowSize } = useValOverlayContext();
|
|
23
24
|
|
|
24
25
|
const handleToggle = () => {
|
|
25
26
|
setIsOpen(!isOpen);
|
|
@@ -62,31 +63,37 @@ const Dropdown: React.FC<DropdownProps> = ({
|
|
|
62
63
|
/>
|
|
63
64
|
}
|
|
64
65
|
>
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
66
|
+
<div className="relative">
|
|
67
|
+
<span className="flex flex-row items-center justify-center gap-1">
|
|
68
|
+
{label}
|
|
69
|
+
{icon && icon}
|
|
70
|
+
</span>
|
|
71
|
+
{isOpen && (
|
|
72
|
+
<div
|
|
73
|
+
className="absolute -top-[4px] overflow-scroll shadow-lg -left-2 text-primary bg-border w-fit z-overlay"
|
|
74
|
+
style={{ maxHeight: windowSize?.innerHeight }}
|
|
75
|
+
>
|
|
76
|
+
<div className="flex flex-col ">
|
|
77
|
+
{options?.map((option, idx) => (
|
|
78
|
+
<button
|
|
79
|
+
key={option}
|
|
80
|
+
onClick={(ev) => {
|
|
81
|
+
ev.preventDefault();
|
|
82
|
+
handleSelect(option, idx);
|
|
83
|
+
}}
|
|
84
|
+
className={`text-left px-2 py-1 hover:bg-base hover:text-highlight ${
|
|
85
|
+
idx === selectedOption &&
|
|
86
|
+
"font-bold bg-base hover:bg-base truncate"
|
|
87
|
+
}`}
|
|
88
|
+
>
|
|
89
|
+
{option}
|
|
90
|
+
</button>
|
|
91
|
+
))}
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
)}
|
|
88
95
|
</div>
|
|
89
|
-
|
|
96
|
+
</Button>
|
|
90
97
|
</div>
|
|
91
98
|
);
|
|
92
99
|
};
|
|
@@ -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,18 +8,23 @@ 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";
|
|
12
15
|
import { AutoFocus } from "./Plugins/AutoFocus";
|
|
13
16
|
import ImagesPlugin from "./Plugins/ImagePlugin";
|
|
14
17
|
import Toolbar from "./Plugins/Toolbar";
|
|
15
|
-
import { AnyRichTextOptions,
|
|
18
|
+
import { AnyRichTextOptions, RichTextSource } from "@valbuild/core";
|
|
16
19
|
import { HeadingNode } from "@lexical/rich-text";
|
|
17
|
-
import {
|
|
20
|
+
import { richTextSourceToLexical } from "../../richtext/conversion/richTextSourceToLexical";
|
|
21
|
+
import { useValOverlayContext } from "../ValOverlayContext";
|
|
22
|
+
import { parseRichTextSource } from "../../exports";
|
|
23
|
+
import { LinkNode } from "@lexical/link";
|
|
24
|
+
import LinkEditorPlugin from "./Plugins/LinkEditorPlugin";
|
|
18
25
|
|
|
19
26
|
export interface RichTextEditorProps {
|
|
20
|
-
richtext:
|
|
27
|
+
richtext: RichTextSource<AnyRichTextOptions>;
|
|
21
28
|
onEditor?: (editor: LexicalEditor) => void; // Not the ideal way of passing the editor to the upper context, we need it to be able to save
|
|
22
29
|
}
|
|
23
30
|
|
|
@@ -25,21 +32,25 @@ function onError(error: any) {
|
|
|
25
32
|
console.error(error);
|
|
26
33
|
}
|
|
27
34
|
|
|
35
|
+
const TOOLBAR_HEIGHT = 28;
|
|
36
|
+
|
|
28
37
|
export const RichTextEditor: FC<RichTextEditorProps> = ({
|
|
29
38
|
richtext,
|
|
30
39
|
onEditor,
|
|
31
40
|
}) => {
|
|
41
|
+
const { windowSize } = useValOverlayContext();
|
|
32
42
|
const prePopulatedState = (editor: LexicalEditor) => {
|
|
33
43
|
editor.setEditorState(
|
|
34
|
-
editor.parseEditorState({
|
|
44
|
+
editor.parseEditorState({
|
|
45
|
+
root: richTextSourceToLexical(parseRichTextSource(richtext)),
|
|
46
|
+
})
|
|
35
47
|
);
|
|
36
48
|
};
|
|
37
49
|
const initialConfig = {
|
|
38
50
|
namespace: "val",
|
|
39
51
|
editorState: prePopulatedState,
|
|
40
|
-
nodes: [HeadingNode, ImageNode, ListNode, ListItemNode],
|
|
52
|
+
nodes: [HeadingNode, ImageNode, ListNode, ListItemNode, LinkNode],
|
|
41
53
|
theme: {
|
|
42
|
-
root: "p-4 bg-fill text-white font-roboto border-b border-highlight",
|
|
43
54
|
text: {
|
|
44
55
|
bold: "font-semibold",
|
|
45
56
|
underline: "underline",
|
|
@@ -58,8 +69,9 @@ export const RichTextEditor: FC<RichTextEditorProps> = ({
|
|
|
58
69
|
h3: "text-2xl font-bold",
|
|
59
70
|
h4: "text-xl font-bold",
|
|
60
71
|
h5: "text-lg font-bold",
|
|
61
|
-
h6: "text-
|
|
72
|
+
h6: "text-md font-bold",
|
|
62
73
|
},
|
|
74
|
+
link: "text-highlight underline",
|
|
63
75
|
},
|
|
64
76
|
onError,
|
|
65
77
|
};
|
|
@@ -68,10 +80,23 @@ export const RichTextEditor: FC<RichTextEditorProps> = ({
|
|
|
68
80
|
<AutoFocus />
|
|
69
81
|
<Toolbar onEditor={onEditor} />
|
|
70
82
|
<RichTextPlugin
|
|
71
|
-
contentEditable={
|
|
83
|
+
contentEditable={
|
|
84
|
+
<div
|
|
85
|
+
className="text-white border-b border-highlight font-roboto"
|
|
86
|
+
style={{
|
|
87
|
+
minHeight: windowSize?.innerHeight
|
|
88
|
+
? windowSize?.innerHeight - TOOLBAR_HEIGHT
|
|
89
|
+
: undefined,
|
|
90
|
+
}}
|
|
91
|
+
>
|
|
92
|
+
<LexicalContentEditable className="p-4 outline-none bg-fill" />
|
|
93
|
+
</div>
|
|
94
|
+
}
|
|
72
95
|
placeholder={<div className="">Enter some text...</div>}
|
|
73
96
|
ErrorBoundary={LexicalErrorBoundary}
|
|
74
97
|
/>
|
|
98
|
+
<LinkPlugin />
|
|
99
|
+
<LinkEditorPlugin />
|
|
75
100
|
<ListPlugin />
|
|
76
101
|
<ImagesPlugin />
|
|
77
102
|
<HistoryPlugin />
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
1
3
|
import {
|
|
2
4
|
Dispatch,
|
|
3
5
|
SetStateAction,
|
|
@@ -8,7 +10,12 @@ import {
|
|
|
8
10
|
} from "react";
|
|
9
11
|
import { Session } from "../dto/Session";
|
|
10
12
|
import { ValMenu } from "./ValMenu";
|
|
11
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
EditMode,
|
|
15
|
+
Theme,
|
|
16
|
+
ValOverlayContext,
|
|
17
|
+
WindowSize,
|
|
18
|
+
} from "./ValOverlayContext";
|
|
12
19
|
import { Remote } from "../utils/Remote";
|
|
13
20
|
import { ValWindow } from "./ValWindow";
|
|
14
21
|
import { result } from "@valbuild/core/fp";
|
|
@@ -16,7 +23,7 @@ import {
|
|
|
16
23
|
AnyRichTextOptions,
|
|
17
24
|
FileSource,
|
|
18
25
|
Internal,
|
|
19
|
-
|
|
26
|
+
RichTextSource,
|
|
20
27
|
SerializedSchema,
|
|
21
28
|
SourcePath,
|
|
22
29
|
VAL_EXTENSION,
|
|
@@ -25,9 +32,10 @@ import { Modules, resolvePath } from "../utils/resolvePath";
|
|
|
25
32
|
import { ValApi } from "@valbuild/core";
|
|
26
33
|
import { RichTextEditor } from "../exports";
|
|
27
34
|
import { LexicalEditor } from "lexical";
|
|
28
|
-
import { LexicalRootNode
|
|
35
|
+
import { LexicalRootNode } from "../richtext/conversion/richTextSourceToLexical";
|
|
29
36
|
import { PatchJSON } from "@valbuild/core/patch";
|
|
30
37
|
import { readImage } from "../utils/readImage";
|
|
38
|
+
import { lexicalToRichTextSource } from "../richtext/conversion/lexicalToRichTextSource";
|
|
31
39
|
|
|
32
40
|
export type ValOverlayProps = {
|
|
33
41
|
defaultTheme?: "dark" | "light";
|
|
@@ -54,7 +62,7 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
54
62
|
);
|
|
55
63
|
|
|
56
64
|
const [state, setState] = useState<{
|
|
57
|
-
[path: SourcePath]: () => PatchJSON
|
|
65
|
+
[path: SourcePath]: () => Promise<PatchJSON>;
|
|
58
66
|
}>({});
|
|
59
67
|
const initPatchCallback = useCallback((currentPath: SourcePath | null) => {
|
|
60
68
|
return (callback: PatchCallback) => {
|
|
@@ -83,6 +91,8 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
83
91
|
});
|
|
84
92
|
}, [windowTarget?.path]);
|
|
85
93
|
|
|
94
|
+
const [windowSize, setWindowSize] = useState<WindowSize>();
|
|
95
|
+
|
|
86
96
|
return (
|
|
87
97
|
<ValOverlayContext.Provider
|
|
88
98
|
value={{
|
|
@@ -94,6 +104,8 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
94
104
|
highlight,
|
|
95
105
|
setHighlight,
|
|
96
106
|
setTheme,
|
|
107
|
+
windowSize,
|
|
108
|
+
setWindowSize,
|
|
97
109
|
}}
|
|
98
110
|
>
|
|
99
111
|
<div data-mode={theme}>
|
|
@@ -137,7 +149,9 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
137
149
|
selectedSource[VAL_EXTENSION] === "richtext" && (
|
|
138
150
|
<RichTextField
|
|
139
151
|
registerPatchCallback={initPatchCallback(windowTarget.path)}
|
|
140
|
-
defaultValue={
|
|
152
|
+
defaultValue={
|
|
153
|
+
selectedSource as RichTextSource<AnyRichTextOptions>
|
|
154
|
+
}
|
|
141
155
|
/>
|
|
142
156
|
)}
|
|
143
157
|
{selectedSource &&
|
|
@@ -157,16 +171,22 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
157
171
|
const [moduleId] = Internal.splitModuleIdAndModulePath(
|
|
158
172
|
windowTarget.path
|
|
159
173
|
);
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
+
}
|
|
170
190
|
}
|
|
171
191
|
}}
|
|
172
192
|
/>
|
|
@@ -178,7 +198,7 @@ export function ValOverlay({ defaultTheme, api }: ValOverlayProps) {
|
|
|
178
198
|
);
|
|
179
199
|
}
|
|
180
200
|
|
|
181
|
-
type PatchCallback = (modulePath: string) => PatchJSON
|
|
201
|
+
type PatchCallback = (modulePath: string) => Promise<PatchJSON>;
|
|
182
202
|
|
|
183
203
|
function ImageField({
|
|
184
204
|
defaultValue,
|
|
@@ -195,7 +215,7 @@ function ImageField({
|
|
|
195
215
|
} | null>(null);
|
|
196
216
|
const url = defaultValue && Internal.convertFileSource(defaultValue).url;
|
|
197
217
|
useEffect(() => {
|
|
198
|
-
registerPatchCallback((path) => {
|
|
218
|
+
registerPatchCallback(async (path) => {
|
|
199
219
|
const pathParts = path.split("/");
|
|
200
220
|
if (!data) {
|
|
201
221
|
return [];
|
|
@@ -256,27 +276,31 @@ function RichTextField({
|
|
|
256
276
|
registerPatchCallback,
|
|
257
277
|
}: {
|
|
258
278
|
registerPatchCallback: (callback: PatchCallback) => void;
|
|
259
|
-
defaultValue?:
|
|
279
|
+
defaultValue?: RichTextSource<AnyRichTextOptions>;
|
|
260
280
|
}) {
|
|
261
281
|
const [editor, setEditor] = useState<LexicalEditor | null>(null);
|
|
262
282
|
useEffect(() => {
|
|
263
283
|
if (editor) {
|
|
264
|
-
registerPatchCallback((path) => {
|
|
265
|
-
const {
|
|
266
|
-
?
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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: [],
|
|
272
293
|
files: {},
|
|
273
|
-
}
|
|
294
|
+
} as RichTextSource<AnyRichTextOptions> & {
|
|
295
|
+
files: Record<string, string>;
|
|
296
|
+
});
|
|
274
297
|
return [
|
|
275
298
|
{
|
|
276
|
-
op: "replace",
|
|
299
|
+
op: "replace" as const,
|
|
277
300
|
path,
|
|
278
301
|
value: {
|
|
279
|
-
|
|
302
|
+
templateStrings,
|
|
303
|
+
exprs,
|
|
280
304
|
[VAL_EXTENSION]: "richtext",
|
|
281
305
|
},
|
|
282
306
|
},
|
|
@@ -301,7 +325,7 @@ function RichTextField({
|
|
|
301
325
|
({
|
|
302
326
|
children: [],
|
|
303
327
|
[VAL_EXTENSION]: "root",
|
|
304
|
-
} as unknown as
|
|
328
|
+
} as unknown as RichTextSource<AnyRichTextOptions>)
|
|
305
329
|
}
|
|
306
330
|
/>
|
|
307
331
|
);
|
|
@@ -322,7 +346,7 @@ function TextField({
|
|
|
322
346
|
// to avoid registering a new callback every time the value changes
|
|
323
347
|
const ref = useRef<HTMLTextAreaElement>(null);
|
|
324
348
|
useEffect(() => {
|
|
325
|
-
registerPatchCallback((path) => {
|
|
349
|
+
registerPatchCallback(async (path) => {
|
|
326
350
|
return [
|
|
327
351
|
{
|
|
328
352
|
op: "replace",
|
|
@@ -5,6 +5,11 @@ import { ValApi } from "@valbuild/core";
|
|
|
5
5
|
|
|
6
6
|
export type Theme = "dark" | "light";
|
|
7
7
|
export type EditMode = "off" | "hover" | "window" | "full";
|
|
8
|
+
export type WindowSize = {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
innerHeight: number;
|
|
12
|
+
};
|
|
8
13
|
|
|
9
14
|
export const ValOverlayContext = React.createContext<{
|
|
10
15
|
api: ValApi;
|
|
@@ -15,6 +20,8 @@ export const ValOverlayContext = React.createContext<{
|
|
|
15
20
|
setEditMode: Dispatch<SetStateAction<EditMode>>;
|
|
16
21
|
theme: Theme;
|
|
17
22
|
setTheme: (theme: Theme) => void;
|
|
23
|
+
setWindowSize: (size: WindowSize) => void;
|
|
24
|
+
windowSize?: WindowSize;
|
|
18
25
|
}>({
|
|
19
26
|
get api(): never {
|
|
20
27
|
throw Error(
|
|
@@ -56,6 +63,16 @@ export const ValOverlayContext = React.createContext<{
|
|
|
56
63
|
"ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
|
|
57
64
|
);
|
|
58
65
|
},
|
|
66
|
+
get setWindowSize(): never {
|
|
67
|
+
throw Error(
|
|
68
|
+
"ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
|
|
69
|
+
);
|
|
70
|
+
},
|
|
71
|
+
get windowSize(): never {
|
|
72
|
+
throw Error(
|
|
73
|
+
"ValOverlayContext not found. Ensure components are wrapped by ValOverlayProvider!"
|
|
74
|
+
);
|
|
75
|
+
},
|
|
59
76
|
});
|
|
60
77
|
|
|
61
78
|
export function useValOverlayContext() {
|