@codemirror-toolkit/react
Advanced tools
Comparing version 0.6.0 to 0.7.0
import type { FunctionComponent, PropsWithChildren } from 'react'; | ||
import type { CodeMirror, GetView, ProvidedCodeMirrorConfig, UseContainerRefHook, UseViewDispatchHook, UseViewEffectHook, UseViewHook } from './types.js'; | ||
import type { CodeMirror, CodeMirrorConfig } from './types'; | ||
export interface CodeMirrorProps { | ||
config?: ProvidedCodeMirrorConfig; | ||
config?: CodeMirrorConfig; | ||
} | ||
@@ -10,14 +10,7 @@ export interface CodeMirrorProviderProps extends PropsWithChildren<CodeMirrorProps> { | ||
} | ||
export type UseCodeMirrorContextHook<ContainerElement extends Element = Element> = () => CodeMirror<ContainerElement>; | ||
export type UseGetViewHook = () => GetView; | ||
export interface CodeMirrorWithContext<ContainerElement extends Element = Element> { | ||
export interface CodeMirrorContext { | ||
Provider: CodeMirrorProvider; | ||
useContext: UseCodeMirrorContextHook<ContainerElement>; | ||
useGetView: UseGetViewHook; | ||
useView: UseViewHook; | ||
useViewEffect: UseViewEffectHook; | ||
useViewDispatch: UseViewDispatchHook; | ||
useContainerRef: UseContainerRefHook<ContainerElement>; | ||
useCodeMirror: () => CodeMirror; | ||
} | ||
export declare function createCodeMirrorWithContext<ContainerElement extends Element>(displayName?: string | false): CodeMirrorWithContext<ContainerElement>; | ||
export declare function createContext(): CodeMirrorContext; | ||
//# sourceMappingURL=context.d.ts.map |
@@ -1,3 +0,3 @@ | ||
import type { CodeMirror, ProvidedCodeMirrorConfig } from './types.js'; | ||
export declare function createCodeMirror<ContainerElement extends Element>(config?: ProvidedCodeMirrorConfig): CodeMirror<ContainerElement>; | ||
import type { CodeMirrorConfig, CodeMirrorWithHooks } from './types'; | ||
export declare function create(config?: CodeMirrorConfig): CodeMirrorWithHooks; | ||
//# sourceMappingURL=create.d.ts.map |
@@ -1,4 +0,6 @@ | ||
export * from './context.js'; | ||
export * from './create.js'; | ||
export * from './types.js'; | ||
export * from './context'; | ||
export * from './core'; | ||
export * from './create'; | ||
export * from './hooks'; | ||
export * from './types'; | ||
//# sourceMappingURL=index.d.ts.map |
@@ -1,152 +0,93 @@ | ||
import { useRef, useCallback, useInsertionEffect, useEffect, useSyncExternalStore, useDebugValue, createContext, createElement, useContext } from "react"; | ||
import * as React from "react"; | ||
import { useRef, createElement, useContext, useState, useEffect } from "react"; | ||
import { EditorView } from "@codemirror/view"; | ||
function queueTask(callback) { | ||
let task = callback; | ||
queueMicrotask(() => task == null ? void 0 : task()); | ||
return () => task = null; | ||
function createScheduler() { | ||
let task = []; | ||
return { | ||
request: (callback) => { | ||
if (!task.length) { | ||
queueMicrotask(() => task.shift()()); | ||
} | ||
task = [callback]; | ||
}, | ||
idle: () => !task.length | ||
}; | ||
} | ||
function createAsyncScheduler() { | ||
let cancelTask; | ||
function createSubject(_value) { | ||
const observers = /* @__PURE__ */ new Set(); | ||
return { | ||
request: (callback) => cancelTask = queueTask(callback), | ||
cancel: () => cancelTask == null ? void 0 : cancelTask() | ||
get value() { | ||
return _value; | ||
}, | ||
getValue: () => _value, | ||
next: (value) => { | ||
if (_value !== value) { | ||
_value = value; | ||
[...observers].forEach((next) => next(value)); | ||
} | ||
}, | ||
subscribe: (next) => { | ||
observers.add(next); | ||
next(_value); | ||
return () => observers.delete(next); | ||
} | ||
}; | ||
} | ||
function isFunction(value) { | ||
return typeof value === "function"; | ||
} | ||
const useIsomorphicInsertionEffect = typeof window !== "undefined" ? useInsertionEffect : useEffect; | ||
function useEffectEvent(event) { | ||
const eventRef = useRef(null); | ||
useIsomorphicInsertionEffect(() => { | ||
eventRef.current = event; | ||
}, [event]); | ||
return useCallback((...args) => { | ||
const fn = eventRef.current; | ||
return fn(...args); | ||
}, []); | ||
} | ||
function createCodeMirror(config) { | ||
let prevState; | ||
let currentView = null; | ||
function createConfig() { | ||
return (isFunction(config) ? config : () => config)(prevState); | ||
function createCodeMirror(initialConfig) { | ||
const config$ = createSubject(initialConfig); | ||
function getViewConfig(prevState) { | ||
const config = config$.value; | ||
return typeof config === "function" ? config(prevState) : { ...config }; | ||
} | ||
const view$ = createSubject(null); | ||
const container$ = createSubject(null); | ||
function createView(container) { | ||
var _a; | ||
const prevState = (_a = view$.value) == null ? void 0 : _a.state; | ||
return new EditorView({ | ||
...createConfig(), | ||
...getViewConfig(prevState), | ||
parent: container | ||
}); | ||
} | ||
const viewChangeCallbacks = /* @__PURE__ */ new Set(); | ||
function subscribeViewChange(callback) { | ||
viewChangeCallbacks.add(callback); | ||
return () => viewChangeCallbacks.delete(callback); | ||
} | ||
function publishViewChange() { | ||
viewChangeCallbacks.forEach((callback) => callback()); | ||
} | ||
function setView(view) { | ||
if (view === currentView) { | ||
return; | ||
} | ||
if (currentView) { | ||
prevState = currentView.state; | ||
currentView.destroy(); | ||
} | ||
currentView = view; | ||
publishViewChange(); | ||
} | ||
const getView = () => currentView; | ||
const getServerView = () => null; | ||
const useView = () => { | ||
const view = useSyncExternalStore(subscribeViewChange, getView, getServerView); | ||
useDebugValue(view); | ||
return view; | ||
}; | ||
const useViewEffect = (setup) => { | ||
const setupEvent = useEffectEvent((view) => view && setup(view)); | ||
useEffect(() => { | ||
let cleanup = setupEvent(getView()); | ||
const unsubscribe = subscribeViewChange(() => { | ||
cleanup == null ? void 0 : cleanup(); | ||
cleanup = setupEvent(getView()); | ||
const scheduler = createScheduler(); | ||
container$.subscribe((container) => { | ||
scheduler.request(() => { | ||
var _a; | ||
(_a = view$.value) == null ? void 0 : _a.destroy(); | ||
view$.next(container && createView(container)); | ||
}); | ||
}); | ||
config$.subscribe(() => { | ||
if (scheduler.idle()) { | ||
scheduler.request(() => { | ||
var _a; | ||
const container = container$.value; | ||
if (container) { | ||
(_a = view$.value) == null ? void 0 : _a.destroy(); | ||
view$.next(createView(container)); | ||
} | ||
}); | ||
return () => { | ||
unsubscribe(); | ||
cleanup == null ? void 0 : cleanup(); | ||
}; | ||
}, [setupEvent]); | ||
}; | ||
const useViewDispatch = () => useCallback((...specs) => { | ||
const view = getView(); | ||
if (!view) { | ||
throw new TypeError("Cannot dispatch transaction without a view"); | ||
} | ||
view.dispatch(...specs); | ||
}, []); | ||
function createContainerRef() { | ||
let currentContainer = null; | ||
const scheduler = createAsyncScheduler(); | ||
return Object.seal({ | ||
get current() { | ||
return currentContainer; | ||
}, | ||
set current(container) { | ||
if (container === currentContainer) { | ||
return; | ||
} | ||
currentContainer = container; | ||
scheduler.cancel(); | ||
scheduler.request(() => { | ||
setView(null); | ||
setView(container && createView(container)); | ||
}); | ||
} | ||
}); | ||
} | ||
let currentContainerRef; | ||
function getContainerRef() { | ||
return currentContainerRef != null ? currentContainerRef : currentContainerRef = createContainerRef(); | ||
} | ||
const useContainerRef = () => { | ||
const containerRef = getContainerRef(); | ||
useDebugValue(containerRef); | ||
return containerRef; | ||
}; | ||
}); | ||
return { | ||
getView, | ||
useView, | ||
useViewEffect, | ||
useViewDispatch, | ||
useContainerRef | ||
getView: view$.getValue, | ||
subscribe: view$.subscribe, | ||
setContainer: container$.next, | ||
setConfig: config$.next | ||
}; | ||
} | ||
function toPascalCase(str) { | ||
return str.split(/[-_]/).map((word) => word && word[0].toUpperCase() + word.slice(1)).join(""); | ||
} | ||
function useSingleton(createInstance) { | ||
const instanceRef = useRef(null); | ||
if (instanceRef.current == null) { | ||
instanceRef.current = createInstance(); | ||
} | ||
return instanceRef.current; | ||
} | ||
function createCodeMirrorWithContext(displayName) { | ||
const InternalCodeMirrorContext = createContext(null); | ||
function createContext() { | ||
const CodeMirrorContext = React.createContext(null); | ||
const CodeMirrorProvider = ({ config, children }) => { | ||
const instance = useSingleton(() => createCodeMirror(config)); | ||
return /* @__PURE__ */ createElement( | ||
InternalCodeMirrorContext.Provider, | ||
{ value: instance }, | ||
children | ||
); | ||
const instanceRef = useRef(null); | ||
function getInstance() { | ||
if (!instanceRef.current) { | ||
instanceRef.current = createCodeMirror(config); | ||
} | ||
return instanceRef.current; | ||
} | ||
return createElement(CodeMirrorContext.Provider, { value: getInstance() }, children); | ||
}; | ||
if (displayName) { | ||
displayName = toPascalCase(displayName); | ||
CodeMirrorProvider.displayName = `${displayName}.Provider`; | ||
InternalCodeMirrorContext.displayName = `Internal${displayName}`; | ||
} | ||
const useCodeMirrorContext = () => { | ||
const instance = useContext(InternalCodeMirrorContext); | ||
function useCodeMirrorContext() { | ||
const instance = useContext(CodeMirrorContext); | ||
if (!instance) { | ||
@@ -158,37 +99,48 @@ throw new Error( | ||
return instance; | ||
}; | ||
const useGetView = () => { | ||
const { getView: getContextView } = useCodeMirrorContext(); | ||
return getContextView; | ||
}; | ||
const useView = () => { | ||
const { useView: useContextView } = useCodeMirrorContext(); | ||
return useContextView(); | ||
}; | ||
const useViewEffect = (setup) => { | ||
const { useViewEffect: useContextViewEffect } = useCodeMirrorContext(); | ||
return useContextViewEffect(setup); | ||
}; | ||
const useViewDispatch = () => { | ||
const { useViewDispatch: useContextViewDispatch } = useCodeMirrorContext(); | ||
return useContextViewDispatch(); | ||
}; | ||
const useContainerRef = () => { | ||
const { useContainerRef: useContextContainerRef } = useCodeMirrorContext(); | ||
return useContextContainerRef(); | ||
}; | ||
} | ||
return { | ||
Provider: CodeMirrorProvider, | ||
useContext: useCodeMirrorContext, | ||
useGetView, | ||
useView, | ||
useViewEffect, | ||
useViewDispatch, | ||
useContainerRef | ||
useCodeMirror: useCodeMirrorContext | ||
}; | ||
} | ||
function useView({ getView, subscribe }) { | ||
const [view, setView] = useState(getView); | ||
useEffect(() => subscribe(setView), [subscribe]); | ||
return view; | ||
} | ||
function defineViewEffect(setup) { | ||
return setup; | ||
} | ||
function useViewEffect({ subscribe }, setup) { | ||
useEffect(() => { | ||
function setupEffect(view) { | ||
return view && setup(view); | ||
} | ||
let cleanup; | ||
const unsubscribe = subscribe((view) => { | ||
cleanup == null ? void 0 : cleanup(); | ||
cleanup = setupEffect(view); | ||
}); | ||
return () => { | ||
unsubscribe(); | ||
cleanup == null ? void 0 : cleanup(); | ||
}; | ||
}, [setup, subscribe]); | ||
} | ||
function create(config) { | ||
const cm = createCodeMirror(config); | ||
return { | ||
...cm, | ||
useView: () => useView(cm), | ||
useViewEffect: (setup) => useViewEffect(cm, setup) | ||
}; | ||
} | ||
export { | ||
create, | ||
createCodeMirror, | ||
createCodeMirrorWithContext | ||
createContext, | ||
defineViewEffect, | ||
useView, | ||
useViewEffect | ||
}; | ||
//# sourceMappingURL=index.js.map |
import type { EditorState } from '@codemirror/state'; | ||
import type { EditorView, EditorViewConfig } from '@codemirror/view'; | ||
import type { EffectCallback, MutableRefObject } from 'react'; | ||
export type EditorViewConfigWithoutParentElement = Omit<EditorViewConfig, 'parent'>; | ||
export interface CodeMirrorConfig extends EditorViewConfigWithoutParentElement { | ||
import type { EffectCallback } from 'react'; | ||
export type EditorViewConfigCreator = (prevState: EditorState | undefined) => EditorViewConfig; | ||
export type CodeMirrorConfig = EditorViewConfig | EditorViewConfigCreator; | ||
export type ViewChangeHandler = (view: EditorView | null) => void; | ||
export type ViewContainer = Element | DocumentFragment; | ||
export interface CodeMirror { | ||
getView: () => EditorView | null; | ||
subscribe: (onViewChange: ViewChangeHandler) => () => void; | ||
setContainer: (container: ViewContainer | null) => void; | ||
setConfig: (config: CodeMirrorConfig) => void; | ||
} | ||
export type CodeMirrorConfigCreator = (prevState: EditorState | undefined) => CodeMirrorConfig; | ||
export type ProvidedCodeMirrorConfig = CodeMirrorConfig | CodeMirrorConfigCreator; | ||
export type GetView = () => EditorView | null; | ||
export type UseViewHook = () => EditorView | null; | ||
export type ViewEffectCleanup = ReturnType<EffectCallback>; | ||
export type ViewEffectSetup = (view: EditorView) => ViewEffectCleanup; | ||
export type UseViewEffectHook = (setup: ViewEffectSetup) => void; | ||
export type ViewDispath = typeof EditorView.prototype.dispatch; | ||
export type UseViewDispatchHook = () => ViewDispath; | ||
export type ContainerRef<ContainerElement extends Element = Element> = MutableRefObject<ContainerElement | null>; | ||
export type UseContainerRefHook<ContainerElement extends Element = Element> = () => ContainerRef<ContainerElement>; | ||
export interface CodeMirror<ContainerElement extends Element = Element> { | ||
getView: GetView; | ||
useView: UseViewHook; | ||
useViewEffect: UseViewEffectHook; | ||
useViewDispatch: UseViewDispatchHook; | ||
useContainerRef: UseContainerRefHook<ContainerElement>; | ||
export interface CodeMirrorHooks { | ||
useView: () => EditorView | null; | ||
useViewEffect: (setup: ViewEffectSetup) => void; | ||
} | ||
export type CodeMirrorWithHooks = CodeMirror & CodeMirrorHooks; | ||
//# sourceMappingURL=types.d.ts.map |
{ | ||
"name": "@codemirror-toolkit/react", | ||
"version": "0.6.0", | ||
"version": "0.7.0", | ||
"description": "A small and flexible solution for binding CodeMirror 6 to React", | ||
@@ -50,6 +50,6 @@ "type": "module", | ||
"@codemirror/view": "^6.0.0", | ||
"@types/react": "^18.0.0", | ||
"@types/react-dom": "^18.0.0", | ||
"react": "^18.0.0", | ||
"react-dom": "^18.0.0" | ||
"@types/react": ">=16.7.0", | ||
"@types/react-dom": ">=16.7.0", | ||
"react": ">=16.8.0", | ||
"react-dom": ">=16.8.0" | ||
}, | ||
@@ -65,10 +65,10 @@ "peerDependenciesMeta": { | ||
"devDependencies": { | ||
"@codemirror/state": "^6.3.3", | ||
"@codemirror/view": "^6.22.3", | ||
"@testing-library/react": "^14.1.2", | ||
"@types/react": "^18.2.45", | ||
"@types/react-dom": "^18.2.18", | ||
"react": "^18.2.0", | ||
"react-dom": "^18.2.0" | ||
"@codemirror/state": "^6.4.1", | ||
"@codemirror/view": "^6.33.0", | ||
"@testing-library/react": "^16.0.1", | ||
"@types/react": "^18.3.5", | ||
"@types/react-dom": "^18.3.0", | ||
"react": "^18.3.1", | ||
"react-dom": "^18.3.1" | ||
} | ||
} |
300
README.md
@@ -24,2 +24,5 @@ # @codemirror-toolkit/react | ||
yarn add @codemirror-toolkit/react | ||
# pnpm | ||
pnpm add @codemirror-toolkit/react | ||
``` | ||
@@ -29,2 +32,106 @@ | ||
<details> | ||
<summary><h2>Migrate from 0.6.x</h2></summary> | ||
- `createCodeMirror` is refactored to a core function without hooks. Use `create` instead for a similar functionality with hooks. | ||
Before: | ||
```typescript | ||
const cm = createCodeMirror(config) | ||
``` | ||
After: | ||
```typescript | ||
const cm = create(config) | ||
``` | ||
- `create` now provides `useView` and `useViewEffect` hooks. | ||
Before: | ||
```typescript | ||
const { useView, useViewEffect } = createCodeMirror(config) | ||
``` | ||
After: | ||
```typescript | ||
const { useView, useViewEffect } = create(config) | ||
``` | ||
- `createCodeMirrorContext` is renamed to `createContext` and does not provide hooks directly. Use `useCodeMirror` to access the CodeMirror instance, then use the exported hooks with this instance. | ||
Before: | ||
```typescript | ||
const { Provider, useView, useViewEffect } = createCodeMirrorContext() | ||
``` | ||
After: | ||
```typescript | ||
import { useView, useViewEffect } from '@codemirror-toolkit/react' | ||
const { Provider, useCodeMirror } = createContext() | ||
// Then in your component: | ||
const cm = useCodeMirror() | ||
useView(cm) | ||
useViewEffect(cm, effectSetup) | ||
``` | ||
- `useViewEffect` now requires the setup function to be memoized or have a stable reference to prevent the effect from firing on every render. | ||
Before: | ||
```typescript | ||
useViewEffect((view) => { | ||
// Effect logic | ||
}) | ||
``` | ||
After: | ||
<!-- prettier-ignore --> | ||
```typescript | ||
const effectSetup = useCallback((view) => { | ||
// Effect logic | ||
}, [/* dependencies */]) | ||
useViewEffect(cm, effectSetup) | ||
``` | ||
- The `useContainerRef` hook has been replaced with a `setContainer` function. | ||
Before: | ||
```typescript | ||
const { useContainerRef } = createCodeMirror(config) | ||
function Editor() { | ||
const containerRef = useContainerRef() | ||
return <div ref={containerRef} /> | ||
} | ||
``` | ||
After: | ||
```typescript | ||
const { setContainer } = create(config) | ||
function Editor() { | ||
return <div ref={setContainer} /> | ||
} | ||
``` | ||
- Configuration can now be set using `setConfig`. | ||
```typescript | ||
const { setConfig } = create() | ||
setConfig(config) | ||
``` | ||
</details> | ||
## Usage | ||
@@ -34,78 +141,78 @@ | ||
<!-- prettier-ignore --> | ||
```ts | ||
import { createCodeMirror } from '@codemirror-toolkit/react' | ||
import { create } from '@codemirror-toolkit/react' | ||
const codeMirror = createCodeMirror<HTMLDivElement>((prevState) => ({ | ||
doc: prevState?.doc ?? 'Hello World!', | ||
// ...otherConfig, | ||
const cm = create((prevState) => ({ | ||
state: prevState, // useful for HMR | ||
doc: 'Hello World!', | ||
})) | ||
// if you want to use them in other files | ||
export const { useViewEffect, useContainerRef /* ... */ } = codeMirror | ||
export const { | ||
getView, | ||
useView, | ||
useViewEffect, | ||
setContainer, | ||
setConfig, | ||
subscribe, | ||
} = cm | ||
``` | ||
Then bind your components with the hooks: | ||
Then bind your components with a callback ref: | ||
```tsx | ||
function Editor() { | ||
const containerRef = useContainerRef() | ||
return <div ref={containerRef} /> | ||
return <div ref={setContainer} /> | ||
} | ||
``` | ||
function App() { | ||
const [showEditor, setShowEditor] = useState(true) | ||
const [lastInput, setLastInput] = useState('') | ||
useViewEffect((view) => { | ||
console.log('EditorView is created') | ||
return () => { | ||
console.log('EditorView is destroyed') | ||
// expect(view.dom.parentElement).toBeNull() | ||
setLastInput(view.state.doc.toString()) | ||
### Initiate with data from component | ||
#### Option 1: | ||
<!-- prettier-ignore --> | ||
```tsx | ||
import { create } from '@codemirror-toolkit/react' | ||
import { useCallback } from 'react' | ||
const { setContainer, setConfig } = create() | ||
interface Props { | ||
initialInput: string | ||
} | ||
function Editor({ initialInput }: Props) { | ||
const ref = useCallback((node: HTMLDivElement | null) => { | ||
setContainer(node) | ||
if (node) { | ||
setConfig({ | ||
doc: initialInput, | ||
}) | ||
} | ||
}) | ||
return ( | ||
<> | ||
<button onClick={() => setShowEditor(!showEditor)}> | ||
{showEditor ? 'Destroy' : 'Create'} Editor | ||
</button> | ||
{showEditor ? ( | ||
<Editor /> | ||
) : ( | ||
<div> | ||
<p>Editor destroyed</p> | ||
<p>Last input: {lastInput}</p> | ||
</div> | ||
)} | ||
</> | ||
) | ||
}, [initialInput]) | ||
return <div ref={ref} /> | ||
} | ||
``` | ||
:warning: An instance of `EditorView` will be created **only when** a DOM node is assigned to `containerRef.current`, and will be destroyed **only when** `containerRef.current` is set back to `null`. | ||
#### Option 2: | ||
Use `createContext`, see below. | ||
### With Context Provider | ||
All the functions and hooks created with `createCodeMirror` don't require a context provider to use in different components, but in some cases you may want to instantiate `EditorView` with props from a component. In this case, you can use `createCodeMirrorWithContext` to create an instance within a context: | ||
```tsx | ||
import { createCodeMirrorWithContext } from '@codemirror-toolkit/react' | ||
import { createContext } from '@codemirror-toolkit/react' | ||
const { | ||
Provider: CodeMirrorProvider, | ||
useView, | ||
useContainerRef, | ||
// ... | ||
} = createCodeMirrorWithContext<HTMLDivElement>('CodeMirrorContext') | ||
const { Provider: CodeMirrorProvider, useCodeMirror } = createContext() | ||
function MenuBar() { | ||
const view = useView() | ||
// ... | ||
function Editor() { | ||
const { setContainer } = useCodeMirror() | ||
return <div ref={setContainer} /> | ||
} | ||
function Editor() { | ||
const containerRef = useContainerRef() | ||
return <div ref={containerRef} /> | ||
interface Props { | ||
initialInput: string | ||
} | ||
function App({ initialInput }: { initialInput: string }) { | ||
function EditorWrapper({ initialInput }: Props) { | ||
return ( | ||
@@ -115,5 +222,3 @@ <CodeMirrorProvider | ||
doc: initialInput, | ||
// ...otherConfig, | ||
}}> | ||
<MenuBar /> | ||
<Editor /> | ||
@@ -129,4 +234,2 @@ </CodeMirrorProvider> | ||
There are only two functions exported: `createCodeMirror` and `createCodeMirrorWithContext`. | ||
### Common Types | ||
@@ -137,32 +240,27 @@ | ||
import type { EditorView, EditorViewConfig } from '@codemirror/view' | ||
import type { EffectCallback, MutableRefObject } from 'react' | ||
import type { EffectCallback } from 'react' | ||
type EditorViewConfigWithoutParentElement = Omit<EditorViewConfig, 'parent'> | ||
interface CodeMirrorConfig extends EditorViewConfigWithoutParentElement {} | ||
type EditorViewConfigCreator = (prevState: EditorState | undefined) => EditorViewConfig | ||
type CodeMirrorConfig = EditorViewConfig | EditorViewConfigCreator | ||
type CodeMirrorConfigCreator = (prevState: EditorState | undefined) => CodeMirrorConfig | ||
type ProvidedCodeMirrorConfig = CodeMirrorConfig | CodeMirrorConfigCreator | ||
type ViewChangeHandler = (view: EditorView | null) => void | ||
type GetView = () => EditorView | null | ||
type UseViewHook = () => EditorView | null | ||
type ViewContainer = Element | DocumentFragment | ||
interface CodeMirror { | ||
getView: () => EditorView | null | ||
subscribe: (onViewChange: ViewChangeHandler) => () => void | ||
setContainer: (container: ViewContainer | null) => void | ||
setConfig: (config: CodeMirrorConfig) => void | ||
} | ||
type ViewEffectCleanup = ReturnType<EffectCallback> | ||
type ViewEffectSetup = (view: EditorView) => ViewEffectCleanup | ||
type UseViewEffectHook = (setup: ViewEffectSetup) => void | ||
type ViewDispath = typeof EditorView.prototype.dispatch | ||
type UseViewDispatchHook = () => ViewDispath | ||
interface CodeMirrorHooks { | ||
useView: () => EditorView | null | ||
useViewEffect: (setup: ViewEffectSetup) => void | ||
} | ||
type ContainerRef<ContainerElement extends Element = Element> = | ||
MutableRefObject<ContainerElement | null> | ||
type UseContainerRefHook<ContainerElement extends Element = Element> = | ||
() => ContainerRef<ContainerElement> | ||
interface CodeMirror<ContainerElement extends Element = Element> { | ||
getView: GetView | ||
useView: UseViewHook | ||
useViewEffect: UseViewEffectHook | ||
useViewDispatch: UseViewDispatchHook | ||
useContainerRef: UseContainerRefHook<ContainerElement> | ||
} | ||
type CodeMirrorWithHooks = CodeMirror & CodeMirrorHooks | ||
``` | ||
@@ -173,40 +271,48 @@ | ||
```ts | ||
function createCodeMirror<ContainerElement extends Element>( | ||
config?: ProvidedCodeMirrorConfig, | ||
): CodeMirror<ContainerElement> | ||
function createCodeMirror(initialConfig?: CodeMirrorConfig): CodeMirror | ||
``` | ||
### `createCodeMirrorWithContext` | ||
### `create` | ||
```ts | ||
function create(config?: CodeMirrorConfig): CodeMirrorWithHooks | ||
``` | ||
### `createContext` | ||
```ts | ||
import type { FunctionComponent, PropsWithChildren } from 'react' | ||
interface CodeMirrorProps { | ||
config?: ProvidedCodeMirrorConfig | ||
config?: CodeMirrorConfig | ||
} | ||
interface CodeMirrorProviderProps extends PropsWithChildren<CodeMirrorProps> {} | ||
interface CodeMirrorProvider extends FunctionComponent<CodeMirrorProviderProps> {} | ||
type UseCodeMirrorContextHook<ContainerElement extends Element = Element> = | ||
() => CodeMirror<ContainerElement> | ||
type UseGetViewHook = () => GetView | ||
interface CodeMirrorWithContext<ContainerElement extends Element = Element> { | ||
interface CodeMirrorContext { | ||
Provider: CodeMirrorProvider | ||
useContext: UseCodeMirrorContextHook<ContainerElement> | ||
useGetView: UseGetViewHook | ||
useView: UseViewHook | ||
useViewEffect: UseViewEffectHook | ||
useViewDispatch: UseViewDispatchHook | ||
useContainerRef: UseContainerRefHook<ContainerElement> | ||
useCodeMirror: () => CodeMirror | ||
} | ||
function createCodeMirrorWithContext<ContainerElement extends Element>( | ||
displayName?: string | false, | ||
): CodeMirrorWithContext<ContainerElement> | ||
function createContext(): CodeMirrorContext | ||
``` | ||
### `useView` | ||
```ts | ||
function useView(cm: CodeMirror): EditorView | null | ||
``` | ||
### `useViewEffect` | ||
```ts | ||
function defineViewEffect(setup: ViewEffectSetup): ViewEffectSetup | ||
function useViewEffect(cm: CodeMirror, setup: ViewEffectSetup): void | ||
``` | ||
## License | ||
MIT License @ 2022-Present [Xuanbo Cheng](https://github.com/exuanbo) |
import type { FunctionComponent, PropsWithChildren } from 'react' | ||
import { createContext, createElement, useContext } from 'react' | ||
import * as React from 'react' | ||
import { createElement, useContext, useRef } from 'react' | ||
import { createCodeMirror } from './create.js' | ||
import type { | ||
CodeMirror, | ||
GetView, | ||
ProvidedCodeMirrorConfig, | ||
UseContainerRefHook, | ||
UseViewDispatchHook, | ||
UseViewEffectHook, | ||
UseViewHook, | ||
} from './types.js' | ||
import { toPascalCase } from './utils/toPascalCase.js' | ||
import { useSingleton } from './utils/useSingleton.js' | ||
import { createCodeMirror } from './core' | ||
import type { CodeMirror, CodeMirrorConfig } from './types' | ||
export interface CodeMirrorProps { | ||
config?: ProvidedCodeMirrorConfig | ||
config?: CodeMirrorConfig | ||
} | ||
@@ -25,39 +16,23 @@ | ||
export type UseCodeMirrorContextHook<ContainerElement extends Element = Element> = | ||
() => CodeMirror<ContainerElement> | ||
export type UseGetViewHook = () => GetView | ||
export interface CodeMirrorWithContext<ContainerElement extends Element = Element> { | ||
export interface CodeMirrorContext { | ||
Provider: CodeMirrorProvider | ||
useContext: UseCodeMirrorContextHook<ContainerElement> | ||
useGetView: UseGetViewHook | ||
useView: UseViewHook | ||
useViewEffect: UseViewEffectHook | ||
useViewDispatch: UseViewDispatchHook | ||
useContainerRef: UseContainerRefHook<ContainerElement> | ||
useCodeMirror: () => CodeMirror | ||
} | ||
export function createCodeMirrorWithContext<ContainerElement extends Element>( | ||
displayName?: string | false, | ||
): CodeMirrorWithContext<ContainerElement> { | ||
const InternalCodeMirrorContext = createContext<CodeMirror<ContainerElement> | null>(null) | ||
export function createContext(): CodeMirrorContext { | ||
const CodeMirrorContext = React.createContext<CodeMirror | null>(null) | ||
const CodeMirrorProvider: CodeMirrorProvider = ({ config, children }) => { | ||
const instance = useSingleton(() => createCodeMirror<ContainerElement>(config)) | ||
return /*#__PURE__*/ createElement( | ||
InternalCodeMirrorContext.Provider, | ||
{ value: instance }, | ||
children, | ||
) | ||
const instanceRef = useRef<CodeMirror | null>(null) | ||
function getInstance() { | ||
if (!instanceRef.current) { | ||
instanceRef.current = createCodeMirror(config) | ||
} | ||
return instanceRef.current | ||
} | ||
return createElement(CodeMirrorContext.Provider, { value: getInstance() }, children) | ||
} | ||
if (displayName) { | ||
displayName = toPascalCase(displayName) | ||
CodeMirrorProvider.displayName = `${displayName}.Provider` | ||
InternalCodeMirrorContext.displayName = `Internal${displayName}` | ||
} | ||
const useCodeMirrorContext: UseCodeMirrorContextHook<ContainerElement> = () => { | ||
const instance = useContext(InternalCodeMirrorContext) | ||
function useCodeMirrorContext() { | ||
const instance = useContext(CodeMirrorContext) | ||
if (!instance) { | ||
@@ -71,36 +46,6 @@ throw new Error( | ||
const useGetView: UseGetViewHook = () => { | ||
const { getView: getContextView } = useCodeMirrorContext() | ||
return getContextView | ||
} | ||
const useView: UseViewHook = () => { | ||
const { useView: useContextView } = useCodeMirrorContext() | ||
return useContextView() | ||
} | ||
const useViewEffect: UseViewEffectHook = (setup) => { | ||
const { useViewEffect: useContextViewEffect } = useCodeMirrorContext() | ||
return useContextViewEffect(setup) | ||
} | ||
const useViewDispatch: UseViewDispatchHook = () => { | ||
const { useViewDispatch: useContextViewDispatch } = useCodeMirrorContext() | ||
return useContextViewDispatch() | ||
} | ||
const useContainerRef: UseContainerRefHook<ContainerElement> = () => { | ||
const { useContainerRef: useContextContainerRef } = useCodeMirrorContext() | ||
return useContextContainerRef() | ||
} | ||
return { | ||
Provider: CodeMirrorProvider, | ||
useContext: useCodeMirrorContext, | ||
useGetView, | ||
useView, | ||
useViewEffect, | ||
useViewDispatch, | ||
useContainerRef, | ||
useCodeMirror: useCodeMirrorContext, | ||
} | ||
} |
@@ -1,137 +0,12 @@ | ||
import type { EditorState } from '@codemirror/state' | ||
import { EditorView } from '@codemirror/view' | ||
import { useCallback, useDebugValue, useEffect, useSyncExternalStore } from 'react' | ||
import { createCodeMirror } from './core' | ||
import { useView, useViewEffect } from './hooks' | ||
import type { CodeMirrorConfig, CodeMirrorWithHooks } from './types' | ||
import type { | ||
CodeMirror, | ||
ContainerRef, | ||
GetView, | ||
ProvidedCodeMirrorConfig, | ||
UseContainerRefHook, | ||
UseViewDispatchHook, | ||
UseViewEffectHook, | ||
UseViewHook, | ||
} from './types.js' | ||
import { createAsyncScheduler } from './utils/asyncScheduler.js' | ||
import { isFunction } from './utils/isFunction.js' | ||
import { useEffectEvent } from './utils/useEffectEventShim.js' | ||
export function createCodeMirror<ContainerElement extends Element>( | ||
config?: ProvidedCodeMirrorConfig, | ||
): CodeMirror<ContainerElement> { | ||
let prevState: EditorState | undefined | ||
let currentView: EditorView | null = null | ||
function createConfig() { | ||
return (isFunction(config) ? config : () => config)(prevState) | ||
} | ||
function createView(container: ContainerElement) { | ||
return new EditorView({ | ||
...createConfig(), | ||
parent: container, | ||
}) | ||
} | ||
type ViewChangeCallback = () => void | ||
const viewChangeCallbacks = new Set<ViewChangeCallback>() | ||
type UnsubscribeViewChange = () => void | ||
function subscribeViewChange(callback: ViewChangeCallback): UnsubscribeViewChange { | ||
viewChangeCallbacks.add(callback) | ||
return () => viewChangeCallbacks.delete(callback) | ||
} | ||
function publishViewChange() { | ||
viewChangeCallbacks.forEach((callback) => callback()) | ||
} | ||
function setView(view: EditorView | null) { | ||
if (view === currentView) { | ||
return | ||
} | ||
if (currentView) { | ||
prevState = currentView.state | ||
currentView.destroy() | ||
} | ||
currentView = view | ||
publishViewChange() | ||
} | ||
const getView: GetView = () => currentView | ||
/* v8 ignore next */ | ||
const getServerView: GetView = () => null | ||
const useView: UseViewHook = () => { | ||
const view = useSyncExternalStore(subscribeViewChange, getView, getServerView) | ||
useDebugValue(view) | ||
return view | ||
} | ||
const useViewEffect: UseViewEffectHook = (setup) => { | ||
const setupEvent = useEffectEvent((view: EditorView | null) => view && setup(view)) | ||
useEffect(() => { | ||
let cleanup = setupEvent(getView()) | ||
const unsubscribe = subscribeViewChange(() => { | ||
cleanup?.() | ||
cleanup = setupEvent(getView()) | ||
}) | ||
return () => { | ||
unsubscribe() | ||
cleanup?.() | ||
} | ||
}, [setupEvent]) | ||
} | ||
const useViewDispatch: UseViewDispatchHook = () => | ||
useCallback((...specs) => { | ||
const view = getView() | ||
if (!view) { | ||
throw new TypeError('Cannot dispatch transaction without a view') | ||
} | ||
// @ts-expect-error: overloaded signature | ||
view.dispatch(...specs) | ||
}, []) | ||
function createContainerRef(): ContainerRef<ContainerElement> { | ||
let currentContainer: ContainerElement | null = null | ||
const scheduler = createAsyncScheduler() | ||
return Object.seal({ | ||
get current() { | ||
return currentContainer | ||
}, | ||
set current(container) { | ||
if (container === currentContainer) { | ||
return | ||
} | ||
currentContainer = container | ||
scheduler.cancel() | ||
scheduler.request(() => { | ||
setView(null) | ||
setView(container && createView(container)) | ||
}) | ||
}, | ||
}) | ||
} | ||
let currentContainerRef: ContainerRef<ContainerElement> | undefined | ||
function getContainerRef() { | ||
return currentContainerRef ?? (currentContainerRef = createContainerRef()) | ||
} | ||
const useContainerRef: UseContainerRefHook<ContainerElement> = () => { | ||
const containerRef = getContainerRef() | ||
useDebugValue(containerRef) | ||
return containerRef | ||
} | ||
export function create(config?: CodeMirrorConfig): CodeMirrorWithHooks { | ||
const cm = createCodeMirror(config) | ||
return { | ||
getView, | ||
useView, | ||
useViewEffect, | ||
useViewDispatch, | ||
useContainerRef, | ||
...cm, | ||
useView: () => useView(cm), | ||
useViewEffect: (setup) => useViewEffect(cm, setup), | ||
} | ||
} |
@@ -1,3 +0,5 @@ | ||
export * from './context.js' | ||
export * from './create.js' | ||
export * from './types.js' | ||
export * from './context' | ||
export * from './core' | ||
export * from './create' | ||
export * from './hooks' | ||
export * from './types' |
import type { EditorState } from '@codemirror/state' | ||
import type { EditorView, EditorViewConfig } from '@codemirror/view' | ||
import type { EffectCallback, MutableRefObject } from 'react' | ||
import type { EffectCallback } from 'react' | ||
export type EditorViewConfigWithoutParentElement = Omit<EditorViewConfig, 'parent'> | ||
export interface CodeMirrorConfig extends EditorViewConfigWithoutParentElement {} | ||
export type EditorViewConfigCreator = (prevState: EditorState | undefined) => EditorViewConfig | ||
export type CodeMirrorConfig = EditorViewConfig | EditorViewConfigCreator | ||
export type CodeMirrorConfigCreator = (prevState: EditorState | undefined) => CodeMirrorConfig | ||
export type ProvidedCodeMirrorConfig = CodeMirrorConfig | CodeMirrorConfigCreator | ||
export type ViewChangeHandler = (view: EditorView | null) => void | ||
export type GetView = () => EditorView | null | ||
export type UseViewHook = () => EditorView | null | ||
export type ViewContainer = Element | DocumentFragment | ||
export interface CodeMirror { | ||
getView: () => EditorView | null | ||
subscribe: (onViewChange: ViewChangeHandler) => () => void | ||
setContainer: (container: ViewContainer | null) => void | ||
setConfig: (config: CodeMirrorConfig) => void | ||
} | ||
export type ViewEffectCleanup = ReturnType<EffectCallback> | ||
export type ViewEffectSetup = (view: EditorView) => ViewEffectCleanup | ||
export type UseViewEffectHook = (setup: ViewEffectSetup) => void | ||
export type ViewDispath = typeof EditorView.prototype.dispatch | ||
export type UseViewDispatchHook = () => ViewDispath | ||
export interface CodeMirrorHooks { | ||
useView: () => EditorView | null | ||
useViewEffect: (setup: ViewEffectSetup) => void | ||
} | ||
export type ContainerRef<ContainerElement extends Element = Element> = | ||
MutableRefObject<ContainerElement | null> | ||
export type UseContainerRefHook<ContainerElement extends Element = Element> = | ||
() => ContainerRef<ContainerElement> | ||
export interface CodeMirror<ContainerElement extends Element = Element> { | ||
getView: GetView | ||
useView: UseViewHook | ||
useViewEffect: UseViewEffectHook | ||
useViewDispatch: UseViewDispatchHook | ||
useContainerRef: UseContainerRefHook<ContainerElement> | ||
} | ||
export type CodeMirrorWithHooks = CodeMirror & CodeMirrorHooks |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
41
312
62284
904