Skip to content

Commit 3741cf8

Browse files
committed
Merge branch 'next' into feature/keyValueParameterType-CMEM-6277
# Conflicts: # src/extensions/codemirror/CodeMirror.tsx
2 parents 0f727a9 + cf8f29a commit 3741cf8

5 files changed

Lines changed: 148 additions & 34 deletions

File tree

CHANGELOG.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,20 @@ This is a major release, and it might be not compatible with your current usage
1111
### Added
1212

1313
- Extended existing height and readOnly props from `CodeEditorProps` to `AutoSuggestionProps` & `ExtendedCodeEditorProps` to be configurable from `<CodeAutocompleteField />`
14-
- `<CodeAutocompleteField />:
14+
- `<CodeAutocompleteField />`:
1515
- outerDivAttributes parameter: Allows to set parameter of the container div element of the code complete field.
1616

17+
### Fixed
18+
19+
- <CodeMirror />:
20+
- Editor is re-created after certain property changes and is reset, i.e. loses it current state.
21+
- <CodeAutocompleteField />:
22+
- Read-only mode does not work correctly. It is still possible to change the value via pressing Enter (in multiline mode) or clicking the clear button.
23+
- First auto-completion item not marked as active when drop down first shown.
24+
- `<CodeEditor />`:
25+
- Enter key handling (adding new line) broken when `onKeyDown` is defined.
26+
27+
1728
## [24.3.0] - 2025-06-05
1829

1930
### Added

src/cmem/react-flow/ReactFlow/ReactFlow.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import React, {ReactElement, Ref} from "react";
22
import { KeyCode as KeyCodeV9 } from "react-flow-renderer";
33
import { KeyCode as KeyCodeV10 } from "react-flow-renderer-lts";
4+
import { KeyCode as KeyCodeV12} from "@xyflow/react";
45

56
import { CLASSPREFIX as eccgui } from "../../../configuration/constants";
67
import { ReactFlowMarkers } from "../../../extensions/react-flow/markers/ReactFlowMarkers";
@@ -165,7 +166,12 @@ const ReactFlowExtendedPlain = <T extends ReactFlowExtendedProps>({
165166
};
166167
break;
167168
case "v12":
168-
// FIXME: necessary for v12?
169+
keyCodeConfig = {
170+
selectionKeyCode: hotKeysDisabled ? null : (selectionKeyCode as KeyCodeV12),
171+
deleteKeyCode: hotKeysDisabled ? null : (deleteKeyCode as KeyCodeV12),
172+
multiSelectionKeyCode: hotKeysDisabled ? null : (multiSelectionKeyCode as KeyCodeV12),
173+
zoomActivationKeyCode: hotKeysDisabled ? null : (zoomActivationKeyCode as KeyCodeV12),
174+
};
169175
break;
170176
}
171177

src/components/AutoSuggestion/AutoSuggestion.tsx

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ const AutoSuggestion = ({
363363
editorState.suggestions = [];
364364
setSuggestions([]);
365365
}
366-
editorState.index = 0;
366+
setCurrentIndex(0);
367367
}, [suggestionResponse, editorState]);
368368

369369
const getOffsetRange = (cm: EditorView, from: number, to: number) => {
@@ -377,7 +377,7 @@ const AutoSuggestion = ({
377377
return { fromOffset, toOffset };
378378
};
379379

380-
const inputactionsDisplayed = React.useCallback((node) => {
380+
const inputActionsDisplayed = React.useCallback((node) => {
381381
if (!node) return;
382382
const width = node.offsetWidth;
383383
const slCodeEditor = node.parentElement.getElementsByClassName(`${eccgui}-singlelinecodeeditor`);
@@ -491,8 +491,7 @@ const AutoSuggestion = ({
491491
}, 1);
492492
};
493493

494-
//todo check out typings for event type
495-
const handleInputEditorKeyPress = (event: any) => {
494+
const handleInputEditorKeyPress = (event: KeyboardEvent) => {
496495
const overWrittenKeys: Array<string> = Object.values(OVERWRITTEN_KEYS);
497496
if (overWrittenKeys.includes(event.key) && (useTabForCompletions || event.key !== OVERWRITTEN_KEYS.Tab)) {
498497
//don't prevent when enter should create new line (multiline config) and dropdown isn't shown
@@ -628,6 +627,7 @@ const AutoSuggestion = ({
628627
break;
629628
default:
630629
//do nothing
630+
closeDropDown();
631631
}
632632
}
633633
};
@@ -676,6 +676,7 @@ const AutoSuggestion = ({
676676
showScrollBar,
677677
multiline,
678678
handleInputMouseDown,
679+
readOnly
679680
]);
680681

681682
const hasError = !!value.current && !pathIsValid && !pathValidationPending;
@@ -715,11 +716,12 @@ const AutoSuggestion = ({
715716
{codeEditor}
716717
</ContextOverlay>
717718
{!!value.current && (
718-
<span className={BlueprintClassNames.INPUT_ACTION} ref={inputactionsDisplayed}>
719+
<span className={BlueprintClassNames.INPUT_ACTION} ref={inputActionsDisplayed}>
719720
<IconButton
720721
data-test-id={"value-path-clear-btn"}
721722
name="operation-clear"
722723
text={clearIconText}
724+
disabled={readOnly}
723725
onClick={handleInputEditorClear}
724726
/>
725727
</span>

src/extensions/codemirror/CodeMirror.tsx

Lines changed: 102 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React, { useMemo, useRef } from "react";
22
import { defaultKeymap, indentWithTab } from "@codemirror/commands";
33
import { defaultHighlightStyle, foldKeymap } from "@codemirror/language";
4-
import { EditorState, Extension } from "@codemirror/state";
4+
import { EditorState, Extension, Compartment } from "@codemirror/state";
55
import { DOMEventHandlers, EditorView, KeyBinding, keymap, Rect, ViewUpdate } from "@codemirror/view";
66
import { minimalSetup } from "codemirror";
77

@@ -31,6 +31,7 @@ import {
3131
adaptedLineNumbers,
3232
adaptedLintGutter,
3333
adaptedPlaceholder,
34+
compartment,
3435
adaptedSyntaxHighlighting,
3536
} from "./tests/codemirrorTestHelper";
3637
import { ExtensionCreator } from "./types";
@@ -55,7 +56,7 @@ export interface CodeEditorProps extends TestableComponent {
5556
* Handler method to receive onChange events.
5657
* As input the new value is given.
5758
*/
58-
onChange?: (v: any) => void;
59+
onChange?: (v: string) => void;
5960
/**
6061
* Called when the focus status changes
6162
*/
@@ -75,7 +76,7 @@ export interface CodeEditorProps extends TestableComponent {
7576
/**
7677
* Called when the cursor position changes
7778
*/
78-
onCursorChange?: (pos: number, coords: Rect, scrollinfo: HTMLElement, cm: EditorView) => any;
79+
onCursorChange?: (pos: number, coords: Rect, scrollinfo: HTMLElement, cm: EditorView) => void;
7980

8081
/**
8182
* Syntax mode of the code editor.
@@ -84,7 +85,7 @@ export interface CodeEditorProps extends TestableComponent {
8485
/**
8586
* Default value used first when the editor is instanciated.
8687
*/
87-
defaultValue?: any;
88+
defaultValue?: string;
8889
/**
8990
* If enabled the code editor won't show numbers before each line.
9091
*/
@@ -170,7 +171,7 @@ export interface CodeEditorProps extends TestableComponent {
170171
}
171172

172173
const addExtensionsFor = (flag: boolean, ...extensions: Extension[]) => (flag ? [...extensions] : []);
173-
const addToKeyMapConfigFor = (flag: boolean, ...keys: any) => (flag ? [...keys] : []);
174+
const addToKeyMapConfigFor = (flag: boolean, ...keys: KeyBinding[]) => (flag ? [...keys] : []);
174175
const addHandlersFor = (flag: boolean, handlerName: string, handler: any) =>
175176
flag ? ({ [handlerName]: handler } as DOMEventHandlers<any>) : {};
176177

@@ -222,7 +223,24 @@ export const CodeEditor = ({
222223
}: CodeEditorProps) => {
223224
const parent = useRef<any>(undefined);
224225
const [view, setView] = React.useState<EditorView | undefined>();
226+
const currentView = React.useRef<EditorView>()
227+
currentView.current = view
228+
const currentReadOnly = React.useRef(readOnly)
229+
currentReadOnly.current = readOnly
225230
const [showPreview, setShowPreview] = React.useState<boolean>(false);
231+
// CodeMirror Compartments in order to allow for re-configuration after initialization
232+
const readOnlyCompartment = React.useRef<Compartment>(compartment())
233+
const wrapLinesCompartment = React.useRef<Compartment>(compartment())
234+
const preventLineNumbersCompartment = React.useRef<Compartment>(compartment())
235+
const shouldHaveMinimalSetupCompartment = React.useRef<Compartment>(compartment())
236+
const placeholderCompartment = React.useRef<Compartment>(compartment())
237+
const modeCompartment = React.useRef<Compartment>(compartment())
238+
const keyMapConfigsCompartment = React.useRef<Compartment>(compartment())
239+
const tabIntentSizeCompartment = React.useRef<Compartment>(compartment())
240+
const disabledCompartment = React.useRef<Compartment>(compartment())
241+
const supportCodeFoldingCompartment = React.useRef<Compartment>(compartment())
242+
const useLintingCompartment = React.useRef<Compartment>(compartment())
243+
const shouldHighlightActiveLineCompartment = React.useRef<Compartment>(compartment())
226244

227245
const linters = useMemo(() => {
228246
if (!mode) {
@@ -241,17 +259,15 @@ export const CodeEditor = ({
241259

242260
const onKeyDownHandler = (event: KeyboardEvent, view: EditorView) => {
243261
if (onKeyDown && !onKeyDown(event)) {
244-
if (event.key === "Enter") {
262+
if (event.key === "Enter" && !currentReadOnly.current) {
245263
const cursor = view.state.selection.main.head;
246-
const cursorLine = view.state.doc.lineAt(cursor).number;
247-
const offsetFromFirstLine = view.state.doc.line(cursorLine).to;
248264
view.dispatch({
249265
changes: {
250-
from: offsetFromFirstLine,
266+
from: cursor,
251267
insert: "\n",
252268
},
253269
selection: {
254-
anchor: offsetFromFirstLine + 1,
270+
anchor: cursor + 1,
255271
},
256272
});
257273
}
@@ -266,14 +282,17 @@ export const CodeEditor = ({
266282
return false;
267283
};
268284

269-
React.useEffect(() => {
285+
const createKeyMapConfigs = () => {
270286
const tabIndent =
271287
!!(tabIntentStyle === "tab" && mode && !(tabForceSpaceForModes ?? []).includes(mode)) || enableTab;
272-
const keyMapConfigs = [
288+
return [
273289
defaultKeymap as KeyBinding,
274-
...addToKeyMapConfigFor(supportCodeFolding, foldKeymap),
290+
...addToKeyMapConfigFor(supportCodeFolding, ...foldKeymap),
275291
...addToKeyMapConfigFor(tabIndent, indentWithTab),
276292
];
293+
}
294+
295+
React.useEffect(() => {
277296
const domEventHandlers = {
278297
...addHandlersFor(!!onScroll, "scroll", onScroll),
279298
...addHandlersFor(
@@ -287,13 +306,13 @@ export const CodeEditor = ({
287306
} as DOMEventHandlers<any>;
288307
const extensions = [
289308
markField,
290-
adaptedPlaceholder(placeholder),
309+
placeholderCompartment.current.of(adaptedPlaceholder(placeholder)),
291310
adaptedHighlightSpecialChars(),
292-
useCodeMirrorModeExtension(mode),
293-
keymap?.of(keyMapConfigs),
294-
EditorState?.tabSize.of(tabIntentSize),
295-
EditorState?.readOnly.of(readOnly),
296-
EditorView?.editable.of(!disabled),
311+
modeCompartment.current.of(useCodeMirrorModeExtension(mode)),
312+
keyMapConfigsCompartment.current.of(keymap?.of(createKeyMapConfigs())),
313+
tabIntentSizeCompartment.current.of(EditorState?.tabSize.of(tabIntentSize)),
314+
readOnlyCompartment.current.of(EditorState?.readOnly.of(readOnly)),
315+
disabledCompartment.current.of(EditorView?.editable.of(!disabled)),
297316
AdaptedEditorViewDomEventHandlers(domEventHandlers) as Extension,
298317
EditorView?.updateListener.of((v: ViewUpdate) => {
299318
if (disabled) return;
@@ -329,12 +348,12 @@ export const CodeEditor = ({
329348
}
330349
}
331350
}),
332-
addExtensionsFor(shouldHaveMinimalSetup, minimalSetup),
333-
addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()),
334-
addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine()),
335-
addExtensionsFor(wrapLines, EditorView?.lineWrapping),
336-
addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding()),
337-
addExtensionsFor(useLinting, ...linters),
351+
shouldHaveMinimalSetupCompartment.current.of(addExtensionsFor(shouldHaveMinimalSetup, minimalSetup)),
352+
preventLineNumbersCompartment.current.of(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers())),
353+
shouldHighlightActiveLineCompartment.current.of(addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine())),
354+
wrapLinesCompartment.current.of(addExtensionsFor(wrapLines, EditorView?.lineWrapping)),
355+
supportCodeFoldingCompartment.current.of(addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding())),
356+
useLintingCompartment.current.of(addExtensionsFor(useLinting, ...linters)),
338357
adaptedSyntaxHighlighting(defaultHighlightStyle),
339358
additionalExtensions,
340359
];
@@ -377,7 +396,64 @@ export const CodeEditor = ({
377396
setView(undefined);
378397
}
379398
};
380-
}, [parent.current, mode, preventLineNumbers, wrapLines]);
399+
}, [parent.current]);
400+
401+
// Updates an extension for a specific parameter that has changed after the initialization
402+
const updateExtension = (extension: Extension | undefined, parameterCompartment: Compartment): void => {
403+
if(extension) {
404+
currentView.current?.dispatch({
405+
effects: parameterCompartment.reconfigure(extension)
406+
})
407+
}
408+
}
409+
410+
React.useEffect(() => {
411+
updateExtension(EditorState?.readOnly.of(readOnly!), readOnlyCompartment.current)
412+
}, [readOnly])
413+
414+
React.useEffect(() => {
415+
updateExtension(adaptedPlaceholder(placeholder), placeholderCompartment.current)
416+
}, [placeholder])
417+
418+
React.useEffect(() => {
419+
updateExtension(useCodeMirrorModeExtension(mode), modeCompartment.current)
420+
}, [mode])
421+
422+
React.useEffect(() => {
423+
updateExtension(keymap?.of(createKeyMapConfigs()), keyMapConfigsCompartment.current)
424+
}, [supportCodeFolding, mode, tabIntentStyle, (tabForceSpaceForModes ?? []).join(", "), enableTab])
425+
426+
React.useEffect(() => {
427+
updateExtension(EditorState?.tabSize.of(tabIntentSize ?? 2), tabIntentSizeCompartment.current)
428+
}, [tabIntentSize])
429+
430+
React.useEffect(() => {
431+
updateExtension(EditorView?.editable.of(!disabled), disabledCompartment.current)
432+
}, [disabled])
433+
434+
React.useEffect(() => {
435+
updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
436+
}, [shouldHaveMinimalSetup])
437+
438+
React.useEffect(() => {
439+
updateExtension(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
440+
}, [preventLineNumbers])
441+
442+
React.useEffect(() => {
443+
updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
444+
}, [shouldHighlightActiveLine])
445+
446+
React.useEffect(() => {
447+
updateExtension(addExtensionsFor(wrapLines ?? false, EditorView?.lineWrapping), wrapLinesCompartment.current)
448+
}, [wrapLines])
449+
450+
React.useEffect(() => {
451+
updateExtension(addExtensionsFor(supportCodeFolding ?? false, adaptedFoldGutter(), adaptedCodeFolding()), supportCodeFoldingCompartment.current)
452+
}, [supportCodeFolding])
453+
454+
React.useEffect(() => {
455+
updateExtension(addExtensionsFor(useLinting ?? false, ...linters), useLintingCompartment.current)
456+
}, [mode, useLinting])
381457

382458
const hasToolbarSupport = mode && ModeToolbarSupport.indexOf(mode) > -1 && useToolbar;
383459

src/extensions/codemirror/tests/codemirrorTestHelper.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99

1010
import { EditorView, placeholder, highlightSpecialChars, lineNumbers, highlightActiveLine } from "@codemirror/view";
1111
import { syntaxHighlighting, foldGutter, codeFolding } from "@codemirror/language";
12-
import { Extension } from "@codemirror/state";
12+
import {Extension, Compartment, StateEffect, EditorState} from "@codemirror/state";
1313
import { lintGutter } from "@codemirror/lint";
1414

1515
/** placeholder extension, current error '_view.placeholder is not a function' */
@@ -34,6 +34,25 @@ export const AdaptedEditorView = isConstructor(EditorView)
3434
destroy() {}
3535
} as any);
3636

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+
3756
const emptyExtension = (() => {}) as any;
3857
/** extension adding event handlers, current error '(view, domEventHandlers) is not a function' */
3958
export const AdaptedEditorViewDomEventHandlers =

0 commit comments

Comments
 (0)