Skip to content

Commit d2b885b

Browse files
authored
Merge pull request #315 from eccenca/maintain/mergeReleaseTag-v24.4.0
Release process changes from v24.4.0
2 parents a5b45a7 + a3c5346 commit d2b885b

File tree

6 files changed

+1348
-674
lines changed

6 files changed

+1348
-674
lines changed

CHANGELOG.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,39 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
66

77
## [Unreleased]
88

9+
## [24.4.0] - 2025-08-07
10+
911
### Added
1012

11-
- Extended existing height and readOnly props from `CodeEditorProps` to `AutoSuggestionProps` & `ExtendedCodeEditorProps` to be configurable from `<CodeAutocompleteField />`
13+
- `<ExtendedCodeEditor />`
14+
- `height` and `readOnly` properties to forward them to `<CodeEditor/>`
15+
- `<CodeAutocompleteField />`:
16+
- `outerDivAttributes` property: allows to set parameter of the container element
17+
- `height` and `readOnly` properties to forward them to `<ExtendedCodeEditor/>`
1218
- `<ActivityControlWidget />`
1319
- `additionalActions` property to include other more complex components between the action buttons and the context menu of the widget
1420
- `<Tooltip />`
1521
- `swapPlaceholderDelay` property to allow configuration of the delay time before the placeholder element is replaced by the actual tooltip component
1622

23+
### Fixed
24+
25+
- `<CodeEditor />`
26+
- Editor is re-created after certain property changes and is reset, i.e. loses it current state.
27+
- Enter key handling (adding new line) was broken when `onKeyDown` is defined.
28+
- `<CodeAutocompleteField />`
29+
- First auto-completion item not marked as active when drop down first shown.
30+
- 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.
31+
1732
### Changed
1833

1934
- `<NodeContent />`
2035
- prevent start of a react flow drag action of a node when user clicks in the node menu section
2136

37+
### Deprecated
38+
39+
- `<CodeEditor />`
40+
- `onChange` property: support for `(v: any) => void` type will be exchanged to more specific `(v: string) => void`
41+
2242
## [24.3.0] - 2025-06-05
2343

2444
### Added

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "@eccenca/gui-elements",
33
"description": "GUI elements based on other libraries, usable in React application, written in Typescript.",
4-
"version": "24.3.0",
4+
"version": "24.4.0",
55
"license": "Apache-2.0",
66
"homepage": "https://github.com/eccenca/gui-elements",
77
"bugs": "https://github.com/eccenca/gui-elements/issues",
@@ -149,8 +149,8 @@
149149
"eslint-plugin-simple-import-sort": "^12.1.1",
150150
"husky": "4",
151151
"identity-obj-proxy": "^3.0.0",
152-
"jest": "^29.7.0",
153-
"jest-environment-jsdom": "^29.7.0",
152+
"jest": "^30.0.5",
153+
"jest-environment-jsdom": "^30.0.5",
154154
"jest-pnp-resolver": "^1.2.3",
155155
"lint-staged": "^15.5.1",
156156
"node-sass-package-importer": "^5.3.3",

src/components/AutoSuggestion/AutoSuggestion.tsx

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ export interface AutoSuggestionProps {
170170
height?: number | string;
171171
/** Set read-only mode. Default: false */
172172
readOnly?: boolean;
173+
/** Properties that should be added to the outer div container. */
174+
outerDivAttributes?: Omit<React.HTMLAttributes<HTMLDivElement>, "id" | "data-test-id">;
173175
}
174176

175177
// Meta data regarding a request
@@ -202,8 +204,9 @@ const AutoSuggestion = ({
202204
mode,
203205
multiline = false,
204206
reInitOnInitialValueChange = false,
205-
height,
206-
readOnly
207+
height,
208+
readOnly,
209+
outerDivAttributes
207210
}: AutoSuggestionProps) => {
208211
const value = React.useRef<string>(initialValue);
209212
const cursorPosition = React.useRef(0);
@@ -360,7 +363,7 @@ const AutoSuggestion = ({
360363
editorState.suggestions = [];
361364
setSuggestions([]);
362365
}
363-
editorState.index = 0;
366+
setCurrentIndex(0);
364367
}, [suggestionResponse, editorState]);
365368

366369
const getOffsetRange = (cm: EditorView, from: number, to: number) => {
@@ -374,7 +377,7 @@ const AutoSuggestion = ({
374377
return { fromOffset, toOffset };
375378
};
376379

377-
const inputactionsDisplayed = React.useCallback((node) => {
380+
const inputActionsDisplayed = React.useCallback((node) => {
378381
if (!node) return;
379382
const width = node.offsetWidth;
380383
const slCodeEditor = node.parentElement.getElementsByClassName(`${eccgui}-singlelinecodeeditor`);
@@ -488,8 +491,7 @@ const AutoSuggestion = ({
488491
}, 1);
489492
};
490493

491-
//todo check out typings for event type
492-
const handleInputEditorKeyPress = (event: any) => {
494+
const handleInputEditorKeyPress = (event: KeyboardEvent) => {
493495
const overWrittenKeys: Array<string> = Object.values(OVERWRITTEN_KEYS);
494496
if (overWrittenKeys.includes(event.key) && (useTabForCompletions || event.key !== OVERWRITTEN_KEYS.Tab)) {
495497
//don't prevent when enter should create new line (multiline config) and dropdown isn't shown
@@ -625,6 +627,7 @@ const AutoSuggestion = ({
625627
break;
626628
default:
627629
//do nothing
630+
closeDropDown();
628631
}
629632
}
630633
};
@@ -673,6 +676,7 @@ const AutoSuggestion = ({
673676
showScrollBar,
674677
multiline,
675678
handleInputMouseDown,
679+
readOnly
676680
]);
677681

678682
const hasError = !!value.current && !pathIsValid && !pathValidationPending;
@@ -681,6 +685,7 @@ const AutoSuggestion = ({
681685
id={id}
682686
ref={autoSuggestionDivRef}
683687
className={`${eccgui}-autosuggestion` + (className ? ` ${className}` : "")}
688+
{...outerDivAttributes}
684689
>
685690
<div
686691
className={` ${eccgui}-autosuggestion__inputfield ${BlueprintClassNames.INPUT_GROUP} ${
@@ -711,11 +716,12 @@ const AutoSuggestion = ({
711716
{codeEditor}
712717
</ContextOverlay>
713718
{!!value.current && (
714-
<span className={BlueprintClassNames.INPUT_ACTION} ref={inputactionsDisplayed}>
719+
<span className={BlueprintClassNames.INPUT_ACTION} ref={inputActionsDisplayed}>
715720
<IconButton
716721
data-test-id={"value-path-clear-btn"}
717722
name="operation-clear"
718723
text={clearIconText}
724+
disabled={readOnly}
719725
onClick={handleInputEditorClear}
720726
/>
721727
</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 { 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

@@ -30,7 +30,7 @@ import {
3030
adaptedHighlightSpecialChars,
3131
adaptedLineNumbers,
3232
adaptedLintGutter,
33-
adaptedPlaceholder,
33+
adaptedPlaceholder, compartment,
3434
} from "./tests/codemirrorTestHelper";
3535
import { ExtensionCreator } from "./types";
3636

@@ -53,6 +53,7 @@ export interface CodeEditorProps extends TestableComponent {
5353
/**
5454
* Handler method to receive onChange events.
5555
* As input the new value is given.
56+
* @deprecated (v25) use `(v: string) => void` in future
5657
*/
5758
onChange?: (v: any) => void;
5859
/**
@@ -74,7 +75,7 @@ export interface CodeEditorProps extends TestableComponent {
7475
/**
7576
* Called when the cursor position changes
7677
*/
77-
onCursorChange?: (pos: number, coords: Rect, scrollinfo: HTMLElement, cm: EditorView) => any;
78+
onCursorChange?: (pos: number, coords: Rect, scrollinfo: HTMLElement, cm: EditorView) => void;
7879

7980
/**
8081
* Syntax mode of the code editor.
@@ -83,7 +84,7 @@ export interface CodeEditorProps extends TestableComponent {
8384
/**
8485
* Default value used first when the editor is instanciated.
8586
*/
86-
defaultValue?: any;
87+
defaultValue?: string;
8788
/**
8889
* If enabled the code editor won't show numbers before each line.
8990
*/
@@ -169,7 +170,7 @@ export interface CodeEditorProps extends TestableComponent {
169170
}
170171

171172
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] : []);
173174
const addHandlersFor = (flag: boolean, handlerName: string, handler: any) =>
174175
flag ? ({ [handlerName]: handler } as DOMEventHandlers<any>) : {};
175176

@@ -221,7 +222,24 @@ export const CodeEditor = ({
221222
}: CodeEditorProps) => {
222223
const parent = useRef<any>(undefined);
223224
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
224229
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())
225243

226244
const linters = useMemo(() => {
227245
if (!mode) {
@@ -240,17 +258,15 @@ export const CodeEditor = ({
240258

241259
const onKeyDownHandler = (event: KeyboardEvent, view: EditorView) => {
242260
if (onKeyDown && !onKeyDown(event)) {
243-
if (event.key === "Enter") {
261+
if (event.key === "Enter" && !currentReadOnly.current) {
244262
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;
247263
view.dispatch({
248264
changes: {
249-
from: offsetFromFirstLine,
265+
from: cursor,
250266
insert: "\n",
251267
},
252268
selection: {
253-
anchor: offsetFromFirstLine + 1,
269+
anchor: cursor + 1,
254270
},
255271
});
256272
}
@@ -265,14 +281,17 @@ export const CodeEditor = ({
265281
return false;
266282
};
267283

268-
React.useEffect(() => {
284+
const createKeyMapConfigs = () => {
269285
const tabIndent =
270286
!!(tabIntentStyle === "tab" && mode && !(tabForceSpaceForModes ?? []).includes(mode)) || enableTab;
271-
const keyMapConfigs = [
287+
return [
272288
defaultKeymap as KeyBinding,
273-
...addToKeyMapConfigFor(supportCodeFolding, foldKeymap),
289+
...addToKeyMapConfigFor(supportCodeFolding, ...foldKeymap),
274290
...addToKeyMapConfigFor(tabIndent, indentWithTab),
275291
];
292+
}
293+
294+
React.useEffect(() => {
276295
const domEventHandlers = {
277296
...addHandlersFor(!!onScroll, "scroll", onScroll),
278297
...addHandlersFor(
@@ -286,13 +305,13 @@ export const CodeEditor = ({
286305
} as DOMEventHandlers<any>;
287306
const extensions = [
288307
markField,
289-
adaptedPlaceholder(placeholder),
308+
placeholderCompartment.current.of(adaptedPlaceholder(placeholder)),
290309
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)),
296315
AdaptedEditorViewDomEventHandlers(domEventHandlers) as Extension,
297316
EditorView?.updateListener.of((v: ViewUpdate) => {
298317
if (disabled) return;
@@ -328,12 +347,12 @@ export const CodeEditor = ({
328347
}
329348
}
330349
}),
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)),
337356
additionalExtensions,
338357
];
339358

@@ -375,7 +394,64 @@ export const CodeEditor = ({
375394
setView(undefined);
376395
}
377396
};
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])
379455

380456
const hasToolbarSupport = mode && ModeToolbarSupport.indexOf(mode) > -1 && useToolbar;
381457

0 commit comments

Comments
 (0)