|
| 1 | +# DS-Material Code |
| 2 | + |
| 3 | +Code editor widgets using CodeMirror, for UI-Schema and Material-UI. |
| 4 | + |
| 5 | +[](#demo-ui-generator) |
| 6 | + |
| 7 | +- type: `string`, `array` |
| 8 | +- widget keywords: |
| 9 | + - `Code` for single `format` |
| 10 | + - `CodeSelectable` for selectable `format: string[]` and data as `[format, code]` array tuple or `{lang, code}` object |
| 11 | + |
| 12 | +> |
| 13 | +> 🚧 Work in progress, semi stable [#188](https://github.com/ui-schema/ui-schema/issues/188) |
| 14 | +> |
| 15 | +> For issues and more check the separate [ui-schema/react-codemirror repository](https://github.com/ui-schema/react-codemirror) |
| 16 | +
|
| 17 | +## Install |
| 18 | + |
| 19 | +```bash |
| 20 | +npm install --save @ui-schema/ds-material @ui-schema/material-code @ui-schema/kit-codemirror @codemirror/state @codemirror/view @codemirror/language |
| 21 | +``` |
| 22 | + |
| 23 | +**Uses CodeMirror v6 since `0.4.0-beta.0`.** |
| 24 | + |
| 25 | +## Widgets |
| 26 | + |
| 27 | +**Keywords:** |
| 28 | + |
| 29 | +- `view.hideTitle` when `true` it does not show the title, only the current format |
| 30 | +- `format` keyword to select the enabled language mode, `string` or `string[]` |
| 31 | +- uses translations: `formats.<schema.format>` for nicer labels, check [examples in docs](https://github.com/ui-schema/ui-schema/blob/master/packages/dictionary/src/en/formats.js) |
| 32 | +- only for `CodeSelectable`: |
| 33 | + - `formatDefault` keyword to specify the initial code-language without persisting it in the array |
| 34 | + - must be implemented in your custom widget wire-up |
| 35 | + |
| 36 | +## Hooks |
| 37 | + |
| 38 | +### useEditorTheme |
| 39 | + |
| 40 | +Code editor theming, built using the current MUI theming context, makes it look similar to any other `TextField`. |
| 41 | + |
| 42 | +**Params:** |
| 43 | + |
| 44 | +- `readOnly`: when true, doesn't apply focus / interactive styles |
| 45 | + |
| 46 | +```typescript jsx |
| 47 | +import { useEditorTheme } from '@ui-schema/material-code/useEditorTheme' |
| 48 | + |
| 49 | +// in a component: |
| 50 | +const {onChange} = props |
| 51 | +const theme = useEditorTheme(typeof onChange === 'undefined') |
| 52 | +``` |
| 53 | + |
| 54 | +### useHighlightStyle |
| 55 | + |
| 56 | +Syntax highlighting theming, built using the current MUI theming context, *not yet that optimized*. |
| 57 | + |
| 58 | +```typescript jsx |
| 59 | +import { useHighlightStyle } from '@ui-schema/material-code/useHighlightStyle' |
| 60 | + |
| 61 | +// in a component: |
| 62 | +const highlightStyle = useHighlightStyle() |
| 63 | +``` |
| 64 | + |
| 65 | +## Example |
| 66 | + |
| 67 | +First create a `CustomCodeMirror` component, this component can be used to build the UI-Schema widgets and outside UI-Schema as pure CodeMirror v6 React integration: |
| 68 | + |
| 69 | +```typescript jsx |
| 70 | +import React from 'react' |
| 71 | +import { |
| 72 | + lineNumbers, highlightActiveLineGutter, highlightSpecialChars, |
| 73 | + drawSelection, dropCursor, |
| 74 | + rectangularSelection, highlightActiveLine, keymap, |
| 75 | +} from '@codemirror/view' |
| 76 | +import { foldGutter, indentOnInput, syntaxHighlighting, defaultHighlightStyle, bracketMatching, foldKeymap } from '@codemirror/language' |
| 77 | +import { history, defaultKeymap, historyKeymap, indentWithTab } from '@codemirror/commands' |
| 78 | +import { highlightSelectionMatches, searchKeymap } from '@codemirror/search' |
| 79 | +import { closeBrackets, autocompletion, closeBracketsKeymap, completionKeymap } from '@codemirror/autocomplete' |
| 80 | +import { lintKeymap } from '@codemirror/lint' |
| 81 | +import { Compartment, EditorState } from '@codemirror/state' |
| 82 | +import { CodeMirrorComponentProps, CodeMirror, CodeMirrorProps } from '@ui-schema/kit-codemirror/CodeMirror' |
| 83 | +import { useEditorTheme } from '@ui-schema/material-code/useEditorTheme' |
| 84 | +import { useHighlightStyle } from '@ui-schema/material-code/useHighlightStyle' |
| 85 | + |
| 86 | +export const CustomCodeMirror: React.FC<CodeMirrorComponentProps> = ( |
| 87 | + { |
| 88 | + // values we want to override in this component |
| 89 | + value, extensions, |
| 90 | + // everything else is just passed down |
| 91 | + ...props |
| 92 | + }, |
| 93 | +) => { |
| 94 | + const {onChange} = props |
| 95 | + const theme = useEditorTheme(typeof onChange === 'undefined') |
| 96 | + const highlightStyle = useHighlightStyle() |
| 97 | + |
| 98 | + const extensionsAll = React.useMemo(() => [ |
| 99 | + lineNumbers(), |
| 100 | + highlightActiveLineGutter(), |
| 101 | + highlightSpecialChars(), |
| 102 | + history(), |
| 103 | + foldGutter(), |
| 104 | + drawSelection(), |
| 105 | + dropCursor(), |
| 106 | + EditorState.allowMultipleSelections.of(true), |
| 107 | + indentOnInput(), |
| 108 | + syntaxHighlighting(highlightStyle || defaultHighlightStyle, {fallback: true}), |
| 109 | + bracketMatching(), |
| 110 | + closeBrackets(), |
| 111 | + autocompletion(), |
| 112 | + rectangularSelection(), |
| 113 | + // crosshairCursor(), |
| 114 | + highlightActiveLine(), |
| 115 | + highlightSelectionMatches(), |
| 116 | + new Compartment().of(EditorState.tabSize.of(4)), |
| 117 | + keymap.of([ |
| 118 | + ...closeBracketsKeymap, |
| 119 | + ...defaultKeymap, |
| 120 | + ...searchKeymap, |
| 121 | + ...historyKeymap, |
| 122 | + ...foldKeymap, |
| 123 | + ...completionKeymap, |
| 124 | + ...lintKeymap, |
| 125 | + indentWithTab, |
| 126 | + ]), |
| 127 | + theme, |
| 128 | + ...(extensions || []), |
| 129 | + ], [highlightStyle, extensions, theme]) |
| 130 | + |
| 131 | + const onViewLifecycle: CodeMirrorProps['onViewLifecycle'] = React.useCallback((view) => { |
| 132 | + console.log('on-view-lifecycle', view) |
| 133 | + }, []) |
| 134 | + |
| 135 | + return <CodeMirror |
| 136 | + value={value || ''} |
| 137 | + extensions={extensionsAll} |
| 138 | + onViewLifecycle={onViewLifecycle} |
| 139 | + {...props} |
| 140 | + // className={className} |
| 141 | + /> |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +Then wire up the widgets and build your own widgets binding: |
| 146 | + |
| 147 | +```typescript jsx |
| 148 | +import React from 'react' |
| 149 | +import { |
| 150 | + WidgetsBindingFactory, |
| 151 | + WidgetProps, WithScalarValue, memo, WithValue, StoreKeyType, |
| 152 | +} from '@ui-schema/ui-schema' |
| 153 | +import { MuiWidgetsBindingCustom, MuiWidgetsBindingTypes, widgets } from '@ui-schema/ds-material/widgetsBinding' |
| 154 | +import Button from '@mui/material/Button' |
| 155 | +import { json } from '@codemirror/lang-json' |
| 156 | +import { javascript } from '@codemirror/lang-javascript' |
| 157 | +import { html } from '@codemirror/lang-html' |
| 158 | +import { css } from '@codemirror/lang-css' |
| 159 | +import { extractValue } from '@ui-schema/ui-schema/UIStore' |
| 160 | +import { WidgetCode } from '@ui-schema/material-code' |
| 161 | +import { WidgetCodeSelectable } from '@ui-schema/material-code/WidgetCodeSelectable' |
| 162 | +import { CustomCodeMirror } from './CustomCodeMirror' |
| 163 | + |
| 164 | +export const CustomWidgetCode: React.ComponentType<WidgetProps & WithScalarValue> = (props) => { |
| 165 | + const format = props.schema.get('format') |
| 166 | + // map the to-be-supported CodeMirror language, or add other extensions |
| 167 | + const extensions = React.useMemo(() => [ |
| 168 | + ...(format === 'json' ? [json()] : []), |
| 169 | + ...(format === 'javascript' ? [javascript()] : []), |
| 170 | + ...(format === 'html' ? [html()] : []), |
| 171 | + ...(format === 'css' ? [css()] : []), |
| 172 | + ], [format]) |
| 173 | + |
| 174 | + return <WidgetCode |
| 175 | + {...props} |
| 176 | + CodeMirror={CustomCodeMirror} |
| 177 | + // `extensions` will be passed down again to `CustomCodeMirror` |
| 178 | + extensions={extensions} |
| 179 | + formatValue={format} |
| 180 | + /> |
| 181 | +} |
| 182 | + |
| 183 | +const CustomWidgetCodeSelectableBase: React.ComponentType<WidgetProps & WithValue> = ( |
| 184 | + {value, ...props}, |
| 185 | +) => { |
| 186 | + const {schema, onChange, storeKeys} = props |
| 187 | + const valueType = schema.get('type') as 'array' | 'object' |
| 188 | + // supporting different types requires mapping the actual key of `format` and `value` inside the non-scalar value of this component |
| 189 | + // - for tuples: [0: format, 1: code] |
| 190 | + // - for objects: {lang, code} |
| 191 | + const formatKey: StoreKeyType = valueType === 'array' ? 0 : 'lang' |
| 192 | + const valueKey: StoreKeyType = valueType === 'array' ? 1 : 'code' |
| 193 | + const format = value?.get(formatKey) as string | undefined || schema.get('formatDefault') as string | undefined |
| 194 | + const codeValue = value?.get(valueKey) as string | undefined |
| 195 | + |
| 196 | + // map the to-be-supported CodeMirror language, or add other extensions |
| 197 | + const extensions = React.useMemo(() => [ |
| 198 | + ...(format === 'json' ? [json()] : []), |
| 199 | + ...(format === 'javascript' ? [javascript()] : []), |
| 200 | + ...(format === 'html' ? [html()] : []), |
| 201 | + ...(format === 'css' ? [css()] : []), |
| 202 | + ], [format]) |
| 203 | + |
| 204 | + return <WidgetCodeSelectable |
| 205 | + {...props} |
| 206 | + CodeMirror={CustomCodeMirror} |
| 207 | + // `extensions` will be passed down again to `CustomCodeMirror` |
| 208 | + extensions={extensions} |
| 209 | + formatKey={formatKey} |
| 210 | + valueKey={valueKey} |
| 211 | + value={codeValue} |
| 212 | + formatValue={format} |
| 213 | + /> |
| 214 | +} |
| 215 | +const CustomWidgetCodeSelectable = extractValue(memo(CustomWidgetCodeSelectableBase)) |
| 216 | + |
| 217 | +export type CustomWidgetsBinding = WidgetsBindingFactory<{}, MuiWidgetsBindingTypes<{}>, MuiWidgetsBindingCustom<{}>> |
| 218 | + |
| 219 | +export const customWidgets: CustomWidgetsBinding = { |
| 220 | + ...widgets, |
| 221 | + types: widgets.types, |
| 222 | + custom: { |
| 223 | + ...widgets.custom, |
| 224 | + Code: CustomWidgetCode, |
| 225 | + CodeSelectable: CustomWidgetCodeSelectable, |
| 226 | + }, |
| 227 | +} |
| 228 | +``` |
0 commit comments