Skip to content

Commit d5f36c7

Browse files
authored
Merge pull request #378 from eccenca/feature/wrapLinesCodeEditor-CMEM-6891
Allow user config for CodeEditor appearancer (CMEM-6891)
2 parents d49dfbc + d4162aa commit d5f36c7

8 files changed

Lines changed: 433 additions & 36 deletions

File tree

CHANGELOG.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,14 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
1515
- component for hiding elements in specific media
1616
- `<InlineText />`
1717
- force children to get displayed as inline content
18-
- `<DecoupledOverlay />`
18+
- `<DecoupledOverlay />`
1919
- similar to `ContextOverlay` component but not directly linked to a React element, it specifies the target in the DOM to get connected lazy
2020
- `<StringPreviewContentBlobToggler />`
2121
- `useOnly` property: specify if only parts of the content should be used for the shortened preview, this property replaces `firstNonEmptyLineOnly`
2222
- `<ContextOverlay />`
2323
- `paddingSize` property to add easily some white space
24+
- `<CodeEditor />`
25+
- toolbar in `markdown` mode provides a user config menu for the editor appearance
2426
- CSS custom properties
2527
- beside the color palette we now mirror the most important layout configuration variables as CSS custom properties
2628
- new icons:
@@ -49,14 +51,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p
4951
- `<CodeMirror />`
5052
- use the latest provided `onChange` function
5153
- `<TextField />`, `<TextArea />`
52-
- fix emoji false-positives in invisible character detection
54+
- fix emoji false-positives in invisible character detection
5355

5456
### Changed
5557

5658
- `<MultiSelect />`:
5759
- Change default filter predicate to match multi-word queries.
5860
- `<EdgeDefault />`
5961
- reduce stroke width to only 1px
62+
- `<CodeMirror />`
63+
- `wrapLines` and `preventLineNumber` do use `false` default value but if not set then it will be interpreted as `false`
64+
- in this way it can be overwritten by new user config for the markdown mode
6065
- automatically hide user interaction elements in print view
6166
- all application header components except `<WorkspaceHeader />`
6267
- `<CardActions />` and `<CardOptions />`

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ export const StickyNoteModal: React.FC<StickyNoteModalProps> = React.memo(
130130
name={translate("noteLabel")}
131131
id={"sticky-note-input"}
132132
mode="markdown"
133-
preventLineNumbers
133+
useToolbar
134134
onChange={(value) => {
135135
refNote.current = value;
136136
}}

src/extensions/codemirror/CodeMirror.stories.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,17 +24,22 @@ const TemplateFull: StoryFn<typeof CodeEditor> = (args) => <CodeEditor {...args}
2424

2525
export const BasicExample = TemplateFull.bind({});
2626
BasicExample.args = {
27-
name: "codeinput",
27+
name: "jsinput",
28+
mode: "json",
29+
defaultValue: '{ json: "true" }',
30+
};
31+
32+
export const MarkdownWithToolbar = TemplateFull.bind({});
33+
MarkdownWithToolbar.args = {
34+
name: "mdinput",
2835
mode: "markdown",
2936
defaultValue: "**test me**",
3037
useToolbar: true,
31-
disabled: false,
32-
readOnly: true,
3338
};
3439

3540
export const LinterExample = TemplateFull.bind({});
3641
LinterExample.args = {
37-
name: "codeinput",
42+
name: "lintinput",
3843
defaultValue: "**test me**",
3944
mode: "javascript",
4045
useLinting: true,

src/extensions/codemirror/CodeMirror.tsx

Lines changed: 71 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DOMEventHandlers, EditorView, KeyBinding, keymap, Rect, ViewUpdate } fr
66
import { minimalSetup } from "codemirror";
77

88
import { Markdown } from "../../cmem/markdown/Markdown";
9+
import { EditorAppearanceConfigMenu } from "./toolbars/EditorAppearanceConfigMenu";
910
import { IntentTypes } from "../../common/Intent";
1011
import { markField } from "../../components/AutoSuggestion/extensions/markText";
1112
import { TestableComponent } from "../../components/interfaces";
@@ -36,7 +37,17 @@ import {
3637
import { MarkdownToolbar } from "./toolbars/markdown.toolbar";
3738
import { ExtensionCreator } from "./types";
3839

39-
export interface CodeEditorProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "translate" | "onChange" | "onKeyDown" | "onMouseDown" | "onScroll">, TestableComponent {
40+
interface EditorAppearance {
41+
/**
42+
* If enabled the code editor won't show numbers before each line.
43+
*/
44+
preventLineNumbers?: boolean;
45+
46+
/** Long lines are wrapped and displayed on multiple lines */
47+
wrapLines?: boolean;
48+
}
49+
50+
export interface CodeEditorProps extends EditorAppearance, Omit<React.HTMLAttributes<HTMLDivElement>, "translate" | "onChange" | "onKeyDown" | "onMouseDown" | "onScroll">, TestableComponent {
4051
// Is called with the editor instance that allows access via the CodeMirror API
4152
setEditorView?: (editor: EditorView | undefined) => void;
4253
/**
@@ -86,22 +97,15 @@ export interface CodeEditorProps extends Omit<React.HTMLAttributes<HTMLDivElemen
8697
* Default value used first when the editor is instanciated.
8798
*/
8899
defaultValue?: string;
89-
/**
90-
* If enabled the code editor won't show numbers before each line.
91-
*/
92-
preventLineNumbers?: boolean;
93100

94101
/** Set read-only mode. Default: false */
95102
readOnly?: boolean;
96103

97104
/** Optional height of the component */
98105
height?: number | string;
99106

100-
/** Long lines are wrapped and displayed on multiple lines */
101-
wrapLines?: boolean;
102-
103107
/**
104-
* Add properties to the `div` used as warpper element.
108+
* Add properties to the `div` used as wrapper element.
105109
* @deprecated (v26) You can now use all properties directly on `CodeEditor`.
106110
*/
107111
outerDivAttributes?: Omit<React.HTMLAttributes<HTMLDivElement>, "id" | "data-test-id" | "data-testid" | "translate" | "onChange" | "onKeyDown" | "onMouseDown" | "onScroll">;
@@ -186,6 +190,18 @@ const ModeLinterMap: ReadonlyMap<SupportedCodeEditorModes, ReadonlyArray<Extensi
186190

187191
const ModeToolbarSupport: ReadonlyArray<SupportedCodeEditorModes> = ["markdown"];
188192

193+
const defaultAppearanceForModeWithToolbar: ReadonlyMap<SupportedCodeEditorModes, EditorAppearance> = new Map([
194+
["markdown", { wrapLines: true, preventLineNumbers: true }]
195+
]);
196+
197+
const getDefaultAppearanceForModeWithToolbar = (hasToolbar: boolean, mode?: SupportedCodeEditorModes): EditorAppearance | undefined => {
198+
if (hasToolbar && mode) {
199+
return defaultAppearanceForModeWithToolbar.get(mode);
200+
}
201+
202+
return undefined;
203+
}
204+
189205
/**
190206
* Includes a code editor, currently we use CodeMirror library as base.
191207
*/
@@ -200,11 +216,11 @@ export const CodeEditor = ({
200216
name,
201217
id,
202218
mode,
203-
preventLineNumbers = false,
219+
preventLineNumbers,
220+
wrapLines,
204221
defaultValue = "",
205222
readOnly = false,
206223
shouldHaveMinimalSetup = true,
207-
wrapLines = false,
208224
onScroll,
209225
setEditorView,
210226
supportCodeFolding = false,
@@ -221,12 +237,20 @@ export const CodeEditor = ({
221237
autoFocus = false,
222238
disabled = false,
223239
intent,
224-
useToolbar,
240+
useToolbar = false,
225241
translate,
226242
...otherCodeEditorProps
227243
}: CodeEditorProps) => {
228244
const parent = useRef<any>(undefined);
229245
const [view, setView] = React.useState<EditorView | undefined>();
246+
const defaultAppearanceForModeWithToolbar = getDefaultAppearanceForModeWithToolbar(useToolbar, mode);
247+
const [editorAppearance, setEditorAppearance] = React.useState<{[s: string]: boolean;}>(
248+
{
249+
// we also set the fallback default here
250+
wrapLines: wrapLines ?? defaultAppearanceForModeWithToolbar?.wrapLines ?? false,
251+
preventLineNumbers: preventLineNumbers ?? defaultAppearanceForModeWithToolbar?.preventLineNumbers ?? false,
252+
}
253+
)
230254
const currentView = React.useRef<EditorView>()
231255
currentView.current = view
232256
const currentReadOnly = React.useRef(readOnly)
@@ -235,6 +259,8 @@ export const CodeEditor = ({
235259
currentOnChange.current = onChange
236260
const currentDisabled = React.useRef(disabled)
237261
currentDisabled.current = disabled
262+
const currentIntent = React.useRef(intent)
263+
currentIntent.current = intent
238264
const [showPreview, setShowPreview] = React.useState<boolean>(false);
239265
// CodeMirror Compartments in order to allow for re-configuration after initialization
240266
const readOnlyCompartment = React.useRef<Compartment>(compartment())
@@ -333,8 +359,8 @@ export const CodeEditor = ({
333359
if (onSelection)
334360
onSelection(v.state.selection.ranges.filter((r) => !r.empty).map(({ from, to }) => ({ from, to })));
335361

336-
if (onFocusChange && intent && !v.view.dom.classList?.contains(`${eccgui}-intent--${intent}`)) {
337-
v.view.dom.classList.add(`${eccgui}-intent--${intent}`);
362+
if (onFocusChange && currentIntent.current && !v.view.dom.classList?.contains(`${eccgui}-intent--${currentIntent.current}`)) {
363+
v.view.dom.classList.add(`${eccgui}-intent--${currentIntent.current}`);
338364
}
339365

340366
if (onCursorChange) {
@@ -357,9 +383,9 @@ export const CodeEditor = ({
357383
}
358384
}),
359385
shouldHaveMinimalSetupCompartment.current.of(addExtensionsFor(shouldHaveMinimalSetup, minimalSetup)),
360-
preventLineNumbersCompartment.current.of(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers())),
386+
preventLineNumbersCompartment.current.of(addExtensionsFor(!editorAppearance.preventLineNumbers, adaptedLineNumbers())),
361387
shouldHighlightActiveLineCompartment.current.of(addExtensionsFor(shouldHighlightActiveLine, adaptedHighlightActiveLine())),
362-
wrapLinesCompartment.current.of(addExtensionsFor(wrapLines, EditorView?.lineWrapping)),
388+
wrapLinesCompartment.current.of(addExtensionsFor((editorAppearance.wrapLines!), EditorView?.lineWrapping)),
363389
supportCodeFoldingCompartment.current.of(addExtensionsFor(supportCodeFolding, adaptedFoldGutter(), adaptedCodeFolding())),
364390
useLintingCompartment.current.of(addExtensionsFor(useLinting, ...linters)),
365391
adaptedSyntaxHighlighting(defaultHighlightStyle),
@@ -384,8 +410,8 @@ export const CodeEditor = ({
384410
view.dom.classList.add(`${eccgui}-disabled`);
385411
}
386412

387-
if (intent) {
388-
view.dom.className += ` ${eccgui}-intent--${intent}`;
413+
if (currentIntent.current) {
414+
view.dom.className += ` ${eccgui}-intent--${currentIntent.current}`;
389415
}
390416

391417
if (autoFocus) {
@@ -447,20 +473,28 @@ export const CodeEditor = ({
447473
}, [disabled])
448474

449475
React.useEffect(() => {
450-
updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
451-
}, [shouldHaveMinimalSetup])
476+
setEditorAppearance({
477+
...editorAppearance,
478+
preventLineNumbers: preventLineNumbers ?? editorAppearance?.preventLineNumbers ?? false,
479+
});
480+
updateExtension(addExtensionsFor(!editorAppearance.preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
481+
}, [preventLineNumbers, editorAppearance.preventLineNumbers])
452482

453483
React.useEffect(() => {
454-
updateExtension(addExtensionsFor(!preventLineNumbers, adaptedLineNumbers()), preventLineNumbersCompartment.current)
455-
}, [preventLineNumbers])
484+
setEditorAppearance({
485+
...editorAppearance,
486+
wrapLines: wrapLines ?? editorAppearance?.wrapLines ?? false,
487+
});
488+
updateExtension(addExtensionsFor(editorAppearance.wrapLines!, EditorView?.lineWrapping), wrapLinesCompartment.current)
489+
}, [wrapLines, editorAppearance.wrapLines])
456490

457491
React.useEffect(() => {
458-
updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
459-
}, [shouldHighlightActiveLine])
492+
updateExtension(addExtensionsFor(shouldHaveMinimalSetup ?? true, minimalSetup), shouldHaveMinimalSetupCompartment.current)
493+
}, [shouldHaveMinimalSetup])
460494

461495
React.useEffect(() => {
462-
updateExtension(addExtensionsFor(wrapLines ?? false, EditorView?.lineWrapping), wrapLinesCompartment.current)
463-
}, [wrapLines])
496+
updateExtension(addExtensionsFor(shouldHighlightActiveLine ?? false, adaptedHighlightActiveLine()), shouldHighlightActiveLineCompartment.current)
497+
}, [shouldHighlightActiveLine])
464498

465499
React.useEffect(() => {
466500
updateExtension(addExtensionsFor(supportCodeFolding ?? false, adaptedFoldGutter(), adaptedCodeFolding()), supportCodeFoldingCompartment.current)
@@ -485,6 +519,17 @@ export const CodeEditor = ({
485519
translate={getTranslation}
486520
disabled={disabled}
487521
readonly={readOnly}
522+
configMenu={(
523+
<EditorAppearanceConfigMenu
524+
config={{...editorAppearance}}
525+
configLocked={{
526+
wrapLines,
527+
preventLineNumbers,
528+
}}
529+
setConfig={setEditorAppearance}
530+
configPropertyTranslate={getTranslation}
531+
/>
532+
)}
488533
/>
489534
</div>
490535
{showPreview && (

0 commit comments

Comments
 (0)