@eccenca/gui-elements 24.3.0 → 24.4.0

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 (50) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/dist/cjs/cmem/ActivityControl/ActivityControlWidget.js +12 -9
  3. package/dist/cjs/cmem/ActivityControl/ActivityControlWidget.js.map +1 -1
  4. package/dist/cjs/components/AutoSuggestion/AutoSuggestion.js +10 -9
  5. package/dist/cjs/components/AutoSuggestion/AutoSuggestion.js.map +1 -1
  6. package/dist/cjs/components/AutoSuggestion/ExtendedCodeEditor.js +2 -2
  7. package/dist/cjs/components/AutoSuggestion/ExtendedCodeEditor.js.map +1 -1
  8. package/dist/cjs/components/Icon/IconButton.js +1 -0
  9. package/dist/cjs/components/Icon/IconButton.js.map +1 -1
  10. package/dist/cjs/components/Tooltip/Tooltip.js +10 -5
  11. package/dist/cjs/components/Tooltip/Tooltip.js.map +1 -1
  12. package/dist/cjs/extensions/codemirror/CodeMirror.js +83 -21
  13. package/dist/cjs/extensions/codemirror/CodeMirror.js.map +1 -1
  14. package/dist/cjs/extensions/codemirror/tests/codemirrorTestHelper.js +22 -1
  15. package/dist/cjs/extensions/codemirror/tests/codemirrorTestHelper.js.map +1 -1
  16. package/dist/cjs/extensions/react-flow/nodes/NodeContent.js +1 -1
  17. package/dist/cjs/extensions/react-flow/nodes/NodeContent.js.map +1 -1
  18. package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js +12 -9
  19. package/dist/esm/cmem/ActivityControl/ActivityControlWidget.js.map +1 -1
  20. package/dist/esm/components/AutoSuggestion/AutoSuggestion.js +10 -9
  21. package/dist/esm/components/AutoSuggestion/AutoSuggestion.js.map +1 -1
  22. package/dist/esm/components/AutoSuggestion/ExtendedCodeEditor.js +2 -2
  23. package/dist/esm/components/AutoSuggestion/ExtendedCodeEditor.js.map +1 -1
  24. package/dist/esm/components/Icon/IconButton.js +1 -0
  25. package/dist/esm/components/Icon/IconButton.js.map +1 -1
  26. package/dist/esm/components/Tooltip/Tooltip.js +12 -7
  27. package/dist/esm/components/Tooltip/Tooltip.js.map +1 -1
  28. package/dist/esm/extensions/codemirror/CodeMirror.js +84 -22
  29. package/dist/esm/extensions/codemirror/CodeMirror.js.map +1 -1
  30. package/dist/esm/extensions/codemirror/tests/codemirrorTestHelper.js +20 -0
  31. package/dist/esm/extensions/codemirror/tests/codemirrorTestHelper.js.map +1 -1
  32. package/dist/esm/extensions/react-flow/nodes/NodeContent.js +1 -1
  33. package/dist/esm/extensions/react-flow/nodes/NodeContent.js.map +1 -1
  34. package/dist/types/cmem/ActivityControl/ActivityControlWidget.d.ts +4 -0
  35. package/dist/types/components/AutoSuggestion/AutoSuggestion.d.ts +7 -1
  36. package/dist/types/components/AutoSuggestion/ExtendedCodeEditor.d.ts +5 -1
  37. package/dist/types/components/Tooltip/Tooltip.d.ts +7 -1
  38. package/dist/types/extensions/codemirror/CodeMirror.d.ts +3 -2
  39. package/dist/types/extensions/codemirror/tests/codemirrorTestHelper.d.ts +3 -1
  40. package/package.json +3 -3
  41. package/src/cmem/ActivityControl/ActivityControlWidget.stories.tsx +45 -5
  42. package/src/cmem/ActivityControl/ActivityControlWidget.tsx +47 -9
  43. package/src/cmem/ActivityControl/tests/ActivityControlWidget.test.tsx +99 -0
  44. package/src/components/AutoSuggestion/AutoSuggestion.tsx +19 -5
  45. package/src/components/AutoSuggestion/ExtendedCodeEditor.tsx +8 -0
  46. package/src/components/Icon/IconButton.tsx +1 -0
  47. package/src/components/Tooltip/Tooltip.tsx +17 -5
  48. package/src/extensions/codemirror/CodeMirror.tsx +102 -26
  49. package/src/extensions/codemirror/tests/codemirrorTestHelper.ts +20 -1
  50. package/src/extensions/react-flow/nodes/NodeContent.tsx +1 -1
@@ -81,6 +81,10 @@ export interface ActivityControlWidgetProps extends TestableComponent {
81
81
  * execution timer messages for waiting and running times.
82
82
  */
83
83
  timerExecutionMsg?: JSX.Element | null;
84
+ /**
85
+ * additional actions that can serve as a complex component, positioned between the default actions and the context menu
86
+ */
87
+ additionalActions?: React.ReactElement<unknown>[];
84
88
  }
85
89
 
86
90
  interface IActivityContextMenu extends TestableComponent {
@@ -110,11 +114,13 @@ interface IActivityMenuAction extends ActivityControlWidgetAction {
110
114
  /** Shows the status of activities and supports actions on these activities. */
111
115
  export function ActivityControlWidget(props: ActivityControlWidgetProps) {
112
116
  const {
113
- "data-test-id": dataTestId,
117
+ "data-test-id": dataTestIdLegacy,
118
+ "data-testid": dataTestId,
114
119
  progressBar,
115
120
  progressSpinner,
116
121
  activityActions,
117
122
  activityContextMenu,
123
+ additionalActions,
118
124
  small,
119
125
  border,
120
126
  hasSpacing,
@@ -126,10 +132,19 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
126
132
  } = props;
127
133
  const spinnerClassNames = (progressSpinner?.className ?? "") + ` ${eccgui}-spinner--permanent`;
128
134
  const widget = (
129
- <OverviewItem data-test-id={dataTestId} hasSpacing={border || hasSpacing} densityHigh={small}>
135
+ <OverviewItem
136
+ data-test-id={dataTestIdLegacy}
137
+ data-testid={dataTestId}
138
+ hasSpacing={border || hasSpacing}
139
+ densityHigh={small}
140
+ >
130
141
  {progressBar && <ProgressBar {...progressBar} />}
131
142
  {(progressSpinner || progressSpinnerFinishedIcon) && (
132
- <OverviewItemDepiction keepColors>
143
+ <OverviewItemDepiction
144
+ data-testid={dataTestId ? `${dataTestId}-progress-spinner` : undefined}
145
+ data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-progress-spinner` : undefined}
146
+ keepColors
147
+ >
133
148
  {progressSpinnerFinishedIcon ? (
134
149
  React.cloneElement(progressSpinnerFinishedIcon as JSX.Element, { small, large: !small })
135
150
  ) : (
@@ -145,13 +160,21 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
145
160
  )}
146
161
  <OverviewItemDescription>
147
162
  {props.label && (
148
- <OverviewItemLine small={small}>
163
+ <OverviewItemLine
164
+ data-testid={dataTestId ? `${dataTestId}-label` : undefined}
165
+ data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-label` : undefined}
166
+ small={small}
167
+ >
149
168
  {React.cloneElement(labelWrapper, {}, props.label)}
150
169
  {timerExecutionMsg && (props.statusMessage || tags) && <>&nbsp;({timerExecutionMsg})</>}
151
170
  </OverviewItemLine>
152
171
  )}
153
172
  {(props.statusMessage || tags) && (
154
- <OverviewItemLine small>
173
+ <OverviewItemLine
174
+ data-testid={dataTestId ? `${dataTestId}-status-message` : undefined}
175
+ data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-status-message` : undefined}
176
+ small
177
+ >
155
178
  {tags}
156
179
  {props.statusMessage && (
157
180
  <OverflowText passDown>
@@ -172,21 +195,35 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
172
195
  </OverviewItemLine>
173
196
  )}
174
197
  {timerExecutionMsg && !(props.statusMessage || tags) && (
175
- <OverviewItemLine small>{timerExecutionMsg}</OverviewItemLine>
198
+ <OverviewItemLine
199
+ data-testid={dataTestId ? `${dataTestId}-status-message` : undefined}
200
+ data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-status-message` : undefined}
201
+ small
202
+ >
203
+ {timerExecutionMsg}
204
+ </OverviewItemLine>
176
205
  )}
177
206
  </OverviewItemDescription>
178
- <OverviewItemActions>
207
+ <OverviewItemActions
208
+ data-testid={dataTestId ? `${dataTestId}-actions` : undefined}
209
+ data-test-id={dataTestIdLegacy ? `${dataTestIdLegacy}-actions` : undefined}
210
+ >
179
211
  {activityActions &&
180
212
  activityActions.map((action, idx) => {
181
213
  return (
182
214
  <IconButton
183
- key={typeof action.icon === "string" ? action.icon : action["data-test-id"] ?? idx}
215
+ key={
216
+ typeof action.icon === "string"
217
+ ? action.icon
218
+ : action["data-test-id"] ?? action["data-testid"] ?? idx
219
+ }
184
220
  data-test-id={action["data-test-id"]}
221
+ data-testid={action["data-testid"]}
185
222
  name={action.icon}
186
223
  text={action.tooltip}
187
224
  onClick={action.action}
188
225
  disabled={action.disabled}
189
- hasStateWarning={action.hasStateWarning}
226
+ intent={action.hasStateWarning ? "warning" : undefined}
190
227
  tooltipProps={{
191
228
  hoverOpenDelay: 200,
192
229
  placement: "bottom",
@@ -194,6 +231,7 @@ export function ActivityControlWidget(props: ActivityControlWidgetProps) {
194
231
  />
195
232
  );
196
233
  })}
234
+ {additionalActions}
197
235
  {activityContextMenu && activityContextMenu.menuItems.length > 0 && (
198
236
  <ContextMenu
199
237
  data-test-id={activityContextMenu["data-test-id"]}
@@ -0,0 +1,99 @@
1
+ import React from "react";
2
+ import { fireEvent, render, screen } from "@testing-library/react";
3
+
4
+ import "@testing-library/jest-dom";
5
+
6
+ import { IconButton, Tag, TagList } from "../../../index";
7
+ import { ActivityControlWidget, ActivityControlWidgetAction } from "../ActivityControlWidget";
8
+
9
+ describe("ActivityControlWidget", () => {
10
+ it("Renders basic widget with actions and handles clicks", () => {
11
+ const mockAction1 = jest.fn();
12
+ const mockAction2 = jest.fn();
13
+ const actions: ActivityControlWidgetAction[] = [
14
+ {
15
+ "data-testid": "action-1",
16
+ icon: "item-reload",
17
+ action: mockAction1,
18
+ tooltip: "Action 1",
19
+ },
20
+ {
21
+ "data-testid": "action-2",
22
+ icon: "item-start",
23
+ action: mockAction2,
24
+ tooltip: "Action 2",
25
+ },
26
+ ];
27
+
28
+ render(
29
+ <ActivityControlWidget
30
+ label="Basic widget"
31
+ data-testid="basic-widget"
32
+ activityActions={actions}
33
+ statusMessage="Status message"
34
+ />
35
+ );
36
+
37
+ const button1 = screen.getByTestId("action-1");
38
+ const button2 = screen.getByTestId("action-2");
39
+
40
+ const label = screen.getByTestId("basic-widget-label");
41
+ const statusMessage = screen.getByTestId("basic-widget-status-message");
42
+ const actionsContainer = screen.getByTestId("basic-widget-actions");
43
+
44
+ expect(label).toBeInTheDocument();
45
+ expect(statusMessage).toBeInTheDocument();
46
+ expect(actionsContainer).toBeInTheDocument();
47
+
48
+ expect(label).toHaveTextContent("Basic widget");
49
+ expect(statusMessage).toHaveTextContent("Status message");
50
+
51
+ expect(button1).toBeInTheDocument();
52
+ expect(button2).toBeInTheDocument();
53
+
54
+ fireEvent.click(button1);
55
+ expect(mockAction1).toHaveBeenCalledTimes(1);
56
+
57
+ fireEvent.click(button2);
58
+ expect(mockAction2).toHaveBeenCalledTimes(1);
59
+ });
60
+
61
+ it("Renders widget with tags", () => {
62
+ const tags = (
63
+ <TagList>
64
+ <Tag>Tag one</Tag>
65
+ <Tag>Other tag</Tag>
66
+ </TagList>
67
+ );
68
+ render(<ActivityControlWidget label="Widget with tags" tags={tags} data-testid="widget-with-tags" />);
69
+
70
+ const label = screen.getByTestId("widget-with-tags-label");
71
+ const statusMessage = screen.getByTestId("widget-with-tags-status-message");
72
+
73
+ expect(label).toBeInTheDocument();
74
+ expect(statusMessage).toBeInTheDocument();
75
+
76
+ expect(label).toHaveTextContent("Widget with tags");
77
+ expect(statusMessage).toHaveTextContent("Tag one");
78
+ expect(statusMessage).toHaveTextContent("Other tag");
79
+ });
80
+
81
+ it("Renders widget with additional actions and handles click", () => {
82
+ const mockAction = jest.fn();
83
+ const additionalActions = [
84
+ <IconButton
85
+ key="add-btn"
86
+ name="application-explore"
87
+ onClick={mockAction}
88
+ data-testid="additional-action"
89
+ />,
90
+ ];
91
+ render(<ActivityControlWidget additionalActions={additionalActions} />);
92
+
93
+ const customButton = screen.getByTestId("additional-action");
94
+ expect(customButton).toBeInTheDocument();
95
+
96
+ fireEvent.click(customButton);
97
+ expect(mockAction).toHaveBeenCalledTimes(1);
98
+ });
99
+ });
@@ -166,6 +166,12 @@ export interface AutoSuggestionProps {
166
166
  /** If this is enabled the value of the editor is replaced with the initialValue if it changes.
167
167
  * FIXME: This property is a workaround for some "controlled" usages of the component via the initialValue property. */
168
168
  reInitOnInitialValueChange?: boolean;
169
+ /** Optional height of the component */
170
+ height?: number | string;
171
+ /** Set read-only mode. Default: false */
172
+ readOnly?: boolean;
173
+ /** Properties that should be added to the outer div container. */
174
+ outerDivAttributes?: Omit<React.HTMLAttributes<HTMLDivElement>, "id" | "data-test-id">;
169
175
  }
170
176
 
171
177
  // Meta data regarding a request
@@ -198,6 +204,9 @@ const AutoSuggestion = ({
198
204
  mode,
199
205
  multiline = false,
200
206
  reInitOnInitialValueChange = false,
207
+ height,
208
+ readOnly,
209
+ outerDivAttributes
201
210
  }: AutoSuggestionProps) => {
202
211
  const value = React.useRef<string>(initialValue);
203
212
  const cursorPosition = React.useRef(0);
@@ -354,7 +363,7 @@ const AutoSuggestion = ({
354
363
  editorState.suggestions = [];
355
364
  setSuggestions([]);
356
365
  }
357
- editorState.index = 0;
366
+ setCurrentIndex(0);
358
367
  }, [suggestionResponse, editorState]);
359
368
 
360
369
  const getOffsetRange = (cm: EditorView, from: number, to: number) => {
@@ -368,7 +377,7 @@ const AutoSuggestion = ({
368
377
  return { fromOffset, toOffset };
369
378
  };
370
379
 
371
- const inputactionsDisplayed = React.useCallback((node) => {
380
+ const inputActionsDisplayed = React.useCallback((node) => {
372
381
  if (!node) return;
373
382
  const width = node.offsetWidth;
374
383
  const slCodeEditor = node.parentElement.getElementsByClassName(`${eccgui}-singlelinecodeeditor`);
@@ -482,8 +491,7 @@ const AutoSuggestion = ({
482
491
  }, 1);
483
492
  };
484
493
 
485
- //todo check out typings for event type
486
- const handleInputEditorKeyPress = (event: any) => {
494
+ const handleInputEditorKeyPress = (event: KeyboardEvent) => {
487
495
  const overWrittenKeys: Array<string> = Object.values(OVERWRITTEN_KEYS);
488
496
  if (overWrittenKeys.includes(event.key) && (useTabForCompletions || event.key !== OVERWRITTEN_KEYS.Tab)) {
489
497
  //don't prevent when enter should create new line (multiline config) and dropdown isn't shown
@@ -619,6 +627,7 @@ const AutoSuggestion = ({
619
627
  break;
620
628
  default:
621
629
  //do nothing
630
+ closeDropDown();
622
631
  }
623
632
  }
624
633
  };
@@ -653,6 +662,8 @@ const AutoSuggestion = ({
653
662
  showScrollBar={showScrollBar}
654
663
  multiline={multiline}
655
664
  onMouseDown={handleInputMouseDown}
665
+ height={height}
666
+ readOnly={readOnly}
656
667
  />
657
668
  );
658
669
  }, [
@@ -665,6 +676,7 @@ const AutoSuggestion = ({
665
676
  showScrollBar,
666
677
  multiline,
667
678
  handleInputMouseDown,
679
+ readOnly
668
680
  ]);
669
681
 
670
682
  const hasError = !!value.current && !pathIsValid && !pathValidationPending;
@@ -673,6 +685,7 @@ const AutoSuggestion = ({
673
685
  id={id}
674
686
  ref={autoSuggestionDivRef}
675
687
  className={`${eccgui}-autosuggestion` + (className ? ` ${className}` : "")}
688
+ {...outerDivAttributes}
676
689
  >
677
690
  <div
678
691
  className={` ${eccgui}-autosuggestion__inputfield ${BlueprintClassNames.INPUT_GROUP} ${
@@ -703,11 +716,12 @@ const AutoSuggestion = ({
703
716
  {codeEditor}
704
717
  </ContextOverlay>
705
718
  {!!value.current && (
706
- <span className={BlueprintClassNames.INPUT_ACTION} ref={inputactionsDisplayed}>
719
+ <span className={BlueprintClassNames.INPUT_ACTION} ref={inputActionsDisplayed}>
707
720
  <IconButton
708
721
  data-test-id={"value-path-clear-btn"}
709
722
  name="operation-clear"
710
723
  text={clearIconText}
724
+ disabled={readOnly}
711
725
  onClick={handleInputEditorClear}
712
726
  />
713
727
  </span>
@@ -61,6 +61,10 @@ export interface ExtendedCodeEditorProps {
61
61
  | "additionalExtensions"
62
62
  | "outerDivAttributes"
63
63
  >;
64
+ /** Optional height of the component */
65
+ height?: number | string;
66
+ /** Set read-only mode. Default: false */
67
+ readOnly?: boolean;
64
68
  }
65
69
 
66
70
  export type IEditorProps = ExtendedCodeEditorProps;
@@ -80,6 +84,8 @@ export const ExtendedCodeEditor = ({
80
84
  onCursorChange,
81
85
  onSelection,
82
86
  codeEditorProps,
87
+ height,
88
+ readOnly
83
89
  }: ExtendedCodeEditorProps) => {
84
90
  const initialContent = React.useRef(multiline ? initialValue : initialValue.replace(/[\r\n]/g, " "));
85
91
  const multilineExtensions = multiline
@@ -102,6 +108,8 @@ export const ExtendedCodeEditor = ({
102
108
  shouldHaveMinimalSetup={false}
103
109
  preventLineNumbers={!multiline}
104
110
  mode={mode}
111
+ height={height}
112
+ readOnly={readOnly}
105
113
  name=""
106
114
  enableTab={enableTab}
107
115
  additionalExtensions={[...multilineExtensions]}
@@ -50,6 +50,7 @@ export const IconButton = ({
50
50
  const defaultIconTooltipProps = {
51
51
  hoverOpenDelay: 1000,
52
52
  openOnTargetFocus: restProps.disabled || (restProps.tabIndex ?? 0) < 0 ? false : undefined,
53
+ swapPlaceholderDelay: 1,
53
54
  };
54
55
  const iconProps = {
55
56
  small: restProps.small,
@@ -42,6 +42,12 @@ export interface TooltipProps extends Omit<BlueprintTooltipProps, "position"> {
42
42
  * You can prevent it in any case by setting it to `false`.
43
43
  */
44
44
  usePlaceholder?: boolean;
45
+ /**
46
+ * Time after the placeholder element is replaced by the actual tooltip component.
47
+ * Must be greater than 0.
48
+ * For the first display of the tooltip this time adds up to `hoverOpenDelay`.
49
+ */
50
+ swapPlaceholderDelay?: number;
45
51
  }
46
52
 
47
53
  export const Tooltip = ({
@@ -53,18 +59,21 @@ export const Tooltip = ({
53
59
  markdownEnabler = "\n\n",
54
60
  markdownProps,
55
61
  usePlaceholder,
56
- hoverOpenDelay = 500,
62
+ swapPlaceholderDelay = 100,
63
+ hoverOpenDelay = 450,
57
64
  ...otherTooltipProps
58
65
  }: TooltipProps) => {
59
66
  const placeholderRef = React.useRef(null);
60
67
  const eventMemory = React.useRef<null | "afterhover" | "afterfocus">(null);
61
68
  const searchId = React.useRef<null | string>(null);
62
- const swapDelayTime = 100;
69
+ const swapDelay = React.useRef<null | NodeJS.Timeout>(null);
70
+ const swapDelayTime = swapPlaceholderDelay;
63
71
  const [placeholder, setPlaceholder] = React.useState<boolean>(
64
72
  !otherTooltipProps.disabled &&
65
73
  !otherTooltipProps.defaultIsOpen &&
66
74
  !otherTooltipProps.isOpen &&
67
75
  otherTooltipProps.renderTarget === undefined &&
76
+ swapDelayTime > 0 &&
68
77
  hoverOpenDelay > swapDelayTime &&
69
78
  (usePlaceholder === true || (typeof content === "string" && usePlaceholder !== false))
70
79
  );
@@ -77,7 +86,10 @@ export const Tooltip = ({
77
86
  React.useEffect(() => {
78
87
  if (placeholderRef.current !== null) {
79
88
  const swap = (ev: MouseEvent | globalThis.FocusEvent) => {
80
- const swapDelay = setTimeout(() => {
89
+ if (swapDelay.current) {
90
+ clearTimeout(swapDelay.current);
91
+ }
92
+ swapDelay.current = setTimeout(() => {
81
93
  // we delay the swap to prevent unwanted effects
82
94
  // (e.g. forced mouseover after the swap but the cursor is already somewhere else)
83
95
  eventMemory.current = ev.type === "focusin" ? "afterfocus" : "afterhover";
@@ -93,7 +105,7 @@ export const Tooltip = ({
93
105
  ) {
94
106
  eventMemory.current = null;
95
107
  }
96
- clearTimeout(swapDelay);
108
+ clearTimeout(swapDelay.current as NodeJS.Timeout);
97
109
  });
98
110
  }
99
111
  };
@@ -166,7 +178,7 @@ export const Tooltip = ({
166
178
  ) : (
167
179
  <BlueprintTooltip
168
180
  lazy={true}
169
- hoverOpenDelay={hoverOpenDelay - swapDelayTime}
181
+ hoverOpenDelay={hoverOpenDelay}
170
182
  {...otherTooltipProps}
171
183
  content={tooltipContent}
172
184
  className={targetClassName}
@@ -1,7 +1,7 @@
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 { EditorState, Extension } from "@codemirror/state";
4
+ import { EditorState, Extension, Compartment } from "@codemirror/state";
5
5
  import { DOMEventHandlers, EditorView, KeyBinding, keymap, Rect, ViewUpdate } from "@codemirror/view";
6
6
  import { minimalSetup } from "codemirror";
7
7
 
@@ -30,7 +30,7 @@ import {
30
30
  adaptedHighlightSpecialChars,
31
31
  adaptedLineNumbers,
32
32
  adaptedLintGutter,
33
- adaptedPlaceholder,
33
+ adaptedPlaceholder, compartment,
34
34
  } from "./tests/codemirrorTestHelper";
35
35
  import { ExtensionCreator } from "./types";
36
36
 
@@ -53,6 +53,7 @@ export interface CodeEditorProps extends TestableComponent {
53
53
  /**
54
54
  * Handler method to receive onChange events.
55
55
  * As input the new value is given.
56
+ * @deprecated (v25) use `(v: string) => void` in future
56
57
  */
57
58
  onChange?: (v: any) => void;
58
59
  /**
@@ -74,7 +75,7 @@ export interface CodeEditorProps extends TestableComponent {
74
75
  /**
75
76
  * Called when the cursor position changes
76
77
  */
77
- onCursorChange?: (pos: number, coords: Rect, scrollinfo: HTMLElement, cm: EditorView) => any;
78
+ onCursorChange?: (pos: number, coords: Rect, scrollinfo: HTMLElement, cm: EditorView) => void;
78
79
 
79
80
  /**
80
81
  * Syntax mode of the code editor.
@@ -83,7 +84,7 @@ export interface CodeEditorProps extends TestableComponent {
83
84
  /**
84
85
  * Default value used first when the editor is instanciated.
85
86
  */
86
- defaultValue?: any;
87
+ defaultValue?: string;
87
88
  /**
88
89
  * If enabled the code editor won't show numbers before each line.
89
90
  */
@@ -169,7 +170,7 @@ export interface CodeEditorProps extends TestableComponent {
169
170
  }
170
171
 
171
172
  const addExtensionsFor = (flag: boolean, ...extensions: Extension[]) => (flag ? [...extensions] : []);
172
- const addToKeyMapConfigFor = (flag: boolean, ...keys: any) => (flag ? [...keys] : []);
173
+ const addToKeyMapConfigFor = (flag: boolean, ...keys: KeyBinding[]) => (flag ? [...keys] : []);
173
174
  const addHandlersFor = (flag: boolean, handlerName: string, handler: any) =>
174
175
  flag ? ({ [handlerName]: handler } as DOMEventHandlers<any>) : {};
175
176
 
@@ -221,7 +222,24 @@ export const CodeEditor = ({
221
222
  }: CodeEditorProps) => {
222
223
  const parent = useRef<any>(undefined);
223
224
  const [view, setView] = React.useState<EditorView | undefined>();
225
+ const currentView = React.useRef<EditorView>()
226
+ currentView.current = view
227
+ const currentReadOnly = React.useRef(readOnly)
228
+ currentReadOnly.current = readOnly
224
229
  const [showPreview, setShowPreview] = React.useState<boolean>(false);
230
+ // CodeMirror Compartments in order to allow for re-configuration after initialization
231
+ const readOnlyCompartment = React.useRef<Compartment>(compartment())
232
+ const wrapLinesCompartment = React.useRef<Compartment>(compartment())
233
+ const preventLineNumbersCompartment = React.useRef<Compartment>(compartment())
234
+ const shouldHaveMinimalSetupCompartment = React.useRef<Compartment>(compartment())
235
+ const placeholderCompartment = React.useRef<Compartment>(compartment())
236
+ const modeCompartment = React.useRef<Compartment>(compartment())
237
+ const keyMapConfigsCompartment = React.useRef<Compartment>(compartment())
238
+ const tabIntentSizeCompartment = React.useRef<Compartment>(compartment())
239
+ const disabledCompartment = React.useRef<Compartment>(compartment())
240
+ const supportCodeFoldingCompartment = React.useRef<Compartment>(compartment())
241
+ const useLintingCompartment = React.useRef<Compartment>(compartment())
242
+ const shouldHighlightActiveLineCompartment = React.useRef<Compartment>(compartment())
225
243
 
226
244
  const linters = useMemo(() => {
227
245
  if (!mode) {
@@ -240,17 +258,15 @@ export const CodeEditor = ({
240
258
 
241
259
  const onKeyDownHandler = (event: KeyboardEvent, view: EditorView) => {
242
260
  if (onKeyDown && !onKeyDown(event)) {
243
- if (event.key === "Enter") {
261
+ if (event.key === "Enter" && !currentReadOnly.current) {
244
262
  const cursor = view.state.selection.main.head;
245
- const cursorLine = view.state.doc.lineAt(cursor).number;
246
- const offsetFromFirstLine = view.state.doc.line(cursorLine).to;
247
263
  view.dispatch({
248
264
  changes: {
249
- from: offsetFromFirstLine,
265
+ from: cursor,
250
266
  insert: "\n",
251
267
  },
252
268
  selection: {
253
- anchor: offsetFromFirstLine + 1,
269
+ anchor: cursor + 1,
254
270
  },
255
271
  });
256
272
  }
@@ -265,14 +281,17 @@ export const CodeEditor = ({
265
281
  return false;
266
282
  };
267
283
 
268
- React.useEffect(() => {
284
+ const createKeyMapConfigs = () => {
269
285
  const tabIndent =
270
286
  !!(tabIntentStyle === "tab" && mode && !(tabForceSpaceForModes ?? []).includes(mode)) || enableTab;
271
- const keyMapConfigs = [
287
+ return [
272
288
  defaultKeymap as KeyBinding,
273
- ...addToKeyMapConfigFor(supportCodeFolding, foldKeymap),
289
+ ...addToKeyMapConfigFor(supportCodeFolding, ...foldKeymap),
274
290
  ...addToKeyMapConfigFor(tabIndent, indentWithTab),
275
291
  ];
292
+ }
293
+
294
+ React.useEffect(() => {
276
295
  const domEventHandlers = {
277
296
  ...addHandlersFor(!!onScroll, "scroll", onScroll),
278
297
  ...addHandlersFor(
@@ -286,13 +305,13 @@ export const CodeEditor = ({
286
305
  } as DOMEventHandlers<any>;
287
306
  const extensions = [
288
307
  markField,
289
- adaptedPlaceholder(placeholder),
308
+ placeholderCompartment.current.of(adaptedPlaceholder(placeholder)),
290
309
  adaptedHighlightSpecialChars(),
291
- useCodeMirrorModeExtension(mode),
292
- keymap?.of(keyMapConfigs),
293
- EditorState?.tabSize.of(tabIntentSize),
294
- EditorState?.readOnly.of(readOnly),
295
- EditorView?.editable.of(!disabled),
310
+ modeCompartment.current.of(useCodeMirrorModeExtension(mode)),
311
+ keyMapConfigsCompartment.current.of(keymap?.of(createKeyMapConfigs())),
312
+ tabIntentSizeCompartment.current.of(EditorState?.tabSize.of(tabIntentSize)),
313
+ readOnlyCompartment.current.of(EditorState?.readOnly.of(readOnly)),
314
+ disabledCompartment.current.of(EditorView?.editable.of(!disabled)),
296
315
  AdaptedEditorViewDomEventHandlers(domEventHandlers) as Extension,
297
316
  EditorView?.updateListener.of((v: ViewUpdate) => {
298
317
  if (disabled) return;
@@ -328,12 +347,12 @@ export const CodeEditor = ({
328
347
  }
329
348
  }
330
349
  }),
331
- addExtensionsFor(shouldHaveMinimalSetup, minimalSetup),
332
- addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()),
333
- addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine()),
334
- addExtensionsFor(wrapLines, EditorView?.lineWrapping),
335
- addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding()),
336
- addExtensionsFor(useLinting, ...linters),
350
+ shouldHaveMinimalSetupCompartment.current.of(addExtensionsFor(shouldHaveMinimalSetup, minimalSetup)),
351
+ preventLineNumbersCompartment.current.of(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers())),
352
+ shouldHighlightActiveLineCompartment.current.of(addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine())),
353
+ wrapLinesCompartment.current.of(addExtensionsFor(wrapLines, EditorView?.lineWrapping)),
354
+ supportCodeFoldingCompartment.current.of(addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding())),
355
+ useLintingCompartment.current.of(addExtensionsFor(useLinting, ...linters)),
337
356
  additionalExtensions,
338
357
  ];
339
358
 
@@ -375,7 +394,64 @@ export const CodeEditor = ({
375
394
  setView(undefined);
376
395
  }
377
396
  };
378
- }, [parent.current, mode, preventLineNumbers, wrapLines]);
397
+ }, [parent.current]);
398
+
399
+ // Updates an extension for a specific parameter that has changed after the initialization
400
+ const updateExtension = (extension: Extension | undefined, parameterCompartment: Compartment): void => {
401
+ if(extension) {
402
+ currentView.current?.dispatch({
403
+ effects: parameterCompartment.reconfigure(extension)
404
+ })
405
+ }
406
+ }
407
+
408
+ React.useEffect(() => {
409
+ updateExtension(EditorState?.readOnly.of(readOnly!), readOnlyCompartment.current)
410
+ }, [readOnly])
411
+
412
+ React.useEffect(() => {
413
+ updateExtension(adaptedPlaceholder(placeholder), placeholderCompartment.current)
414
+ }, [placeholder])
415
+
416
+ React.useEffect(() => {
417
+ updateExtension(useCodeMirrorModeExtension(mode), modeCompartment.current)
418
+ }, [mode])
419
+
420
+ React.useEffect(() => {
421
+ updateExtension(keymap?.of(createKeyMapConfigs()), keyMapConfigsCompartment.current)
422
+ }, [supportCodeFolding, mode, tabIntentStyle, (tabForceSpaceForModes ?? []).join(", "), enableTab])
423
+
424
+ React.useEffect(() => {
425
+ updateExtension(EditorState?.tabSize.of(tabIntentSize ?? 2), tabIntentSizeCompartment.current)
426
+ }, [tabIntentSize])
427
+
428
+ React.useEffect(() => {
429
+ updateExtension(EditorView?.editable.of(!disabled), disabledCompartment.current)
430
+ }, [disabled])
431
+
432
+ React.useEffect(() => {
433
+ updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
434
+ }, [shouldHaveMinimalSetup])
435
+
436
+ React.useEffect(() => {
437
+ updateExtension(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
438
+ }, [preventLineNumbers])
439
+
440
+ React.useEffect(() => {
441
+ updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
442
+ }, [shouldHighlightActiveLine])
443
+
444
+ React.useEffect(() => {
445
+ updateExtension(addExtensionsFor(wrapLines ?? false, EditorView?.lineWrapping), wrapLinesCompartment.current)
446
+ }, [wrapLines])
447
+
448
+ React.useEffect(() => {
449
+ updateExtension(addExtensionsFor(supportCodeFolding ?? false, adaptedFoldGutter(), adaptedCodeFolding()), supportCodeFoldingCompartment.current)
450
+ }, [supportCodeFolding])
451
+
452
+ React.useEffect(() => {
453
+ updateExtension(addExtensionsFor(useLinting ?? false, ...linters), useLintingCompartment.current)
454
+ }, [mode, useLinting])
379
455
 
380
456
  const hasToolbarSupport = mode && ModeToolbarSupport.indexOf(mode) > -1 && useToolbar;
381
457
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { EditorView, placeholder, highlightSpecialChars, lineNumbers, highlightActiveLine } from "@codemirror/view";
11
11
  import { syntaxHighlighting, foldGutter, codeFolding } from "@codemirror/language";
12
- import { Extension } from "@codemirror/state";
12
+ import {Extension, Compartment, StateEffect, EditorState} from "@codemirror/state";
13
13
  import { lintGutter } from "@codemirror/lint";
14
14
 
15
15
  /** placeholder extension, current error '_view.placeholder is not a function' */
@@ -34,6 +34,25 @@ export const AdaptedEditorView = isConstructor(EditorView)
34
34
  destroy() {}
35
35
  } as any);
36
36
 
37
+ /** Creates a new compartment or a mock of a compartment. */
38
+ export const compartment = () => {
39
+ if(isConstructor(Compartment)) {
40
+ return new Compartment()
41
+ } else {
42
+ let extension: Extension | undefined = undefined
43
+ return {
44
+ of: (ext: Extension): Extension => {
45
+ extension = ext
46
+ return ext
47
+ },
48
+ reconfigure: (_content: Extension): StateEffect<unknown> => {
49
+ return {} as StateEffect<any>
50
+ },
51
+ get: (_state: EditorState): Extension | undefined => extension
52
+ }
53
+ }
54
+ }
55
+
37
56
  const emptyExtension = (() => {}) as any;
38
57
  /** extension adding event handlers, current error '(view, domEventHandlers) is not a function' */
39
58
  export const AdaptedEditorViewDomEventHandlers =