5htp-core 0.4.8-1 → 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 +1 -1
- package/src/client/components/inputv3/Rte/currentEditor.ts +40 -0
- package/src/client/components/inputv3/Rte/index.tsx +63 -10
- package/src/client/components/inputv3/file/index.tsx +11 -5
- package/src/common/validation/validators.ts +10 -3
- package/src/{common/data/rte/index.ts → server/utils/rte.ts} +22 -14
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>
|
|
@@ -60,7 +60,7 @@ export type Props = {
|
|
|
60
60
|
|
|
61
61
|
// Input
|
|
62
62
|
title: ComponentChild,
|
|
63
|
-
value?: FileToUpload,
|
|
63
|
+
value?: string | FileToUpload, // string = already registered
|
|
64
64
|
|
|
65
65
|
// Display
|
|
66
66
|
emptyText?: ComponentChild,
|
|
@@ -96,7 +96,7 @@ export default ({
|
|
|
96
96
|
|
|
97
97
|
const [previewUrl, setPreviewUrl] = React.useState<string | undefined>(previewUrlInit);
|
|
98
98
|
|
|
99
|
-
className = 'input upload ' + (className === undefined ? '' : ' ' + className)
|
|
99
|
+
className = 'input upload ' + (className === undefined ? '' : ' ' + className);
|
|
100
100
|
|
|
101
101
|
/*----------------------------------
|
|
102
102
|
- ACTIONS
|
|
@@ -115,7 +115,7 @@ export default ({
|
|
|
115
115
|
React.useEffect(() => {
|
|
116
116
|
|
|
117
117
|
// Image = decode & display preview
|
|
118
|
-
if (file !== undefined && file.type.startsWith('image/'))
|
|
118
|
+
if (file !== undefined && typeof file === 'object' && file.type.startsWith('image/'))
|
|
119
119
|
createImagePreview(file.data).then(setPreviewUrl);
|
|
120
120
|
else
|
|
121
121
|
setPreviewUrl(undefined);
|
|
@@ -135,14 +135,20 @@ export default ({
|
|
|
135
135
|
|
|
136
136
|
{previewUrl ? (
|
|
137
137
|
<img src={previewUrl} />
|
|
138
|
-
) : file ? <>
|
|
138
|
+
) : typeof file === 'string' ? <>
|
|
139
|
+
<strong>A file has been selected</strong>
|
|
140
|
+
</> : file ? <>
|
|
139
141
|
<strong>{file.name}</strong>
|
|
140
142
|
</> : null}
|
|
141
143
|
</div>
|
|
142
144
|
|
|
143
145
|
<div class="row actions sp-05">
|
|
146
|
+
|
|
147
|
+
{typeof file === 'string' && <>
|
|
148
|
+
<Bouton type="secondary" icon="eye" shape="pill" size="s" link={file} />
|
|
149
|
+
</>}
|
|
144
150
|
|
|
145
|
-
<Bouton class='
|
|
151
|
+
<Bouton class='bg error' icon="trash" shape="pill" size="s"
|
|
146
152
|
async onClick={() => onChange(undefined)} />
|
|
147
153
|
</div>
|
|
148
154
|
</>}
|
|
@@ -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).");
|
|
@@ -402,14 +409,14 @@ export default class SchemaValidators {
|
|
|
402
409
|
|
|
403
410
|
} = {}) => new Validator<FileToUpload>('file', (val, options, path) => {
|
|
404
411
|
|
|
405
|
-
if (!(val instanceof FileToUpload))
|
|
406
|
-
throw new InputError(`Must be a File (${typeof val} received)`);
|
|
407
|
-
|
|
408
412
|
// Chaine = url ancien fichier = exclusion de la valeur pour conserver l'ancien fichier
|
|
409
413
|
// NOTE: Si la valeur est présente mais undefined, alors on supprimera le fichier
|
|
410
414
|
if (typeof val === 'string')
|
|
411
415
|
return EXCLUDE_VALUE;
|
|
412
416
|
|
|
417
|
+
if (!(val instanceof FileToUpload))
|
|
418
|
+
throw new InputError(`Must be a File (${typeof val} received)`);
|
|
419
|
+
|
|
413
420
|
// MIME
|
|
414
421
|
if (type !== undefined) {
|
|
415
422
|
|
|
@@ -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;
|