@openeditor/react 0.0.22 → 0.0.24
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 +51 -5
- package/dist/index.d.ts +22 -1
- package/dist/index.js +62 -30
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -9,6 +9,7 @@ Pass `attachmentRuntime` to `useOpenEditorController` for editable attachments a
|
|
|
9
9
|
- `useOpenEditorController` for editor state, commands, document exports, and selection state
|
|
10
10
|
- `OpenEditorContent` for the editable Tiptap-backed content surface
|
|
11
11
|
- `OpenEditorViewer` for read-only React rendering
|
|
12
|
+
- `getDefaultBlockPickerItems` for host-owned block rails, pickers, and command palettes
|
|
12
13
|
- `getDefaultSlashMenuItems` for UI packages that want the built-in insert actions
|
|
13
14
|
- position-based block targeting and transaction-backed copy, duplicate, move, and delete commands
|
|
14
15
|
- `defineOpenEditorReactNode` for consumer-owned React blocks
|
|
@@ -34,6 +35,49 @@ Pass `blockActions` to contribute product-specific actions. The optional
|
|
|
34
35
|
Copy, Duplicate, and Delete. Clipboard copies use a versioned OpenEditor block
|
|
35
36
|
envelope so blocks can move between independent editor instances and documents.
|
|
36
37
|
|
|
38
|
+
## Host-owned block pickers
|
|
39
|
+
|
|
40
|
+
Use `getDefaultBlockPickerItems` when blocks need to be discoverable outside the
|
|
41
|
+
slash menu. The catalog contains built-in insert presets and registered custom
|
|
42
|
+
blocks, but deliberately excludes slash-only actions such as moving a block or
|
|
43
|
+
converting a whole document. Use `sortBlockPickerItems` when merging additional
|
|
44
|
+
host-owned entries into the catalog.
|
|
45
|
+
|
|
46
|
+
```tsx
|
|
47
|
+
import {
|
|
48
|
+
getDefaultBlockPickerItems,
|
|
49
|
+
OpenEditorContent,
|
|
50
|
+
useOpenEditorController,
|
|
51
|
+
} from "@openeditor/react";
|
|
52
|
+
|
|
53
|
+
export function EditorWithBlockRail() {
|
|
54
|
+
const controller = useOpenEditorController({ extensions });
|
|
55
|
+
const items = getDefaultBlockPickerItems(controller);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<>
|
|
59
|
+
<aside aria-label="Blocks">
|
|
60
|
+
{items.map((item) => (
|
|
61
|
+
<button
|
|
62
|
+
key={item.key}
|
|
63
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
64
|
+
onClick={item.insert}
|
|
65
|
+
type="button"
|
|
66
|
+
>
|
|
67
|
+
{item.label}
|
|
68
|
+
</button>
|
|
69
|
+
))}
|
|
70
|
+
</aside>
|
|
71
|
+
<OpenEditorContent controller={controller} />
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`item.insert()` restores focus and inserts at the controller's current editor
|
|
78
|
+
selection. Preventing the button's mouse-down default keeps the caret visibly
|
|
79
|
+
anchored while the user moves from the editor to a host-owned picker.
|
|
80
|
+
|
|
37
81
|
The built-in web catalog includes editable Toggle Lists, rich-content Callouts,
|
|
38
82
|
and Mermaid-powered Diagrams. Diagrams use `beautiful-mermaid` for synchronous,
|
|
39
83
|
themeable SVG rendering and retain their canonical Mermaid source in the document.
|
|
@@ -114,7 +158,7 @@ const productCard = defineOpenEditorReactNode({
|
|
|
114
158
|
],
|
|
115
159
|
},
|
|
116
160
|
component: ProductCardNode,
|
|
117
|
-
|
|
161
|
+
insertMenu: {
|
|
118
162
|
icon: PackageOpen,
|
|
119
163
|
keywords: ["product", "commerce", "card"],
|
|
120
164
|
order: 250,
|
|
@@ -143,13 +187,15 @@ Pass the same extension array to `OpenEditorViewer`. The controller automaticall
|
|
|
143
187
|
uses registered exporters for `controller.exports` and registered blocks appear
|
|
144
188
|
in `getDefaultSlashMenuItems`, which powers `@openeditor/ui`.
|
|
145
189
|
|
|
146
|
-
|
|
190
|
+
Insert-menu entries accept an optional React `icon` component and numeric `order`.
|
|
147
191
|
OpenEditor assigns built-in entries orders in increments of 100, so consumer
|
|
148
192
|
blocks can be placed between them (for example, `order: 250` appears between
|
|
149
193
|
Heading 1 and Heading 2). Entries with the same order retain their declaration
|
|
150
|
-
order, and entries without an order appear after ordered entries.
|
|
151
|
-
|
|
152
|
-
|
|
194
|
+
order, and entries without an order appear after ordered entries. This metadata
|
|
195
|
+
is shared by slash menus and `getDefaultBlockPickerItems`; use `insertMenu: false`
|
|
196
|
+
to keep a block out of both discovery surfaces. The legacy `slashMenu` extension
|
|
197
|
+
field remains supported. The same fields are available on direct
|
|
198
|
+
`slashMenuItems`; a slash menu's explicit `items` prop is also sorted by this rule.
|
|
153
199
|
|
|
154
200
|
Block `name` is the stable public identifier. Namespace consumer blocks to avoid
|
|
155
201
|
collisions. `nodeType` is the serialized ProseMirror node type and defaults to
|
package/dist/index.d.ts
CHANGED
|
@@ -161,6 +161,22 @@ type OpenEditorSlashMenuItem = {
|
|
|
161
161
|
range: InsertRange;
|
|
162
162
|
}) => boolean;
|
|
163
163
|
};
|
|
164
|
+
/**
|
|
165
|
+
* An insertable block exposed to host-owned pickers and command palettes.
|
|
166
|
+
*
|
|
167
|
+
* Unlike `OpenEditorSlashMenuItem`, this contract contains only block insertion
|
|
168
|
+
* and is not coupled to a slash query range. Calling `insert` restores editor
|
|
169
|
+
* focus and inserts at the controller's current selection.
|
|
170
|
+
*/
|
|
171
|
+
type OpenEditorBlockPickerItem = {
|
|
172
|
+
key: string;
|
|
173
|
+
label: string;
|
|
174
|
+
group: string;
|
|
175
|
+
icon?: OpenEditorSlashMenuIcon;
|
|
176
|
+
keywords?: readonly string[];
|
|
177
|
+
order?: number;
|
|
178
|
+
insert: () => boolean;
|
|
179
|
+
};
|
|
164
180
|
type OpenEditorViewerRenderer = (context: {
|
|
165
181
|
node: ProseMirrorNode;
|
|
166
182
|
children: ReactNode;
|
|
@@ -180,6 +196,9 @@ type OpenEditorExtensionMenu = {
|
|
|
180
196
|
type OpenEditorReactExtension = {
|
|
181
197
|
block: BlockSpec;
|
|
182
198
|
tiptapExtensions: readonly AnyExtension[];
|
|
199
|
+
/** Discovery metadata shared by slash menus and host-owned block pickers. */
|
|
200
|
+
insertMenu?: false | OpenEditorExtensionMenu;
|
|
201
|
+
/** @deprecated Use `insertMenu`. */
|
|
183
202
|
slashMenu?: false | OpenEditorExtensionMenu;
|
|
184
203
|
viewer: OpenEditorViewerRenderer;
|
|
185
204
|
exporters?: OpenEditorExporters;
|
|
@@ -209,9 +228,11 @@ declare module "@tiptap/core" {
|
|
|
209
228
|
declare const formatAttachmentSize: (size: number | null) => string;
|
|
210
229
|
declare const useOpenEditorController: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
|
|
211
230
|
declare const sortSlashMenuItems: (items: readonly OpenEditorSlashMenuItem[]) => OpenEditorSlashMenuItem[];
|
|
231
|
+
declare const sortBlockPickerItems: (items: readonly OpenEditorBlockPickerItem[]) => OpenEditorBlockPickerItem[];
|
|
232
|
+
declare const getDefaultBlockPickerItems: (controller: OpenEditorController) => OpenEditorBlockPickerItem[];
|
|
212
233
|
declare const getDefaultSlashMenuItems: (controller: OpenEditorController) => OpenEditorSlashMenuItem[];
|
|
213
234
|
declare const OpenEditorContent: ({ controller, className, }: OpenEditorContentProps) => react.JSX.Element;
|
|
214
235
|
declare const OpenEditorViewer: ({ document, className, renderers, extensions, pageRuntime, attachmentRuntime, }: OpenEditorViewerProps) => react.JSX.Element;
|
|
215
236
|
declare const useOpenEditor: ({ initialDocument, editable, placeholder, onChange, onFilePasteDrop, pageRuntime, attachmentRuntime, slashMenuItems, extensions, blockActions, }?: OpenEditorReactProps) => OpenEditorController;
|
|
216
237
|
|
|
217
|
-
export { type InsertRange, type OpenEditorBlockAction, type OpenEditorBlockActionContext, type OpenEditorBlockTarget, OpenEditorContent, type OpenEditorContentProps, type OpenEditorController, type OpenEditorExtensionMenu, type OpenEditorOverlayRect, type OpenEditorPageRuntime, type OpenEditorPageSnapshot, type OpenEditorReactConfig, type OpenEditorReactExtension, type OpenEditorReactNodeExtension, type OpenEditorReactProps, type OpenEditorSlashMenuIcon, type OpenEditorSlashMenuIconProps, type OpenEditorSlashMenuItem, type OpenEditorTheme, OpenEditorThemeProvider, type OpenEditorThemeStyle, type OpenEditorThemeToken, OpenEditorViewer, type OpenEditorViewerProps, type OpenEditorViewerRenderer, type SelectionBubbleState, type SlashState, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortSlashMenuItems, useOpenEditor, useOpenEditorController, useOpenEditorThemeStyle };
|
|
238
|
+
export { type InsertRange, type OpenEditorBlockAction, type OpenEditorBlockActionContext, type OpenEditorBlockPickerItem, type OpenEditorBlockTarget, OpenEditorContent, type OpenEditorContentProps, type OpenEditorController, type OpenEditorExtensionMenu, type OpenEditorOverlayRect, type OpenEditorPageRuntime, type OpenEditorPageSnapshot, type OpenEditorReactConfig, type OpenEditorReactExtension, type OpenEditorReactNodeExtension, type OpenEditorReactProps, type OpenEditorSlashMenuIcon, type OpenEditorSlashMenuIconProps, type OpenEditorSlashMenuItem, type OpenEditorTheme, OpenEditorThemeProvider, type OpenEditorThemeStyle, type OpenEditorThemeToken, OpenEditorViewer, type OpenEditorViewerProps, type OpenEditorViewerRenderer, type SelectionBubbleState, type SlashState, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultBlockPickerItems, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortBlockPickerItems, sortSlashMenuItems, useOpenEditor, useOpenEditorController, useOpenEditorThemeStyle };
|
package/dist/index.js
CHANGED
|
@@ -32904,15 +32904,34 @@ var pageSnapshotFromNode = (node) => {
|
|
|
32904
32904
|
href: typeof node.attrs?.href === "string" ? node.attrs.href : null
|
|
32905
32905
|
};
|
|
32906
32906
|
};
|
|
32907
|
+
var useResolvedPageSnapshot = (cached, runtime) => {
|
|
32908
|
+
const [resolved, setResolved] = useState(null);
|
|
32909
|
+
useEffect(() => {
|
|
32910
|
+
let active = true;
|
|
32911
|
+
setResolved(null);
|
|
32912
|
+
if (cached?.pageId && runtime?.resolvePage) {
|
|
32913
|
+
void runtime.resolvePage(cached.pageId).then((page) => {
|
|
32914
|
+
if (active && page) setResolved(page);
|
|
32915
|
+
}).catch(() => {
|
|
32916
|
+
if (active) setResolved(null);
|
|
32917
|
+
});
|
|
32918
|
+
}
|
|
32919
|
+
return () => {
|
|
32920
|
+
active = false;
|
|
32921
|
+
};
|
|
32922
|
+
}, [cached?.pageId, runtime]);
|
|
32923
|
+
return [resolved ?? cached, setResolved];
|
|
32924
|
+
};
|
|
32907
32925
|
var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes2 }) => {
|
|
32908
32926
|
const runtime = use(OpenEditorPageRuntimeContext);
|
|
32909
32927
|
const themeStyle = useOpenEditorThemeStyle();
|
|
32910
32928
|
const [creating, setCreating] = useState(false);
|
|
32911
32929
|
const [error, setError] = useState(null);
|
|
32912
|
-
const
|
|
32913
|
-
const
|
|
32930
|
+
const cached = pageSnapshotFromNode(node);
|
|
32931
|
+
const [page, setResolvedPage] = useResolvedPageSnapshot(cached, runtime);
|
|
32932
|
+
const icon = normalizeEmoji(page?.icon, DEFAULT_PAGE_EMOJI);
|
|
32914
32933
|
const openPage = () => {
|
|
32915
|
-
if (
|
|
32934
|
+
if (page) void runtime?.openPage?.(page);
|
|
32916
32935
|
};
|
|
32917
32936
|
const createPage = async () => {
|
|
32918
32937
|
if (!runtime?.createPage || creating) return;
|
|
@@ -32929,10 +32948,11 @@ var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes
|
|
|
32929
32948
|
};
|
|
32930
32949
|
const updateIcon = async (nextIcon) => {
|
|
32931
32950
|
updateAttributes2({ icon: nextIcon });
|
|
32932
|
-
if (!
|
|
32951
|
+
if (!page?.pageId || !runtime?.updatePageIcon) return;
|
|
32952
|
+
setResolvedPage({ ...page, icon: nextIcon });
|
|
32933
32953
|
setError(null);
|
|
32934
32954
|
try {
|
|
32935
|
-
await runtime.updatePageIcon(
|
|
32955
|
+
await runtime.updatePageIcon(page.pageId, nextIcon);
|
|
32936
32956
|
} catch (cause) {
|
|
32937
32957
|
setError(cause instanceof Error ? cause.message : "Could not update the page icon.");
|
|
32938
32958
|
}
|
|
@@ -32940,22 +32960,22 @@ var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes
|
|
|
32940
32960
|
return /* @__PURE__ */ jsxs(
|
|
32941
32961
|
NodeViewWrapper,
|
|
32942
32962
|
{
|
|
32943
|
-
"aria-label":
|
|
32963
|
+
"aria-label": page ? `Open ${page.title}` : void 0,
|
|
32944
32964
|
className: "oe-page",
|
|
32945
|
-
"data-page-id":
|
|
32965
|
+
"data-page-id": page?.pageId ?? "",
|
|
32946
32966
|
onClick: (event) => {
|
|
32947
32967
|
if (event.target.closest(".oe-emoji-trigger, .oe-emoji-popover")) return;
|
|
32948
32968
|
openPage();
|
|
32949
32969
|
},
|
|
32950
32970
|
onKeyDown: (event) => {
|
|
32951
32971
|
if (event.target.closest(".oe-emoji-trigger, .oe-emoji-popover")) return;
|
|
32952
|
-
if (
|
|
32972
|
+
if (page && (event.key === "Enter" || event.key === " ")) {
|
|
32953
32973
|
event.preventDefault();
|
|
32954
32974
|
openPage();
|
|
32955
32975
|
}
|
|
32956
32976
|
},
|
|
32957
|
-
role:
|
|
32958
|
-
tabIndex:
|
|
32977
|
+
role: page ? "button" : void 0,
|
|
32978
|
+
tabIndex: page ? 0 : void 0,
|
|
32959
32979
|
children: [
|
|
32960
32980
|
/* @__PURE__ */ jsx("span", { contentEditable: false, children: /* @__PURE__ */ jsx(
|
|
32961
32981
|
OpenEditorEmojiPicker,
|
|
@@ -32968,7 +32988,7 @@ var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes
|
|
|
32968
32988
|
onEmojiSelect: (nextIcon) => void updateIcon(nextIcon)
|
|
32969
32989
|
}
|
|
32970
32990
|
) }),
|
|
32971
|
-
/* @__PURE__ */ jsx(
|
|
32991
|
+
page && page !== cached ? /* @__PURE__ */ jsx("div", { className: "oe-page-title", contentEditable: false, children: page.title }) : /* @__PURE__ */ jsx(
|
|
32972
32992
|
NodeViewContent,
|
|
32973
32993
|
{
|
|
32974
32994
|
as: "div",
|
|
@@ -32976,7 +32996,7 @@ var OpenEditorPageNodeView = ({ editor, node, updateAttributes: updateAttributes
|
|
|
32976
32996
|
contentEditable: false
|
|
32977
32997
|
}
|
|
32978
32998
|
),
|
|
32979
|
-
!
|
|
32999
|
+
!page && runtime?.createPage && editor.isEditable ? /* @__PURE__ */ jsx(
|
|
32980
33000
|
"button",
|
|
32981
33001
|
{
|
|
32982
33002
|
className: "oe-page-action",
|
|
@@ -33015,20 +33035,7 @@ var Page = Node3.create({
|
|
|
33015
33035
|
var OpenEditorPageViewer = ({ node }) => {
|
|
33016
33036
|
const runtime = use(OpenEditorPageRuntimeContext);
|
|
33017
33037
|
const cached = pageSnapshotFromNode(node);
|
|
33018
|
-
const [
|
|
33019
|
-
useEffect(() => {
|
|
33020
|
-
let active = true;
|
|
33021
|
-
setResolved(null);
|
|
33022
|
-
if (cached?.pageId && runtime?.resolvePage) {
|
|
33023
|
-
void runtime.resolvePage(cached.pageId).then((page2) => {
|
|
33024
|
-
if (active && page2) setResolved(page2);
|
|
33025
|
-
});
|
|
33026
|
-
}
|
|
33027
|
-
return () => {
|
|
33028
|
-
active = false;
|
|
33029
|
-
};
|
|
33030
|
-
}, [cached?.pageId, runtime]);
|
|
33031
|
-
const page = resolved ?? cached;
|
|
33038
|
+
const [page] = useResolvedPageSnapshot(cached, runtime);
|
|
33032
33039
|
const title = page?.title ?? (getNodeText(node) || "Untitled");
|
|
33033
33040
|
const content = /* @__PURE__ */ jsxs(Fragment$1, { children: [
|
|
33034
33041
|
/* @__PURE__ */ jsx("span", { className: "oe-page-icon", "aria-hidden": "true", children: normalizeEmoji(page?.icon, DEFAULT_PAGE_EMOJI) }),
|
|
@@ -33651,12 +33658,37 @@ var useOpenEditorController = ({
|
|
|
33651
33658
|
};
|
|
33652
33659
|
return controller;
|
|
33653
33660
|
};
|
|
33654
|
-
var
|
|
33661
|
+
var sortMenuItems = (items) => [...items].sort((left, right) => {
|
|
33655
33662
|
const leftOrder = typeof left.order === "number" && Number.isFinite(left.order) ? left.order : Number.POSITIVE_INFINITY;
|
|
33656
33663
|
const rightOrder = typeof right.order === "number" && Number.isFinite(right.order) ? right.order : Number.POSITIVE_INFINITY;
|
|
33657
33664
|
if (leftOrder === rightOrder) return 0;
|
|
33658
33665
|
return leftOrder - rightOrder;
|
|
33659
33666
|
});
|
|
33667
|
+
var sortSlashMenuItems = (items) => sortMenuItems(items);
|
|
33668
|
+
var sortBlockPickerItems = (items) => sortMenuItems(items);
|
|
33669
|
+
var getExtensionInsertMenu = (extension) => extension.insertMenu ?? extension.slashMenu;
|
|
33670
|
+
var getDefaultBlockPickerItems = (controller) => sortBlockPickerItems([
|
|
33671
|
+
...openEditorInsertPresets.map((preset, index) => ({
|
|
33672
|
+
key: preset.key,
|
|
33673
|
+
label: preset.label,
|
|
33674
|
+
group: preset.group,
|
|
33675
|
+
keywords: [preset.key],
|
|
33676
|
+
order: (index + 1) * 100,
|
|
33677
|
+
insert: () => controller.insertBlock(preset.key)
|
|
33678
|
+
})),
|
|
33679
|
+
...controller.extensions.filter((extension) => getExtensionInsertMenu(extension) !== false).map((extension) => {
|
|
33680
|
+
const menu = getExtensionInsertMenu(extension) || void 0;
|
|
33681
|
+
return {
|
|
33682
|
+
key: extension.block.name,
|
|
33683
|
+
label: menu?.label ?? extension.block.label,
|
|
33684
|
+
group: menu?.group ?? extension.block.group,
|
|
33685
|
+
icon: menu?.icon,
|
|
33686
|
+
keywords: menu?.keywords ?? [extension.block.name],
|
|
33687
|
+
order: menu?.order,
|
|
33688
|
+
insert: () => controller.insertBlock(extension.block.name)
|
|
33689
|
+
};
|
|
33690
|
+
})
|
|
33691
|
+
]);
|
|
33660
33692
|
var getDefaultSlashMenuItems = (controller) => {
|
|
33661
33693
|
const internalController = controller;
|
|
33662
33694
|
return sortSlashMenuItems([
|
|
@@ -33692,8 +33724,8 @@ var getDefaultSlashMenuItems = (controller) => {
|
|
|
33692
33724
|
return true;
|
|
33693
33725
|
}
|
|
33694
33726
|
},
|
|
33695
|
-
...controller.extensions.filter((extension) => extension
|
|
33696
|
-
const menu = extension
|
|
33727
|
+
...controller.extensions.filter((extension) => getExtensionInsertMenu(extension) !== false).map((extension) => {
|
|
33728
|
+
const menu = getExtensionInsertMenu(extension) || void 0;
|
|
33697
33729
|
return {
|
|
33698
33730
|
key: extension.block.name,
|
|
33699
33731
|
label: menu?.label ?? extension.block.label,
|
|
@@ -33847,6 +33879,6 @@ var OpenEditorViewer = ({
|
|
|
33847
33879
|
};
|
|
33848
33880
|
var useOpenEditor = useOpenEditorController;
|
|
33849
33881
|
|
|
33850
|
-
export { NodeViewContent, NodeViewWrapper, OpenEditorContent, DragHandle2 as OpenEditorDragHandle, OpenEditorThemeProvider, OpenEditorViewer, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortSlashMenuItems, useOpenEditor, useOpenEditorController, useOpenEditorThemeStyle };
|
|
33882
|
+
export { NodeViewContent, NodeViewWrapper, OpenEditorContent, DragHandle2 as OpenEditorDragHandle, OpenEditorThemeProvider, OpenEditorViewer, createOpenEditorThemeStyle, defineOpenEditorReactExtension, defineOpenEditorReactNode, formatAttachmentSize, getDefaultBlockPickerItems, getDefaultSlashMenuItems, openEditorThemeTokenNames, sortBlockPickerItems, sortSlashMenuItems, useOpenEditor, useOpenEditorController, useOpenEditorThemeStyle };
|
|
33851
33883
|
//# sourceMappingURL=index.js.map
|
|
33852
33884
|
//# sourceMappingURL=index.js.map
|