Skip to content

Commit ae9f896

Browse files
committed
Merge branch 'develop' into main
2 parents 8d0e3dd + ef8ce65 commit ae9f896

File tree

13 files changed

+112
-85
lines changed

13 files changed

+112
-85
lines changed

packages/demo/package-lock.json

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/demo/src/components/CustomCodeMirror.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,13 @@ import { CodeMirrorComponentProps, CodeMirror, CodeMirrorProps } from '@ui-schem
1818
import { useExtension } from '@ui-schema/kit-codemirror/useExtension'
1919
import { MuiCodeMirrorStyleProps } from '@ui-schema/material-code'
2020

21-
export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirrorStyleProps> = (
21+
export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirrorStyleProps & { minHeight?: number }> = (
2222
{
2323
// values we want to override in this component
2424
value, extensions, effects,
2525
dense, variant,
26+
// custom prop by this `demo` package:
27+
minHeight,
2628
// everything else is just passed down
2729
...props
2830
},
@@ -82,9 +84,11 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirror
8284

8385
// attach each plugin effect separately (thus only the one which changes get reconfigured)
8486
React.useMemo(() => {
87+
if(!effectsHighlightExt) return
8588
effectsRef.current.push(...effectsHighlightExt)
8689
}, [effectsHighlightExt])
8790
React.useMemo(() => {
91+
if(!effectsThemeExt) return
8892
effectsRef.current.push(...effectsThemeExt)
8993
}, [effectsThemeExt])
9094

@@ -98,6 +102,12 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirror
98102
onViewLifecycle={onViewLifecycle}
99103
effects={effectsRef.current.splice(0, effectsRef.current.length)}
100104
{...props}
105+
106+
// use this to force any min height:
107+
style={minHeight ? {
108+
display: 'flex',
109+
minHeight: minHeight,
110+
} : undefined}
101111
// className={className}
102112
/>
103113
}

packages/demo/src/pages/PageDemoComponentMui.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirror
145145
}, [theme])
146146

147147
React.useMemo(() => {
148+
if(!effectsHighlightExt) return
148149
effectsRef.current.push(...effectsHighlightExt)
149150
}, [effectsHighlightExt])
150151

packages/demo/src/pages/PageDemoWidget.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ const schema = createOrderedMap({
7272
widget: 'Code',
7373
format: 'css',
7474
maxLength: 25,
75-
default: '.h1 {\n font-size: 1rem;\n font-weight: 700;\n border-bottom: 1px solid #fefefe;\n}',
75+
default: '.h1 {\n font-size: 1rem;\n font-weight: 700;\n border-bottom: 1px solid #fefefe;\n}\n\n.h2 {\n font-size: 1rem;\n font-weight: 700;\n border-bottom: 1px solid #fefefe;\n}\n\n.h3 {\n font-size: 1rem;\n font-weight: 700;\n border-bottom: 1px solid #fefefe;\n}',
7676
view: {
7777
hideTitle: true,
7878
},

packages/kit-codemirror/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/kit-codemirror/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@ui-schema/kit-codemirror",
3-
"version": "0.1.0-alpha.0",
3+
"version": "0.1.0-alpha.1",
44
"description": "CodeMirror v6 as React Component, with hooks and stuff - but only the necessities.",
55
"homepage": "https://ui-schema.bemit.codes/docs/kit-codemirror/kit-codemirror",
66
"author": {

packages/kit-codemirror/src/CodeMirror/CodeMirror.tsx

Lines changed: 7 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from 'react'
22
import { EditorView } from '@codemirror/view'
33
import { Compartment, Extension } from '@codemirror/state'
4-
import { CodeMirrorOnChange, useCodeMirror } from '@ui-schema/kit-codemirror/useCodeMirror'
4+
import { CodeMirrorOnChange, CodeMirrorOnExternalChange, CodeMirrorOnViewLifeCycle, useCodeMirror } from '@ui-schema/kit-codemirror/useCodeMirror'
55
import { useEditorClasses } from '@ui-schema/kit-codemirror/useEditorClasses'
66

77
export interface CodeMirrorComponentProps {
@@ -14,10 +14,8 @@ export interface CodeMirrorComponentProps {
1414
}
1515

1616
export interface CodeMirrorProps extends CodeMirrorComponentProps {
17-
// can be called multiple times, every time an editor is re-created, e.g. because of theming change
18-
// - called when editor is created with `value`
19-
// - when editor was created, will be called with `undefined` after the editor is destroyed OR on unmount
20-
onViewLifecycle?: (editor: EditorView | undefined) => void
17+
onViewLifecycle?: CodeMirrorOnViewLifeCycle
18+
onExternalChange?: CodeMirrorOnExternalChange
2119
className?: string
2220
}
2321

@@ -26,7 +24,7 @@ export const CodeMirror: React.FC<CodeMirrorProps> = (
2624
className,
2725
classNamesContent,
2826
onChange,
29-
onViewLifecycle,
27+
onViewLifecycle, onExternalChange,
3028
value = '',
3129
style,
3230
extensions,
@@ -43,22 +41,18 @@ export const CodeMirror: React.FC<CodeMirrorProps> = (
4341
], [extensions])
4442

4543
const editor = useCodeMirror(
46-
containerRef,
4744
onChange,
4845
value,
4946
extensionsAll,
5047
effects,
48+
containerRef,
49+
onExternalChange,
50+
onViewLifecycle,
5151
)
5252

5353
// but extensions need to receive both: Compartment and Editor (and optionally their values)
5454
// to be able to dispatch the correct effects
5555
useEditorClasses(editorAttributesCompartment.current, editor, classNamesContent)
5656

57-
React.useEffect(() => {
58-
if(!onViewLifecycle || !editor) return
59-
onViewLifecycle(editor)
60-
return () => onViewLifecycle(undefined)
61-
}, [onViewLifecycle, editor])
62-
6357
return <div className={className} style={style} ref={containerRef}/>
6458
}

packages/kit-codemirror/src/createEditorView/createEditorView.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
import React from 'react'
21
import { EditorView } from '@codemirror/view'
32
import { EditorState, Extension } from '@codemirror/state'
43
import { CodeMirrorOnChange, CodeMirrorOnExternalChange } from '@ui-schema/kit-codemirror/useCodeMirror'
54

5+
/**
6+
* changes whole doc with new text
7+
*/
68
export const replaceWholeDoc: CodeMirrorOnExternalChange = (editor, nextValue) => {
79
editor?.dispatch({
810
changes: {
@@ -14,10 +16,10 @@ export const replaceWholeDoc: CodeMirrorOnExternalChange = (editor, nextValue) =
1416
}
1517

1618
export const createEditorView = (
17-
parent: Element,
18-
lastValueRef: React.MutableRefObject<string>,
19+
lastValueRef: { current: string },
1920
extensions?: Extension[],
20-
onChangeRef?: React.MutableRefObject<CodeMirrorOnChange | undefined>,
21+
onChangeRef?: { current: CodeMirrorOnChange | undefined },
22+
parent?: Element,
2123
) => {
2224
return new EditorView({
2325
state: EditorState.create({

packages/kit-codemirror/src/useCodeMirror/useCodeMirror.ts

Lines changed: 45 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,9 @@ export type CodeMirrorOnChange = (view: ViewUpdate, nextValue: string | undefine
77

88
export type CodeMirrorOnExternalChange = (editor: EditorView, nextValue: string, prevValue: string) => void
99

10+
export type CodeMirrorOnViewLifeCycle = (editor: EditorView | undefined, destroyed?: boolean) => void
11+
1012
export const useCodeMirror = (
11-
containerRef: React.MutableRefObject<HTMLDivElement | null>,
1213
onChange?: CodeMirrorOnChange,
1314
// the latest value of the editor in the parent state
1415
value: string = '',
@@ -17,68 +18,63 @@ export const useCodeMirror = (
1718
// use e.g. `effectsRef.current.splice(0, effectsRef.current.length)`,
1819
// to pass them down and remove them - so they won't get run again on next render
1920
effects?: ((editor: EditorView) => void)[],
21+
// the container, if set must be set from start on, otherwise editor won't behave correctly
22+
// if not set, use the `onViewLifecycle` callback to mount the editor yourself
23+
containerRef?: { current: HTMLDivElement | null },
2024
// handle when `value` has changed from some other instance than this
2125
onExternalChange: CodeMirrorOnExternalChange = replaceWholeDoc,
22-
): EditorView => {
26+
// could be called multiple times, every time an editor is re-created, e.g. because of full extensions change
27+
// - will receive the previous editor, and `true` if deleted
28+
// - is called after setting to state (and if containerRef is set, after mounting to container), but within same render cycle
29+
// - is called in cleanup, but before actual destroying the editor (directly afterwards)
30+
onViewLifecycle?: CodeMirrorOnViewLifeCycle,
31+
): EditorView | undefined => {
2332
const lastValueRef = React.useRef<string>(value)
2433
const onChangeRef = React.useRef<CodeMirrorOnChange | undefined>(undefined)
34+
const [editor, setEditor] = React.useState<EditorView | undefined>(undefined)
2535
// as onChange relies on the mounting state, this can't be solved with a "normal Compartment style" extension,
2636
// these ref hacks should be the safest/fastest option
2737
onChangeRef.current = onChange
2838

2939
const readOnlyCompartment = React.useRef<Compartment>(new Compartment())
3040

31-
const editor: EditorView = React.useMemo(() => {
32-
return createEditorView(
33-
containerRef.current as Element,
41+
React.useLayoutEffect(() => {
42+
const editor = createEditorView(
3443
lastValueRef,
3544
[
3645
...extensions || [],
3746
readOnlyCompartment.current.of(EditorView.editable.of(Boolean(onChangeRef.current))),
3847
],
3948
onChangeRef,
4049
)
41-
}, [containerRef, lastValueRef, extensions, onChangeRef])
4250

43-
React.useLayoutEffect(() => {
44-
if(!editor) {
45-
return
51+
if(containerRef) {
52+
containerRef.current?.append(editor.dom)
4653
}
47-
containerRef.current?.append(editor.dom)
54+
setEditor(editor)
55+
onViewLifecycle?.(editor)
56+
4857
return () => {
58+
onViewLifecycle?.(editor, true)
4959
editor?.destroy()
60+
setEditor(undefined)
5061
}
51-
}, [containerRef, editor])
52-
53-
// re-execution protection for no-effects with "splice"
54-
effects = effects?.length === 0 ? undefined : effects
55-
React.useLayoutEffect(() => {
56-
if(effects && effects.length > 0 && !editor) {
57-
console.error('received effects but editor is not ready', effects)
58-
return
59-
}
60-
if(!effects || !editor) return
61-
effects.forEach(effect => {
62-
effect(editor)
63-
})
64-
}, [effects, editor])
62+
}, [containerRef, extensions, onViewLifecycle])
6563

6664
React.useEffect(() => {
67-
if(!editor) {
68-
return
69-
}
65+
if(!editor) return
7066
editor.dispatch({
7167
effects: readOnlyCompartment.current.reconfigure(EditorView.editable.of(Boolean(onChange))),
7268
})
7369
}, [editor, onChange])
7470

71+
//
72+
// ! 1. process external changes
7573
React.useLayoutEffect(() => {
76-
// changing whole doc when value changed - and change was not the last one from within CodeMirror
77-
if(editor && containerRef.current && lastValueRef.current !== value) {
78-
if(lastValueRef.current === value) {
79-
// be sure that it still isn't the same value to not unnecessarily dispatch a re-draw
80-
return
81-
}
74+
if(!editor) return
75+
// changes doc when props-value changed - and change was not the last one from within this `CodeMirror`
76+
// = maybe changes from another user
77+
if(lastValueRef.current !== value) {
8278
// todo: really rely on `state.doc`?
8379
// as `lastValueRef.current` may be updated before `editor` has finished consuming the last change,
8480
// building a diff with that "actual-latest" value will produce invalid `changes.from/to` ranges.
@@ -89,7 +85,22 @@ export const useCodeMirror = (
8985
} else {
9086
lastValueRef.current = value
9187
}
92-
}, [containerRef, value, editor, onExternalChange])
88+
}, [value, editor, onExternalChange, containerRef])
89+
90+
//
91+
// ! 2. process own changes
92+
//
93+
effects = effects?.length === 0 ? undefined : effects // re-execution protection for no-effects with "splice"
94+
React.useLayoutEffect(() => {
95+
if(!editor && effects) {
96+
console.error('received effects but editor is not ready', effects)
97+
return
98+
}
99+
if(!effects || !editor) return
100+
effects.forEach(effect => {
101+
effect(editor)
102+
})
103+
}, [effects, editor])
93104

94105
return editor
95106
}

packages/kit-codemirror/src/useExtension/useExtension.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ export const useExtension = (ext: () => Extension, deps?: any[]) => {
1111
const init = React.useCallback((): Extension => {
1212
hasInit.current = true
1313
return compartment.current.of(extRef.current())
14-
}, [hasInit, compartment, extRef])
14+
}, [])
1515

16-
const effects: ((editor: EditorView) => void)[] = React.useMemo(() => {
17-
if(!hasInit.current) return []
16+
const effects: ((editor: EditorView) => void)[] | undefined = React.useMemo(() => {
17+
if(!hasInit.current) return undefined
1818
return [
1919
function updateExtension(editor) {
2020
editor.dispatch({
@@ -23,7 +23,7 @@ export const useExtension = (ext: () => Extension, deps?: any[]) => {
2323
},
2424
]
2525
// eslint-disable-next-line react-hooks/exhaustive-deps
26-
}, [hasInit, extRef, ...deps || []])
26+
}, deps || [])
2727

2828
return {
2929
init: init,

0 commit comments

Comments
 (0)