@eccenca/gui-elements 24.1.0-rc.3 → 24.1.0-rc.5

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.
Files changed (125) hide show
  1. package/CHANGELOG.md +57 -2
  2. package/dist/cjs/cmem/ActivityControl/ActivityControlWidget.js +7 -2
  3. package/dist/cjs/cmem/ActivityControl/ActivityControlWidget.js.map +1 -1
  4. package/dist/cjs/components/AutoSuggestion/AutoSuggestion.js +5 -3
  5. package/dist/cjs/components/AutoSuggestion/AutoSuggestion.js.map +1 -1
  6. package/dist/cjs/components/AutocompleteField/AutoCompleteField.js +4 -2
  7. package/dist/cjs/components/AutocompleteField/AutoCompleteField.js.map +1 -1
  8. package/dist/cjs/components/Card/CardActions.js +2 -1
  9. package/dist/cjs/components/Card/CardActions.js.map +1 -1
  10. package/dist/cjs/components/Card/CardContent.js +4 -6
  11. package/dist/cjs/components/Card/CardContent.js.map +1 -1
  12. package/dist/cjs/components/Dialog/SimpleDialog.js +3 -3
  13. package/dist/cjs/components/Dialog/SimpleDialog.js.map +1 -1
  14. package/dist/cjs/components/Icon/canonicalIconNames.js +12 -0
  15. package/dist/cjs/components/Icon/canonicalIconNames.js.map +1 -1
  16. package/dist/cjs/components/Label/Label.js +2 -1
  17. package/dist/cjs/components/Label/Label.js.map +1 -1
  18. package/dist/cjs/components/Menu/MenuItem.js +3 -2
  19. package/dist/cjs/components/Menu/MenuItem.js.map +1 -1
  20. package/dist/cjs/components/Switch/Switch.js +6 -4
  21. package/dist/cjs/components/Switch/Switch.js.map +1 -1
  22. package/dist/cjs/components/Tag/TagList.js +1 -1
  23. package/dist/cjs/components/Tag/TagList.js.map +1 -1
  24. package/dist/cjs/components/Typography/OverflowText.js +1 -1
  25. package/dist/cjs/components/Typography/OverflowText.js.map +1 -1
  26. package/dist/cjs/extensions/codemirror/CodeMirror.js +52 -23
  27. package/dist/cjs/extensions/codemirror/CodeMirror.js.map +1 -1
  28. package/dist/cjs/extensions/codemirror/tests/codemirrorTestHelper.js +4 -1
  29. package/dist/cjs/extensions/codemirror/tests/codemirrorTestHelper.js.map +1 -1
  30. package/dist/cjs/extensions/codemirror/toolbars/commands/markdown.command.js +278 -0
  31. package/dist/cjs/extensions/codemirror/toolbars/commands/markdown.command.js.map +1 -0
  32. package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js +47 -0
  33. package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -0
  34. package/dist/cjs/extensions/react-flow/nodes/NodeContent.js +136 -41
  35. package/dist/cjs/extensions/react-flow/nodes/NodeContent.js.map +1 -1
  36. package/dist/cjs/extensions/react-flow/nodes/nodeUtils.js +5 -6
  37. package/dist/cjs/extensions/react-flow/nodes/nodeUtils.js.map +1 -1
  38. package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js +7 -2
  39. package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js.map +1 -1
  40. package/dist/esm/components/AutoSuggestion/AutoSuggestion.js +5 -3
  41. package/dist/esm/components/AutoSuggestion/AutoSuggestion.js.map +1 -1
  42. package/dist/esm/components/AutocompleteField/AutoCompleteField.js +4 -3
  43. package/dist/esm/components/AutocompleteField/AutoCompleteField.js.map +1 -1
  44. package/dist/esm/components/Card/CardActions.js +2 -1
  45. package/dist/esm/components/Card/CardActions.js.map +1 -1
  46. package/dist/esm/components/Card/CardContent.js +4 -5
  47. package/dist/esm/components/Card/CardContent.js.map +1 -1
  48. package/dist/esm/components/Dialog/SimpleDialog.js +4 -4
  49. package/dist/esm/components/Dialog/SimpleDialog.js.map +1 -1
  50. package/dist/esm/components/Icon/canonicalIconNames.js +12 -0
  51. package/dist/esm/components/Icon/canonicalIconNames.js.map +1 -1
  52. package/dist/esm/components/Label/Label.js +2 -1
  53. package/dist/esm/components/Label/Label.js.map +1 -1
  54. package/dist/esm/components/Menu/MenuItem.js +3 -2
  55. package/dist/esm/components/Menu/MenuItem.js.map +1 -1
  56. package/dist/esm/components/Switch/Switch.js +7 -5
  57. package/dist/esm/components/Switch/Switch.js.map +1 -1
  58. package/dist/esm/components/Tag/TagList.js +1 -1
  59. package/dist/esm/components/Tag/TagList.js.map +1 -1
  60. package/dist/esm/components/Typography/OverflowText.js +1 -1
  61. package/dist/esm/components/Typography/OverflowText.js.map +1 -1
  62. package/dist/esm/extensions/codemirror/CodeMirror.js +53 -24
  63. package/dist/esm/extensions/codemirror/CodeMirror.js.map +1 -1
  64. package/dist/esm/extensions/codemirror/tests/codemirrorTestHelper.js +4 -0
  65. package/dist/esm/extensions/codemirror/tests/codemirrorTestHelper.js.map +1 -1
  66. package/dist/esm/extensions/codemirror/toolbars/commands/markdown.command.js +283 -0
  67. package/dist/esm/extensions/codemirror/toolbars/commands/markdown.command.js.map +1 -0
  68. package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js +41 -0
  69. package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -0
  70. package/dist/esm/extensions/react-flow/nodes/NodeContent.js +145 -48
  71. package/dist/esm/extensions/react-flow/nodes/NodeContent.js.map +1 -1
  72. package/dist/esm/extensions/react-flow/nodes/nodeUtils.js +5 -6
  73. package/dist/esm/extensions/react-flow/nodes/nodeUtils.js.map +1 -1
  74. package/dist/types/cmem/ActivityControl/ActivityControlWidget.d.ts +1 -1
  75. package/dist/types/components/Card/CardActions.d.ts +5 -1
  76. package/dist/types/components/Card/CardContent.d.ts +1 -2
  77. package/dist/types/components/Dialog/SimpleDialog.d.ts +4 -1
  78. package/dist/types/components/Icon/canonicalIconNames.d.ts +12 -0
  79. package/dist/types/components/Label/Label.d.ts +3 -1
  80. package/dist/types/components/Menu/MenuItem.d.ts +8 -1
  81. package/dist/types/components/Switch/Switch.d.ts +3 -3
  82. package/dist/types/components/Typography/OverflowText.d.ts +23 -2
  83. package/dist/types/extensions/codemirror/CodeMirror.d.ts +10 -1
  84. package/dist/types/extensions/codemirror/tests/codemirrorTestHelper.d.ts +1 -0
  85. package/dist/types/extensions/codemirror/toolbars/commands/markdown.command.d.ts +55 -0
  86. package/dist/types/extensions/codemirror/toolbars/markdown.toolbar.d.ts +12 -0
  87. package/dist/types/extensions/react-flow/nodes/NodeContent.d.ts +18 -4
  88. package/dist/types/extensions/react-flow/nodes/nodeUtils.d.ts +7 -6
  89. package/package.json +22 -20
  90. package/src/cmem/ActivityControl/ActivityControlWidget.tsx +5 -2
  91. package/src/cmem/react-flow/configuration/_colors-graph.scss +4 -1
  92. package/src/cmem/react-flow/configuration/_colors-workflow.scss +3 -0
  93. package/src/components/AutoSuggestion/AutoSuggestion.tsx +5 -3
  94. package/src/components/AutocompleteField/AutoCompleteField.tsx +5 -3
  95. package/src/components/Card/CardActions.tsx +6 -0
  96. package/src/components/Card/CardContent.tsx +8 -4
  97. package/src/components/Card/card.scss +15 -0
  98. package/src/components/CodeAutocompleteField/CodeAutocompleteField.stories.tsx +3 -2
  99. package/src/components/ContextOverlay/ContextOverlay.stories.tsx +15 -4
  100. package/src/components/Dialog/SimpleDialog.tsx +9 -2
  101. package/src/components/Dialog/stories/AlertDialog.stories.tsx +5 -1
  102. package/src/components/Dialog/stories/Modal.stories.tsx +4 -2
  103. package/src/components/Dialog/stories/SimpleDialog.stories.tsx +5 -2
  104. package/src/components/Icon/canonicalIconNames.tsx +12 -0
  105. package/src/components/Label/Label.tsx +4 -0
  106. package/src/components/Label/label.scss +5 -1
  107. package/src/components/Menu/MenuItem.tsx +27 -1
  108. package/src/components/Menu/menu.scss +1 -0
  109. package/src/components/OverviewItem/overviewitem.scss +4 -1
  110. package/src/components/Select/Select.stories.tsx +4 -1
  111. package/src/components/Switch/Switch.tsx +27 -8
  112. package/src/components/Tag/TagList.tsx +2 -2
  113. package/src/components/Typography/OverflowText.tsx +24 -3
  114. package/src/components/Typography/stories/OverflowText.stories.tsx +33 -0
  115. package/src/extensions/codemirror/CodeMirror.stories.tsx +5 -17
  116. package/src/extensions/codemirror/CodeMirror.tsx +82 -22
  117. package/src/extensions/codemirror/_codemirror.scss +35 -2
  118. package/src/extensions/codemirror/tests/codemirrorTestHelper.ts +4 -0
  119. package/src/extensions/codemirror/toolbars/commands/markdown.command.ts +340 -0
  120. package/src/extensions/codemirror/toolbars/markdown.toolbar.tsx +117 -0
  121. package/src/extensions/react-flow/_config.scss +1 -0
  122. package/src/extensions/react-flow/nodes/NodeContent.tsx +166 -52
  123. package/src/extensions/react-flow/nodes/_nodes.scss +71 -35
  124. package/src/extensions/react-flow/nodes/nodeUtils.tsx +16 -14
  125. package/src/extensions/react-flow/nodes/stories/NodeContent.stories.tsx +6 -3
@@ -19,13 +19,17 @@ export default {
19
19
  },
20
20
  } as Meta<typeof CodeEditor>;
21
21
 
22
- const TemplateFull: StoryFn<typeof CodeEditor> = (args) => <CodeEditor {...args} />;
22
+ let forcedUpdateKey = 0; // @see https://github.com/storybookjs/storybook/issues/13375#issuecomment-1291011856
23
+ const TemplateFull: StoryFn<typeof CodeEditor> = (args) => <CodeEditor {...args} key={++forcedUpdateKey} />;
23
24
 
24
25
  export const BasicExample = TemplateFull.bind({});
25
26
  BasicExample.args = {
26
27
  name: "codeinput",
27
28
  mode: "markdown",
28
29
  defaultValue: "**test me**",
30
+ useToolbar: true,
31
+ disabled: false,
32
+ readOnly: true,
29
33
  };
30
34
 
31
35
  export const LinterExample = TemplateFull.bind({});
@@ -36,19 +40,3 @@ LinterExample.args = {
36
40
  useLinting: true,
37
41
  autoFocus: true,
38
42
  };
39
-
40
- export const DisabledExample = TemplateFull.bind({});
41
- DisabledExample.args = {
42
- name: "codeinput",
43
- defaultValue: "**test me**",
44
- mode: "javascript",
45
- disabled: true,
46
- };
47
-
48
- export const IntentExample = TemplateFull.bind({});
49
- IntentExample.args = {
50
- name: "codeinput",
51
- defaultValue: "**test me**",
52
- mode: "javascript",
53
- intent: "warning",
54
- };
@@ -1,7 +1,6 @@
1
1
  import React, { useMemo, useRef } from "react";
2
2
  import { defaultKeymap, indentWithTab } from "@codemirror/commands";
3
3
  import { foldKeymap } from "@codemirror/language";
4
- import { lintGutter } from "@codemirror/lint";
5
4
  import { EditorState, Extension } from "@codemirror/state";
6
5
  import { DOMEventHandlers, EditorView, KeyBinding, keymap, Rect, ViewUpdate } from "@codemirror/view";
7
6
  import { minimalSetup } from "codemirror";
@@ -9,6 +8,8 @@ import { minimalSetup } from "codemirror";
9
8
  import { IntentTypes } from "../../common/Intent";
10
9
  import { markField } from "../../components/AutoSuggestion/extensions/markText";
11
10
  import { TestableComponent } from "../../components/interfaces";
11
+ import { MarkdownToolbar } from "./toolbars/markdown.toolbar";
12
+ import { Markdown } from "../../cmem/markdown/Markdown";
12
13
  import { CLASSPREFIX as eccgui } from "../../configuration/constants";
13
14
 
14
15
  //hooks
@@ -28,6 +29,7 @@ import {
28
29
  adaptedHighlightActiveLine,
29
30
  adaptedHighlightSpecialChars,
30
31
  adaptedLineNumbers,
32
+ adaptedLintGutter,
31
33
  adaptedPlaceholder,
32
34
  } from "./tests/codemirrorTestHelper";
33
35
  import { ExtensionCreator } from "./types";
@@ -77,7 +79,6 @@ export interface CodeEditorProps extends TestableComponent {
77
79
  /**
78
80
  * Syntax mode of the code editor.
79
81
  */
80
-
81
82
  mode?: SupportedCodeEditorModes;
82
83
  /**
83
84
  * Default value used first when the editor is instanciated.
@@ -156,6 +157,15 @@ export interface CodeEditorProps extends TestableComponent {
156
157
  * Disables the editor.
157
158
  */
158
159
  disabled?: boolean;
160
+ /**
161
+ * Add toolbar for mode.
162
+ * Currently only `markdown` is supported.
163
+ */
164
+ useToolbar?: boolean;
165
+ /**
166
+ * Get the translation for a specific key
167
+ */
168
+ translate?: (key: string) => string | false;
159
169
  }
160
170
 
161
171
  const addExtensionsFor = (flag: boolean, ...extensions: Extension[]) => (flag ? [...extensions] : []);
@@ -168,6 +178,8 @@ const ModeLinterMap: ReadonlyMap<SupportedCodeEditorModes, ReadonlyArray<Extensi
168
178
  ["javascript", [jsLinter]],
169
179
  ]);
170
180
 
181
+ const ModeToolbarSupport: ReadonlyArray<SupportedCodeEditorModes> = ["markdown"];
182
+
171
183
  /**
172
184
  * Includes a code editor, currently we use CodeMirror library as base.
173
185
  */
@@ -203,16 +215,20 @@ export const CodeEditor = ({
203
215
  autoFocus = false,
204
216
  disabled = false,
205
217
  intent,
218
+ useToolbar,
219
+ translate,
206
220
  ...otherCodeEditorProps
207
221
  }: CodeEditorProps) => {
208
222
  const parent = useRef<any>(undefined);
223
+ const [view, setView] = React.useState<EditorView | undefined>();
224
+ const [showPreview, setShowPreview] = React.useState<boolean>(false);
209
225
 
210
226
  const linters = useMemo(() => {
211
227
  if (!mode) {
212
228
  return [];
213
229
  }
214
230
 
215
- const values = [lintGutter()];
231
+ const values = [adaptedLintGutter()];
216
232
 
217
233
  const linters = ModeLinterMap.get(mode);
218
234
  if (linters) {
@@ -241,6 +257,14 @@ export const CodeEditor = ({
241
257
  }
242
258
  };
243
259
 
260
+ const getTranslation = (key: string): string | false => {
261
+ if (translate && typeof translate === "function") {
262
+ return translate(key);
263
+ }
264
+
265
+ return false;
266
+ };
267
+
244
268
  React.useEffect(() => {
245
269
  const tabIndent =
246
270
  !!(tabIntentStyle === "tab" && mode && !(tabForceSpaceForModes ?? []).includes(mode)) || enableTab;
@@ -280,8 +304,8 @@ export const CodeEditor = ({
280
304
  if (onSelection)
281
305
  onSelection(v.state.selection.ranges.filter((r) => !r.empty).map(({ from, to }) => ({ from, to })));
282
306
 
283
- if (onFocusChange) {
284
- v.view.dom.className += ` ${eccgui}-intent--${intent}`;
307
+ if (onFocusChange && intent && !v.view.dom.classList?.contains(`${eccgui}-intent--${intent}`)) {
308
+ v.view.dom.classList.add(`${eccgui}-intent--${intent}`);
285
309
  }
286
310
 
287
311
  if (onCursorChange) {
@@ -319,34 +343,67 @@ export const CodeEditor = ({
319
343
  }),
320
344
  parent: parent.current,
321
345
  });
346
+ setView(view);
322
347
 
323
- if (height) {
324
- view.dom.style.height = typeof height === "string" ? height : `${height}px`;
325
- }
348
+ if (view?.dom) {
349
+ if (height) {
350
+ view.dom.style.height = typeof height === "string" ? height : `${height}px`;
351
+ }
326
352
 
327
- if (disabled) {
328
- view.dom.className += ` ${eccgui}-disabled`;
329
- }
353
+ if (disabled) {
354
+ view.dom.className += ` ${eccgui}-disabled`;
355
+ }
330
356
 
331
- if (intent) {
332
- view.dom.className += ` ${eccgui}-intent--${intent}`;
333
- }
357
+ if (intent) {
358
+ view.dom.className += ` ${eccgui}-intent--${intent}`;
359
+ }
334
360
 
335
- if (autoFocus) {
336
- view.focus();
337
- }
361
+ if (autoFocus) {
362
+ view.focus();
363
+ }
338
364
 
339
- if (setEditorView) {
340
- setEditorView(view);
365
+ if (setEditorView) {
366
+ setEditorView(view);
367
+ }
341
368
  }
342
369
 
343
370
  return () => {
344
371
  view.destroy();
345
372
  if (setEditorView) {
346
373
  setEditorView(undefined);
374
+ setView(undefined);
347
375
  }
348
376
  };
349
- }, [parent.current, mode, preventLineNumbers]);
377
+ }, [parent.current, mode, preventLineNumbers, wrapLines]);
378
+
379
+ const hasToolbarSupport = mode && ModeToolbarSupport.indexOf(mode) > -1 && useToolbar;
380
+
381
+ const editorToolbar = (mode?: SupportedCodeEditorModes): JSX.Element => {
382
+ switch (mode) {
383
+ case "markdown":
384
+ return (
385
+ <div>
386
+ <div className={`${eccgui}-codeeditor__toolbar`}>
387
+ <MarkdownToolbar
388
+ view={view}
389
+ togglePreviewStatus={() => setShowPreview((p) => !p)}
390
+ showPreview={showPreview}
391
+ translate={getTranslation}
392
+ disabled={disabled}
393
+ readonly={readOnly}
394
+ />
395
+ </div>
396
+ {showPreview && (
397
+ <div className={`${eccgui}-codeeditor__preview`}>
398
+ <Markdown>{view?.state.doc.toString() ?? ""}</Markdown>
399
+ </div>
400
+ )}
401
+ </div>
402
+ );
403
+ default:
404
+ return <></>;
405
+ }
406
+ };
350
407
 
351
408
  return (
352
409
  <div
@@ -358,10 +415,13 @@ export const CodeEditor = ({
358
415
  data-test-id={dataTestId ? dataTestId : "codemirror-wrapper"}
359
416
  className={
360
417
  `${eccgui}-codeeditor ${eccgui}-codeeditor--mode-${mode}` +
361
- (outerDivAttributes?.className ? ` ${outerDivAttributes?.className}` : "")
418
+ (outerDivAttributes?.className ? ` ${outerDivAttributes?.className}` : "") +
419
+ (hasToolbarSupport ? ` ${eccgui}-codeeditor--has-toolbar` : "")
362
420
  }
363
421
  {...otherCodeEditorProps}
364
- />
422
+ >
423
+ {hasToolbarSupport && editorToolbar(mode)}
424
+ </div>
365
425
  );
366
426
  };
367
427
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  // own vars
4
4
  $eccgui-color-codeeditor-background: $eccgui-color-textfield-background !default;
5
+ $eccgui-color-codeeditor-separation: $eccgui-color-separation-divider !default;
6
+ $eccgui-size-codeeditor-height: 20rem !default;
7
+ $eccgui-size-codeeditor-toolbar-height: $button-height !default;
5
8
 
6
9
  // adjustments
7
10
  // stylelint-disable selector-class-pattern
@@ -14,9 +17,39 @@ $eccgui-color-codeeditor-background: $eccgui-color-textfield-background !default
14
17
  width: 100%;
15
18
  }
16
19
 
20
+ &__toolbar {
21
+ position: absolute;
22
+ z-index: 3;
23
+ left: 1px;
24
+ right: 1px;
25
+ top: 1px;
26
+ border-radius: $pt-border-radius $pt-border-radius 0 0;
27
+ border-bottom: solid 1px $eccgui-color-codeeditor-separation;
28
+ background-color: $eccgui-color-codeeditor-background;
29
+ }
30
+
31
+ &--has-toolbar {
32
+ .cm-scroller {
33
+ margin-top: $eccgui-size-codeeditor-toolbar-height !important;
34
+ }
35
+ }
36
+
37
+ &__preview {
38
+ position: absolute;
39
+ top: calc(#{$eccgui-size-codeeditor-toolbar-height} + 1px) !important;
40
+ left: 1px;
41
+ right: 1px;
42
+ bottom: 1px;
43
+ z-index: 2;
44
+ padding: $button-padding;
45
+ overflow-y: auto;
46
+ background-color: $eccgui-color-codeeditor-background;
47
+ border-radius: 0 0 $pt-border-radius $pt-border-radius;
48
+ }
49
+
17
50
  .cm-editor {
18
51
  width: 100%;
19
- height: 290px;
52
+ height: $eccgui-size-codeeditor-height;
20
53
  clip-path: unset !important; // we may check later why they set inset(0) now
21
54
  background-color: $eccgui-color-codeeditor-background;
22
55
  border-radius: $pt-border-radius;
@@ -27,7 +60,7 @@ $eccgui-color-codeeditor-background: $eccgui-color-textfield-background !default
27
60
  &.#{eccgui}-disabled {
28
61
  @extend .#{$ns}-input, .#{$ns}-disabled;
29
62
 
30
- height: 290px;
63
+ height: $eccgui-size-codeeditor-height;
31
64
  padding: 0;
32
65
  }
33
66
 
@@ -10,6 +10,7 @@
10
10
  import { EditorView, placeholder, highlightSpecialChars, lineNumbers, highlightActiveLine } from "@codemirror/view";
11
11
  import { syntaxHighlighting, foldGutter, codeFolding } from "@codemirror/language";
12
12
  import { Extension } from "@codemirror/state";
13
+ import { lintGutter } from "@codemirror/lint";
13
14
 
14
15
  /** placeholder extension, current error '_view.placeholder is not a function' */
15
16
  export const adaptedPlaceholder = (text?: string) =>
@@ -55,3 +56,6 @@ export const adaptedFoldGutter = (props?: any) =>
55
56
 
56
57
  export const adaptedCodeFolding = (props?: any) =>
57
58
  typeof codeFolding === "function" ? codeFolding(props) : emptyExtension;
59
+
60
+ export const adaptedLintGutter = (props?: any) =>
61
+ typeof lintGutter === "function" ? lintGutter(props) : emptyExtension;
@@ -0,0 +1,340 @@
1
+ import { type ChangeSpec, EditorSelection } from "@codemirror/state";
2
+ import { EditorView } from "codemirror";
3
+
4
+ import { ValidIconName } from "../../../../components/Icon/canonicalIconNames";
5
+
6
+ enum Commands {
7
+ header1 = "Heading 1",
8
+ header2 = "Heading 2",
9
+ header3 = "Heading 3",
10
+ header4 = "Heading 4",
11
+ header5 = "Heading 5",
12
+ header6 = "Heading 6",
13
+ codeBlock = "Code block",
14
+ quote = "Block quote",
15
+ bold = "Bold",
16
+ italic = "Italic",
17
+ strike = "StrikeThrough",
18
+ inlineCode = "Inline code",
19
+ unorderedList = "Unordered list",
20
+ orderedList = "Ordered list",
21
+ todoList = "Todo list",
22
+ link = "Link",
23
+ image = "Image",
24
+ }
25
+
26
+ type formatConfig = { start: number; startDelimiter: string; stop?: number; endDelimiter?: string };
27
+ type headerLevels = 1 | 2 | 3 | 4 | 5 | 6;
28
+ type ListType = "ul" | "ol" | "todo";
29
+
30
+ //contains all utilities for markdown toolbar
31
+ export default class MarkdownCommand {
32
+ private view: EditorView | null = null;
33
+
34
+ //list of supported commands as well as the valid icon names.
35
+ public static commands = {
36
+ paragraphs: [
37
+ Commands.header1,
38
+ Commands.header2,
39
+ Commands.header3,
40
+ Commands.header4,
41
+ Commands.header5,
42
+ Commands.header6,
43
+ Commands.quote,
44
+ Commands.codeBlock,
45
+ ],
46
+ basic: [
47
+ { title: Commands.bold, icon: "operation-format-text-bold" },
48
+ { title: Commands.italic, icon: "operation-format-text-italic" },
49
+ { title: Commands.strike, icon: "operation-format-text-strikethrough" },
50
+ { title: Commands.inlineCode, icon: "operation-format-text-code" },
51
+ ] as { title: Commands; icon: ValidIconName }[],
52
+ lists: [
53
+ { title: Commands.unorderedList, icon: "operation-format-list-bullet", moniker: "ul" },
54
+ { title: Commands.orderedList, icon: "operation-format-list-numbered", moniker: "ol" },
55
+ { title: Commands.todoList, icon: "operation-format-list-checked", moniker: "todo" },
56
+ ] as { title: Commands; icon: ValidIconName; moniker: string }[],
57
+ attachments: [
58
+ { title: Commands.link, icon: "operation-link" },
59
+ { title: Commands.image, icon: "item-image" },
60
+ ] as { title: Commands; icon: ValidIconName }[],
61
+ } as const;
62
+
63
+ constructor(view: EditorView) {
64
+ this.view = view;
65
+ }
66
+
67
+ /**
68
+ * Supported list types are ol, ul, todo.
69
+ * utility helps to determine which at the start of the line
70
+ */
71
+ private getListTypeOfLine = (text: string): [ListType, number?] | undefined => {
72
+ if (!text) return;
73
+ text = text?.trimStart();
74
+
75
+ if (text.startsWith("- ")) {
76
+ if (text.startsWith("- [ ] ") || text.startsWith("- [x] ")) return ["todo"];
77
+ return ["ul"];
78
+ }
79
+
80
+ const v = text.match(/^(\d+)\. /);
81
+
82
+ return v ? ["ol", Number.parseInt(v[1], 10)] : undefined;
83
+ };
84
+
85
+ //inserts the list delimiters of "-", "- [ ]" and "{number}."
86
+ private createListDelimiter(text: string, type: string, orderedList: { currentIndex: number }) {
87
+ return text.replace(/^(( *)(-( \[[x ]])?|\d+\.) )?/, (...args) => {
88
+ const { space = "" } = args[args.length - 1];
89
+
90
+ let newFlag = "- ";
91
+
92
+ if (type === "ol") {
93
+ newFlag = `${orderedList.currentIndex}. `;
94
+ orderedList.currentIndex++;
95
+ } else if (type === "todo") {
96
+ newFlag = "- [ ] ";
97
+ }
98
+
99
+ return space + newFlag;
100
+ });
101
+ }
102
+
103
+ //factory for different list types.
104
+ private createList = (type: ListType) => {
105
+ if (!this.view) return;
106
+ const view = this.view;
107
+ const doc = view.state.doc;
108
+
109
+ const orderedList = { currentIndex: 1 };
110
+
111
+ view.dispatch(
112
+ view.state.changeByRange((range) => {
113
+ const text = doc.slice(range.from, range.to);
114
+ const changes: ChangeSpec[] = [];
115
+
116
+ let selectionStart: number = range.from;
117
+ let selectionLength: number = range.to - range.from;
118
+
119
+ Array.from({ length: text.lines }).forEach((_, index) => {
120
+ const line = doc.line(doc.lineAt(range.from).number + index);
121
+
122
+ const currentListType = this.getListTypeOfLine(line.text);
123
+
124
+ if (currentListType && currentListType[0] === type) {
125
+ if (currentListType[0] === "ol" && currentListType[1]) {
126
+ orderedList.currentIndex = currentListType[1];
127
+ }
128
+
129
+ return;
130
+ }
131
+ const content = this.createListDelimiter(line.text, type, orderedList);
132
+
133
+ const diffLength = content.length - line.length;
134
+
135
+ changes.push({
136
+ from: line.from,
137
+ to: line.to,
138
+ insert: content,
139
+ });
140
+
141
+ if (index === 0) {
142
+ selectionStart = selectionStart + diffLength;
143
+ } else {
144
+ selectionLength = selectionLength + diffLength;
145
+ }
146
+ });
147
+
148
+ return {
149
+ changes,
150
+ range: EditorSelection.range(selectionStart, selectionStart + selectionLength),
151
+ };
152
+ })
153
+ );
154
+
155
+ view.focus();
156
+ };
157
+
158
+ private enforceCursorFocus = (cursorPosition: number) => {
159
+ if (!this.view) return;
160
+ const view = this.view;
161
+ setTimeout(() => {
162
+ view.dispatch({
163
+ selection: EditorSelection.cursor(cursorPosition),
164
+ });
165
+ view.focus();
166
+ }, 50);
167
+ };
168
+
169
+ //supported headers from h1-h6, h6 being the smallest
170
+ private createHeading = (level: headerLevels) => {
171
+ if (!this.view) return;
172
+ const view = this.view;
173
+ const state = view.state;
174
+
175
+ const flags = "#".repeat(level) + " ";
176
+
177
+ let lastCursorPosition = 0;
178
+
179
+ view.dispatch(
180
+ state.changeByRange((range) => {
181
+ const line = state.doc.lineAt(range.from);
182
+
183
+ const content = line.text.replace(/^((#+) )?/, flags);
184
+
185
+ const diffLength = content.length - line.length;
186
+ lastCursorPosition = line.to + diffLength;
187
+ return {
188
+ changes: {
189
+ from: line.from,
190
+ to: line.to,
191
+ insert: content,
192
+ },
193
+ range: EditorSelection.range(range.anchor + diffLength, range.head + diffLength),
194
+ };
195
+ })
196
+ );
197
+
198
+ this.enforceCursorFocus(lastCursorPosition);
199
+ };
200
+
201
+ private applyFormatting = ({
202
+ start,
203
+ startDelimiter,
204
+ endDelimiter = startDelimiter,
205
+ stop = start,
206
+ }: formatConfig) => {
207
+ if (!this.view) return;
208
+ const view = this.view;
209
+ const { from, to } = view.state.selection.main;
210
+ const text = view.state.sliceDoc(from, to);
211
+ view.dispatch(
212
+ view.state.changeByRange((range) => {
213
+ return {
214
+ changes: [{ from: range.from, to: range.to, insert: `${startDelimiter}${text}${endDelimiter}` }],
215
+ range: EditorSelection.range(range.from + start, range.to + stop),
216
+ };
217
+ })
218
+ );
219
+ view.focus();
220
+ };
221
+
222
+ private applyAttachment = (type: Commands.link | Commands.image) => {
223
+ if (!this.view) return;
224
+ const view = this.view;
225
+ const { state } = view;
226
+ const isImageAttachmentType = type === Commands.image;
227
+
228
+ const { doc } = state;
229
+
230
+ view.dispatch(
231
+ state.changeByRange((range) => {
232
+ const { from, to } = range;
233
+
234
+ const text = doc.sliceString(from, to);
235
+
236
+ const link = `${isImageAttachmentType ? `!` : ""}[${text}]()`;
237
+
238
+ const cursor = from + (text.length ? 3 + text.length : 1 + Number(isImageAttachmentType));
239
+
240
+ return {
241
+ changes: [
242
+ {
243
+ from,
244
+ to,
245
+ insert: link,
246
+ },
247
+ ],
248
+ range: EditorSelection.range(cursor, cursor),
249
+ };
250
+ })
251
+ );
252
+
253
+ view.focus();
254
+ };
255
+
256
+ private applyQuoteFormatting = () => {
257
+ if (!this.view) return;
258
+ const view = this.view;
259
+ const { state } = view;
260
+ const { doc } = state;
261
+
262
+ let lastCursorPosition = 0;
263
+
264
+ view.dispatch(
265
+ view.state.changeByRange((range) => {
266
+ const startLine = doc.lineAt(range.from);
267
+
268
+ const text = doc.slice(range.from, range.to);
269
+
270
+ const lineCount = text.lines;
271
+
272
+ const changes: ChangeSpec[] = [];
273
+
274
+ let selectionStart: number = range.from;
275
+ let selectionLength: number = range.to - range.from;
276
+
277
+ new Array(lineCount).fill(0).forEach((_, index) => {
278
+ const line = doc.line(startLine.number + index);
279
+
280
+ if (line.text.startsWith("> ")) {
281
+ return;
282
+ }
283
+ changes.push({
284
+ from: line.from,
285
+ insert: "> ",
286
+ });
287
+
288
+ if (index === 0) {
289
+ selectionStart = selectionStart + 2;
290
+ } else {
291
+ selectionLength += 2;
292
+ }
293
+ });
294
+
295
+ lastCursorPosition = selectionStart + selectionLength;
296
+
297
+ return {
298
+ changes,
299
+ range: EditorSelection.range(selectionStart, selectionStart + selectionLength),
300
+ };
301
+ })
302
+ );
303
+ this.enforceCursorFocus(lastCursorPosition);
304
+ };
305
+
306
+ executeCommand = (command: Commands): true | void => {
307
+ switch (command) {
308
+ case Commands.bold:
309
+ return this.applyFormatting({ start: 2, startDelimiter: "**" });
310
+ case Commands.italic:
311
+ return this.applyFormatting({ start: 1, startDelimiter: "*" });
312
+ case Commands.codeBlock:
313
+ return this.applyFormatting({ start: 3, startDelimiter: "```\n", endDelimiter: "\n```" });
314
+ case Commands.strike:
315
+ return this.applyFormatting({ start: 2, startDelimiter: "~~" });
316
+ case Commands.inlineCode:
317
+ return this.applyFormatting({ start: 1, startDelimiter: "`" });
318
+ case Commands.header1:
319
+ case Commands.header2:
320
+ case Commands.header3:
321
+ case Commands.header4:
322
+ case Commands.header5:
323
+ case Commands.header6:
324
+ return this.createHeading(Number(command.slice(-1)) as headerLevels);
325
+ case Commands.unorderedList:
326
+ case Commands.orderedList:
327
+ case Commands.todoList:
328
+ return this.createList(
329
+ MarkdownCommand.commands.lists.find((l) => l.title === command)?.moniker as ListType
330
+ );
331
+ case Commands.image:
332
+ case Commands.link:
333
+ return this.applyAttachment(command);
334
+ case Commands.quote:
335
+ return this.applyQuoteFormatting();
336
+ default:
337
+ return; //do nothing;
338
+ }
339
+ };
340
+ }