|
| 1 | +import * as CSS from 'csstype' |
1 | 2 | import { EditorState, EditorStateConfig, Compartment, Extension, StateEffect } from '@codemirror/state'
|
2 |
| -import { EditorView, ViewUpdate, keymap, placeholder } from '@codemirror/view' |
| 3 | +import { EditorView, EditorViewConfig, ViewUpdate, keymap, placeholder } from '@codemirror/view' |
3 | 4 | import { indentWithTab } from '@codemirror/commands'
|
4 | 5 | import { indentUnit } from '@codemirror/language'
|
5 |
| -import * as CSS from 'csstype' |
6 | 6 |
|
7 |
| -// state |
8 |
| -export interface EditorStateCreatorOptions { |
9 |
| - config: EditorStateConfig |
10 |
| - onUpdate(viewUpdate: ViewUpdate): void |
| 7 | +export interface CreateStateOptions extends EditorStateConfig { |
11 | 8 | onChange(doc: string, viewUpdate: ViewUpdate): void
|
| 9 | + onUpdate(viewUpdate: ViewUpdate): void |
12 | 10 | onFocus(viewUpdate: ViewUpdate): void
|
13 | 11 | onBlur(viewUpdate: ViewUpdate): void
|
14 | 12 | }
|
15 |
| -export const createState = ({ config, ...events }: EditorStateCreatorOptions): EditorState => { |
16 |
| - const extensions = Array.isArray(config.extensions) ? config.extensions : [config.extensions] |
| 13 | + |
| 14 | +export const createEditorState = ({ onUpdate, onChange, onFocus, onBlur, ...config }: CreateStateOptions) => { |
17 | 15 | return EditorState.create({
|
18 | 16 | doc: config.doc,
|
19 | 17 | selection: config.selection,
|
20 | 18 | extensions: [
|
21 |
| - ...extensions, |
| 19 | + ...(Array.isArray(config.extensions) ? config.extensions : [config.extensions]), |
22 | 20 | EditorView.updateListener.of((viewUpdate) => {
|
23 | 21 | // https://discuss.codemirror.net/t/codemirror-6-proper-way-to-listen-for-changes/2395/11
|
24 |
| - events.onUpdate(viewUpdate) |
| 22 | + onUpdate(viewUpdate) |
| 23 | + // doc changed |
25 | 24 | if (viewUpdate.docChanged) {
|
26 |
| - events.onChange(viewUpdate.state.doc.toString(), viewUpdate) |
| 25 | + onChange(viewUpdate.state.doc.toString(), viewUpdate) |
27 | 26 | }
|
| 27 | + // focus state change |
28 | 28 | if (viewUpdate.focusChanged) {
|
29 |
| - viewUpdate.view.hasFocus ? events.onFocus(viewUpdate) : events.onBlur(viewUpdate) |
| 29 | + viewUpdate.view.hasFocus ? onFocus(viewUpdate) : onBlur(viewUpdate) |
30 | 30 | }
|
31 | 31 | })
|
32 | 32 | ]
|
33 | 33 | })
|
34 | 34 | }
|
35 | 35 |
|
36 |
| -// doc |
37 |
| -export const getDoc = (view: EditorView) => view.state.doc.toString() |
38 |
| -export const setDoc = (view: EditorView, newDoc: string) => { |
39 |
| - return view.dispatch({ |
40 |
| - changes: { |
41 |
| - from: 0, |
42 |
| - to: view.state.doc.length, |
43 |
| - insert: newDoc |
44 |
| - } |
45 |
| - }) |
| 36 | +export const destroyEditorView = (view: EditorView) => view.destroy() |
| 37 | +export const createEditorView = (config: EditorViewConfig) => { |
| 38 | + return new EditorView({ ...config }) |
46 | 39 | }
|
47 | 40 |
|
48 |
| -// effects |
49 |
| -export const destroy = (view: EditorView) => view.destroy() |
50 |
| -export const focus = (view: EditorView) => view.focus() // TODO: focus on the last word |
51 |
| - |
52 | 41 | // https://codemirror.net/examples/config/
|
53 | 42 | // https://github.com/uiwjs/react-codemirror/blob/22cc81971a/src/useCodeMirror.ts#L144
|
54 | 43 | // https://gist.github.com/s-cork/e7104bace090702f6acbc3004228f2cb
|
55 |
| -const rerunCompartment = () => { |
| 44 | +export const createEditorCompartment = (view: EditorView) => { |
56 | 45 | const compartment = new Compartment()
|
57 |
| - const run = (view: EditorView, extension: Extension) => { |
58 |
| - if (compartment.get(view.state)) { |
59 |
| - // reconfigure |
60 |
| - view.dispatch({ effects: compartment.reconfigure(extension) }) |
61 |
| - } else { |
62 |
| - // inject |
63 |
| - view.dispatch({ effects: StateEffect.appendConfig.of(compartment.of(extension)) }) |
64 |
| - } |
| 46 | + const run = (extension: Extension) => { |
| 47 | + compartment.get(view.state) |
| 48 | + ? view.dispatch({ effects: compartment.reconfigure(extension) }) // reconfigure |
| 49 | + : view.dispatch({ effects: StateEffect.appendConfig.of(compartment.of(extension)) }) // inject |
65 | 50 | }
|
66 | 51 | return { compartment, run }
|
67 | 52 | }
|
68 | 53 |
|
69 | 54 | // https://codemirror.net/examples/reconfigure/
|
70 |
| -export const rerunExtension = () => rerunCompartment().run |
71 |
| -export const toggleExtension = (extension: Extension) => { |
72 |
| - const { compartment, run } = rerunCompartment() |
73 |
| - return (view: EditorView, targetApply?: boolean) => { |
| 55 | +export const createEditorExtensionToggler = (view: EditorView, extension: Extension) => { |
| 56 | + const { compartment, run } = createEditorCompartment(view) |
| 57 | + return (targetApply?: boolean) => { |
74 | 58 | const exExtension = compartment.get(view.state)
|
75 | 59 | const apply = targetApply ?? exExtension !== extension
|
76 |
| - run(view, apply ? extension : []) |
| 60 | + run(apply ? extension : []) |
77 | 61 | }
|
78 | 62 | }
|
79 | 63 |
|
80 |
| -// extensions |
81 |
| -export const extensions = { |
82 |
| - placeholder: (string: string) => placeholder(string), |
83 |
| - disable: () => [EditorView.editable.of(false), EditorState.readOnly.of(true)], |
84 |
| - enable: () => [EditorView.editable.of(true), EditorState.readOnly.of(false)], |
| 64 | +export const createEditorTools = (view: EditorView) => { |
| 65 | + // UE state |
| 66 | + const focus = () => view.focus() |
| 67 | + |
| 68 | + // doc state |
| 69 | + const getDoc = () => view.state.doc.toString() |
| 70 | + const setDoc = (newDoc: string) => { |
| 71 | + if (newDoc !== getDoc()) { |
| 72 | + view.dispatch({ |
| 73 | + changes: { |
| 74 | + from: 0, |
| 75 | + to: view.state.doc.length, |
| 76 | + insert: newDoc |
| 77 | + } |
| 78 | + }) |
| 79 | + } |
| 80 | + } |
| 81 | + |
| 82 | + // reconfigure extension |
| 83 | + const { run: reExtensions } = createEditorCompartment(view) |
| 84 | + |
| 85 | + // disabled editor |
| 86 | + const toggleDisabled = createEditorExtensionToggler(view, [ |
| 87 | + EditorView.editable.of(false), |
| 88 | + EditorState.readOnly.of(true) |
| 89 | + ]) |
| 90 | + |
85 | 91 | // https://codemirror.net/examples/tab/
|
86 |
| - indentWithTab: () => keymap.of([indentWithTab]), |
87 |
| - tabSize: (tabSize: number) => [EditorState.tabSize.of(tabSize), indentUnit.of(' '.repeat(tabSize))], |
| 92 | + const toggleIndentWithTab = createEditorExtensionToggler(view, keymap.of([indentWithTab])) |
| 93 | + |
| 94 | + // tab size |
| 95 | + // https://gist.github.com/s-cork/e7104bace090702f6acbc3004228f2cb |
| 96 | + const { run: reTabSize } = createEditorCompartment(view) |
| 97 | + const setTabSize = (tabSize: number) => { |
| 98 | + reTabSize([EditorState.tabSize.of(tabSize), indentUnit.of(' '.repeat(tabSize))]) |
| 99 | + } |
| 100 | + |
| 101 | + // set editor's placeholder |
| 102 | + const { run: rePlaceholder } = createEditorCompartment(view) |
| 103 | + const setPlaceholder = (value: string) => { |
| 104 | + rePlaceholder(placeholder(value)) |
| 105 | + } |
| 106 | + |
| 107 | + // set style to editor element |
88 | 108 | // https://codemirror.net/examples/styling/
|
89 |
| - style: (style: CSS.Properties) => EditorView.theme({ '&': { ...style } }) |
| 109 | + const { run: reStyle } = createEditorCompartment(view) |
| 110 | + const setStyle = (style: CSS.Properties = {}) => { |
| 111 | + reStyle(EditorView.theme({ '&': { ...style } })) |
| 112 | + } |
| 113 | + |
| 114 | + return { |
| 115 | + focus, |
| 116 | + getDoc, |
| 117 | + setDoc, |
| 118 | + reExtensions, |
| 119 | + toggleDisabled, |
| 120 | + toggleIndentWithTab, |
| 121 | + setTabSize, |
| 122 | + setPlaceholder, |
| 123 | + setStyle |
| 124 | + } |
90 | 125 | }
|
0 commit comments