Skip to content

Commit

Permalink
Merge branch 'develop' into main
Browse files Browse the repository at this point in the history
  • Loading branch information
elbakerino committed Oct 14, 2022
2 parents 8d0e3dd + ef8ce65 commit ae9f896
Show file tree
Hide file tree
Showing 13 changed files with 112 additions and 85 deletions.
12 changes: 6 additions & 6 deletions packages/demo/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion packages/demo/src/components/CustomCodeMirror.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,13 @@ import { CodeMirrorComponentProps, CodeMirror, CodeMirrorProps } from '@ui-schem
import { useExtension } from '@ui-schema/kit-codemirror/useExtension'
import { MuiCodeMirrorStyleProps } from '@ui-schema/material-code'

export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirrorStyleProps> = (
export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirrorStyleProps & { minHeight?: number }> = (
{
// values we want to override in this component
value, extensions, effects,
dense, variant,
// custom prop by this `demo` package:
minHeight,
// everything else is just passed down
...props
},
Expand Down Expand Up @@ -82,9 +84,11 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirror

// attach each plugin effect separately (thus only the one which changes get reconfigured)
React.useMemo(() => {
if(!effectsHighlightExt) return
effectsRef.current.push(...effectsHighlightExt)
}, [effectsHighlightExt])
React.useMemo(() => {
if(!effectsThemeExt) return
effectsRef.current.push(...effectsThemeExt)
}, [effectsThemeExt])

Expand All @@ -98,6 +102,12 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirror
onViewLifecycle={onViewLifecycle}
effects={effectsRef.current.splice(0, effectsRef.current.length)}
{...props}

// use this to force any min height:
style={minHeight ? {
display: 'flex',
minHeight: minHeight,
} : undefined}
// className={className}
/>
}
1 change: 1 addition & 0 deletions packages/demo/src/pages/PageDemoComponentMui.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ export const CustomCodeMirror: React.FC<CodeMirrorComponentProps & MuiCodeMirror
}, [theme])

React.useMemo(() => {
if(!effectsHighlightExt) return
effectsRef.current.push(...effectsHighlightExt)
}, [effectsHighlightExt])

Expand Down
2 changes: 1 addition & 1 deletion packages/demo/src/pages/PageDemoWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ const schema = createOrderedMap({
widget: 'Code',
format: 'css',
maxLength: 25,
default: '.h1 {\n font-size: 1rem;\n font-weight: 700;\n border-bottom: 1px solid #fefefe;\n}',
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}',
view: {
hideTitle: true,
},
Expand Down
4 changes: 2 additions & 2 deletions packages/kit-codemirror/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/kit-codemirror/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ui-schema/kit-codemirror",
"version": "0.1.0-alpha.0",
"version": "0.1.0-alpha.1",
"description": "CodeMirror v6 as React Component, with hooks and stuff - but only the necessities.",
"homepage": "https://ui-schema.bemit.codes/docs/kit-codemirror/kit-codemirror",
"author": {
Expand Down
20 changes: 7 additions & 13 deletions packages/kit-codemirror/src/CodeMirror/CodeMirror.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react'
import { EditorView } from '@codemirror/view'
import { Compartment, Extension } from '@codemirror/state'
import { CodeMirrorOnChange, useCodeMirror } from '@ui-schema/kit-codemirror/useCodeMirror'
import { CodeMirrorOnChange, CodeMirrorOnExternalChange, CodeMirrorOnViewLifeCycle, useCodeMirror } from '@ui-schema/kit-codemirror/useCodeMirror'
import { useEditorClasses } from '@ui-schema/kit-codemirror/useEditorClasses'

export interface CodeMirrorComponentProps {
Expand All @@ -14,10 +14,8 @@ export interface CodeMirrorComponentProps {
}

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

Expand All @@ -26,7 +24,7 @@ export const CodeMirror: React.FC<CodeMirrorProps> = (
className,
classNamesContent,
onChange,
onViewLifecycle,
onViewLifecycle, onExternalChange,
value = '',
style,
extensions,
Expand All @@ -43,22 +41,18 @@ export const CodeMirror: React.FC<CodeMirrorProps> = (
], [extensions])

const editor = useCodeMirror(
containerRef,
onChange,
value,
extensionsAll,
effects,
containerRef,
onExternalChange,
onViewLifecycle,
)

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

React.useEffect(() => {
if(!onViewLifecycle || !editor) return
onViewLifecycle(editor)
return () => onViewLifecycle(undefined)
}, [onViewLifecycle, editor])

return <div className={className} style={style} ref={containerRef}/>
}
10 changes: 6 additions & 4 deletions packages/kit-codemirror/src/createEditorView/createEditorView.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React from 'react'
import { EditorView } from '@codemirror/view'
import { EditorState, Extension } from '@codemirror/state'
import { CodeMirrorOnChange, CodeMirrorOnExternalChange } from '@ui-schema/kit-codemirror/useCodeMirror'

/**
* changes whole doc with new text
*/
export const replaceWholeDoc: CodeMirrorOnExternalChange = (editor, nextValue) => {
editor?.dispatch({
changes: {
Expand All @@ -14,10 +16,10 @@ export const replaceWholeDoc: CodeMirrorOnExternalChange = (editor, nextValue) =
}

export const createEditorView = (
parent: Element,
lastValueRef: React.MutableRefObject<string>,
lastValueRef: { current: string },
extensions?: Extension[],
onChangeRef?: React.MutableRefObject<CodeMirrorOnChange | undefined>,
onChangeRef?: { current: CodeMirrorOnChange | undefined },
parent?: Element,
) => {
return new EditorView({
state: EditorState.create({
Expand Down
79 changes: 45 additions & 34 deletions packages/kit-codemirror/src/useCodeMirror/useCodeMirror.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ export type CodeMirrorOnChange = (view: ViewUpdate, nextValue: string | undefine

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

export type CodeMirrorOnViewLifeCycle = (editor: EditorView | undefined, destroyed?: boolean) => void

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

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

const editor: EditorView = React.useMemo(() => {
return createEditorView(
containerRef.current as Element,
React.useLayoutEffect(() => {
const editor = createEditorView(
lastValueRef,
[
...extensions || [],
readOnlyCompartment.current.of(EditorView.editable.of(Boolean(onChangeRef.current))),
],
onChangeRef,
)
}, [containerRef, lastValueRef, extensions, onChangeRef])

React.useLayoutEffect(() => {
if(!editor) {
return
if(containerRef) {
containerRef.current?.append(editor.dom)
}
containerRef.current?.append(editor.dom)
setEditor(editor)
onViewLifecycle?.(editor)

return () => {
onViewLifecycle?.(editor, true)
editor?.destroy()
setEditor(undefined)
}
}, [containerRef, editor])

// re-execution protection for no-effects with "splice"
effects = effects?.length === 0 ? undefined : effects
React.useLayoutEffect(() => {
if(effects && effects.length > 0 && !editor) {
console.error('received effects but editor is not ready', effects)
return
}
if(!effects || !editor) return
effects.forEach(effect => {
effect(editor)
})
}, [effects, editor])
}, [containerRef, extensions, onViewLifecycle])

React.useEffect(() => {
if(!editor) {
return
}
if(!editor) return
editor.dispatch({
effects: readOnlyCompartment.current.reconfigure(EditorView.editable.of(Boolean(onChange))),
})
}, [editor, onChange])

//
// ! 1. process external changes
React.useLayoutEffect(() => {
// changing whole doc when value changed - and change was not the last one from within CodeMirror
if(editor && containerRef.current && lastValueRef.current !== value) {
if(lastValueRef.current === value) {
// be sure that it still isn't the same value to not unnecessarily dispatch a re-draw
return
}
if(!editor) return
// changes doc when props-value changed - and change was not the last one from within this `CodeMirror`
// = maybe changes from another user
if(lastValueRef.current !== value) {
// todo: really rely on `state.doc`?
// as `lastValueRef.current` may be updated before `editor` has finished consuming the last change,
// building a diff with that "actual-latest" value will produce invalid `changes.from/to` ranges.
Expand All @@ -89,7 +85,22 @@ export const useCodeMirror = (
} else {
lastValueRef.current = value
}
}, [containerRef, value, editor, onExternalChange])
}, [value, editor, onExternalChange, containerRef])

//
// ! 2. process own changes
//
effects = effects?.length === 0 ? undefined : effects // re-execution protection for no-effects with "splice"
React.useLayoutEffect(() => {
if(!editor && effects) {
console.error('received effects but editor is not ready', effects)
return
}
if(!effects || !editor) return
effects.forEach(effect => {
effect(editor)
})
}, [effects, editor])

return editor
}
8 changes: 4 additions & 4 deletions packages/kit-codemirror/src/useExtension/useExtension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ export const useExtension = (ext: () => Extension, deps?: any[]) => {
const init = React.useCallback((): Extension => {
hasInit.current = true
return compartment.current.of(extRef.current())
}, [hasInit, compartment, extRef])
}, [])

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

return {
init: init,
Expand Down
4 changes: 2 additions & 2 deletions packages/material-code/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/material-code/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ui-schema/material-code",
"version": "0.4.0",
"version": "0.4.1",
"description": "Code editor widgets using CodeMirror, with Material-UI styling for UI-Schema",
"homepage": "https://ui-schema.bemit.codes/docs/material-code/material-code",
"author": {
Expand Down
Loading

0 comments on commit ae9f896

Please sign in to comment.