@eccenca/gui-elements 24.1.0-rc.4 → 24.1.0-rc.6

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 (74) hide show
  1. package/CHANGELOG.md +35 -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 +8 -0
  5. package/dist/cjs/components/AutoSuggestion/AutoSuggestion.js.map +1 -1
  6. package/dist/cjs/components/Icon/canonicalIconNames.js +10 -0
  7. package/dist/cjs/components/Icon/canonicalIconNames.js.map +1 -1
  8. package/dist/cjs/components/OverviewItem/OverviewItem.js +5 -2
  9. package/dist/cjs/components/OverviewItem/OverviewItem.js.map +1 -1
  10. package/dist/cjs/components/OverviewItem/OverviewItemList.js +2 -2
  11. package/dist/cjs/components/OverviewItem/OverviewItemList.js.map +1 -1
  12. package/dist/cjs/components/TextField/SearchField.js +19 -2
  13. package/dist/cjs/components/TextField/SearchField.js.map +1 -1
  14. package/dist/cjs/components/Typography/OverflowText.js +1 -1
  15. package/dist/cjs/components/Typography/OverflowText.js.map +1 -1
  16. package/dist/cjs/extensions/codemirror/CodeMirror.js +37 -8
  17. package/dist/cjs/extensions/codemirror/CodeMirror.js.map +1 -1
  18. package/dist/cjs/extensions/codemirror/toolbars/commands/markdown.command.js +278 -0
  19. package/dist/cjs/extensions/codemirror/toolbars/commands/markdown.command.js.map +1 -0
  20. package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js +47 -0
  21. package/dist/cjs/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -0
  22. package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js +7 -2
  23. package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js.map +1 -1
  24. package/dist/esm/components/AutoSuggestion/AutoSuggestion.js +8 -0
  25. package/dist/esm/components/AutoSuggestion/AutoSuggestion.js.map +1 -1
  26. package/dist/esm/components/Icon/canonicalIconNames.js +10 -0
  27. package/dist/esm/components/Icon/canonicalIconNames.js.map +1 -1
  28. package/dist/esm/components/OverviewItem/OverviewItem.js +5 -2
  29. package/dist/esm/components/OverviewItem/OverviewItem.js.map +1 -1
  30. package/dist/esm/components/OverviewItem/OverviewItemList.js +2 -2
  31. package/dist/esm/components/OverviewItem/OverviewItemList.js.map +1 -1
  32. package/dist/esm/components/TextField/SearchField.js +35 -2
  33. package/dist/esm/components/TextField/SearchField.js.map +1 -1
  34. package/dist/esm/components/Typography/OverflowText.js +1 -1
  35. package/dist/esm/components/Typography/OverflowText.js.map +1 -1
  36. package/dist/esm/extensions/codemirror/CodeMirror.js +38 -9
  37. package/dist/esm/extensions/codemirror/CodeMirror.js.map +1 -1
  38. package/dist/esm/extensions/codemirror/toolbars/commands/markdown.command.js +283 -0
  39. package/dist/esm/extensions/codemirror/toolbars/commands/markdown.command.js.map +1 -0
  40. package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js +41 -0
  41. package/dist/esm/extensions/codemirror/toolbars/markdown.toolbar.js.map +1 -0
  42. package/dist/types/cmem/ActivityControl/ActivityControlWidget.d.ts +1 -1
  43. package/dist/types/components/Icon/canonicalIconNames.d.ts +10 -0
  44. package/dist/types/components/OverviewItem/OverviewItem.d.ts +13 -1
  45. package/dist/types/components/OverviewItem/OverviewItemList.d.ts +3 -2
  46. package/dist/types/components/TextField/SearchField.d.ts +1 -1
  47. package/dist/types/components/Typography/OverflowText.d.ts +23 -2
  48. package/dist/types/extensions/codemirror/CodeMirror.d.ts +10 -1
  49. package/dist/types/extensions/codemirror/toolbars/commands/markdown.command.d.ts +55 -0
  50. package/dist/types/extensions/codemirror/toolbars/markdown.toolbar.d.ts +12 -0
  51. package/package.json +21 -19
  52. package/src/cmem/ActivityControl/ActivityControlWidget.tsx +5 -2
  53. package/src/components/AutoSuggestion/AutoSuggestion.tsx +9 -0
  54. package/src/components/CodeAutocompleteField/CodeAutocompleteField.stories.tsx +3 -2
  55. package/src/components/ContextOverlay/ContextOverlay.stories.tsx +15 -4
  56. package/src/components/Depiction/depiction.scss +7 -0
  57. package/src/components/Dialog/stories/AlertDialog.stories.tsx +5 -1
  58. package/src/components/Dialog/stories/Modal.stories.tsx +4 -2
  59. package/src/components/Dialog/stories/SimpleDialog.stories.tsx +5 -2
  60. package/src/components/Icon/canonicalIconNames.tsx +10 -0
  61. package/src/components/OverviewItem/OverviewItem.tsx +24 -1
  62. package/src/components/OverviewItem/OverviewItemList.tsx +3 -2
  63. package/src/components/OverviewItem/stories/OverviewItem.stories.tsx +6 -12
  64. package/src/components/Select/Select.stories.tsx +4 -1
  65. package/src/components/TextField/SearchField.tsx +37 -9
  66. package/src/components/TextField/stories/SearchField.stories.tsx +15 -1
  67. package/src/components/TextField/textfield.scss +17 -3
  68. package/src/components/Typography/OverflowText.tsx +24 -3
  69. package/src/components/Typography/stories/OverflowText.stories.tsx +33 -0
  70. package/src/extensions/codemirror/CodeMirror.stories.tsx +5 -17
  71. package/src/extensions/codemirror/CodeMirror.tsx +67 -8
  72. package/src/extensions/codemirror/_codemirror.scss +35 -2
  73. package/src/extensions/codemirror/toolbars/commands/markdown.command.ts +340 -0
  74. package/src/extensions/codemirror/toolbars/markdown.toolbar.tsx +117 -0
@@ -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
+ }
@@ -0,0 +1,117 @@
1
+ import React from "react";
2
+ import { EditorView } from "codemirror";
3
+
4
+ import { Button } from "../../../components/Button/Button";
5
+ import { ContextMenu } from "../../../components/ContextOverlay";
6
+ import { Icon, IconButton } from "../../../components/Icon";
7
+ import { MenuItem } from "../../../components/Menu";
8
+ import { Spacing } from "../../../components/Separation/Spacing";
9
+ import { Toolbar, ToolbarSection } from "../../../components/Toolbar";
10
+
11
+ import MarkdownCommand from "./commands/markdown.command";
12
+
13
+ interface MarkdownToolbarProps {
14
+ view?: EditorView;
15
+ togglePreviewStatus: () => void;
16
+ showPreview: boolean;
17
+ translate: (key: string) => string | false;
18
+ disabled?: boolean;
19
+ readonly?: boolean;
20
+ }
21
+
22
+ export const MarkdownToolbar: React.FC<MarkdownToolbarProps> = ({
23
+ view,
24
+ togglePreviewStatus,
25
+ showPreview,
26
+ disabled,
27
+ readonly,
28
+ translate
29
+ }) => {
30
+ const commandRef = React.useRef<MarkdownCommand | null>(null);
31
+
32
+ React.useEffect(() => {
33
+ if (view) {
34
+ commandRef.current = new MarkdownCommand(view);
35
+ }
36
+ }, [view]);
37
+
38
+ const getTranslation = (fallback: string) : string => {
39
+ const key = fallback.toLowerCase().replace(" ", "-");
40
+ return translate(key) || fallback;
41
+ }
42
+
43
+ const { basic, lists, attachments } = MarkdownCommand.commands;
44
+ return (
45
+ <Toolbar noWrap>
46
+ <ToolbarSection canShrink hideOverflow>
47
+ <ContextMenu
48
+ togglerElement={
49
+ <Button
50
+ rightIcon={<Icon name="toggler-showmore" />}
51
+ text={getTranslation("Paragraphs")}
52
+ minimal
53
+ fill
54
+ ellipsizeText
55
+ disabled={showPreview || disabled || readonly}
56
+ />
57
+ }
58
+ >
59
+ {MarkdownCommand.commands.paragraphs.map((p, i) => (
60
+ <MenuItem
61
+ key={p}
62
+ text={
63
+ <>
64
+ <span style={p.startsWith("Head") ? { fontSize: 22 - (i * (22 - 12)) / 5 } : {}}>
65
+ {getTranslation(p)}
66
+ </span>
67
+ </>
68
+ }
69
+ onClick={() => commandRef.current?.executeCommand(p)}
70
+ />
71
+ ))}
72
+ </ContextMenu>
73
+ </ToolbarSection>
74
+ <ToolbarSection canShrink>
75
+ <Spacing vertical hasDivider size="tiny" />
76
+ </ToolbarSection>
77
+
78
+ {[basic, lists, attachments].map((section, i) => {
79
+ return (
80
+ <React.Fragment key={i}>
81
+ <ToolbarSection>
82
+ {section.map((command) => {
83
+ return (
84
+ <IconButton
85
+ key={command.title}
86
+ name={command.icon}
87
+ onClick={() => commandRef.current?.executeCommand(command.title)}
88
+ text={getTranslation(command.title)}
89
+ disabled={showPreview || disabled || readonly}
90
+ />
91
+ );
92
+ })}
93
+ </ToolbarSection>
94
+ {i < 2 && (
95
+ <ToolbarSection canShrink>
96
+ <Spacing vertical hasDivider size="tiny" />
97
+ </ToolbarSection>
98
+ )}
99
+ </React.Fragment>
100
+ );
101
+ })}
102
+ <ToolbarSection canGrow canShrink>
103
+ <Spacing vertical size="small" />
104
+ </ToolbarSection>
105
+ <ToolbarSection canShrink hideOverflow>
106
+ <Button
107
+ minimal
108
+ ellipsizeText
109
+ onClick={togglePreviewStatus}
110
+ text={showPreview ? getTranslation("Continue editing") : getTranslation("Preview")}
111
+ icon={showPreview ? "item-edit" : "item-viewdetails"}
112
+ disabled={disabled}
113
+ />
114
+ </ToolbarSection>
115
+ </Toolbar>
116
+ );
117
+ };