@openeditor/native 0.0.27 → 0.0.29
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/README.md +15 -1
- package/dist/index.d.ts +35 -13
- package/dist/index.js +139 -48
- package/dist/toolbar.d.ts +3 -2
- package/dist/toolbar.js +47 -2
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -7,6 +7,7 @@ Pass `attachmentRuntime` to use the system picker and host storage lifecycle. Th
|
|
|
7
7
|
## Public surface
|
|
8
8
|
|
|
9
9
|
- `OpenEditorNative` as the primary editing surface
|
|
10
|
+
- `OpenEditorNativePageHeader` for host-backed page title and icon editing
|
|
10
11
|
- `OpenEditorNativeViewer` for read-only rendering
|
|
11
12
|
- `OpenEditorNativeEmojiPicker` for standalone native emoji selection
|
|
12
13
|
- native document bridge helpers where renderer adaptation is required
|
|
@@ -25,11 +26,15 @@ history. Inline emoji continue to use the device keyboard.
|
|
|
25
26
|
## Quick start
|
|
26
27
|
|
|
27
28
|
```tsx
|
|
28
|
-
import {
|
|
29
|
+
import { useRef } from "react";
|
|
30
|
+
import { OpenEditorNative, type OpenEditorNativeController } from "@openeditor/native";
|
|
29
31
|
|
|
30
32
|
export function Example() {
|
|
33
|
+
const editor = useRef<OpenEditorNativeController>(null);
|
|
31
34
|
return (
|
|
32
35
|
<OpenEditorNative
|
|
36
|
+
ref={editor}
|
|
37
|
+
initialDocument={document}
|
|
33
38
|
onChange={(document) => {
|
|
34
39
|
console.log(document);
|
|
35
40
|
}}
|
|
@@ -38,6 +43,15 @@ export function Example() {
|
|
|
38
43
|
}
|
|
39
44
|
```
|
|
40
45
|
|
|
46
|
+
`OpenEditorNative` follows the same uncontrolled lifecycle as web:
|
|
47
|
+
`initialDocument` is read once and `ref.current.setContent(document)` performs an
|
|
48
|
+
undoable programmatic replacement. Undo and redo delegate directly to the Rust
|
|
49
|
+
editor engine; the React wrapper does not keep document snapshots.
|
|
50
|
+
|
|
51
|
+
Pass `enabledBlocks` to restrict authoring. Page and attachment controls are
|
|
52
|
+
automatically omitted until their required host runtimes are present, while
|
|
53
|
+
existing nodes remain readable.
|
|
54
|
+
|
|
41
55
|
## Standalone emoji picker
|
|
42
56
|
|
|
43
57
|
The editor wires this automatically for callouts and pages. Hosts can also use
|
package/dist/index.d.ts
CHANGED
|
@@ -1,29 +1,33 @@
|
|
|
1
|
-
import { type OpenEditorDocument, type OpenEditorAttachmentRuntime, type ProseMirrorNode } from "@openeditor/core";
|
|
1
|
+
import { type OpenEditorDocument, type OpenEditorAttachmentRuntime, type OpenEditorAuthoringCapabilities, type OpenEditorPageRuntime, type OpenEditorPageSnapshot, type ProseMirrorNode } from "@openeditor/core";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
3
|
import type { NativeRichTextEditorToolbarPlacement } from "@openeditor/react-native-prose-editor";
|
|
4
4
|
export { normalizeNativeThemeColor } from "./theme.js";
|
|
5
5
|
export { OpenEditorNativeEmojiPicker, type OpenEditorNativeEmojiPickerProps, type OpenEditorNativeEmojiPickerTheme, } from "./emoji-picker.js";
|
|
6
6
|
export { emojiForNativeSkinTone, getOpenEditorNativeEmojiSections, openEditorNativeEmojiSkinToneOptions, type OpenEditorNativeEmoji, type OpenEditorNativeEmojiSection, type OpenEditorNativeEmojiSkinTone, } from "./emoji-data.js";
|
|
7
|
-
export type OpenEditorNativeProps = {
|
|
8
|
-
|
|
7
|
+
export type OpenEditorNativeProps = OpenEditorAuthoringCapabilities & {
|
|
8
|
+
initialDocument?: OpenEditorDocument;
|
|
9
9
|
editable?: boolean;
|
|
10
10
|
placeholder?: string;
|
|
11
11
|
showToolbar?: boolean;
|
|
12
12
|
theme?: Partial<OpenEditorNativeTheme>;
|
|
13
13
|
toolbarPlacement?: NativeRichTextEditorToolbarPlacement;
|
|
14
14
|
onChange?: (document: OpenEditorDocument) => void;
|
|
15
|
-
|
|
16
|
-
pageId: string;
|
|
17
|
-
title: string;
|
|
18
|
-
icon?: string | null;
|
|
19
|
-
href?: string | null;
|
|
20
|
-
}) => void;
|
|
15
|
+
pageRuntime?: OpenEditorPageRuntime;
|
|
21
16
|
attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
|
|
22
17
|
};
|
|
23
|
-
export type OpenEditorNativeViewerProps =
|
|
24
|
-
|
|
18
|
+
export type OpenEditorNativeViewerProps = {
|
|
19
|
+
document: OpenEditorDocument;
|
|
20
|
+
theme?: Partial<OpenEditorNativeTheme>;
|
|
21
|
+
pageRuntime?: OpenEditorPageRuntime;
|
|
22
|
+
attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
|
|
25
23
|
renderers?: Partial<Record<string, OpenEditorNativeViewerRenderer>>;
|
|
26
24
|
};
|
|
25
|
+
export type OpenEditorNativeController = {
|
|
26
|
+
getContent: () => OpenEditorDocument;
|
|
27
|
+
setContent: (document: OpenEditorDocument) => void;
|
|
28
|
+
undo: () => void;
|
|
29
|
+
redo: () => void;
|
|
30
|
+
};
|
|
27
31
|
export type OpenEditorNativeToolbarAction = {
|
|
28
32
|
key: string;
|
|
29
33
|
label: string;
|
|
@@ -72,8 +76,26 @@ export type OpenEditorNativeTheme = {
|
|
|
72
76
|
toolbar?: OpenEditorNativeToolbarTheme;
|
|
73
77
|
};
|
|
74
78
|
export declare const OpenEditorNativeToolbar: ({ actions, onActionPress, theme, }: OpenEditorNativeToolbarProps) => import("react").JSX.Element;
|
|
75
|
-
export
|
|
76
|
-
|
|
79
|
+
export type OpenEditorNativePageHeaderProps = {
|
|
80
|
+
page: OpenEditorPageSnapshot;
|
|
81
|
+
runtime: OpenEditorPageRuntime;
|
|
82
|
+
theme?: Partial<OpenEditorNativeTheme>;
|
|
83
|
+
onPageChange?: (page: OpenEditorPageSnapshot) => void;
|
|
84
|
+
};
|
|
85
|
+
/** Canonical native page-title and page-icon editor for opened page surfaces. */
|
|
86
|
+
export declare const OpenEditorNativePageHeader: ({ page, runtime, theme, onPageChange }: OpenEditorNativePageHeaderProps) => import("react").JSX.Element;
|
|
87
|
+
export declare const OpenEditorNative: import("react").ForwardRefExoticComponent<OpenEditorAuthoringCapabilities & {
|
|
88
|
+
initialDocument?: OpenEditorDocument;
|
|
89
|
+
editable?: boolean;
|
|
90
|
+
placeholder?: string;
|
|
91
|
+
showToolbar?: boolean;
|
|
92
|
+
theme?: Partial<OpenEditorNativeTheme>;
|
|
93
|
+
toolbarPlacement?: NativeRichTextEditorToolbarPlacement;
|
|
94
|
+
onChange?: (document: OpenEditorDocument) => void;
|
|
95
|
+
pageRuntime?: OpenEditorPageRuntime;
|
|
96
|
+
attachmentRuntime?: OpenEditorAttachmentRuntime<any>;
|
|
97
|
+
} & import("react").RefAttributes<OpenEditorNativeController>>;
|
|
98
|
+
export declare const OpenEditorNativeViewer: ({ document, theme, renderers, pageRuntime, attachmentRuntime, }: OpenEditorNativeViewerProps) => import("react").JSX.Element;
|
|
77
99
|
export { createNativeEditorDocument, fromNativeEditorDocument, sanitizeNativeEditorDocument, toNativeEditorDocument, } from "./document.js";
|
|
78
100
|
export { createOpenEditorNativeToolbarItems, defaultDocumentForToolbarAction, nativeToolbarParityCoverage, openEditorNativeToolbarActionKeys, openEditorNativeToolbarItems, } from "./toolbar.js";
|
|
79
101
|
export type { OpenEditorNativeToolbarActionKey } from "./toolbar.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { DEFAULT_CALLOUT_EMOJI, DEFAULT_PAGE_EMOJI, createDocument, findBlockSpecForNode, moveTopLevelBlock, normalizeEmoji, } from "@openeditor/core";
|
|
3
|
-
import { useMemo, useRef, useState } from "react";
|
|
2
|
+
import { DEFAULT_CALLOUT_EMOJI, DEFAULT_PAGE_EMOJI, createDocument, findBlockSpecForNode, isOpenEditorBlockEnabled, moveTopLevelBlock, normalizeEmoji, } from "@openeditor/core";
|
|
3
|
+
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react";
|
|
4
4
|
import { Image as NativeImage, Keyboard, Modal, Platform, Pressable, StyleSheet, Text, TextInput, View } from "react-native";
|
|
5
5
|
import { fromNativeEditorDocument, sanitizeNativeEditorDocument, toNativeEditorDocument, } from "./document.js";
|
|
6
6
|
import { resolveSelectedTopLevelIndex } from "./selection.js";
|
|
@@ -332,11 +332,29 @@ const NativeToggleListItem = ({ children, initiallyOpen, theme, }) => {
|
|
|
332
332
|
open ? styles.viewerToggleCaretOpen : null,
|
|
333
333
|
] }) }), _jsx(View, { style: styles.viewerListContent, children: open ? childArray : childArray.slice(0, 1) })] }));
|
|
334
334
|
};
|
|
335
|
-
const
|
|
335
|
+
const OpenEditorNativePageViewer = ({ cachedPage, pageRuntime, theme, }) => {
|
|
336
|
+
const [page, setPage] = useState(cachedPage);
|
|
337
|
+
useEffect(() => {
|
|
338
|
+
setPage(cachedPage);
|
|
339
|
+
if (!pageRuntime?.resolvePage)
|
|
340
|
+
return;
|
|
341
|
+
let active = true;
|
|
342
|
+
void pageRuntime.resolvePage(cachedPage.pageId).then((resolved) => {
|
|
343
|
+
if (active && resolved)
|
|
344
|
+
setPage(resolved);
|
|
345
|
+
}).catch(() => undefined);
|
|
346
|
+
return () => {
|
|
347
|
+
active = false;
|
|
348
|
+
};
|
|
349
|
+
}, [cachedPage.href, cachedPage.icon, cachedPage.pageId, cachedPage.title, pageRuntime]);
|
|
350
|
+
const content = _jsxs(_Fragment, { children: [_jsx(Text, { style: [styles.viewerPageIcon, { color: theme.text }], children: normalizeEmoji(page.icon, DEFAULT_PAGE_EMOJI) }), _jsx(Text, { style: [styles.viewerPageTitle, { color: theme.text, textDecorationColor: theme.muted }], children: page.title }), _jsx(Text, { style: { color: theme.muted }, children: "\u2192" })] });
|
|
351
|
+
return pageRuntime?.openPage ? (_jsx(Pressable, { accessibilityLabel: `Open ${page.title}`, accessibilityRole: "button", onPress: () => void pageRuntime.openPage?.(page), style: { alignItems: "center", borderRadius: 8, flexDirection: "row", gap: 8, marginBottom: 8, padding: 8 }, children: content })) : (_jsx(View, { style: { alignItems: "center", flexDirection: "row", gap: 8, marginBottom: 8, padding: 8 }, children: content }));
|
|
352
|
+
};
|
|
353
|
+
const renderNativeViewerNode = (node, key, theme, renderers, pageRuntime, attachmentRuntime) => {
|
|
336
354
|
if (node.type === "text") {
|
|
337
355
|
return _jsx(Text, { style: { color: theme.text }, children: node.text ?? "" }, key);
|
|
338
356
|
}
|
|
339
|
-
const children = node.content?.map((child, index) => renderNativeViewerNode(child, `${key}-${index}`, theme, renderers,
|
|
357
|
+
const children = node.content?.map((child, index) => renderNativeViewerNode(child, `${key}-${index}`, theme, renderers, pageRuntime, attachmentRuntime)) ?? null;
|
|
340
358
|
const blockKey = findBlockSpecForNode(defaultBlockRegistry, node)?.name ?? node.type;
|
|
341
359
|
const customRenderer = renderers?.[blockKey];
|
|
342
360
|
if (customRenderer) {
|
|
@@ -375,8 +393,7 @@ const renderNativeViewerNode = (node, key, theme, renderers, onOpenPage, attachm
|
|
|
375
393
|
const pageId = typeof node.attrs?.pageId === "string" ? node.attrs.pageId : "";
|
|
376
394
|
const title = node.content?.map((child) => child.text ?? "").join("") || "Untitled";
|
|
377
395
|
const icon = normalizeEmoji(node.attrs?.icon, DEFAULT_PAGE_EMOJI);
|
|
378
|
-
|
|
379
|
-
return onOpenPage && pageId ? (_jsx(Pressable, { accessibilityRole: "button", onPress: () => onOpenPage({ pageId, title, icon, href: typeof node.attrs?.href === "string" ? node.attrs.href : null }), style: { alignItems: "center", borderRadius: 8, flexDirection: "row", gap: 8, marginBottom: 8, padding: 8 }, children: content }, key)) : _jsx(View, { style: { alignItems: "center", flexDirection: "row", gap: 8, marginBottom: 8, padding: 8 }, children: content }, key);
|
|
396
|
+
return _jsx(OpenEditorNativePageViewer, { cachedPage: { pageId, title, icon, href: typeof node.attrs?.href === "string" ? node.attrs.href : null }, pageRuntime: pageId ? pageRuntime : undefined, theme: theme }, key);
|
|
380
397
|
}
|
|
381
398
|
case "attachment": {
|
|
382
399
|
const snapshot = {
|
|
@@ -434,9 +451,45 @@ export const OpenEditorNativeToolbar = ({ actions = [
|
|
|
434
451
|
onActionPress?.(action.key);
|
|
435
452
|
}, style: [styles.toolbarButton, { backgroundColor: resolvedTheme.accent, borderColor: resolvedTheme.borderStrong }], children: _jsx(Text, { style: [styles.toolbarButtonText, { color: resolvedTheme.accentText }], children: action.label }) }, action.key))) }));
|
|
436
453
|
};
|
|
437
|
-
|
|
454
|
+
/** Canonical native page-title and page-icon editor for opened page surfaces. */
|
|
455
|
+
export const OpenEditorNativePageHeader = ({ page, runtime, theme, onPageChange }) => {
|
|
456
|
+
const [title, setTitle] = useState(page.title);
|
|
457
|
+
const [icon, setIcon] = useState(normalizeEmoji(page.icon, DEFAULT_PAGE_EMOJI));
|
|
458
|
+
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
|
459
|
+
const [error, setError] = useState(null);
|
|
460
|
+
const resolvedTheme = { ...defaultNativeTheme, ...theme };
|
|
461
|
+
useEffect(() => setTitle(page.title), [page.pageId, page.title]);
|
|
462
|
+
useEffect(() => setIcon(normalizeEmoji(page.icon, DEFAULT_PAGE_EMOJI)), [page.icon, page.pageId]);
|
|
463
|
+
const updatePage = async (update) => {
|
|
464
|
+
setError(null);
|
|
465
|
+
try {
|
|
466
|
+
const resolved = await runtime.updatePage(page.pageId, update);
|
|
467
|
+
const next = resolved ?? { ...page, ...update };
|
|
468
|
+
setTitle(next.title);
|
|
469
|
+
setIcon(normalizeEmoji(next.icon, DEFAULT_PAGE_EMOJI));
|
|
470
|
+
onPageChange?.(next);
|
|
471
|
+
}
|
|
472
|
+
catch (cause) {
|
|
473
|
+
setError(cause instanceof Error ? cause.message : "Could not update the page.");
|
|
474
|
+
}
|
|
475
|
+
};
|
|
476
|
+
return _jsxs(View, { style: styles.pageHeader, children: [_jsx(Pressable, { accessibilityLabel: "Change page icon", accessibilityRole: "button", onPress: () => setShowEmojiPicker(true), children: _jsx(Text, { style: styles.pageHeaderIcon, children: icon }) }), _jsx(TextInput, { accessibilityLabel: "Page title", multiline: true, onBlur: () => {
|
|
477
|
+
const nextTitle = title.trim() || "Untitled";
|
|
478
|
+
setTitle(nextTitle);
|
|
479
|
+
if (nextTitle !== page.title)
|
|
480
|
+
void updatePage({ title: nextTitle });
|
|
481
|
+
}, onChangeText: setTitle, placeholder: "Untitled", placeholderTextColor: resolvedTheme.muted, style: [styles.pageHeaderTitle, { color: resolvedTheme.text }], value: title }), error ? _jsx(Text, { accessibilityRole: "alert", style: styles.pageHeaderError, children: error }) : null, _jsx(OpenEditorNativeEmojiPicker, { onEmojiSelect: (nextIcon) => {
|
|
482
|
+
setIcon(nextIcon);
|
|
483
|
+
void updatePage({ icon: nextIcon });
|
|
484
|
+
}, onRequestClose: () => setShowEmojiPicker(false), theme: { surfaceMuted: resolvedTheme.surfaceMuted, text: resolvedTheme.text, muted: resolvedTheme.muted }, visible: showEmojiPicker })] });
|
|
485
|
+
};
|
|
486
|
+
export const OpenEditorNative = forwardRef(function OpenEditorNative({ initialDocument = createDocument(), editable = true, placeholder = "Start writing...", showToolbar = true, theme, toolbarPlacement = "keyboard", onChange, pageRuntime, attachmentRuntime, enabledBlocks, }, forwardedRef) {
|
|
438
487
|
const editorRef = useRef(null);
|
|
439
|
-
const [currentDocument, setCurrentDocument] = useState(() => normalizeForNativeEditor(
|
|
488
|
+
const [currentDocument, setCurrentDocument] = useState(() => normalizeForNativeEditor(initialDocument));
|
|
489
|
+
const currentDocumentRef = useRef(currentDocument);
|
|
490
|
+
currentDocumentRef.current = currentDocument;
|
|
491
|
+
const onChangeRef = useRef(onChange);
|
|
492
|
+
onChangeRef.current = onChange;
|
|
440
493
|
const [selection, setSelection] = useState();
|
|
441
494
|
const [linkRequest, setLinkRequest] = useState(null);
|
|
442
495
|
const [linkUrl, setLinkUrl] = useState("https://");
|
|
@@ -445,10 +498,7 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
|
|
|
445
498
|
const [imageUrl, setImageUrl] = useState("https://placehold.co/960x420/png");
|
|
446
499
|
const [attachmentUpload, setAttachmentUpload] = useState(null);
|
|
447
500
|
const attachmentAbortRef = useRef(null);
|
|
448
|
-
const [history, setHistory] = useState({
|
|
449
|
-
undoStack: [],
|
|
450
|
-
redoStack: [],
|
|
451
|
-
});
|
|
501
|
+
const [history, setHistory] = useState({ canUndo: false, canRedo: false });
|
|
452
502
|
const nativeDocument = currentDocument;
|
|
453
503
|
const NativeEditor = useMemo(() => loadNativeEditor(), []);
|
|
454
504
|
const resolvedTheme = useMemo(() => ({
|
|
@@ -460,24 +510,35 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
|
|
|
460
510
|
},
|
|
461
511
|
}), [theme]);
|
|
462
512
|
const nativeEditorTheme = useMemo(() => toNativeEditorTheme(resolvedTheme), [resolvedTheme]);
|
|
513
|
+
const authorableBlocks = useMemo(() => [...defaultBlockRegistry.values()]
|
|
514
|
+
.filter((block) => block.support?.native !== "unsupported")
|
|
515
|
+
.map((block) => block.name)
|
|
516
|
+
.filter((blockName) => isOpenEditorBlockEnabled(blockName, { enabledBlocks }))
|
|
517
|
+
.filter((blockName) => blockName !== "page" || Boolean(pageRuntime?.createPage))
|
|
518
|
+
.filter((blockName) => blockName !== "attachment" || Boolean(attachmentRuntime?.selectAttachment && attachmentRuntime.uploadAttachment)), [attachmentRuntime, enabledBlocks, pageRuntime]);
|
|
519
|
+
const isBlockEnabled = (blockName) => authorableBlocks.includes(/^heading[1-6]$/.test(blockName) ? "heading" : blockName);
|
|
463
520
|
const toolbarItems = useMemo(() => createOpenEditorNativeToolbarItems({
|
|
464
|
-
canUndo: history.
|
|
465
|
-
canRedo: history.
|
|
466
|
-
|
|
521
|
+
canUndo: history.canUndo,
|
|
522
|
+
canRedo: history.canRedo,
|
|
523
|
+
enabledBlocks: authorableBlocks,
|
|
524
|
+
}), [authorableBlocks, history.canRedo, history.canUndo]);
|
|
467
525
|
const syncDocument = (nextDocument, updateEditor = true) => {
|
|
468
526
|
const normalized = normalizeForNativeEditor(nextDocument);
|
|
469
|
-
if (
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
redoStack: [],
|
|
473
|
-
}));
|
|
474
|
-
}
|
|
527
|
+
if (documentsMatch(normalized, currentDocumentRef.current))
|
|
528
|
+
return;
|
|
529
|
+
currentDocumentRef.current = normalized;
|
|
475
530
|
setCurrentDocument(normalized);
|
|
476
|
-
|
|
531
|
+
onChangeRef.current?.(normalized);
|
|
477
532
|
if (updateEditor) {
|
|
478
533
|
editorRef.current?.setContentJson(toNativeEditorDocument(normalized));
|
|
479
534
|
}
|
|
480
535
|
};
|
|
536
|
+
useImperativeHandle(forwardedRef, () => ({
|
|
537
|
+
getContent: () => currentDocumentRef.current,
|
|
538
|
+
setContent: (nextDocument) => syncDocument(nextDocument, true),
|
|
539
|
+
undo: () => editorRef.current?.undo(),
|
|
540
|
+
redo: () => editorRef.current?.redo(),
|
|
541
|
+
}));
|
|
481
542
|
const moveSelectedBlock = (direction) => {
|
|
482
543
|
const selectedIndex = resolveSelectedTopLevelIndex(nativeDocument, selection);
|
|
483
544
|
const nextIndex = selectedIndex + direction;
|
|
@@ -529,31 +590,11 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
|
|
|
529
590
|
return;
|
|
530
591
|
}
|
|
531
592
|
if (key === openEditorNativeToolbarActionKeys.undo) {
|
|
532
|
-
|
|
533
|
-
if (!previous) {
|
|
534
|
-
return;
|
|
535
|
-
}
|
|
536
|
-
setHistory((currentHistory) => ({
|
|
537
|
-
undoStack: currentHistory.undoStack.slice(0, -1),
|
|
538
|
-
redoStack: [nativeDocument, ...currentHistory.redoStack].slice(0, 100),
|
|
539
|
-
}));
|
|
540
|
-
setCurrentDocument(previous);
|
|
541
|
-
onChange?.(previous);
|
|
542
|
-
editorRef.current?.setContentJson(toNativeEditorDocument(previous));
|
|
593
|
+
editorRef.current?.undo();
|
|
543
594
|
return;
|
|
544
595
|
}
|
|
545
596
|
if (key === openEditorNativeToolbarActionKeys.redo) {
|
|
546
|
-
|
|
547
|
-
if (!next) {
|
|
548
|
-
return;
|
|
549
|
-
}
|
|
550
|
-
setHistory((currentHistory) => ({
|
|
551
|
-
undoStack: [...currentHistory.undoStack.slice(-99), nativeDocument],
|
|
552
|
-
redoStack: currentHistory.redoStack.slice(1),
|
|
553
|
-
}));
|
|
554
|
-
setCurrentDocument(next);
|
|
555
|
-
onChange?.(next);
|
|
556
|
-
editorRef.current?.setContentJson(toNativeEditorDocument(next));
|
|
597
|
+
editorRef.current?.redo();
|
|
557
598
|
return;
|
|
558
599
|
}
|
|
559
600
|
if (key === openEditorNativeToolbarActionKeys.bulletList) {
|
|
@@ -572,6 +613,21 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
|
|
|
572
613
|
editorRef.current?.insertTable();
|
|
573
614
|
return;
|
|
574
615
|
}
|
|
616
|
+
if (!isBlockEnabled(key))
|
|
617
|
+
return;
|
|
618
|
+
if (key === openEditorNativeToolbarActionKeys.page && pageRuntime?.createPage) {
|
|
619
|
+
void pageRuntime.createPage({ title: "Untitled", icon: DEFAULT_PAGE_EMOJI }).then((page) => {
|
|
620
|
+
editorRef.current?.insertContentJson({
|
|
621
|
+
type: "doc",
|
|
622
|
+
content: [{
|
|
623
|
+
type: "page",
|
|
624
|
+
attrs: { pageId: page.pageId, icon: page.icon ?? DEFAULT_PAGE_EMOJI, href: page.href ?? null },
|
|
625
|
+
content: [{ type: "text", text: page.title || "Untitled" }],
|
|
626
|
+
}],
|
|
627
|
+
});
|
|
628
|
+
}).catch(() => undefined);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
575
631
|
const actionDocument = defaultDocumentForToolbarAction(key);
|
|
576
632
|
if (actionDocument) {
|
|
577
633
|
editorRef.current?.insertContentJson(actionDocument);
|
|
@@ -606,7 +662,21 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
|
|
|
606
662
|
}, onRequestImage: (request) => {
|
|
607
663
|
setImageUrl("https://placehold.co/960x420/png");
|
|
608
664
|
setImageRequest(request);
|
|
609
|
-
}, onOpenPage:
|
|
665
|
+
}, onOpenPage: (page) => {
|
|
666
|
+
if (!pageRuntime?.openPage)
|
|
667
|
+
return;
|
|
668
|
+
if (!pageRuntime.resolvePage) {
|
|
669
|
+
void pageRuntime.openPage(page);
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
void pageRuntime.resolvePage(page.pageId)
|
|
673
|
+
.then((resolved) => pageRuntime.openPage?.(resolved ?? page))
|
|
674
|
+
.catch(() => pageRuntime.openPage?.(page));
|
|
675
|
+
}, onRequestEmoji: (request) => {
|
|
676
|
+
if (request.nodeType === "page" && !pageRuntime)
|
|
677
|
+
return;
|
|
678
|
+
setEmojiRequest(request);
|
|
679
|
+
}, onOpenAttachment: (attachment) => void attachmentRuntime?.openAttachment?.(attachment), onSelectionChange: setSelection, onHistoryStateChange: setHistory, onContentChangeJSON: (json) => {
|
|
610
680
|
syncDocument(contentFromNativeJson(json), false);
|
|
611
681
|
}, style: styles.nativeEditor }), _jsx(UrlDialog, { title: "Link", value: linkUrl, visible: linkRequest !== null, placeholder: "https://", confirmLabel: linkUrl.trim() ? "Apply" : "Remove", onChangeText: setLinkUrl, onCancel: () => setLinkRequest(null), onSubmit: submitLinkDialog, theme: resolvedTheme }), _jsx(Modal, { animationType: "fade", onRequestClose: () => { attachmentAbortRef.current?.abort(); setAttachmentUpload(null); }, transparent: true, visible: attachmentUpload !== null, children: _jsx(View, { style: [styles.dialogBackdrop, { backgroundColor: resolvedTheme.backdrop }], children: _jsxs(View, { accessibilityViewIsModal: true, style: [styles.dialog, { backgroundColor: resolvedTheme.surface, borderColor: resolvedTheme.border }], children: [_jsx(Text, { style: [styles.unavailableTitle, { color: resolvedTheme.text }], children: attachmentUpload?.input.name }), _jsx(Text, { style: [styles.unavailableText, { color: attachmentUpload?.error ? "#b42318" : resolvedTheme.muted }], children: attachmentUpload?.error ?? `Uploading ${Math.round((attachmentUpload?.progress ?? 0) * 100)}%` }), _jsxs(View, { style: styles.dialogActions, children: [attachmentUpload?.error ? _jsx(CommandButton, { label: "Retry", onPress: () => void uploadAttachment(), theme: resolvedTheme }) : null, _jsx(CommandButton, { label: "Cancel", onPress: () => { attachmentAbortRef.current?.abort(); setAttachmentUpload(null); }, theme: resolvedTheme })] })] }) }) }), _jsx(UrlDialog, { title: "Image", value: imageUrl, visible: imageRequest !== null, placeholder: "https://", confirmLabel: "Insert", onChangeText: setImageUrl, onCancel: () => setImageRequest(null), onSubmit: submitImageDialog, theme: resolvedTheme }), _jsx(OpenEditorNativeEmojiPicker, { onEmojiSelect: (emoji) => {
|
|
612
682
|
if (!emojiRequest)
|
|
@@ -616,14 +686,17 @@ export const OpenEditorNative = ({ document = createDocument(), editable = true,
|
|
|
616
686
|
...emojiRequest.attrs,
|
|
617
687
|
[attribute]: emoji,
|
|
618
688
|
});
|
|
689
|
+
if (emojiRequest.nodeType === "page" && typeof emojiRequest.attrs.pageId === "string") {
|
|
690
|
+
void pageRuntime?.updatePage?.(emojiRequest.attrs.pageId, { icon: emoji });
|
|
691
|
+
}
|
|
619
692
|
setEmojiRequest(null);
|
|
620
693
|
}, onRequestClose: () => setEmojiRequest(null), theme: {
|
|
621
694
|
surfaceMuted: resolvedTheme.surfaceMuted,
|
|
622
695
|
text: resolvedTheme.text,
|
|
623
696
|
muted: resolvedTheme.muted,
|
|
624
697
|
}, visible: emojiRequest !== null })] }));
|
|
625
|
-
};
|
|
626
|
-
export const OpenEditorNativeViewer = ({ document = createDocument(), theme, renderers,
|
|
698
|
+
});
|
|
699
|
+
export const OpenEditorNativeViewer = ({ document = createDocument(), theme, renderers, pageRuntime, attachmentRuntime, }) => {
|
|
627
700
|
const resolvedTheme = {
|
|
628
701
|
...defaultNativeTheme,
|
|
629
702
|
...theme,
|
|
@@ -633,7 +706,7 @@ export const OpenEditorNativeViewer = ({ document = createDocument(), theme, ren
|
|
|
633
706
|
},
|
|
634
707
|
};
|
|
635
708
|
const normalizedDocument = normalizeForNativeEditor(document);
|
|
636
|
-
return (_jsx(View, { style: [styles.viewer, { backgroundColor: resolvedTheme.surface, borderColor: resolvedTheme.border }], children: normalizedDocument.content.map((node, index) => renderNativeViewerNode(node, `native-viewer-${index}`, resolvedTheme, renderers,
|
|
709
|
+
return (_jsx(View, { style: [styles.viewer, { backgroundColor: resolvedTheme.surface, borderColor: resolvedTheme.border }], children: normalizedDocument.content.map((node, index) => renderNativeViewerNode(node, `native-viewer-${index}`, resolvedTheme, renderers, pageRuntime, attachmentRuntime)) }));
|
|
637
710
|
};
|
|
638
711
|
const CommandButton = ({ label, active = false, primary = false, theme, onPress, }) => (_jsx(Pressable, { accessibilityRole: "button", accessibilityState: { selected: active }, onPress: onPress, style: [
|
|
639
712
|
styles.commandButton,
|
|
@@ -780,6 +853,24 @@ const styles = StyleSheet.create({
|
|
|
780
853
|
fontSize: 13,
|
|
781
854
|
fontWeight: "700",
|
|
782
855
|
},
|
|
856
|
+
pageHeader: {
|
|
857
|
+
gap: 8,
|
|
858
|
+
paddingBottom: 16,
|
|
859
|
+
},
|
|
860
|
+
pageHeaderIcon: {
|
|
861
|
+
fontSize: 40,
|
|
862
|
+
lineHeight: 48,
|
|
863
|
+
},
|
|
864
|
+
pageHeaderTitle: {
|
|
865
|
+
fontSize: 30,
|
|
866
|
+
fontWeight: "700",
|
|
867
|
+
lineHeight: 36,
|
|
868
|
+
padding: 0,
|
|
869
|
+
},
|
|
870
|
+
pageHeaderError: {
|
|
871
|
+
color: "#dc2626",
|
|
872
|
+
fontSize: 12,
|
|
873
|
+
},
|
|
783
874
|
dialogBackdrop: {
|
|
784
875
|
alignItems: "center",
|
|
785
876
|
backgroundColor: "rgba(0, 0, 0, 0.32)",
|
package/dist/toolbar.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ProseMirrorDocument } from "@openeditor/core";
|
|
2
2
|
import type { EditorToolbarItem } from "@openeditor/react-native-prose-editor";
|
|
3
3
|
export declare const openEditorNativeToolbarActionKeys: {
|
|
4
4
|
readonly paragraph: "paragraph";
|
|
@@ -17,9 +17,10 @@ export declare const openEditorNativeToolbarActionKeys: {
|
|
|
17
17
|
readonly dismissKeyboard: "openeditor:keyboard:dismiss";
|
|
18
18
|
};
|
|
19
19
|
export type OpenEditorNativeToolbarActionKey = (typeof openEditorNativeToolbarActionKeys)[keyof typeof openEditorNativeToolbarActionKeys];
|
|
20
|
-
export declare const createOpenEditorNativeToolbarItems: ({ canUndo, canRedo, }?: {
|
|
20
|
+
export declare const createOpenEditorNativeToolbarItems: ({ canUndo, canRedo, enabledBlocks, }?: {
|
|
21
21
|
canUndo?: boolean;
|
|
22
22
|
canRedo?: boolean;
|
|
23
|
+
enabledBlocks?: readonly string[];
|
|
23
24
|
}) => readonly EditorToolbarItem[];
|
|
24
25
|
export declare const openEditorNativeToolbarItems: readonly EditorToolbarItem[];
|
|
25
26
|
export declare const nativeToolbarParityCoverage: () => {
|
package/dist/toolbar.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isOpenEditorBlockEnabled } from "@openeditor/core";
|
|
1
2
|
import { createOpenEditorInsertDocument, defaultBlockSpecs, defaultMarkSpecs, } from "@openeditor/extensions";
|
|
2
3
|
import { toNativeEditorDocument } from "./document.js";
|
|
3
4
|
const glyphIcon = (text) => ({ type: "glyph", text });
|
|
@@ -24,7 +25,7 @@ export const openEditorNativeToolbarActionKeys = {
|
|
|
24
25
|
redo: "openeditor:history:redo",
|
|
25
26
|
dismissKeyboard: "openeditor:keyboard:dismiss",
|
|
26
27
|
};
|
|
27
|
-
export const createOpenEditorNativeToolbarItems = ({ canUndo = true, canRedo = true, } = {}) => {
|
|
28
|
+
export const createOpenEditorNativeToolbarItems = ({ canUndo = true, canRedo = true, enabledBlocks, } = {}) => {
|
|
28
29
|
const items = [
|
|
29
30
|
{
|
|
30
31
|
type: "group",
|
|
@@ -149,7 +150,51 @@ export const createOpenEditorNativeToolbarItems = ({ canUndo = true, canRedo = t
|
|
|
149
150
|
placement: "end",
|
|
150
151
|
},
|
|
151
152
|
];
|
|
152
|
-
|
|
153
|
+
const actionBlocks = new Map([
|
|
154
|
+
[openEditorNativeToolbarActionKeys.paragraph, "paragraph"],
|
|
155
|
+
[openEditorNativeToolbarActionKeys.bulletList, "bulletList"],
|
|
156
|
+
[openEditorNativeToolbarActionKeys.orderedList, "orderedList"],
|
|
157
|
+
[openEditorNativeToolbarActionKeys.columns, "columns"],
|
|
158
|
+
[openEditorNativeToolbarActionKeys.table, "table"],
|
|
159
|
+
[openEditorNativeToolbarActionKeys.toggleList, "toggleList"],
|
|
160
|
+
[openEditorNativeToolbarActionKeys.callout, "callout"],
|
|
161
|
+
[openEditorNativeToolbarActionKeys.page, "page"],
|
|
162
|
+
[openEditorNativeToolbarActionKeys.attachment, "attachment"],
|
|
163
|
+
]);
|
|
164
|
+
const blockForItem = (item) => {
|
|
165
|
+
if (item.type === "action")
|
|
166
|
+
return actionBlocks.get(item.key);
|
|
167
|
+
if (item.type === "heading")
|
|
168
|
+
return "heading";
|
|
169
|
+
if (item.type === "blockquote")
|
|
170
|
+
return "blockquote";
|
|
171
|
+
if (item.type === "image")
|
|
172
|
+
return "image";
|
|
173
|
+
if (item.type === "node" && item.nodeType === "horizontalRule")
|
|
174
|
+
return "divider";
|
|
175
|
+
if (item.type === "command" && item.command.toLowerCase().includes("table"))
|
|
176
|
+
return "table";
|
|
177
|
+
return undefined;
|
|
178
|
+
};
|
|
179
|
+
const filterItems = (source) => {
|
|
180
|
+
const filtered = [];
|
|
181
|
+
for (const item of source) {
|
|
182
|
+
if (item.type === "group") {
|
|
183
|
+
const children = item.items.filter((child) => {
|
|
184
|
+
const block = blockForItem(child);
|
|
185
|
+
return !block || isOpenEditorBlockEnabled(block, { enabledBlocks });
|
|
186
|
+
});
|
|
187
|
+
if (children.length)
|
|
188
|
+
filtered.push({ ...item, items: children });
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
const block = blockForItem(item);
|
|
192
|
+
if (!block || isOpenEditorBlockEnabled(block, { enabledBlocks }))
|
|
193
|
+
filtered.push(item);
|
|
194
|
+
}
|
|
195
|
+
return filtered;
|
|
196
|
+
};
|
|
197
|
+
return filterItems(items);
|
|
153
198
|
};
|
|
154
199
|
export const openEditorNativeToolbarItems = createOpenEditorNativeToolbarItems();
|
|
155
200
|
const walkToolbarItems = (items) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openeditor/native",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.29",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"repository": {
|
|
@@ -28,16 +28,16 @@
|
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@expo/ui": "~56.0.18",
|
|
30
30
|
"@expo/vector-icons": "15.0.3",
|
|
31
|
-
"@openeditor/core": "0.0.
|
|
32
|
-
"@openeditor/exporters": "0.0.
|
|
33
|
-
"@openeditor/extensions": "0.0.
|
|
31
|
+
"@openeditor/core": "0.0.29",
|
|
32
|
+
"@openeditor/exporters": "0.0.29",
|
|
33
|
+
"@openeditor/extensions": "0.0.29",
|
|
34
34
|
"emojibase": "17.0.0",
|
|
35
35
|
"emojibase-data": "16.0.2",
|
|
36
36
|
"react": "19.2.3",
|
|
37
37
|
"react-native": "0.85.3"
|
|
38
38
|
},
|
|
39
39
|
"peerDependencies": {
|
|
40
|
-
"@openeditor/react-native-prose-editor": "0.0.
|
|
40
|
+
"@openeditor/react-native-prose-editor": "0.0.29",
|
|
41
41
|
"react-native-keyboard-controller": ">=1.21",
|
|
42
42
|
"react": ">=19",
|
|
43
43
|
"react-native": ">=0.85"
|