5htp-core 0.4.8-2 → 0.4.8-3
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
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "5htp-core",
|
|
3
3
|
"description": "Convenient TypeScript framework designed for Performance and Productivity.",
|
|
4
|
-
"version": "0.4.8-
|
|
4
|
+
"version": "0.4.8-3",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createEditor, LexicalEditor } from 'lexical';
|
|
2
|
+
import { $generateHtmlFromNodes } from '@lexical/html';
|
|
3
|
+
import editorNodes from '@common/data/rte/nodes';
|
|
4
|
+
|
|
5
|
+
class RichEditorUtils {
|
|
6
|
+
|
|
7
|
+
public active: {
|
|
8
|
+
title: string,
|
|
9
|
+
close: () => void
|
|
10
|
+
} | null = null;
|
|
11
|
+
|
|
12
|
+
private virtualEditor: LexicalEditor | null = null;
|
|
13
|
+
|
|
14
|
+
public async jsonToHtml( value: {} ): Promise<string | null> {
|
|
15
|
+
|
|
16
|
+
if (!this.virtualEditor) {
|
|
17
|
+
// Create a headless Lexical editor instance
|
|
18
|
+
this.virtualEditor = createEditor({
|
|
19
|
+
nodes: editorNodes
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Set the editor state from JSON
|
|
24
|
+
const state = this.virtualEditor.parseEditorState(value);
|
|
25
|
+
if (state.isEmpty())
|
|
26
|
+
return null;
|
|
27
|
+
|
|
28
|
+
this.virtualEditor.setEditorState(state);
|
|
29
|
+
|
|
30
|
+
// Generate HTML from the Lexical nodes
|
|
31
|
+
const html = await this.virtualEditor.getEditorState().read(() => {
|
|
32
|
+
return $generateHtmlFromNodes(this.virtualEditor);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
return html;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default new RichEditorUtils();
|
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// Npm
|
|
6
6
|
import React from 'react';
|
|
7
7
|
|
|
8
|
-
import { EditorState,
|
|
8
|
+
import { EditorState, createEditor } from 'lexical';
|
|
9
|
+
import { $generateHtmlFromNodes } from '@lexical/html';
|
|
9
10
|
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext';
|
|
10
11
|
import { AutoFocusPlugin } from '@lexical/react/LexicalAutoFocusPlugin';
|
|
11
12
|
import { LexicalComposer } from '@lexical/react/LexicalComposer';
|
|
@@ -14,6 +15,7 @@ import { LexicalErrorBoundary } from '@lexical/react/LexicalErrorBoundary';
|
|
|
14
15
|
import { HistoryPlugin } from '@lexical/react/LexicalHistoryPlugin';
|
|
15
16
|
import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin';
|
|
16
17
|
import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin';
|
|
18
|
+
import RichEditorUtils from './currentEditor';
|
|
17
19
|
|
|
18
20
|
// Core libs
|
|
19
21
|
import { useInput, InputBaseProps, InputWrapper } from '../base';
|
|
@@ -31,7 +33,7 @@ const EMPTY_STATE = '{"root":{"children":[{"children":[],"direction":null,"forma
|
|
|
31
33
|
----------------------------------*/
|
|
32
34
|
|
|
33
35
|
export type Props = {
|
|
34
|
-
|
|
36
|
+
preview?: boolean,
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const ValueControlPlugin = ({ props, value }) => {
|
|
@@ -52,21 +54,25 @@ const ValueControlPlugin = ({ props, value }) => {
|
|
|
52
54
|
/*----------------------------------
|
|
53
55
|
- COMPOSANT
|
|
54
56
|
----------------------------------*/
|
|
55
|
-
export default (props: Props & InputBaseProps<
|
|
57
|
+
export default (props: Props & InputBaseProps<string>) => {
|
|
56
58
|
|
|
57
59
|
let {
|
|
58
60
|
// Decoration
|
|
59
61
|
required, size, title, className = '',
|
|
60
62
|
// State
|
|
61
|
-
|
|
63
|
+
errors,
|
|
62
64
|
// Actions
|
|
63
|
-
|
|
65
|
+
preview = true
|
|
64
66
|
} = props;
|
|
65
67
|
|
|
66
68
|
/*----------------------------------
|
|
67
69
|
- INIT
|
|
68
70
|
----------------------------------*/
|
|
69
71
|
|
|
72
|
+
const [isPreview, setIsPreview] = React.useState(preview);
|
|
73
|
+
|
|
74
|
+
const [html, setHTML] = React.useState();
|
|
75
|
+
|
|
70
76
|
const [{ value }, setValue] = useInput(props, EMPTY_STATE, true);
|
|
71
77
|
|
|
72
78
|
// Trigger onchange oly when finished typing
|
|
@@ -78,16 +84,52 @@ export default (props: Props & InputBaseProps<{}>) => {
|
|
|
78
84
|
- ACTIONS
|
|
79
85
|
----------------------------------*/
|
|
80
86
|
|
|
87
|
+
React.useEffect(async () => {
|
|
88
|
+
|
|
89
|
+
if (isPreview)
|
|
90
|
+
renderPreview(value);
|
|
91
|
+
|
|
92
|
+
}, [value, isPreview]);
|
|
93
|
+
|
|
94
|
+
// When isPreview changes, close the active editor
|
|
95
|
+
React.useEffect(() => {
|
|
96
|
+
if (!isPreview) {
|
|
97
|
+
|
|
98
|
+
// Close active editor
|
|
99
|
+
if (RichEditorUtils.active && RichEditorUtils.active?.title !== title)
|
|
100
|
+
RichEditorUtils.active.close();
|
|
101
|
+
|
|
102
|
+
// Set active editor
|
|
103
|
+
RichEditorUtils.active = {
|
|
104
|
+
title,
|
|
105
|
+
close: () => setIsPreview(true)
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
}
|
|
109
|
+
}, [isPreview]);
|
|
110
|
+
|
|
111
|
+
const renderPreview = async (value: {}) => {
|
|
112
|
+
|
|
113
|
+
if (typeof document === 'undefined')
|
|
114
|
+
throw new Error("HTML preview disabled in server side.");
|
|
115
|
+
|
|
116
|
+
const html = await RichEditorUtils.jsonToHtml(value);
|
|
117
|
+
|
|
118
|
+
setHTML(html);
|
|
119
|
+
}
|
|
120
|
+
|
|
81
121
|
const onChange = (editorState: EditorState) => {
|
|
82
122
|
editorState.read(() => {
|
|
83
123
|
|
|
84
|
-
const stateJson = editorState.toJSON();
|
|
85
|
-
|
|
86
124
|
if (refCommit.current !== null)
|
|
87
125
|
clearTimeout(refCommit.current);
|
|
88
126
|
|
|
89
127
|
refCommit.current = setTimeout(() => {
|
|
128
|
+
|
|
129
|
+
const stateJson = JSON.stringify(editorState.toJSON());
|
|
130
|
+
|
|
90
131
|
setValue(stateJson);
|
|
132
|
+
|
|
91
133
|
}, 100);
|
|
92
134
|
});
|
|
93
135
|
};
|
|
@@ -99,9 +141,20 @@ export default (props: Props & InputBaseProps<{}>) => {
|
|
|
99
141
|
<InputWrapper {...props}>
|
|
100
142
|
<div class={className}>
|
|
101
143
|
|
|
102
|
-
{
|
|
144
|
+
{isPreview ? (
|
|
145
|
+
|
|
146
|
+
!html ? (
|
|
147
|
+
<div class="col al-center h-4">
|
|
148
|
+
<i src="spin" />
|
|
149
|
+
</div>
|
|
150
|
+
) : (
|
|
151
|
+
<div class="h-4 scrollable col clickable"
|
|
152
|
+
onClick={() => setIsPreview(false)}
|
|
153
|
+
dangerouslySetInnerHTML={{ __html: html }} />
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
) : typeof window !== 'undefined' && (
|
|
103
157
|
<LexicalComposer initialConfig={{
|
|
104
|
-
namespace: 'React.js Demo',
|
|
105
158
|
editorState: value || EMPTY_STATE,
|
|
106
159
|
nodes: editorNodes,
|
|
107
160
|
// Handling of errors during update
|
|
@@ -125,7 +178,7 @@ export default (props: Props & InputBaseProps<{}>) => {
|
|
|
125
178
|
ErrorBoundary={LexicalErrorBoundary}
|
|
126
179
|
/>
|
|
127
180
|
<HistoryPlugin />
|
|
128
|
-
|
|
181
|
+
<AutoFocusPlugin />
|
|
129
182
|
<OnChangePlugin onChange={onChange} />
|
|
130
183
|
<ValueControlPlugin props={props} value={value} />
|
|
131
184
|
</div>
|
|
@@ -354,6 +354,13 @@ export default class SchemaValidators {
|
|
|
354
354
|
|
|
355
355
|
} = {}) => new Validator<string>('richText', (val, options, path) => {
|
|
356
356
|
|
|
357
|
+
// We get a stringified json as input since the editor workds with JSON string
|
|
358
|
+
try {
|
|
359
|
+
val = JSON.parse(val);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
throw new InputError("Invalid rich text format.");
|
|
362
|
+
}
|
|
363
|
+
|
|
357
364
|
// Check that the root exists and has a valid type
|
|
358
365
|
if (!val || typeof val !== 'object' || typeof val.root !== 'object' || val.root.type !== 'root')
|
|
359
366
|
throw new InputError("Invalid rich text value (1).");
|
|
@@ -1,26 +1,33 @@
|
|
|
1
|
+
/*----------------------------------
|
|
2
|
+
- DEPENDANCES
|
|
3
|
+
----------------------------------*/
|
|
4
|
+
|
|
5
|
+
// Npm
|
|
1
6
|
import { createHeadlessEditor } from '@lexical/headless';
|
|
2
|
-
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
7
|
+
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
3
8
|
import { $getRoot } from 'lexical';
|
|
4
|
-
|
|
5
9
|
import { JSDOM } from 'jsdom';
|
|
6
10
|
|
|
7
|
-
|
|
11
|
+
// Core
|
|
12
|
+
import editorNodes from '@common/data/rte/nodes';
|
|
8
13
|
|
|
14
|
+
/*----------------------------------
|
|
15
|
+
- FUNCTIONS
|
|
16
|
+
----------------------------------*/
|
|
9
17
|
export const htmlToJson = async (htmlString: string) => {
|
|
10
18
|
|
|
11
|
-
const editor = createHeadlessEditor({
|
|
12
|
-
nodes: editorNodes
|
|
19
|
+
const editor = createHeadlessEditor({
|
|
20
|
+
nodes: editorNodes
|
|
13
21
|
});
|
|
14
|
-
|
|
22
|
+
|
|
15
23
|
await editor.update(() => {
|
|
16
24
|
|
|
17
25
|
const root = $getRoot();
|
|
18
26
|
|
|
19
|
-
// In a headless environment you can use a package such as JSDom to parse the HTML string.
|
|
20
27
|
const dom = new JSDOM(htmlString);
|
|
21
28
|
|
|
22
29
|
// Once you have the DOM instance it's easy to generate LexicalNodes.
|
|
23
|
-
const lexicalNodes = $generateNodesFromDOM(editor, dom.window.document);
|
|
30
|
+
const lexicalNodes = $generateNodesFromDOM(editor, dom ? dom.window.document : window.document);
|
|
24
31
|
|
|
25
32
|
lexicalNodes.forEach((node) => root.append(node));
|
|
26
33
|
});
|
|
@@ -30,18 +37,19 @@ export const htmlToJson = async (htmlString: string) => {
|
|
|
30
37
|
};
|
|
31
38
|
|
|
32
39
|
export const jsonToHtml = async (jsonString: string) => {
|
|
33
|
-
|
|
34
40
|
|
|
41
|
+
// Server side: simulate DOM environment
|
|
35
42
|
const dom = new JSDOM(`<!DOCTYPE html><body></body>`);
|
|
36
43
|
global.window = dom.window;
|
|
37
44
|
global.document = dom.window.document;
|
|
38
45
|
global.DOMParser = dom.window.DOMParser;
|
|
39
46
|
global.MutationObserver = dom.window.MutationObserver;
|
|
40
|
-
|
|
47
|
+
|
|
41
48
|
// Create a headless Lexical editor instance
|
|
42
49
|
const editor = createHeadlessEditor({
|
|
43
50
|
namespace: 'headless',
|
|
44
51
|
editable: false,
|
|
52
|
+
nodes: editorNodes
|
|
45
53
|
});
|
|
46
54
|
|
|
47
55
|
// Set the editor state from JSON
|
|
@@ -49,14 +57,14 @@ export const jsonToHtml = async (jsonString: string) => {
|
|
|
49
57
|
if (state.isEmpty())
|
|
50
58
|
return null;
|
|
51
59
|
|
|
52
|
-
editor.setEditorState(
|
|
60
|
+
editor.setEditorState(state);
|
|
53
61
|
|
|
54
62
|
// Generate HTML from the Lexical nodes
|
|
55
63
|
const html = await editor.getEditorState().read(() => {
|
|
56
64
|
return $generateHtmlFromNodes(editor);
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Clean up global variables set for JSDOM to avoid memory leaks
|
|
60
68
|
delete global.window;
|
|
61
69
|
delete global.document;
|
|
62
70
|
delete global.DOMParser;
|