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.
@@ -21,7 +21,7 @@
21
21
  min-height: @sizeComponent;
22
22
 
23
23
  // Shape
24
- border-radius: @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
- &.pill {
212
- border-radius: @sizeComponent / 2;
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 {
@@ -39,7 +39,7 @@ i {
39
39
  &.solid {
40
40
  color: var(--cTxtAccent2);
41
41
  background: var(--cBg);
42
- border-radius: @radius;
42
+ border-radius: 50%;
43
43
 
44
44
  // Normla size must fit inside a normal fit element
45
45
  width: @sizeComponent * 0.75;
@@ -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
- <span class={"label"}>
278
- {children}
279
- </span>
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
- label: ComponentChild,
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: {} | undefined) => {
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 reading h-1-4 scrollable col clickable"
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
- <ListPlugin />
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
- <FloatingTextFormatToolbarPlugin
322
- anchorElem={floatingAnchorElem}
323
- setIsLinkEditMode={setIsLinkEditMode}
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
- {/* <DropdownColorPicker
619
- disabled={!isEditable}
620
- buttonClassName="toolbar-item color-picker"
621
- buttonAriaLabel="Formatting text color"
622
- buttonIconClassName="icon font-color"
623
- color={fontColor}
624
- onChange={onFontColorSelect}
625
- title="text color"
626
- />
627
- <DropdownColorPicker
628
- disabled={!isEditable}
629
- buttonClassName="toolbar-item color-picker"
630
- buttonAriaLabel="Formatting background color"
631
- buttonIconClassName="icon bg-color"
632
- color={bgColor}
633
- onChange={onBgColorSelect}
634
- title="bg color"
635
- /> */}
636
-
637
- {display?.styles !== false && (
638
- <DropDown popover={{ tag: 'li' }} icon="font" size="s"
639
- disabled={!isEditable}
640
- hint="Formatting options for additional text styles"
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
- {display?.styles === true || display?.styles?.strikethrough !== false && (
644
- <Button icon="strikethrough" size="s"
645
- onClick={() => {
646
- activeEditor.dispatchCommand( FORMAT_TEXT_COMMAND, 'strikethrough');
647
- }}
648
- active={isStrikethrough}
649
- title="Format text with a strikethrough"
650
- >
651
- Strikethrough
652
- </Button>
653
- )}
654
-
655
- {display?.styles === true || display?.styles?.subscript !== false && (
656
- <Button icon="subscript" size="s"
657
- onClick={() => {
658
- activeEditor.dispatchCommand(FORMAT_TEXT_COMMAND, 'subscript');
659
- }}
660
- active={isSubscript}
661
- title="Format text with a subscript"
662
- >
663
- Subscript
664
- </Button>
665
- )}
666
-
667
- {display?.styles === true || display?.styles?.superscript !== false && (
668
- <Button icon="superscript" size="s"
669
- onClick={() => {
670
- activeEditor.dispatchCommand(
671
- FORMAT_TEXT_COMMAND,
672
- 'superscript',
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
- {(canViewerSeeInsertDropdown && display?.insert !== false) && (
693
- <>
694
- <Divider />
695
- <DropDown popover={{ tag: 'li' }}
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
- size="s"
698
- icon="plus-circle"
699
- hint="Insert specialized editor node"
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
- {display?.insert === true || display?.insert?.horizontalRule !== false && (
703
- <Button icon="horizontal-rule" size="s" onClick={() => {
704
- activeEditor.dispatchCommand( INSERT_HORIZONTAL_RULE_COMMAND, undefined, );
705
- }}>
706
- Horizontal Rule
707
- </Button>
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
- {display?.insert === true || display?.insert?.image !== false && (
730
- <Button icon="image" size="s" onClick={() => {
731
- modal.show('Insert Image', InsertImageDialog, { editor: activeEditor });
732
- }}>
733
- Image
734
- </Button>
735
- )}
736
-
737
- {display?.insert === true || display?.insert?.inlineImage !== false && (
738
- <Button icon="image" size="s" onClick={() => {
739
- modal.show('Insert Inline Image', InsertInlineImageDialog, { editor: activeEditor });
740
- }}>
741
- Inline Image
742
- </Button>
743
- )}
744
-
745
- {/* <Button
746
- onClick={() => {
747
- activeEditor.dispatchCommand(
748
- INSERT_EXCALIDRAW_COMMAND,
749
- undefined,
750
- );
751
- }}
752
- className="item">
753
- <i className="icon diagram-2" />
754
- <span className="text">Excalidraw</span>
755
- </Button> */}
756
-
757
- {display?.insert === true || display?.insert?.table !== false && (
758
- <Button icon="table" size="s" onClick={() => {
759
- modal.show('Insert Table', InsertTableDialog, { editor: activeEditor });
760
- }}>
761
- Table
762
- </Button>
763
- )}
764
-
765
- {display?.insert === true || display?.insert?.poll !== false && (
766
- <Button icon="poll" size="s" onClick={() => {
767
- modal.show('Insert Poll', InsertPollDialog, { editor: activeEditor });
768
- }}>
769
- Poll
770
- </Button>
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
- class RichEditorUtils {
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();
@@ -20,7 +20,8 @@ import './style.less';
20
20
  export type Props = {
21
21
  preview?: boolean,
22
22
  title: string,
23
- toolbar?: TToolbarDisplay
23
+ toolbar?: TToolbarDisplay,
24
+ decorateText?: boolean
24
25
  }
25
26
 
26
27
  /*----------------------------------
@@ -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 pd-05 row menu">
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-83",
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",
@@ -2,6 +2,9 @@
2
2
  - DEPENDANCES
3
3
  ----------------------------------*/
4
4
 
5
+ // Set timezone
6
+ process.env.TZ = 'UTC';
7
+
5
8
  // Npm
6
9
  import path from 'path';
7
10
 
@@ -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()
@@ -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
- public async render(
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
- private async transformNode(
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
- private async deleteUnusedFile(
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"|"long-arrow-right"|"sack-dollar"|"bell"|"bullseye"|"project-diagram"|"user-friends"|"eye"|"lock"|"comments"|"phone"|"chalkboard-teacher"|"rocket"|"chart-bar"|"user-circle"|"crosshairs"|"user-shield"|"shield-alt"|"chart-line"|"money-bill-wave"|"star"|"link"|"file-alt"|"long-arrow-left"|"arrow-right"|"plus-circle"|"comments-alt"|"key"|"user"|"at"|"user-plus"|"mouse-pointer"|"thumbs-up"|"dollar-sign"|"magnet"|"paper-plane"|"plus"|"binoculars"|"brands/linkedin"|"clock"|"cog"|"trash"|"ellipsis-h"|"times-circle"|"search"|"lightbulb"|"calendar-alt"|"angle-up"|"angle-down"|"solid/crown"|"brands/discord"|"pen"|"file"|"envelope"|"coins"|"download"|"check"|"meh-rolling-eyes"|"arrow-left"|"info-circle"|"check-circle"|"exclamation-circle"|"bars"|"solid/star"|"solid/star-half-alt"|"regular/star"|"chevron-left"|"power-off"|"plane-departure"|"brands/whatsapp"|"wind"|"play"|"minus-circle"|"broom"|"exclamation-triangle"|"external-link"|"solid/check-circle"|"solid/exclamation-triangle"|"solid/times-circle"|"question-circle"|"minus"|"comment-alt"|"map-marker-alt"|"arrow-to-bottom"|"solid/magic"|"users"|"industry"|"map-marker"|"calendar"|"briefcase"|"fire"|"globe"|"magic"|"bug"|"building"|"graduation-cap"|"coin"|"unlink"|"bold"|"italic"|"underline"|"strikethrough"|"subscript"|"superscript"|"code"|"font"|"empty-set"|"horizontal-rule"|"page-break"|"image"|"table"|"poll"|"columns"|"sticky-note"|"caret-right"|"align-left"|"align-center"|"align-right"|"align-justify"|"indent"|"outdent"|"list-ul"|"check-square"|"h1"|"h2"|"h3"|"h4"|"list-ol"|"paragraph"|"quote-left"
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"