5htp-core 0.6.0-83 → 0.6.0-85
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/client/assets/css/components/button.less +13 -4
- package/client/assets/css/text/icons.less +1 -1
- package/client/components/Button.tsx +9 -6
- package/client/components/DropDown.tsx +5 -4
- package/client/components/Rte/Editor.tsx +21 -8
- package/client/components/Rte/ToolbarPlugin/index.tsx +211 -210
- package/client/components/Rte/currentEditor.ts +31 -2
- package/client/components/Rte/index.tsx +2 -1
- package/client/components/Rte/plugins/FloatingTextFormatToolbarPlugin/index.tsx +4 -1
- package/common/utils/rte.ts +183 -0
- package/package.json +1 -1
- package/server/app/container/index.ts +3 -0
- package/server/services/prisma/index.ts +6 -0
- package/server/utils/rte.ts +21 -172
- package/types/icons.d.ts +1 -1
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
min-height: @sizeComponent;
|
|
22
22
|
|
|
23
23
|
// Shape
|
|
24
|
-
border-radius: @
|
|
24
|
+
border-radius: @sizeComponent / 2; // Default = pill
|
|
25
25
|
border: none;
|
|
26
26
|
|
|
27
27
|
// Text
|
|
@@ -208,8 +208,11 @@
|
|
|
208
208
|
}
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
-
&.
|
|
212
|
-
|
|
211
|
+
&.rect,
|
|
212
|
+
&.col,
|
|
213
|
+
.col.menu > &,
|
|
214
|
+
.col.menu > li > & {
|
|
215
|
+
border-radius: @radius;
|
|
213
216
|
}
|
|
214
217
|
|
|
215
218
|
&.link {
|
|
@@ -242,7 +245,6 @@
|
|
|
242
245
|
|
|
243
246
|
// Give less imortance to buttons which are in lists
|
|
244
247
|
color: var(--cTxtBase);
|
|
245
|
-
padding: 0 1em; // Row display = more condensed
|
|
246
248
|
|
|
247
249
|
&.active,
|
|
248
250
|
&:hover {
|
|
@@ -290,6 +292,13 @@
|
|
|
290
292
|
}
|
|
291
293
|
}
|
|
292
294
|
|
|
295
|
+
.row {
|
|
296
|
+
&.menu > .btn:not(.col),
|
|
297
|
+
&.menu > li > .btn:not(.col) {
|
|
298
|
+
padding: 0 1em; // Row display = more condensed
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
293
302
|
.submenu.card {
|
|
294
303
|
animation: aff-submenu 0.1s ease;
|
|
295
304
|
@keyframes aff-submenu {
|
|
@@ -28,7 +28,7 @@ export type Props = {
|
|
|
28
28
|
|
|
29
29
|
tag?: "a" | "button",
|
|
30
30
|
type?: 'guide' | 'primary' | 'secondary' | 'link',
|
|
31
|
-
shape?: 'default' | 'icon' | 'tile' | 'pill',
|
|
31
|
+
shape?: 'default' | 'icon' | 'tile' | 'pill' | 'custom',
|
|
32
32
|
size?: TComponentSize,
|
|
33
33
|
class?: string,
|
|
34
34
|
|
|
@@ -273,11 +273,14 @@ export default ({
|
|
|
273
273
|
}
|
|
274
274
|
}}>
|
|
275
275
|
{prefix}
|
|
276
|
-
{children
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
276
|
+
{children === undefined
|
|
277
|
+
? null
|
|
278
|
+
: shape === 'custom'
|
|
279
|
+
? children : (
|
|
280
|
+
<span class={"label"}>
|
|
281
|
+
{children}
|
|
282
|
+
</span>
|
|
283
|
+
)}
|
|
281
284
|
{suffix}
|
|
282
285
|
</Tag>
|
|
283
286
|
)
|
|
@@ -17,18 +17,19 @@ import { useMantineInput, InputBaseProps } from './utils';
|
|
|
17
17
|
----------------------------------*/
|
|
18
18
|
export type Props = ButtonProps & {
|
|
19
19
|
menuProps?: MenuProps,
|
|
20
|
-
|
|
20
|
+
label: ComponentChild,
|
|
21
|
+
popover?: PopoverProps,
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/*----------------------------------
|
|
24
25
|
- COMPOSANT
|
|
25
26
|
----------------------------------*/
|
|
26
27
|
export default ({
|
|
27
|
-
label, children,
|
|
28
|
+
label, children, popover = {},
|
|
28
29
|
menuProps = {}, size, icon, ...btnProps
|
|
29
30
|
}: Props) => {
|
|
30
31
|
return (
|
|
31
|
-
<Popover content={(
|
|
32
|
+
<Popover {...popover} content={(
|
|
32
33
|
<div class="card bg white col menu">
|
|
33
34
|
{children}
|
|
34
35
|
</div>
|
|
@@ -36,8 +37,8 @@ export default ({
|
|
|
36
37
|
<Button {...btnProps}
|
|
37
38
|
size={size}
|
|
38
39
|
suffix={<i src="angle-down" />}
|
|
40
|
+
icon={icon}
|
|
39
41
|
>
|
|
40
|
-
{icon && <i src={icon} />}
|
|
41
42
|
{label}
|
|
42
43
|
</Button>
|
|
43
44
|
</Popover>
|
|
@@ -73,6 +73,7 @@ import YouTubePlugin from './plugins/YouTubePlugin';
|
|
|
73
73
|
// Core libs
|
|
74
74
|
import editorNodes from '@common/data/rte/nodes';
|
|
75
75
|
import type { Props as TRteProps } from '.';
|
|
76
|
+
import type { LexicalState } from '@common/utils/rte';
|
|
76
77
|
|
|
77
78
|
// Special componets
|
|
78
79
|
import ExampleTheme from './themes/PlaygroundEditorTheme';
|
|
@@ -112,6 +113,7 @@ export default ({ value, setValue, props }: {
|
|
|
112
113
|
// Decoration
|
|
113
114
|
title,
|
|
114
115
|
toolbar,
|
|
116
|
+
decorateText = true,
|
|
115
117
|
// Actions
|
|
116
118
|
preview = true
|
|
117
119
|
} = props;
|
|
@@ -146,7 +148,7 @@ export default ({ value, setValue, props }: {
|
|
|
146
148
|
}
|
|
147
149
|
}, [isPreview]);
|
|
148
150
|
|
|
149
|
-
const renderPreview = async (value:
|
|
151
|
+
const renderPreview = async (value: LexicalState | undefined) => {
|
|
150
152
|
|
|
151
153
|
if (!value)
|
|
152
154
|
return '';
|
|
@@ -166,7 +168,7 @@ export default ({ value, setValue, props }: {
|
|
|
166
168
|
<i src="spin" />
|
|
167
169
|
</div>
|
|
168
170
|
) : (
|
|
169
|
-
<div class="preview
|
|
171
|
+
<div class={"preview h-1-4 scrollable clickable" + (decorateText ? ' reading col' : '')}
|
|
170
172
|
onClick={() => setIsPreview(false)}
|
|
171
173
|
dangerouslySetInnerHTML={{ __html: html }} />
|
|
172
174
|
)
|
|
@@ -247,7 +249,7 @@ export default ({ value, setValue, props }: {
|
|
|
247
249
|
contentEditable={
|
|
248
250
|
<div className="editor" ref={onRef}>
|
|
249
251
|
<ContentEditable
|
|
250
|
-
className="editor-input reading col
|
|
252
|
+
className={"editor-input" + (decorateText ? ' reading col' : '')}
|
|
251
253
|
aria-placeholder={"Type text here ..."}
|
|
252
254
|
placeholder={
|
|
253
255
|
<div className="editor-placeholder">Type text here ...</div>
|
|
@@ -278,7 +280,15 @@ export default ({ value, setValue, props }: {
|
|
|
278
280
|
<SpeechToTextPlugin />
|
|
279
281
|
<AutoLinkPlugin />
|
|
280
282
|
<CodeHighlightPlugin />
|
|
281
|
-
|
|
283
|
+
|
|
284
|
+
{toolbar?.insert !== false && <>
|
|
285
|
+
|
|
286
|
+
{(toolbar?.insert === true || toolbar?.insert?.list !== false) && (
|
|
287
|
+
<ListPlugin />
|
|
288
|
+
)}
|
|
289
|
+
|
|
290
|
+
</>}
|
|
291
|
+
|
|
282
292
|
<CheckListPlugin />
|
|
283
293
|
<ListMaxIndentLevelPlugin maxDepth={7} />
|
|
284
294
|
<TablePlugin
|
|
@@ -318,10 +328,13 @@ export default ({ value, setValue, props }: {
|
|
|
318
328
|
cellMerge={true}
|
|
319
329
|
/>
|
|
320
330
|
<TableHoverActionsPlugin anchorElem={floatingAnchorElem} />
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
331
|
+
|
|
332
|
+
{toolbar?.formatting !== false && (
|
|
333
|
+
<FloatingTextFormatToolbarPlugin
|
|
334
|
+
anchorElem={floatingAnchorElem}
|
|
335
|
+
setIsLinkEditMode={setIsLinkEditMode}
|
|
336
|
+
/>
|
|
337
|
+
)}
|
|
325
338
|
</>
|
|
326
339
|
)}
|
|
327
340
|
|
|
@@ -117,6 +117,7 @@ export type TToolbarDisplay = {
|
|
|
117
117
|
image?: boolean,
|
|
118
118
|
inlineImage?: boolean,
|
|
119
119
|
table?: boolean,
|
|
120
|
+
list?: boolean,
|
|
120
121
|
poll?: boolean,
|
|
121
122
|
columns?: boolean,
|
|
122
123
|
stickyNote?: boolean,
|
|
@@ -514,7 +515,7 @@ export default function ToolbarPlugin({
|
|
|
514
515
|
const canViewerSeeInsertCodeButton = !isImageCaption;
|
|
515
516
|
|
|
516
517
|
return (
|
|
517
|
-
<div className="row menu al-left" style={{
|
|
518
|
+
<div className="row menu al-left pdb-05" style={{
|
|
518
519
|
position: 'sticky',
|
|
519
520
|
top: 0,
|
|
520
521
|
background: 'white',
|
|
@@ -614,223 +615,223 @@ export default function ToolbarPlugin({
|
|
|
614
615
|
title="Insert link"
|
|
615
616
|
/>
|
|
616
617
|
)}
|
|
618
|
+
</>
|
|
619
|
+
)}
|
|
617
620
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
621
|
+
|
|
622
|
+
{/* <DropdownColorPicker
|
|
623
|
+
disabled={!isEditable}
|
|
624
|
+
buttonClassName="toolbar-item color-picker"
|
|
625
|
+
buttonAriaLabel="Formatting text color"
|
|
626
|
+
buttonIconClassName="icon font-color"
|
|
627
|
+
color={fontColor}
|
|
628
|
+
onChange={onFontColorSelect}
|
|
629
|
+
title="text color"
|
|
630
|
+
/>
|
|
631
|
+
<DropdownColorPicker
|
|
632
|
+
disabled={!isEditable}
|
|
633
|
+
buttonClassName="toolbar-item color-picker"
|
|
634
|
+
buttonAriaLabel="Formatting background color"
|
|
635
|
+
buttonIconClassName="icon bg-color"
|
|
636
|
+
color={bgColor}
|
|
637
|
+
onChange={onBgColorSelect}
|
|
638
|
+
title="bg color"
|
|
639
|
+
/> */}
|
|
640
|
+
|
|
641
|
+
{display?.styles !== false && (
|
|
642
|
+
<DropDown popover={{ tag: 'li' }} icon="font" size="s"
|
|
643
|
+
disabled={!isEditable}
|
|
644
|
+
hint="Formatting options for additional text styles"
|
|
645
|
+
>
|
|
646
|
+
|
|
647
|
+
{display?.styles === true || display?.styles?.strikethrough !== false && (
|
|
648
|
+
<Button icon="strikethrough" size="s"
|
|
649
|
+
onClick={() => {
|
|
650
|
+
activeEditor.dispatchCommand( FORMAT_TEXT_COMMAND, 'strikethrough');
|
|
651
|
+
}}
|
|
652
|
+
active={isStrikethrough}
|
|
653
|
+
title="Format text with a strikethrough"
|
|
641
654
|
>
|
|
655
|
+
Strikethrough
|
|
656
|
+
</Button>
|
|
657
|
+
)}
|
|
642
658
|
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
active={isSuperscript}
|
|
676
|
-
title="Format text with a superscript">
|
|
677
|
-
Superscript
|
|
678
|
-
</Button>
|
|
679
|
-
)}
|
|
680
|
-
|
|
681
|
-
{display?.styles === true || display?.styles?.clear !== false && (
|
|
682
|
-
<Button icon="empty-set" size="s"
|
|
683
|
-
onClick={clearFormatting}
|
|
684
|
-
title="Clear all text formatting">
|
|
685
|
-
Clear Formatting
|
|
686
|
-
</Button>
|
|
687
|
-
)}
|
|
688
|
-
|
|
689
|
-
</DropDown>
|
|
659
|
+
{display?.styles === true || display?.styles?.subscript !== false && (
|
|
660
|
+
<Button icon="subscript" size="s"
|
|
661
|
+
onClick={() => {
|
|
662
|
+
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
|
|
663
|
+
}}
|
|
664
|
+
active={isSubscript}
|
|
665
|
+
title="Format text with a subscript"
|
|
666
|
+
>
|
|
667
|
+
Subscript
|
|
668
|
+
</Button>
|
|
669
|
+
)}
|
|
670
|
+
|
|
671
|
+
{display?.styles === true || display?.styles?.superscript !== false && (
|
|
672
|
+
<Button icon="superscript" size="s"
|
|
673
|
+
onClick={() => {
|
|
674
|
+
activeEditor.dispatchCommand(
|
|
675
|
+
FORMAT_TEXT_COMMAND,
|
|
676
|
+
'superscript',
|
|
677
|
+
);
|
|
678
|
+
}}
|
|
679
|
+
active={isSuperscript}
|
|
680
|
+
title="Format text with a superscript">
|
|
681
|
+
Superscript
|
|
682
|
+
</Button>
|
|
683
|
+
)}
|
|
684
|
+
|
|
685
|
+
{display?.styles === true || display?.styles?.clear !== false && (
|
|
686
|
+
<Button icon="empty-set" size="s"
|
|
687
|
+
onClick={clearFormatting}
|
|
688
|
+
title="Clear all text formatting">
|
|
689
|
+
Clear Formatting
|
|
690
|
+
</Button>
|
|
690
691
|
)}
|
|
691
692
|
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
693
|
+
</DropDown>
|
|
694
|
+
)}
|
|
695
|
+
|
|
696
|
+
{(canViewerSeeInsertDropdown && display?.insert !== false) && (
|
|
697
|
+
<>
|
|
698
|
+
<DropDown popover={{ tag: 'li' }}
|
|
699
|
+
disabled={!isEditable}
|
|
700
|
+
size="s"
|
|
701
|
+
icon="plus-circle"
|
|
702
|
+
hint="Insert specialized editor node"
|
|
703
|
+
>
|
|
704
|
+
|
|
705
|
+
{display?.insert === true || display?.insert?.horizontalRule !== false && (
|
|
706
|
+
<Button icon="horizontal-rule" size="s" onClick={() => {
|
|
707
|
+
activeEditor.dispatchCommand( INSERT_HORIZONTAL_RULE_COMMAND, undefined, );
|
|
708
|
+
}}>
|
|
709
|
+
Horizontal Rule
|
|
710
|
+
</Button>
|
|
711
|
+
)}
|
|
712
|
+
|
|
713
|
+
{display?.insert === true || display?.insert?.pageBreak !== false && (
|
|
714
|
+
<Button icon="page-break" size="s" onClick={() => {
|
|
715
|
+
activeEditor.dispatchCommand(INSERT_PAGE_BREAK, undefined);
|
|
716
|
+
}}>
|
|
717
|
+
Page Break
|
|
718
|
+
</Button>
|
|
719
|
+
)}
|
|
720
|
+
|
|
721
|
+
{(canViewerSeeInsertCodeButton && (display?.insert === true || display?.insert?.code !== false)) && (
|
|
722
|
+
<Button icon="code" size="s"
|
|
696
723
|
disabled={!isEditable}
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
724
|
+
onClick={() => {
|
|
725
|
+
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
|
726
|
+
}}
|
|
727
|
+
active={isCode}
|
|
728
|
+
title="Insert code block"
|
|
729
|
+
/>
|
|
730
|
+
)}
|
|
731
|
+
|
|
732
|
+
{display?.insert === true || display?.insert?.image !== false && (
|
|
733
|
+
<Button icon="image" size="s" onClick={() => {
|
|
734
|
+
modal.show('Insert Image', InsertImageDialog, { editor: activeEditor });
|
|
735
|
+
}}>
|
|
736
|
+
Image
|
|
737
|
+
</Button>
|
|
738
|
+
)}
|
|
739
|
+
|
|
740
|
+
{display?.insert === true || display?.insert?.inlineImage !== false && (
|
|
741
|
+
<Button icon="image" size="s" onClick={() => {
|
|
742
|
+
modal.show('Insert Inline Image', InsertInlineImageDialog, { editor: activeEditor });
|
|
743
|
+
}}>
|
|
744
|
+
Inline Image
|
|
745
|
+
</Button>
|
|
746
|
+
)}
|
|
747
|
+
|
|
748
|
+
{/* <Button
|
|
749
|
+
onClick={() => {
|
|
750
|
+
activeEditor.dispatchCommand(
|
|
751
|
+
INSERT_EXCALIDRAW_COMMAND,
|
|
752
|
+
undefined,
|
|
753
|
+
);
|
|
754
|
+
}}
|
|
755
|
+
className="item">
|
|
756
|
+
<i className="icon diagram-2" />
|
|
757
|
+
<span className="text">Excalidraw</span>
|
|
758
|
+
</Button> */}
|
|
759
|
+
|
|
760
|
+
{display?.insert === true || display?.insert?.table !== false && (
|
|
761
|
+
<Button icon="table" size="s" onClick={() => {
|
|
762
|
+
modal.show('Insert Table', InsertTableDialog, { editor: activeEditor });
|
|
763
|
+
}}>
|
|
764
|
+
Table
|
|
765
|
+
</Button>
|
|
766
|
+
)}
|
|
767
|
+
|
|
768
|
+
{display?.insert === true || display?.insert?.poll !== false && (
|
|
769
|
+
<Button icon="poll" size="s" onClick={() => {
|
|
770
|
+
modal.show('Insert Poll', InsertPollDialog, { editor: activeEditor });
|
|
771
|
+
}}>
|
|
772
|
+
Poll
|
|
773
|
+
</Button>
|
|
774
|
+
)}
|
|
775
|
+
|
|
776
|
+
{display?.insert === true || display?.insert?.columns !== false && (
|
|
777
|
+
<Button icon="columns" size="s" onClick={() => {
|
|
778
|
+
modal.show('Insert Columns Layout', InsertLayoutDialog, { editor: activeEditor });
|
|
779
|
+
}} >
|
|
780
|
+
Columns Layout
|
|
781
|
+
</Button>
|
|
782
|
+
)}
|
|
701
783
|
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
)}
|
|
709
|
-
|
|
710
|
-
{display?.insert === true || display?.insert?.pageBreak !== false && (
|
|
711
|
-
<Button icon="page-break" size="s" onClick={() => {
|
|
712
|
-
activeEditor.dispatchCommand(INSERT_PAGE_BREAK, undefined);
|
|
713
|
-
}}>
|
|
714
|
-
Page Break
|
|
715
|
-
</Button>
|
|
716
|
-
)}
|
|
717
|
-
|
|
718
|
-
{(canViewerSeeInsertCodeButton && (display?.insert === true || display?.insert?.code !== false)) && (
|
|
719
|
-
<Button icon="code" size="s"
|
|
720
|
-
disabled={!isEditable}
|
|
721
|
-
onClick={() => {
|
|
722
|
-
activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'code');
|
|
723
|
-
}}
|
|
724
|
-
active={isCode}
|
|
725
|
-
title="Insert code block"
|
|
784
|
+
{/* <Button
|
|
785
|
+
onClick={() => {
|
|
786
|
+
modal.show('Insert Equation', (onClose) => (
|
|
787
|
+
<InsertEquationDialog
|
|
788
|
+
activeEditor={activeEditor}
|
|
789
|
+
onClose={onClose}
|
|
726
790
|
/>
|
|
727
|
-
)
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
)}
|
|
772
|
-
|
|
773
|
-
{display?.insert === true || display?.insert?.columns !== false && (
|
|
774
|
-
<Button icon="columns" size="s" onClick={() => {
|
|
775
|
-
modal.show('Insert Columns Layout', InsertLayoutDialog, { editor: activeEditor });
|
|
776
|
-
}} >
|
|
777
|
-
Columns Layout
|
|
778
|
-
</Button>
|
|
779
|
-
)}
|
|
780
|
-
|
|
781
|
-
{/* <Button
|
|
782
|
-
onClick={() => {
|
|
783
|
-
modal.show('Insert Equation', (onClose) => (
|
|
784
|
-
<InsertEquationDialog
|
|
785
|
-
activeEditor={activeEditor}
|
|
786
|
-
onClose={onClose}
|
|
787
|
-
/>
|
|
788
|
-
));
|
|
789
|
-
}}
|
|
790
|
-
className="item">
|
|
791
|
-
<i className="icon equation" />
|
|
792
|
-
<span className="text">Equation</span>
|
|
793
|
-
</Button> */}
|
|
794
|
-
|
|
795
|
-
{display?.insert === true || display?.insert?.stickyNote !== false && (
|
|
796
|
-
<Button icon="sticky-note" size="s" onClick={() => {
|
|
797
|
-
editor.update(() => {
|
|
798
|
-
const root = $getRoot();
|
|
799
|
-
const stickyNode = $createStickyNode(0, 0);
|
|
800
|
-
root.append(stickyNode);
|
|
801
|
-
});
|
|
802
|
-
}}>
|
|
803
|
-
Sticky Note
|
|
804
|
-
</Button>
|
|
805
|
-
)}
|
|
806
|
-
|
|
807
|
-
{display?.insert === true || display?.insert?.collapsibleContainer !== false && (
|
|
808
|
-
<Button icon="caret-right" size="s" onClick={() => {
|
|
809
|
-
editor.dispatchCommand(
|
|
810
|
-
INSERT_COLLAPSIBLE_COMMAND,
|
|
811
|
-
undefined,
|
|
812
|
-
);
|
|
813
|
-
}}>
|
|
814
|
-
Collapsible container
|
|
815
|
-
</Button>
|
|
816
|
-
)}
|
|
817
|
-
|
|
818
|
-
{/*EmbedConfigs.map((embedConfig) => (
|
|
819
|
-
<Button
|
|
820
|
-
key={embedConfig.type}
|
|
821
|
-
onClick={() => {
|
|
822
|
-
activeEditor.dispatchCommand(
|
|
823
|
-
INSERT_EMBED_COMMAND,
|
|
824
|
-
embedConfig.type,
|
|
825
|
-
);
|
|
826
|
-
}}>
|
|
827
|
-
{embedConfig.icon}
|
|
828
|
-
<span className="text">{embedConfig.contentName}</span>
|
|
829
|
-
</Button>
|
|
830
|
-
))*/}
|
|
831
|
-
</DropDown>
|
|
832
|
-
</>
|
|
833
|
-
)}
|
|
791
|
+
));
|
|
792
|
+
}}
|
|
793
|
+
className="item">
|
|
794
|
+
<i className="icon equation" />
|
|
795
|
+
<span className="text">Equation</span>
|
|
796
|
+
</Button> */}
|
|
797
|
+
|
|
798
|
+
{display?.insert === true || display?.insert?.stickyNote !== false && (
|
|
799
|
+
<Button icon="sticky-note" size="s" onClick={() => {
|
|
800
|
+
editor.update(() => {
|
|
801
|
+
const root = $getRoot();
|
|
802
|
+
const stickyNode = $createStickyNode(0, 0);
|
|
803
|
+
root.append(stickyNode);
|
|
804
|
+
});
|
|
805
|
+
}}>
|
|
806
|
+
Sticky Note
|
|
807
|
+
</Button>
|
|
808
|
+
)}
|
|
809
|
+
|
|
810
|
+
{display?.insert === true || display?.insert?.collapsibleContainer !== false && (
|
|
811
|
+
<Button icon="caret-right" size="s" onClick={() => {
|
|
812
|
+
editor.dispatchCommand(
|
|
813
|
+
INSERT_COLLAPSIBLE_COMMAND,
|
|
814
|
+
undefined,
|
|
815
|
+
);
|
|
816
|
+
}}>
|
|
817
|
+
Collapsible container
|
|
818
|
+
</Button>
|
|
819
|
+
)}
|
|
820
|
+
|
|
821
|
+
{/*EmbedConfigs.map((embedConfig) => (
|
|
822
|
+
<Button
|
|
823
|
+
key={embedConfig.type}
|
|
824
|
+
onClick={() => {
|
|
825
|
+
activeEditor.dispatchCommand(
|
|
826
|
+
INSERT_EMBED_COMMAND,
|
|
827
|
+
embedConfig.type,
|
|
828
|
+
);
|
|
829
|
+
}}>
|
|
830
|
+
{embedConfig.icon}
|
|
831
|
+
<span className="text">{embedConfig.contentName}</span>
|
|
832
|
+
</Button>
|
|
833
|
+
))*/}
|
|
834
|
+
</DropDown>
|
|
834
835
|
</>
|
|
835
836
|
)}
|
|
836
837
|
|
|
@@ -3,7 +3,16 @@ import { $generateHtmlFromNodes } from '@lexical/html';
|
|
|
3
3
|
import editorNodes from '@common/data/rte/nodes';
|
|
4
4
|
import ExampleTheme from '@client/components/Rte/themes/PlaygroundEditorTheme';
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
import {
|
|
7
|
+
default as RteUtils,
|
|
8
|
+
LexicalNode,
|
|
9
|
+
LexicalState,
|
|
10
|
+
TRenderOptions,
|
|
11
|
+
TContentAssets
|
|
12
|
+
} from '@common/utils/rte';
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RichEditorUtils extends RteUtils {
|
|
7
16
|
|
|
8
17
|
public active: {
|
|
9
18
|
title: string,
|
|
@@ -12,7 +21,7 @@ class RichEditorUtils {
|
|
|
12
21
|
|
|
13
22
|
private virtualEditor: LexicalEditor | null = null;
|
|
14
23
|
|
|
15
|
-
public async jsonToHtml( value: {} ): Promise<string | null> {
|
|
24
|
+
public async jsonToHtml( value: LexicalState, options: TRenderOptions = {} ): Promise<string | null> {
|
|
16
25
|
|
|
17
26
|
if (!this.virtualEditor) {
|
|
18
27
|
// Create a headless Lexical editor instance
|
|
@@ -37,6 +46,26 @@ class RichEditorUtils {
|
|
|
37
46
|
return html;
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
protected async processContent(
|
|
50
|
+
node: LexicalNode,
|
|
51
|
+
parent: LexicalNode | null,
|
|
52
|
+
callback: (node: LexicalNode, parent: LexicalNode | null) => Promise<LexicalNode>
|
|
53
|
+
): Promise<LexicalNode> {
|
|
54
|
+
return node;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
protected async transformNode( node: LexicalNode, parent: LexicalNode | null, assets: TContentAssets, options: TRenderOptions ): Promise<LexicalNode> {
|
|
58
|
+
return node;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
protected async deleteUnusedFile(
|
|
62
|
+
node: LexicalNode,
|
|
63
|
+
assets: TContentAssets,
|
|
64
|
+
options: NonNullable<TRenderOptions["attachements"]>
|
|
65
|
+
): Promise<LexicalNode> {
|
|
66
|
+
return node;
|
|
67
|
+
}
|
|
68
|
+
|
|
40
69
|
}
|
|
41
70
|
|
|
42
71
|
export default new RichEditorUtils();
|
|
@@ -180,7 +180,10 @@ function TextFormatFloatingToolbar({
|
|
|
180
180
|
}, [editor, $updateTextFormatFloatingToolbar]);
|
|
181
181
|
|
|
182
182
|
return (
|
|
183
|
-
<div ref={popupCharStylesEditorRef} className="floating-text-format-popup card
|
|
183
|
+
<div ref={popupCharStylesEditorRef} className="floating-text-format-popup card pdv-0 pdh-05 row menu" style={{
|
|
184
|
+
borderRadius: '2rem',
|
|
185
|
+
height: '4rem'
|
|
186
|
+
}}>
|
|
184
187
|
{editor.isEditable() && (
|
|
185
188
|
<>
|
|
186
189
|
<Button size="s" icon="bold" active={isBold} onClick={() => {
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
|
|
2
|
+
import type Driver from '@server/services/disks/driver';
|
|
3
|
+
import { Anomaly } from '@common/errors';
|
|
4
|
+
|
|
5
|
+
export type LexicalNode = {
|
|
6
|
+
version: number,
|
|
7
|
+
type: string,
|
|
8
|
+
children?: LexicalNode[],
|
|
9
|
+
// Attachement
|
|
10
|
+
src?: string;
|
|
11
|
+
// Headhing
|
|
12
|
+
text?: string;
|
|
13
|
+
anchor?: string;
|
|
14
|
+
tag?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type LexicalState = {
|
|
18
|
+
root: LexicalNode
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export type TRenderOptions = {
|
|
22
|
+
|
|
23
|
+
format?: 'html' | 'text', // Default = html
|
|
24
|
+
transform?: RteUtils["transformNode"],
|
|
25
|
+
|
|
26
|
+
render?: (
|
|
27
|
+
node: LexicalNode,
|
|
28
|
+
parent: LexicalNode | null,
|
|
29
|
+
options: TRenderOptions
|
|
30
|
+
) => Promise<LexicalNode>,
|
|
31
|
+
|
|
32
|
+
attachements?: {
|
|
33
|
+
disk: Driver,
|
|
34
|
+
directory: string,
|
|
35
|
+
prevVersion?: string | LexicalState | null,
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type TSkeleton = {
|
|
40
|
+
id: string,
|
|
41
|
+
title: string,
|
|
42
|
+
level: number,
|
|
43
|
+
childrens: TSkeleton
|
|
44
|
+
}[];
|
|
45
|
+
|
|
46
|
+
export type TContentAssets = {
|
|
47
|
+
attachements: string[],
|
|
48
|
+
skeleton: TSkeleton
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export default abstract class RteUtils {
|
|
52
|
+
|
|
53
|
+
public async render(
|
|
54
|
+
content: string | LexicalState,
|
|
55
|
+
options: TRenderOptions = {}
|
|
56
|
+
): Promise<TContentAssets & {
|
|
57
|
+
html: string | null,
|
|
58
|
+
json: string | LexicalState,
|
|
59
|
+
}> {
|
|
60
|
+
|
|
61
|
+
// Transform content
|
|
62
|
+
const assets: TContentAssets = {
|
|
63
|
+
attachements: [],
|
|
64
|
+
skeleton: []
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Parse content if string
|
|
68
|
+
let json = this.parseState(content);
|
|
69
|
+
if (json === false)
|
|
70
|
+
return { html: '', json: content, ...assets }
|
|
71
|
+
|
|
72
|
+
// Parse prev version if string
|
|
73
|
+
if (typeof options?.attachements?.prevVersion === 'string') {
|
|
74
|
+
try {
|
|
75
|
+
options.attachements.prevVersion = JSON.parse(options.attachements.prevVersion) as LexicalState;
|
|
76
|
+
} catch (error) {
|
|
77
|
+
throw new Anomaly("Invalid JSON format for the given JSON RTE prev version.");
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const root = await this.processContent(json.root, null, async (node, parent) => {
|
|
82
|
+
return await this.transformNode(node, parent, assets, options);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
json = { ...json, root };
|
|
86
|
+
|
|
87
|
+
// Delete unused attachements
|
|
88
|
+
const attachementOptions = options?.attachements;
|
|
89
|
+
if (attachementOptions && attachementOptions.prevVersion !== undefined) {
|
|
90
|
+
|
|
91
|
+
await this.processContent(root, null, async (node) => {
|
|
92
|
+
return await this.deleteUnusedFile(node, assets, attachementOptions);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Convert json to HTML
|
|
97
|
+
let html: string | null;
|
|
98
|
+
if (options.format === 'text')
|
|
99
|
+
html = await this.jsonToText( json.root );
|
|
100
|
+
else
|
|
101
|
+
html = await this.jsonToHtml( json, options );
|
|
102
|
+
|
|
103
|
+
return { html, json: content, ...assets };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private parseState( content: string | LexicalState ): LexicalState | false {
|
|
107
|
+
|
|
108
|
+
if (typeof content === 'string' && content.trim().startsWith('{')) {
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(content) as LexicalState;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
throw new Anomaly("Invalid JSON format for the given JSON RTE content.");
|
|
113
|
+
}
|
|
114
|
+
} else if (content && typeof content === 'object' && content.root)
|
|
115
|
+
return content;
|
|
116
|
+
else
|
|
117
|
+
return false;
|
|
118
|
+
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected jsonToText(root: LexicalNode): string {
|
|
122
|
+
let result = '';
|
|
123
|
+
|
|
124
|
+
function traverse(node: LexicalNode) {
|
|
125
|
+
switch (node.type) {
|
|
126
|
+
case 'text':
|
|
127
|
+
// Leaf text node
|
|
128
|
+
result += node.text ?? '';
|
|
129
|
+
break;
|
|
130
|
+
case 'linebreak':
|
|
131
|
+
// Explicit line break node
|
|
132
|
+
result += '\n';
|
|
133
|
+
break;
|
|
134
|
+
default:
|
|
135
|
+
// Container or block node: dive into children if any
|
|
136
|
+
if (node.children) {
|
|
137
|
+
node.children.forEach(traverse);
|
|
138
|
+
}
|
|
139
|
+
// After finishing a block-level node, append newline
|
|
140
|
+
if (isBlockNode(node.type)) {
|
|
141
|
+
result += '\n';
|
|
142
|
+
}
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Heuristic: treat these as blocks
|
|
148
|
+
function isBlockNode(type: string): boolean {
|
|
149
|
+
return [
|
|
150
|
+
'root',
|
|
151
|
+
'paragraph',
|
|
152
|
+
'heading',
|
|
153
|
+
'listitem',
|
|
154
|
+
'unorderedlist',
|
|
155
|
+
'orderedlist',
|
|
156
|
+
'quote',
|
|
157
|
+
'codeblock',
|
|
158
|
+
'table',
|
|
159
|
+
].includes(type);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
traverse(root);
|
|
163
|
+
|
|
164
|
+
// Trim trailing whitespace/newlines
|
|
165
|
+
return result.replace(/\s+$/, '');
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
public abstract jsonToHtml( json: LexicalState, options: TRenderOptions ): Promise<string | null>;
|
|
169
|
+
|
|
170
|
+
protected abstract processContent(
|
|
171
|
+
node: LexicalNode,
|
|
172
|
+
parent: LexicalNode | null,
|
|
173
|
+
callback: (node: LexicalNode, parent: LexicalNode | null) => Promise<LexicalNode>
|
|
174
|
+
): Promise<LexicalNode>;
|
|
175
|
+
|
|
176
|
+
protected abstract transformNode( node: LexicalNode, parent: LexicalNode | null, assets: TContentAssets, options: TRenderOptions ): Promise<LexicalNode>;
|
|
177
|
+
|
|
178
|
+
protected abstract deleteUnusedFile(
|
|
179
|
+
node: LexicalNode,
|
|
180
|
+
assets: TContentAssets,
|
|
181
|
+
options: NonNullable<TRenderOptions["attachements"]>
|
|
182
|
+
): Promise<LexicalNode>;
|
|
183
|
+
}
|
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.6.0-
|
|
4
|
+
"version": "0.6.0-85",
|
|
5
5
|
"author": "Gaetan Le Gac (https://github.com/gaetanlegac)",
|
|
6
6
|
"repository": "git://github.com/gaetanlegac/5htp-core.git",
|
|
7
7
|
"license": "MIT",
|
|
@@ -37,6 +37,12 @@ export type Services = {
|
|
|
37
37
|
export default class ModelsManager extends Service<Config, Hooks, Application> {
|
|
38
38
|
|
|
39
39
|
public client = new PrismaClient();
|
|
40
|
+
|
|
41
|
+
public async ready() {
|
|
42
|
+
|
|
43
|
+
await this.client.$executeRaw`SET time_zone = '+00:00'`;
|
|
44
|
+
|
|
45
|
+
}
|
|
40
46
|
|
|
41
47
|
public async shutdown() {
|
|
42
48
|
await this.client.$disconnect()
|
package/server/utils/rte.ts
CHANGED
|
@@ -9,143 +9,39 @@ import path from 'path';
|
|
|
9
9
|
import md5 from 'md5';
|
|
10
10
|
import { fromBuffer } from 'file-type';
|
|
11
11
|
import { JSDOM } from 'jsdom';
|
|
12
|
-
// Lexical
|
|
13
|
-
import { $getRoot } from 'lexical';
|
|
14
|
-
import { createHeadlessEditor } from '@lexical/headless';
|
|
15
|
-
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
16
12
|
|
|
17
13
|
// Core
|
|
18
|
-
import { Anomaly } from '@common/errors';
|
|
19
14
|
import editorNodes from '@common/data/rte/nodes';
|
|
20
15
|
import ExampleTheme from '@client/components/Rte/themes/PlaygroundEditorTheme';
|
|
21
|
-
import type Driver from '@server/services/disks/driver';
|
|
22
16
|
import Slug from '@server/utils/slug';
|
|
23
17
|
|
|
18
|
+
// Lexical
|
|
19
|
+
import { $getRoot, SerializedEditorState, SerializedLexicalNode } from 'lexical';
|
|
20
|
+
import { createHeadlessEditor } from '@lexical/headless';
|
|
21
|
+
import { $generateNodesFromDOM, $generateHtmlFromNodes } from '@lexical/html';
|
|
22
|
+
|
|
23
|
+
// Specifc
|
|
24
|
+
import {
|
|
25
|
+
default as BaseRteUtils,
|
|
26
|
+
LexicalNode,
|
|
27
|
+
LexicalState,
|
|
28
|
+
TRenderOptions,
|
|
29
|
+
TContentAssets,
|
|
30
|
+
TSkeleton
|
|
31
|
+
} from '@common/utils/rte';
|
|
32
|
+
|
|
24
33
|
/*----------------------------------
|
|
25
34
|
- TYPES
|
|
26
35
|
----------------------------------*/
|
|
27
36
|
|
|
28
|
-
type LexicalState = {
|
|
29
|
-
root: LexicalNode
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type LexicalNode = {
|
|
33
|
-
version: number,
|
|
34
|
-
type: string,
|
|
35
|
-
children?: LexicalNode[],
|
|
36
|
-
// Attachement
|
|
37
|
-
src?: string;
|
|
38
|
-
// Headhing
|
|
39
|
-
text?: string;
|
|
40
|
-
anchor?: string;
|
|
41
|
-
tag?: string;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
type TRenderOptions = {
|
|
45
|
-
|
|
46
|
-
format?: 'html' | 'text', // Default = html
|
|
47
|
-
transform?: RteUtils["transformNode"],
|
|
48
|
-
|
|
49
|
-
render?: (
|
|
50
|
-
node: LexicalNode,
|
|
51
|
-
parent: LexicalNode | null,
|
|
52
|
-
options: TRenderOptions
|
|
53
|
-
) => Promise<LexicalNode>,
|
|
54
|
-
|
|
55
|
-
attachements?: {
|
|
56
|
-
disk: Driver,
|
|
57
|
-
directory: string,
|
|
58
|
-
prevVersion?: string | LexicalState | null,
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
type TSkeleton = {
|
|
63
|
-
id: string,
|
|
64
|
-
title: string,
|
|
65
|
-
level: number,
|
|
66
|
-
childrens: TSkeleton
|
|
67
|
-
}[];
|
|
68
|
-
|
|
69
|
-
type TContentAssets = {
|
|
70
|
-
attachements: string[],
|
|
71
|
-
skeleton: TSkeleton
|
|
72
|
-
}
|
|
73
37
|
|
|
74
38
|
/*----------------------------------
|
|
75
39
|
- FUNCTIONS
|
|
76
40
|
----------------------------------*/
|
|
77
41
|
|
|
78
|
-
export class RteUtils {
|
|
42
|
+
export class RteUtils extends BaseRteUtils {
|
|
79
43
|
|
|
80
|
-
|
|
81
|
-
content: string | LexicalState,
|
|
82
|
-
options: TRenderOptions = {}
|
|
83
|
-
): Promise<TContentAssets & {
|
|
84
|
-
html: string,
|
|
85
|
-
json: string | LexicalState,
|
|
86
|
-
}> {
|
|
87
|
-
|
|
88
|
-
// Transform content
|
|
89
|
-
const assets: TContentAssets = {
|
|
90
|
-
attachements: [],
|
|
91
|
-
skeleton: []
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Parse content if string
|
|
95
|
-
let json = this.parseState(content);
|
|
96
|
-
if (json === false)
|
|
97
|
-
return { html: '', json: content, ...assets }
|
|
98
|
-
|
|
99
|
-
// Parse prev version if string
|
|
100
|
-
if (typeof options?.attachements?.prevVersion === 'string') {
|
|
101
|
-
try {
|
|
102
|
-
options.attachements.prevVersion = JSON.parse(options.attachements.prevVersion) as LexicalState;
|
|
103
|
-
} catch (error) {
|
|
104
|
-
throw new Anomaly("Invalid JSON format for the given JSON RTE prev version.");
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const root = await this.processContent(json.root, null, async (node, parent) => {
|
|
109
|
-
return await this.transformNode(node, parent, assets, options);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
json = { ...json, root };
|
|
113
|
-
|
|
114
|
-
// Delete unused attachements
|
|
115
|
-
const attachementOptions = options?.attachements;
|
|
116
|
-
if (attachementOptions && attachementOptions.prevVersion !== undefined) {
|
|
117
|
-
|
|
118
|
-
await this.processContent(root, null, async (node) => {
|
|
119
|
-
return await this.deleteUnusedFile(node, assets, attachementOptions);
|
|
120
|
-
});
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Convert json to HTML
|
|
124
|
-
let html: string;
|
|
125
|
-
if (options.format === 'text')
|
|
126
|
-
html = await this.jsonToText( json.root );
|
|
127
|
-
else
|
|
128
|
-
html = await this.jsonToHtml( json, options );
|
|
129
|
-
|
|
130
|
-
return { html, json: content, ...assets };
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private parseState( content: string | LexicalState ): LexicalState | false {
|
|
134
|
-
|
|
135
|
-
if (typeof content === 'string' && content.trim().startsWith('{')) {
|
|
136
|
-
try {
|
|
137
|
-
return JSON.parse(content) as LexicalState;
|
|
138
|
-
} catch (error) {
|
|
139
|
-
throw new Anomaly("Invalid JSON format for the given JSON RTE content.");
|
|
140
|
-
}
|
|
141
|
-
} else if (content && typeof content === 'object' && content.root)
|
|
142
|
-
return content;
|
|
143
|
-
else
|
|
144
|
-
return false;
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
private async processContent(
|
|
44
|
+
protected async processContent(
|
|
149
45
|
node: LexicalNode,
|
|
150
46
|
parent: LexicalNode | null,
|
|
151
47
|
callback: (node: LexicalNode, parent: LexicalNode | null) => Promise<LexicalNode>
|
|
@@ -165,7 +61,7 @@ export class RteUtils {
|
|
|
165
61
|
return node;
|
|
166
62
|
}
|
|
167
63
|
|
|
168
|
-
|
|
64
|
+
protected async transformNode(
|
|
169
65
|
node: LexicalNode,
|
|
170
66
|
parent: LexicalNode | null,
|
|
171
67
|
assets: TContentAssets,
|
|
@@ -266,7 +162,7 @@ export class RteUtils {
|
|
|
266
162
|
});
|
|
267
163
|
}
|
|
268
164
|
|
|
269
|
-
|
|
165
|
+
protected async deleteUnusedFile(
|
|
270
166
|
node: LexicalNode,
|
|
271
167
|
assets: TContentAssets,
|
|
272
168
|
options: NonNullable<TRenderOptions["attachements"]>
|
|
@@ -294,7 +190,7 @@ export class RteUtils {
|
|
|
294
190
|
return node;
|
|
295
191
|
}
|
|
296
192
|
|
|
297
|
-
public async jsonToHtml( json: LexicalState, options: TRenderOptions = {} ) {
|
|
193
|
+
public async jsonToHtml( json: LexicalState, options: TRenderOptions = {} ): Promise<string | null> {
|
|
298
194
|
|
|
299
195
|
// Transform before rendering
|
|
300
196
|
const renderTransform = options.render;
|
|
@@ -322,7 +218,7 @@ export class RteUtils {
|
|
|
322
218
|
});
|
|
323
219
|
|
|
324
220
|
// Set the editor state from JSON
|
|
325
|
-
const state = editor.parseEditorState(json);
|
|
221
|
+
const state = editor.parseEditorState(json as SerializedEditorState<SerializedLexicalNode>);
|
|
326
222
|
if (state.isEmpty())
|
|
327
223
|
return '';
|
|
328
224
|
|
|
@@ -346,53 +242,6 @@ export class RteUtils {
|
|
|
346
242
|
return html;
|
|
347
243
|
}
|
|
348
244
|
|
|
349
|
-
private jsonToText(root: LexicalNode): string {
|
|
350
|
-
let result = '';
|
|
351
|
-
|
|
352
|
-
function traverse(node: LexicalNode) {
|
|
353
|
-
switch (node.type) {
|
|
354
|
-
case 'text':
|
|
355
|
-
// Leaf text node
|
|
356
|
-
result += node.text ?? '';
|
|
357
|
-
break;
|
|
358
|
-
case 'linebreak':
|
|
359
|
-
// Explicit line break node
|
|
360
|
-
result += '\n';
|
|
361
|
-
break;
|
|
362
|
-
default:
|
|
363
|
-
// Container or block node: dive into children if any
|
|
364
|
-
if (node.children) {
|
|
365
|
-
node.children.forEach(traverse);
|
|
366
|
-
}
|
|
367
|
-
// After finishing a block-level node, append newline
|
|
368
|
-
if (isBlockNode(node.type)) {
|
|
369
|
-
result += '\n';
|
|
370
|
-
}
|
|
371
|
-
break;
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Heuristic: treat these as blocks
|
|
376
|
-
function isBlockNode(type: string): boolean {
|
|
377
|
-
return [
|
|
378
|
-
'root',
|
|
379
|
-
'paragraph',
|
|
380
|
-
'heading',
|
|
381
|
-
'listitem',
|
|
382
|
-
'unorderedlist',
|
|
383
|
-
'orderedlist',
|
|
384
|
-
'quote',
|
|
385
|
-
'codeblock',
|
|
386
|
-
'table',
|
|
387
|
-
].includes(type);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
traverse(root);
|
|
391
|
-
|
|
392
|
-
// Trim trailing whitespace/newlines
|
|
393
|
-
return result.replace(/\s+$/, '');
|
|
394
|
-
}
|
|
395
|
-
|
|
396
245
|
public async htmlToJson(htmlString: string): Promise<LexicalState> {
|
|
397
246
|
|
|
398
247
|
const editor = createHeadlessEditor({
|
package/types/icons.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export type TIcones = "times"|"solid/spinner-third"|"
|
|
1
|
+
export type TIcones = "long-arrow-right"|"times"|"solid/spinner-third"|"sack-dollar"|"bell"|"bullseye"|"project-diagram"|"user-friends"|"eye"|"lock"|"comments"|"phone"|"chalkboard-teacher"|"rocket"|"user-circle"|"plus-circle"|"comments-alt"|"crosshairs"|"arrow-right"|"chart-bar"|"user-shield"|"shield-alt"|"chart-line"|"money-bill-wave"|"star"|"link"|"file-alt"|"long-arrow-left"|"at"|"calendar-alt"|"paper-plane"|"search"|"lightbulb"|"magnet"|"brands/linkedin"|"user-plus"|"mouse-pointer"|"thumbs-up"|"dollar-sign"|"key"|"user"|"plus"|"clock"|"cog"|"trash"|"ellipsis-h"|"sync"|"times-circle"|"binoculars"|"solid/crown"|"brands/discord"|"pen"|"file"|"envelope"|"angle-up"|"angle-down"|"coins"|"download"|"check"|"info-circle"|"check-circle"|"exclamation-circle"|"arrow-left"|"meh-rolling-eyes"|"bars"|"solid/star"|"solid/star-half-alt"|"regular/star"|"chevron-left"|"power-off"|"play"|"minus-circle"|"external-link"|"question-circle"|"users"|"bug"|"plane-departure"|"brands/whatsapp"|"wind"|"solid/check-circle"|"solid/exclamation-triangle"|"solid/times-circle"|"broom"|"minus"|"coin"|"exclamation-triangle"|"comment-alt"|"arrow-to-bottom"|"map-marker-alt"|"briefcase"|"map-marker"|"fire"|"solid/magic"|"industry"|"calendar"|"globe"|"magic"|"building"|"graduation-cap"|"bold"|"italic"|"underline"|"strikethrough"|"subscript"|"superscript"|"code"|"unlink"|"font"|"empty-set"|"horizontal-rule"|"page-break"|"image"|"table"|"poll"|"columns"|"sticky-note"|"caret-right"|"list-ul"|"check-square"|"h1"|"h2"|"h3"|"h4"|"list-ol"|"paragraph"|"quote-left"|"align-left"|"align-center"|"align-right"|"align-justify"|"indent"|"outdent"|"shield-check"|"regular/shield-check"
|