@canvist/react-native 0.0.0 → 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/src/editor.tsx ADDED
@@ -0,0 +1,268 @@
1
+ /**
2
+ * The editor component and hooks.
3
+ *
4
+ * @module
5
+ */
6
+ import type { CanvistError, Content, StarterKit } from "@canvist/headless";
7
+ import { starterKit } from "@canvist/headless/extensions";
8
+ import { nativeCanvistError, NativeEditor } from "@canvist/headless/native";
9
+ import {
10
+ type ReactElement,
11
+ type Ref,
12
+ type RefObject,
13
+ useCallback,
14
+ useEffect,
15
+ useMemo,
16
+ useRef,
17
+ useState,
18
+ useSyncExternalStore,
19
+ } from "react";
20
+ import type { NativeSyntheticEvent, ViewProps } from "react-native";
21
+
22
+ import { type EditorViewInstance, nativeBridge } from "./bridge.ts";
23
+ import CanvistEditorView, {
24
+ type EditorChangeEvent,
25
+ type EditorErrorEvent,
26
+ type EditorFocusEvent,
27
+ type NativeProps,
28
+ } from "./CanvistEditorViewNativeComponent.ts";
29
+ import CanvistEditors from "./NativeCanvistEditors.ts";
30
+
31
+ /** The extensions the native editors compile in: TipTap's StarterKit, or some of it. */
32
+ export type NativeExtensions = readonly StarterKit[number][];
33
+
34
+ /**
35
+ * A native editor whose commands, queries, and documents are typed from
36
+ * the extensions `E`, with the same API as `@canvist/web`'s editor.
37
+ */
38
+ export type Editor<E extends NativeExtensions> = NativeEditor<E>;
39
+
40
+ /** Options for {@link useCanvistEditor} and {@link CanvistEditor}. */
41
+ export interface EditorOptions<E extends NativeExtensions> {
42
+ /** The extensions to register: every StarterKit extension by default. Read once. */
43
+ readonly extensions?: E;
44
+ /** The document to start with: one empty paragraph by default. Read once. */
45
+ readonly content?: Content<E>;
46
+ /** The body text size before the user's text size setting; the platform's body size by default. */
47
+ readonly fontSize?: number;
48
+ /** Called when the view could not create its editor or load `content`. */
49
+ readonly onError?: (error: CanvistError) => void;
50
+ }
51
+
52
+ /** Props for {@link CanvistView}: what {@link useCanvistEditor} returns, plus the view's own. */
53
+ export interface CanvistViewProps extends NativeProps {
54
+ readonly ref?: Ref<EditorViewInstance>;
55
+ }
56
+
57
+ /** What {@link useCanvistEditor} returns. */
58
+ export interface CanvistEditorHandle<E extends NativeExtensions> {
59
+ /** The editor, once the view has created it; `null` before that. */
60
+ readonly editor: Editor<E> | null;
61
+ /** Spread onto a {@link CanvistView}, alongside its style and accessibility props. */
62
+ readonly props: CanvistViewProps;
63
+ }
64
+
65
+ /**
66
+ * The native editor view. Render it with the props {@link useCanvistEditor}
67
+ * returns; give it a size, as it scrolls its own document.
68
+ */
69
+ export const CanvistView = CanvistEditorView;
70
+
71
+ let editors = 0;
72
+
73
+ /** A key for one view's editor, unique in this JavaScript runtime. */
74
+ function nextEditorId(): string {
75
+ editors += 1;
76
+ return `canvist-${editors}`;
77
+ }
78
+
79
+ /**
80
+ * Create an editor in the {@link CanvistView} you spread `props` onto.
81
+ * `extensions` and `content` are read when the view is created; later
82
+ * changes to them have no effect.
83
+ *
84
+ * ```tsx
85
+ * function Notes() {
86
+ * const { editor, props } = useCanvistEditor({ extensions: [bold, heading] });
87
+ *
88
+ * return (
89
+ * <>
90
+ * <Button title="Bold" onPress={() => editor?.commands.toggleBold()} />
91
+ * <CanvistView {...props} style={{ flex: 1 }} />
92
+ * </>
93
+ * );
94
+ * }
95
+ * ```
96
+ */
97
+ export function useCanvistEditor<
98
+ const E extends NativeExtensions = StarterKit,
99
+ >(options: EditorOptions<E> = {}): CanvistEditorHandle<E> {
100
+ const [editorId] = useState(nextEditorId);
101
+ const [editor, setEditor] = useState<Editor<E> | null>(null);
102
+ const initial = useRef(options);
103
+ const view = useRef<EditorViewInstance>(null);
104
+ // Events arrive through callbacks made once, so they read the editor here.
105
+ const current = useRef<Editor<E> | null>(null);
106
+ const onError = useLatest(options.onError);
107
+
108
+ const onEditorReady = useCallback(() => {
109
+ // Without extensions, `E` is the default: the whole StarterKit.
110
+ const extensions = (initial.current.extensions ?? starterKit) as E;
111
+ const created = new NativeEditor(
112
+ nativeBridge(CanvistEditors, editorId, () => view.current),
113
+ extensions,
114
+ );
115
+ current.current = created;
116
+ setEditor(created);
117
+ }, [editorId]);
118
+
119
+ const onEditorChange = useCallback(
120
+ (_event: NativeSyntheticEvent<EditorChangeEvent>) => {
121
+ current.current?.notifyChange();
122
+ },
123
+ [],
124
+ );
125
+
126
+ const onEditorFocus = useCallback(
127
+ (event: NativeSyntheticEvent<EditorFocusEvent>) => {
128
+ current.current?.notifyFocus(event.nativeEvent.focused);
129
+ },
130
+ [],
131
+ );
132
+
133
+ const onEditorError = useCallback(
134
+ (event: NativeSyntheticEvent<EditorErrorEvent>) => {
135
+ const { code, message } = event.nativeEvent;
136
+ const error = nativeCanvistError(code, message);
137
+
138
+ if (onError.current === undefined) {
139
+ console.error(error);
140
+ } else {
141
+ onError.current(error);
142
+ }
143
+ },
144
+ [onError],
145
+ );
146
+
147
+ const { fontSize } = options;
148
+ const props = useMemo((): CanvistViewProps => {
149
+ const { extensions = starterKit, content } = initial.current;
150
+
151
+ return {
152
+ ref: view,
153
+ editorId,
154
+ extensions: extensions.map((extension) => extension.name),
155
+ ...documentProps(content),
156
+ ...(fontSize === undefined ? {} : { fontSize }),
157
+ onEditorReady,
158
+ onEditorChange,
159
+ onEditorFocus,
160
+ onEditorError,
161
+ };
162
+ }, [
163
+ editorId,
164
+ fontSize,
165
+ onEditorReady,
166
+ onEditorChange,
167
+ onEditorFocus,
168
+ onEditorError,
169
+ ]);
170
+
171
+ return { editor, props };
172
+ }
173
+
174
+ /**
175
+ * Read `select(editor)` and re-render whenever it changes: after any edit,
176
+ * selection change, or change of focus. `select` must return a primitive
177
+ * or a value that stays the same object while nothing changed, as with
178
+ * `useSyncExternalStore`. Until the editor exists, the value is `fallback`.
179
+ */
180
+ export function useEditorState<E extends NativeExtensions, T>(
181
+ editor: Editor<E> | null,
182
+ select: (editor: Editor<E>) => T,
183
+ fallback: T,
184
+ ): T {
185
+ const subscribe = useCallback(
186
+ (listener: () => void) => editor?.subscribe(listener) ?? noop,
187
+ [editor],
188
+ );
189
+
190
+ return useSyncExternalStore(
191
+ subscribe,
192
+ () => editor === null ? fallback : select(editor),
193
+ () => fallback,
194
+ );
195
+ }
196
+
197
+ /** Props for {@link CanvistEditor}: the editor's options and the view's. */
198
+ export interface CanvistEditorProps<E extends NativeExtensions>
199
+ extends EditorOptions<E>, ViewProps {
200
+ /** Called once the editor is ready. */
201
+ readonly onReady?: (editor: Editor<E>) => void;
202
+ /** Called after every change to the document, the selection, or focus. */
203
+ readonly onUpdate?: (editor: Editor<E>) => void;
204
+ }
205
+
206
+ /**
207
+ * A native canvist editor: the iOS or Android `CanvistView`. Give it a
208
+ * size, as it scrolls its own document.
209
+ *
210
+ * ```tsx
211
+ * <CanvistEditor
212
+ * content={{ format: "markdown", input: "# Notes" }}
213
+ * onUpdate={(editor) => save(editor.export("markdown"))}
214
+ * style={{ flex: 1 }}
215
+ * />
216
+ * ```
217
+ */
218
+ export function CanvistEditor<const E extends NativeExtensions = StarterKit>(
219
+ props: CanvistEditorProps<E>,
220
+ ): ReactElement {
221
+ const { extensions, content, fontSize, onError, onReady, onUpdate, ...view } =
222
+ props;
223
+ const { editor, props: editorProps } = useCanvistEditor<E>({
224
+ ...(extensions === undefined ? {} : { extensions }),
225
+ ...(content === undefined ? {} : { content }),
226
+ ...(fontSize === undefined ? {} : { fontSize }),
227
+ ...(onError === undefined ? {} : { onError }),
228
+ });
229
+ const callbacks = useLatest({ onReady, onUpdate });
230
+
231
+ useEffect(() => {
232
+ if (editor === null) {
233
+ return;
234
+ }
235
+
236
+ callbacks.current.onReady?.(editor);
237
+
238
+ return editor.subscribe(() => callbacks.current.onUpdate?.(editor));
239
+ }, [editor, callbacks]);
240
+
241
+ return <CanvistView {...view} {...editorProps} />;
242
+ }
243
+
244
+ /** The view props for the starting document. */
245
+ function documentProps<E extends NativeExtensions>(
246
+ content: Content<E> | undefined,
247
+ ): Pick<NativeProps, "content" | "contentFormat"> {
248
+ if (content === undefined) {
249
+ return {};
250
+ }
251
+
252
+ return "format" in content
253
+ ? { contentFormat: content.format, content: content.input }
254
+ : { contentFormat: "json", content: JSON.stringify(content) };
255
+ }
256
+
257
+ /** A ref that always holds the latest `value`. */
258
+ function useLatest<T>(value: T): RefObject<T> {
259
+ const ref = useRef(value);
260
+
261
+ useEffect(() => {
262
+ ref.current = value;
263
+ });
264
+
265
+ return ref;
266
+ }
267
+
268
+ function noop(): void {}
package/src/mod.ts ADDED
@@ -0,0 +1,61 @@
1
+ /**
2
+ * The canvist rich-text editor for React Native.
3
+ *
4
+ * `CanvistEditor` shows the native iOS or Android editor: canvist's Rust
5
+ * engine, painted natively and edited through the platform's own text
6
+ * input, not a WebView. Commands, queries, and documents are typed from the
7
+ * extensions you register, with the same API as `@canvist/web`: calling a
8
+ * command you did not register is a type error. Calls are synchronous.
9
+ *
10
+ * ```tsx
11
+ * import { CanvistEditor, bold, heading } from "@canvist/react-native";
12
+ *
13
+ * <CanvistEditor
14
+ * extensions={[bold, heading]}
15
+ * content={{ format: "markdown", input: "# Notes" }}
16
+ * onReady={(editor) => editor.commands.toggleHeading({ level: 2 })}
17
+ * style={{ flex: 1 }}
18
+ * />
19
+ * ```
20
+ *
21
+ * @module
22
+ */
23
+ export type {
24
+ CanvistError,
25
+ CommandName,
26
+ CommandParams,
27
+ Commands,
28
+ Content,
29
+ DocJson,
30
+ ErrorCode,
31
+ ExtensionDescriptor,
32
+ Extensions,
33
+ Format,
34
+ Position,
35
+ Queries,
36
+ QueryName,
37
+ QueryParams,
38
+ QueryResult,
39
+ Range,
40
+ Registered,
41
+ StarterKit,
42
+ } from "@canvist/headless";
43
+ export * from "@canvist/headless/extensions";
44
+ export { isCanvistError } from "@canvist/headless/native";
45
+ export type {
46
+ EditorChangeEvent,
47
+ EditorErrorEvent,
48
+ EditorFocusEvent,
49
+ } from "./CanvistEditorViewNativeComponent.ts";
50
+ export {
51
+ CanvistEditor,
52
+ type CanvistEditorHandle,
53
+ type CanvistEditorProps,
54
+ CanvistView,
55
+ type CanvistViewProps,
56
+ type Editor,
57
+ type EditorOptions,
58
+ type NativeExtensions,
59
+ useCanvistEditor,
60
+ useEditorState,
61
+ } from "./editor.tsx";
package/CHANGELOG.md DELETED
@@ -1,3 +0,0 @@
1
- ## 0.0.0
2
-
3
- - Placeholder release.
package/LICENSE DELETED
@@ -1,24 +0,0 @@
1
- This is free and unencumbered software released into the public domain.
2
-
3
- Anyone is free to copy, modify, publish, use, compile, sell, or
4
- distribute this software, either in source code form or as a compiled
5
- binary, for any purpose, commercial or non-commercial, and by any
6
- means.
7
-
8
- In jurisdictions that recognize copyright laws, the author or authors
9
- of this software dedicate any and all copyright interest in the
10
- software to the public domain. We make this dedication for the benefit
11
- of the public at large and to the detriment of our heirs and
12
- successors. We intend this dedication to be an overt act of
13
- relinquishment in perpetuity of all present and future rights to this
14
- software under copyright law.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
- IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
20
- OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
21
- ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
- OTHER DEALINGS IN THE SOFTWARE.
23
-
24
- For more information, please refer to <http://unlicense.org/>
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # @canvist/react-native
2
-
3
- This is a placeholder release published by monochange to bootstrap trusted publishing.