@canvist/react 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mod.d.ts +90 -0
- package/dist/mod.js +162 -0
- package/package.json +40 -5
- package/readme.md +61 -0
- package/README.md +0 -3
package/dist/mod.d.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React bindings for the canvist canvas editor.
|
|
3
|
+
*
|
|
4
|
+
* `CanvistEditor` renders an editor; `useCanvistEditor` gives you the
|
|
5
|
+
* editor itself, and `useEditorState` re-renders a component when
|
|
6
|
+
* something it reads from the editor changes. `BubbleMenu` and
|
|
7
|
+
* `FloatingMenu` keep their children next to the selection. Commands, queries, and
|
|
8
|
+
* plugin APIs are typed from the extensions and plugins you pass, as in
|
|
9
|
+
* `@canvist/web`.
|
|
10
|
+
*
|
|
11
|
+
* ```tsx
|
|
12
|
+
* function Notes() {
|
|
13
|
+
* const { ref, editor } = useCanvistEditor({
|
|
14
|
+
* content: { format: "markdown", input: "Hello **world**" },
|
|
15
|
+
* });
|
|
16
|
+
* const bold = useEditorState(editor, (current) => current.can.toggleBold(), false);
|
|
17
|
+
*
|
|
18
|
+
* return (
|
|
19
|
+
* <>
|
|
20
|
+
* <button disabled={!bold} onClick={() => editor?.commands.toggleBold()}>Bold</button>
|
|
21
|
+
* <div ref={ref} />
|
|
22
|
+
* </>
|
|
23
|
+
* );
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* @module
|
|
28
|
+
*/
|
|
29
|
+
import { type Editor, type EditorOptions, type Extensions, type Plugins, type StarterKit } from "@canvist/web";
|
|
30
|
+
import { type MenuOptions } from "@canvist/web/menus";
|
|
31
|
+
import { type CSSProperties, type ReactElement, type ReactNode, type RefCallback } from "react";
|
|
32
|
+
/** What {@link useCanvistEditor} returns. */
|
|
33
|
+
export interface CanvistEditorHandle<E extends Extensions, P extends Plugins> {
|
|
34
|
+
/** Attach to the element the editor renders into. */
|
|
35
|
+
readonly ref: RefCallback<HTMLElement>;
|
|
36
|
+
/** The editor, once it has loaded; `null` before that and after unmounting. */
|
|
37
|
+
readonly editor: Editor<E, P> | null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Create an editor in the element `ref` is attached to, and destroy it
|
|
41
|
+
* when the element goes away. `options` are read when the editor is
|
|
42
|
+
* created; later changes to them have no effect.
|
|
43
|
+
*/
|
|
44
|
+
export declare function useCanvistEditor<const E extends Extensions = StarterKit, const P extends Plugins = []>(options?: EditorOptions<E, P>): CanvistEditorHandle<E, P>;
|
|
45
|
+
/**
|
|
46
|
+
* Read `select(editor)` and re-render whenever it changes, after any edit
|
|
47
|
+
* or selection change. `select` must return a primitive or a value that
|
|
48
|
+
* stays the same object while nothing changed, as with
|
|
49
|
+
* `useSyncExternalStore`. Until the editor exists, the value is
|
|
50
|
+
* `fallback`.
|
|
51
|
+
*/
|
|
52
|
+
export declare function useEditorState<E extends Extensions, P extends Plugins, T>(editor: Editor<E, P> | null, select: (editor: Editor<E, P>) => T, fallback: T): T;
|
|
53
|
+
/**
|
|
54
|
+
* Props for {@link CanvistEditor}: the editor's options, read once except
|
|
55
|
+
* `editable` and `placeholder`, which follow the props, and the element's.
|
|
56
|
+
*/
|
|
57
|
+
export interface CanvistEditorProps<E extends Extensions, P extends Plugins> extends EditorOptions<E, P> {
|
|
58
|
+
readonly className?: string;
|
|
59
|
+
readonly style?: CSSProperties;
|
|
60
|
+
/**
|
|
61
|
+
* Server-rendered HTML (`.cv-doc` markup from canvist's renderer) to
|
|
62
|
+
* show until the editor takes over. The editor reads the document from
|
|
63
|
+
* it when `content` is not given.
|
|
64
|
+
*/
|
|
65
|
+
readonly html?: string;
|
|
66
|
+
/** Called once the editor has loaded. */
|
|
67
|
+
readonly onReady?: (editor: Editor<E, P>) => void;
|
|
68
|
+
/** Called after every change to the document or selection. */
|
|
69
|
+
readonly onUpdate?: (editor: Editor<E, P>) => void;
|
|
70
|
+
}
|
|
71
|
+
/** A canvist editor. Give it a width; without a height it grows with its document. */
|
|
72
|
+
export declare function CanvistEditor<const E extends Extensions = StarterKit, const P extends Plugins = []>(props: CanvistEditorProps<E, P>): ReactElement;
|
|
73
|
+
/** Props for {@link BubbleMenu} and {@link FloatingMenu}. */
|
|
74
|
+
export interface MenuProps<E extends Extensions, P extends Plugins> extends MenuOptions<E, P> {
|
|
75
|
+
/** The editor to follow; the menu stays hidden while it is `null`. */
|
|
76
|
+
readonly editor: Editor<E, P> | null;
|
|
77
|
+
readonly className?: string;
|
|
78
|
+
readonly style?: CSSProperties;
|
|
79
|
+
readonly children?: ReactNode;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* A menu over selected text, as `bubbleMenu` from `@canvist/web/menus`
|
|
83
|
+
* places it. Pressing inside it keeps the editor's focus and selection.
|
|
84
|
+
*/
|
|
85
|
+
export declare function BubbleMenu<E extends Extensions, P extends Plugins>(props: MenuProps<E, P>): ReactElement;
|
|
86
|
+
/**
|
|
87
|
+
* A menu beside the caret on an empty line, as `floatingMenu` from
|
|
88
|
+
* `@canvist/web/menus` places it.
|
|
89
|
+
*/
|
|
90
|
+
export declare function FloatingMenu<E extends Extensions, P extends Plugins>(props: MenuProps<E, P>): ReactElement;
|
package/dist/mod.js
ADDED
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* React bindings for the canvist canvas editor.
|
|
4
|
+
*
|
|
5
|
+
* `CanvistEditor` renders an editor; `useCanvistEditor` gives you the
|
|
6
|
+
* editor itself, and `useEditorState` re-renders a component when
|
|
7
|
+
* something it reads from the editor changes. `BubbleMenu` and
|
|
8
|
+
* `FloatingMenu` keep their children next to the selection. Commands, queries, and
|
|
9
|
+
* plugin APIs are typed from the extensions and plugins you pass, as in
|
|
10
|
+
* `@canvist/web`.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* function Notes() {
|
|
14
|
+
* const { ref, editor } = useCanvistEditor({
|
|
15
|
+
* content: { format: "markdown", input: "Hello **world**" },
|
|
16
|
+
* });
|
|
17
|
+
* const bold = useEditorState(editor, (current) => current.can.toggleBold(), false);
|
|
18
|
+
*
|
|
19
|
+
* return (
|
|
20
|
+
* <>
|
|
21
|
+
* <button disabled={!bold} onClick={() => editor?.commands.toggleBold()}>Bold</button>
|
|
22
|
+
* <div ref={ref} />
|
|
23
|
+
* </>
|
|
24
|
+
* );
|
|
25
|
+
* }
|
|
26
|
+
* ```
|
|
27
|
+
*
|
|
28
|
+
* @module
|
|
29
|
+
*/
|
|
30
|
+
import { createEditor, } from "@canvist/web";
|
|
31
|
+
import { bubbleMenu, floatingMenu, } from "@canvist/web/menus";
|
|
32
|
+
import { useCallback, useEffect, useRef, useState, useSyncExternalStore, } from "react";
|
|
33
|
+
/**
|
|
34
|
+
* Create an editor in the element `ref` is attached to, and destroy it
|
|
35
|
+
* when the element goes away. `options` are read when the editor is
|
|
36
|
+
* created; later changes to them have no effect.
|
|
37
|
+
*/
|
|
38
|
+
export function useCanvistEditor(options = {}) {
|
|
39
|
+
const [element, setElement] = useState(null);
|
|
40
|
+
const [editor, setEditor] = useState(null);
|
|
41
|
+
const initial = useRef(options);
|
|
42
|
+
useEffect(() => {
|
|
43
|
+
if (element === null) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const mount = { live: true, editor: null };
|
|
47
|
+
void create(element, initial.current, mount, setEditor);
|
|
48
|
+
return () => {
|
|
49
|
+
mount.live = false;
|
|
50
|
+
mount.editor?.destroy();
|
|
51
|
+
setEditor(null);
|
|
52
|
+
};
|
|
53
|
+
}, [element]);
|
|
54
|
+
return { ref: setElement, editor };
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Read `select(editor)` and re-render whenever it changes, after any edit
|
|
58
|
+
* or selection change. `select` must return a primitive or a value that
|
|
59
|
+
* stays the same object while nothing changed, as with
|
|
60
|
+
* `useSyncExternalStore`. Until the editor exists, the value is
|
|
61
|
+
* `fallback`.
|
|
62
|
+
*/
|
|
63
|
+
export function useEditorState(editor, select, fallback) {
|
|
64
|
+
const subscribe = useCallback((listener) => editor?.subscribe(listener) ?? noop, [editor]);
|
|
65
|
+
return useSyncExternalStore(subscribe, () => editor === null ? fallback : select(editor), () => fallback);
|
|
66
|
+
}
|
|
67
|
+
/** A canvist editor. Give it a width; without a height it grows with its document. */
|
|
68
|
+
export function CanvistEditor(props) {
|
|
69
|
+
const { className, style, html, onReady, onUpdate, ...options } = props;
|
|
70
|
+
const { ref, editor } = useCanvistEditor(options);
|
|
71
|
+
const callbacks = useRef({ onReady, onUpdate });
|
|
72
|
+
const { editable = true, placeholder = "" } = options;
|
|
73
|
+
useEffect(() => {
|
|
74
|
+
editor?.setEditable(editable);
|
|
75
|
+
}, [editor, editable]);
|
|
76
|
+
useEffect(() => {
|
|
77
|
+
editor?.setPlaceholder(placeholder);
|
|
78
|
+
}, [editor, placeholder]);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
callbacks.current = { onReady, onUpdate };
|
|
81
|
+
});
|
|
82
|
+
useEffect(() => {
|
|
83
|
+
if (editor === null) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
callbacks.current.onReady?.(editor);
|
|
87
|
+
return editor.subscribe(() => callbacks.current.onUpdate?.(editor));
|
|
88
|
+
}, [editor]);
|
|
89
|
+
return (_jsx("div", { ref: ref, className: className, style: style,
|
|
90
|
+
// Set once: React leaves the editor's elements alone afterwards.
|
|
91
|
+
dangerouslySetInnerHTML: html === undefined
|
|
92
|
+
? undefined
|
|
93
|
+
: { __html: html } }));
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* A menu over selected text, as `bubbleMenu` from `@canvist/web/menus`
|
|
97
|
+
* places it. Pressing inside it keeps the editor's focus and selection.
|
|
98
|
+
*/
|
|
99
|
+
export function BubbleMenu(props) {
|
|
100
|
+
return _jsx(FollowingMenu, { ...props, attach: bubbleMenu });
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* A menu beside the caret on an empty line, as `floatingMenu` from
|
|
104
|
+
* `@canvist/web/menus` places it.
|
|
105
|
+
*/
|
|
106
|
+
export function FloatingMenu(props) {
|
|
107
|
+
return _jsx(FollowingMenu, { ...props, attach: floatingMenu });
|
|
108
|
+
}
|
|
109
|
+
function FollowingMenu(props) {
|
|
110
|
+
const { editor, attach, className, style, children, ...options } = props;
|
|
111
|
+
const element = useRef(null);
|
|
112
|
+
const menu = useRef(null);
|
|
113
|
+
// The latest rule, without re-attaching each time a new closure renders.
|
|
114
|
+
const shouldShow = useRef(options.shouldShow);
|
|
115
|
+
shouldShow.current = options.shouldShow;
|
|
116
|
+
const custom = options.shouldShow !== undefined;
|
|
117
|
+
const { placement, offset, padding } = options;
|
|
118
|
+
useEffect(() => {
|
|
119
|
+
if (editor === null || element.current === null) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const attached = attach(editor, element.current, {
|
|
123
|
+
...(placement === undefined ? {} : { placement }),
|
|
124
|
+
...(offset === undefined ? {} : { offset }),
|
|
125
|
+
...(padding === undefined ? {} : { padding }),
|
|
126
|
+
...(custom
|
|
127
|
+
? { shouldShow: (current) => shouldShow.current?.(current) ?? false }
|
|
128
|
+
: {}),
|
|
129
|
+
});
|
|
130
|
+
menu.current = attached;
|
|
131
|
+
return () => {
|
|
132
|
+
attached.destroy();
|
|
133
|
+
menu.current = null;
|
|
134
|
+
};
|
|
135
|
+
}, [editor, attach, placement, offset, padding, custom]);
|
|
136
|
+
// A re-render can change what `shouldShow` or the children decide.
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
menu.current?.update();
|
|
139
|
+
});
|
|
140
|
+
return (_jsx("div", { ref: element, className: className, style: style, hidden: true, children: children }));
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Create the editor for `mount`. An effect cleaned up while the module
|
|
144
|
+
* loaded (as React's Strict Mode does once) destroys what arrives late.
|
|
145
|
+
*/
|
|
146
|
+
async function create(element, options, mount, created) {
|
|
147
|
+
try {
|
|
148
|
+
const editor = await createEditor(element, options);
|
|
149
|
+
if (!mount.live) {
|
|
150
|
+
editor.destroy();
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
mount.editor = editor;
|
|
154
|
+
created(editor);
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
if (mount.live) {
|
|
158
|
+
reportError(error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
function noop() { }
|
package/package.json
CHANGED
|
@@ -1,6 +1,41 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
2
|
+
"name": "@canvist/react",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "React bindings for the canvist canvas editor: a component, and hooks typed from the extensions you register.",
|
|
5
|
+
"license": "Unlicense",
|
|
6
|
+
"homepage": "https://github.com/canvist/canvist/tree/main/packages/canvist-react#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/canvist/canvist.git",
|
|
10
|
+
"directory": "packages/canvist-react"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/canvist/canvist/issues",
|
|
14
|
+
"email": "support@canvist.dev"
|
|
15
|
+
},
|
|
16
|
+
"type": "module",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/mod.d.ts",
|
|
20
|
+
"default": "./dist/mod.js"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"files": ["dist"],
|
|
24
|
+
"sideEffects": false,
|
|
25
|
+
"scripts": {
|
|
26
|
+
"check:types": "tsc --noEmit",
|
|
27
|
+
"build": "tsc -p tsconfig.build.json",
|
|
28
|
+
"prepack": "npm run build"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@canvist/web": "^0.2.0"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"react": "^18.3.0 || ^19.0.0"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/react": "^19.0.0",
|
|
38
|
+
"react": "^19.0.0",
|
|
39
|
+
"typescript": "^7.0.2"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/readme.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://github.com/canvist/canvist">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/canvist/canvist/main/assets/brand/vector/lockup-dark.svg">
|
|
5
|
+
<img width="220" src="https://raw.githubusercontent.com/canvist/canvist/main/assets/brand/vector/lockup.svg" alt="canvist" />
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
</p>
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<a href="https://github.com/canvist/canvist/blob/main/docs/src/quick-start/react.md"><strong>Quick start</strong></a> ·
|
|
12
|
+
<a href="https://github.com/canvist/canvist/blob/main/docs/src/readme.md"><strong>Docs</strong></a> ·
|
|
13
|
+
<a href="https://github.com/canvist/canvist/blob/main/docs/src/reference/contributing.md"><strong>Contributing</strong></a> ·
|
|
14
|
+
<a href="https://github.com/canvist/canvist"><strong>canvist</strong></a>
|
|
15
|
+
</p>
|
|
16
|
+
|
|
17
|
+
<p align="center">
|
|
18
|
+
<a href="https://www.npmjs.com/package/@canvist/react"><img src="https://img.shields.io/npm/v/%40canvist%2Freact?color=3B2CF5" alt="npm version" /></a>
|
|
19
|
+
<a href="https://github.com/canvist/canvist/actions/workflows/web.yml"><img src="https://github.com/canvist/canvist/actions/workflows/web.yml/badge.svg?branch=main" alt="web status" /></a>
|
|
20
|
+
<a href="https://unlicense.org"><img src="https://img.shields.io/badge/license-Unlicense-3B2CF5" alt="Unlicense" /></a>
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
# @canvist/react
|
|
24
|
+
|
|
25
|
+
React bindings for [`@canvist/web`](../canvist-web), the canvist canvas editor. Commands, queries, and plugin APIs are typed from the extensions and plugins you pass.
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install @canvist/react
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```tsx
|
|
32
|
+
import { useCanvistEditor, useEditorState } from "@canvist/react";
|
|
33
|
+
|
|
34
|
+
function Notes() {
|
|
35
|
+
const { ref, editor } = useCanvistEditor({ label: "Notes" });
|
|
36
|
+
const isBold = useEditorState(
|
|
37
|
+
editor,
|
|
38
|
+
(current) => current.query.isBold(),
|
|
39
|
+
false,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<>
|
|
44
|
+
<button
|
|
45
|
+
aria-pressed={isBold}
|
|
46
|
+
onClick={() => editor?.commands.toggleBold()}
|
|
47
|
+
>
|
|
48
|
+
Bold
|
|
49
|
+
</button>
|
|
50
|
+
<div ref={ref} />
|
|
51
|
+
</>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- `useCanvistEditor(options)` creates the editor in the element `ref` is attached to and destroys it on unmount, Strict Mode included.
|
|
57
|
+
- `useEditorState(editor, select, fallback)` re-renders when `select(editor)` changes, through `useSyncExternalStore`.
|
|
58
|
+
- `<CanvistEditor />` takes the editor's options as props, plus `onReady`, `onUpdate`, and `html` for server-rendered HTML to hydrate. `editable` and `placeholder` follow their props.
|
|
59
|
+
- `<BubbleMenu editor={editor}>` and `<FloatingMenu editor={editor}>` keep their children next to the selection.
|
|
60
|
+
|
|
61
|
+
See [Quick start: React](../../docs/src/quick-start/react.md).
|
package/README.md
DELETED