@octanejs/blocknote 0.1.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/LICENSE +21 -0
- package/README.md +142 -0
- package/UPSTREAM.md +41 -0
- package/package.json +62 -0
- package/src/BlockNoteContext.ts +39 -0
- package/src/BlockNoteViewRaw.tsrx +188 -0
- package/src/hooks/useBlockNoteEditor.ts +33 -0
- package/src/hooks/useCreateBlockNote.ts +37 -0
- package/src/hooks/useEditorChange.ts +28 -0
- package/src/hooks/useEditorSelectionChange.ts +32 -0
- package/src/hooks/usePrefersColorScheme.ts +54 -0
- package/src/index.ts +17 -0
- package/src/internal.ts +18 -0
- package/src/portalTarget.ts +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway and Octane contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# @octanejs/blocknote
|
|
2
|
+
|
|
3
|
+
Headless [BlockNote](https://www.blocknotejs.org) editor for Octane. It covers the editor surface of `@blocknote/react@0.53.0` without the default UI, and uses `@blocknote/core` unchanged.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @octanejs/blocknote
|
|
9
|
+
pnpm add @octanejs/blocknote
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
|
|
14
|
+
```tsx
|
|
15
|
+
import { BlockNoteViewRaw, useCreateBlockNote } from '@octanejs/blocknote';
|
|
16
|
+
|
|
17
|
+
export function Editor() @{
|
|
18
|
+
const editor = useCreateBlockNote({
|
|
19
|
+
initialContent: [{ type: 'paragraph', content: 'Hello' }],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
<BlockNoteViewRaw editor={editor} onChange={() => console.log(editor.document)} />
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`BlockNoteViewRaw` imports `@blocknote/core/style.css`. Toolbars, menus, and other UI are yours to build. Render them as children, read the editor with `useBlockNoteEditor()`, and use `renderEditor={false}` with `<BlockNoteViewEditor />` to control where the editable area goes.
|
|
27
|
+
|
|
28
|
+
## Exports
|
|
29
|
+
|
|
30
|
+
- `BlockNoteViewRaw`, `BlockNoteViewEditor`, `BlockNoteViewProps`
|
|
31
|
+
- `BlockNoteContext`, `useBlockNoteContext`, `BlockNoteContextValue`
|
|
32
|
+
- `useCreateBlockNote`, `useBlockNoteEditor`
|
|
33
|
+
- `useEditorChange`, `useEditorSelectionChange`, `usePrefersColorScheme`
|
|
34
|
+
- `PortalElementsMap`, `PortalTarget`
|
|
35
|
+
|
|
36
|
+
## Building UI
|
|
37
|
+
|
|
38
|
+
This package has no default UI. Each BlockNote menu and toolbar is a framework-neutral `@blocknote/core` extension that exposes a store. The store has `subscribe(listener)` and `state`, so Octane's own `useSyncExternalStore` can read it without any extra dependency.
|
|
39
|
+
|
|
40
|
+
### Menus and toolbars
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { SuggestionMenu, filterSuggestionItems, getDefaultSlashMenuItems } from '@blocknote/core';
|
|
44
|
+
import { useBlockNoteEditor } from '@octanejs/blocknote';
|
|
45
|
+
import { useEffect, useSyncExternalStore } from 'octane';
|
|
46
|
+
|
|
47
|
+
export function SlashMenu() @{
|
|
48
|
+
const editor = useBlockNoteEditor();
|
|
49
|
+
const menu = editor.getExtension(SuggestionMenu)!;
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
menu.addSuggestionMenu({ triggerCharacter: '/' });
|
|
53
|
+
return () => menu.removeSuggestionMenu('/');
|
|
54
|
+
}, [menu]);
|
|
55
|
+
|
|
56
|
+
const state = useSyncExternalStore(menu.store.subscribe, () => menu.store.state);
|
|
57
|
+
const items = state?.show
|
|
58
|
+
? filterSuggestionItems(getDefaultSlashMenuItems(editor), state.query)
|
|
59
|
+
: [];
|
|
60
|
+
|
|
61
|
+
@if (state?.show) {
|
|
62
|
+
<ul
|
|
63
|
+
class="slash-menu"
|
|
64
|
+
style={{ position: 'fixed', left: state.referencePos.left, top: state.referencePos.bottom }}
|
|
65
|
+
>
|
|
66
|
+
@for (const item of items; key item.key) {
|
|
67
|
+
<li
|
|
68
|
+
onMouseDown={(event: MouseEvent) => {
|
|
69
|
+
event.preventDefault(); // keep focus in the editor
|
|
70
|
+
menu.clearQuery();
|
|
71
|
+
menu.closeMenu();
|
|
72
|
+
item.onItemClick();
|
|
73
|
+
}}
|
|
74
|
+
>
|
|
75
|
+
{item.title}
|
|
76
|
+
</li>
|
|
77
|
+
}
|
|
78
|
+
</ul>
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// <BlockNoteViewRaw editor={editor}><SlashMenu /></BlockNoteViewRaw>
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
`FormattingToolbar`, `SideMenu`, `LinkToolbar`, `FilePanel`, and `TableHandles` follow the same pattern. For collision-aware placement, position the element from `referencePos` with a library such as `@octanejs/floating-ui`.
|
|
86
|
+
|
|
87
|
+
The extension stores are TanStack Stores. If your app already uses `@octanejs/tanstack-store`, `useStore(menu.store, (state) => state?.query)` also works and adds selector-based updates.
|
|
88
|
+
|
|
89
|
+
### Custom blocks
|
|
90
|
+
|
|
91
|
+
Core's `createBlockSpec` renders to DOM. To render an Octane component inside a block, mount a root on a node you own and unmount it in `destroy`:
|
|
92
|
+
|
|
93
|
+
```tsx
|
|
94
|
+
import { BlockNoteSchema, createBlockSpec, defaultBlockSpecs } from '@blocknote/core';
|
|
95
|
+
import { createRoot } from 'octane';
|
|
96
|
+
|
|
97
|
+
function CalloutIcon(props: { emoji: string }) @{
|
|
98
|
+
<span class="callout-icon">{props.emoji}</span>
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const Callout = createBlockSpec(
|
|
102
|
+
{ type: 'callout', propSchema: { emoji: { default: '💡' } }, content: 'inline' },
|
|
103
|
+
{
|
|
104
|
+
render: (block) => {
|
|
105
|
+
const dom = document.createElement('div');
|
|
106
|
+
const icon = document.createElement('span');
|
|
107
|
+
const contentDOM = document.createElement('div');
|
|
108
|
+
dom.className = 'callout';
|
|
109
|
+
icon.contentEditable = 'false';
|
|
110
|
+
dom.append(icon, contentDOM);
|
|
111
|
+
const root = createRoot(icon);
|
|
112
|
+
root.render(CalloutIcon, { emoji: block.props.emoji });
|
|
113
|
+
return { dom, contentDOM, destroy: () => root.unmount() };
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
export const schema = BlockNoteSchema.create({
|
|
119
|
+
blockSpecs: { ...defaultBlockSpecs, callout: Callout() },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// useCreateBlockNote({ schema })
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Each block root is separate from your app tree, so it does not see your contexts (theme, router, stores). Pass what it needs through props or module state. Context-aware block and inline content specs are planned.
|
|
126
|
+
|
|
127
|
+
Both recipes run as real-browser tests in `tests/browser/harness`.
|
|
128
|
+
|
|
129
|
+
## Server rendering
|
|
130
|
+
|
|
131
|
+
Server rendering is supported. The server emits the view shell, and the editor mounts into it on the client after hydration. Document content is not part of the server HTML. Without a `theme` prop the server renders the light scheme, and the system scheme applies after hydration. Pass `theme` to fix it on both sides.
|
|
132
|
+
|
|
133
|
+
## Known differences
|
|
134
|
+
|
|
135
|
+
- No default UI. `BlockNoteView`, `BlockNoteDefaultUI`, `ComponentsContext`, and the toolbar, menu, side-menu, table-handle, and comment components are not provided. Upstream disables all of them when no components context exists, so `BlockNoteViewRaw` here matches that upstream configuration.
|
|
136
|
+
- `BlockNoteViewRaw` does not accept the default UI flags (`formattingToolbar`, `slashMenu`, and so on). `portalElements` reads only `default`.
|
|
137
|
+
- Custom React block, inline content, and style specs are not provided.
|
|
138
|
+
- Event handlers receive native DOM events, as everywhere in Octane.
|
|
139
|
+
|
|
140
|
+
## Provenance
|
|
141
|
+
|
|
142
|
+
Independently authored and MIT licensed. See [UPSTREAM.md](./UPSTREAM.md).
|
package/UPSTREAM.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Upstream provenance
|
|
2
|
+
|
|
3
|
+
`@octanejs/blocknote` provides the headless editor surface of `@blocknote/react@0.53.0` for Octane.
|
|
4
|
+
|
|
5
|
+
| Field | Pin |
|
|
6
|
+
| ------------------------ | ----------------------------------------------------------------------------------------------------- |
|
|
7
|
+
| Package | `@blocknote/react` |
|
|
8
|
+
| Version | `0.53.0` exactly |
|
|
9
|
+
| Repository | `https://github.com/TypeCellOS/BlockNote` |
|
|
10
|
+
| Release commit (gitHead) | `d5e8c96e2ad5f6832da60bc3c9acd3f6c16c25c5` |
|
|
11
|
+
| npm integrity | `sha512-W8HHfKnt3mf2ugb1n9XAsjR7XAAX4GdZl+SE3h4DPcHdTnpfQCX7B2+tIP6CFKm6jW+BUZ2Y2fpDrLgJEtGtGw==` |
|
|
12
|
+
| Framework-neutral core | `@blocknote/core@0.53.0` (runtime dependency, MPL-2.0, not vendored) |
|
|
13
|
+
|
|
14
|
+
## Source boundary and licensing
|
|
15
|
+
|
|
16
|
+
`@blocknote/react` is licensed MPL-2.0, which is outside this repository's
|
|
17
|
+
approved copy list (MIT, Unlicense, BSD-3-Clause, Apache-2.0). No upstream
|
|
18
|
+
source, declaration, stylesheet, or test is vendored, copied, or adapted here.
|
|
19
|
+
|
|
20
|
+
The Octane adapter is independently authored and MIT licensed. It was written
|
|
21
|
+
from a behavior and API contract derived from BlockNote's public documentation
|
|
22
|
+
and public API names, by an implementer who did not read the `@blocknote/react`
|
|
23
|
+
source. `@blocknote/core` stays an ordinary MPL-2.0 dependency and supplies the
|
|
24
|
+
editor, schemas, and `@blocknote/core/style.css`.
|
|
25
|
+
|
|
26
|
+
An earlier private scaffold of this package was generated by mechanically
|
|
27
|
+
copying upstream source. That scaffold was removed in full before this surface
|
|
28
|
+
was written.
|
|
29
|
+
|
|
30
|
+
## Supported surface
|
|
31
|
+
|
|
32
|
+
Headless only: `BlockNoteViewRaw`, `BlockNoteViewEditor`, `BlockNoteContext`,
|
|
33
|
+
`useBlockNoteContext`, `useCreateBlockNote`, `useBlockNoteEditor`,
|
|
34
|
+
`useEditorChange`, `useEditorSelectionChange`, and `usePrefersColorScheme`.
|
|
35
|
+
This matches upstream `BlockNoteViewRaw` rendered without a components context,
|
|
36
|
+
where every default UI element is disabled.
|
|
37
|
+
|
|
38
|
+
Not provided: the default UI (`BlockNoteView` from UI-kit packages,
|
|
39
|
+
`BlockNoteDefaultUI`, `ComponentsContext`, toolbars, menus, side menu, table
|
|
40
|
+
handles, comments), custom React block/inline/style specs, and the
|
|
41
|
+
`@blocknote/react/style.css` stylesheet.
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/blocknote",
|
|
3
|
+
"devDependencies": {
|
|
4
|
+
"@blocknote/react": "0.53.0",
|
|
5
|
+
"@tsrx/react": "^0.3.7",
|
|
6
|
+
"esbuild": "^0.28.1",
|
|
7
|
+
"react": "19.2.7",
|
|
8
|
+
"react-dom": "19.2.7",
|
|
9
|
+
"vitest": "^4.1.10",
|
|
10
|
+
"octane": "0.5.0"
|
|
11
|
+
},
|
|
12
|
+
"version": "0.1.0",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=22.22.2"
|
|
17
|
+
},
|
|
18
|
+
"description": "BlockNote for the octane renderer — an Octane binding for @blocknote/react block-based rich text editors.",
|
|
19
|
+
"author": {
|
|
20
|
+
"name": "Dominic Gannaway",
|
|
21
|
+
"email": "dg@domgan.com"
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
29
|
+
"directory": "packages/blocknote"
|
|
30
|
+
},
|
|
31
|
+
"main": "src/index.ts",
|
|
32
|
+
"module": "src/index.ts",
|
|
33
|
+
"types": "src/index.ts",
|
|
34
|
+
"files": [
|
|
35
|
+
"src",
|
|
36
|
+
"README.md",
|
|
37
|
+
"UPSTREAM.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"exports": {
|
|
42
|
+
".": "./src/index.ts",
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@blocknote/core": "0.53.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"octane": "^0.5.0"
|
|
50
|
+
},
|
|
51
|
+
"octane": {
|
|
52
|
+
"hookSlots": {
|
|
53
|
+
"manual": [
|
|
54
|
+
"src"
|
|
55
|
+
]
|
|
56
|
+
}
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"typecheck": "tsrx-tsc --noEmit -p tsconfig.supported.json",
|
|
60
|
+
"test": "vitest run --root ../.. --project blocknote"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
import type {
|
|
3
|
+
BlockNoteEditor,
|
|
4
|
+
BlockNoteSchema,
|
|
5
|
+
BlockSchema,
|
|
6
|
+
DefaultBlockSchema,
|
|
7
|
+
DefaultInlineContentSchema,
|
|
8
|
+
DefaultStyleSchema,
|
|
9
|
+
InlineContentSchema,
|
|
10
|
+
StyleSchema,
|
|
11
|
+
} from '@blocknote/core';
|
|
12
|
+
import { createContext, useContext } from 'octane';
|
|
13
|
+
|
|
14
|
+
/** The value a `BlockNoteViewRaw` shares with its descendants. */
|
|
15
|
+
export type BlockNoteContextValue<
|
|
16
|
+
B extends BlockSchema = DefaultBlockSchema,
|
|
17
|
+
I extends InlineContentSchema = DefaultInlineContentSchema,
|
|
18
|
+
S extends StyleSchema = DefaultStyleSchema,
|
|
19
|
+
> = {
|
|
20
|
+
editor?: BlockNoteEditor<B, I, S>;
|
|
21
|
+
colorSchemePreference?: 'light' | 'dark';
|
|
22
|
+
setContentEditableProps?: (props: Record<string, any> | undefined) => void;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const BlockNoteContext = createContext<BlockNoteContextValue<any, any, any> | undefined>(
|
|
26
|
+
undefined,
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Read the nearest BlockNote context. The optional schema only narrows the
|
|
31
|
+
* returned editor type; it is not checked at runtime.
|
|
32
|
+
*/
|
|
33
|
+
export function useBlockNoteContext<
|
|
34
|
+
B extends BlockSchema = DefaultBlockSchema,
|
|
35
|
+
I extends InlineContentSchema = DefaultInlineContentSchema,
|
|
36
|
+
S extends StyleSchema = DefaultStyleSchema,
|
|
37
|
+
>(_schema?: BlockNoteSchema<B, I, S>): BlockNoteContextValue<B, I, S> | undefined {
|
|
38
|
+
return useContext(BlockNoteContext) as BlockNoteContextValue<B, I, S> | undefined;
|
|
39
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
import '@blocknote/core/style.css';
|
|
3
|
+
|
|
4
|
+
import type {
|
|
5
|
+
BlockNoteEditor,
|
|
6
|
+
BlockSchema,
|
|
7
|
+
DefaultBlockSchema,
|
|
8
|
+
DefaultInlineContentSchema,
|
|
9
|
+
DefaultStyleSchema,
|
|
10
|
+
InlineContentSchema,
|
|
11
|
+
StyleSchema,
|
|
12
|
+
} from '@blocknote/core';
|
|
13
|
+
import {
|
|
14
|
+
createContext,
|
|
15
|
+
type OctaneNode,
|
|
16
|
+
useCallback,
|
|
17
|
+
useContext,
|
|
18
|
+
useEffect,
|
|
19
|
+
useMemo,
|
|
20
|
+
useState,
|
|
21
|
+
} from 'octane';
|
|
22
|
+
import type { Octane } from 'octane/jsx-runtime';
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
BlockNoteContext,
|
|
26
|
+
type BlockNoteContextValue,
|
|
27
|
+
useBlockNoteContext,
|
|
28
|
+
} from './BlockNoteContext';
|
|
29
|
+
import { useEditorChange } from './hooks/useEditorChange';
|
|
30
|
+
import { useEditorSelectionChange } from './hooks/useEditorSelectionChange';
|
|
31
|
+
import { usePrefersColorScheme } from './hooks/usePrefersColorScheme';
|
|
32
|
+
import { type PortalElementsMap, resolvePortalTarget } from './portalTarget';
|
|
33
|
+
|
|
34
|
+
export type BlockNoteViewProps<
|
|
35
|
+
B extends BlockSchema = DefaultBlockSchema,
|
|
36
|
+
I extends InlineContentSchema = DefaultInlineContentSchema,
|
|
37
|
+
S extends StyleSchema = DefaultStyleSchema,
|
|
38
|
+
> =
|
|
39
|
+
{
|
|
40
|
+
editor: BlockNoteEditor<B, I, S>;
|
|
41
|
+
theme?: 'light' | 'dark';
|
|
42
|
+
/** @default true */
|
|
43
|
+
editable?: boolean;
|
|
44
|
+
onChange?: Parameters<BlockNoteEditor<B, I, S>['onChange']>[0];
|
|
45
|
+
onSelectionChange?: () => void;
|
|
46
|
+
/**
|
|
47
|
+
* Render the editable surface as the container's first child. Set to
|
|
48
|
+
* `false` to place `BlockNoteViewEditor` among `children` yourself.
|
|
49
|
+
* @default true
|
|
50
|
+
*/
|
|
51
|
+
renderEditor?: boolean;
|
|
52
|
+
autoFocus?: boolean;
|
|
53
|
+
portalElements?: PortalElementsMap;
|
|
54
|
+
className?: string;
|
|
55
|
+
children?: OctaneNode;
|
|
56
|
+
ref?: React.Ref<HTMLDivElement>;
|
|
57
|
+
} & Omit<
|
|
58
|
+
Octane.HTMLAttributes<HTMLDivElement>,
|
|
59
|
+
'onChange' | 'onSelectionChange' | 'children' | 'class' | 'className'
|
|
60
|
+
>;
|
|
61
|
+
|
|
62
|
+
/** What `BlockNoteViewRaw` hands to the `BlockNoteViewEditor` it contains. */
|
|
63
|
+
type ViewEditorSettings = {
|
|
64
|
+
autoFocus: boolean | undefined;
|
|
65
|
+
editable: boolean | undefined;
|
|
66
|
+
portalTarget: HTMLElement | null;
|
|
67
|
+
contentEditableProps: Record<string, any> | undefined;
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const ViewEditorContext = createContext<ViewEditorSettings | undefined>(undefined);
|
|
71
|
+
|
|
72
|
+
const ignore = () => {};
|
|
73
|
+
|
|
74
|
+
export function BlockNoteViewRaw<
|
|
75
|
+
B extends BlockSchema = DefaultBlockSchema,
|
|
76
|
+
I extends InlineContentSchema = DefaultInlineContentSchema,
|
|
77
|
+
S extends StyleSchema = DefaultStyleSchema,
|
|
78
|
+
>(props: BlockNoteViewProps<B, I, S>) @{
|
|
79
|
+
const {
|
|
80
|
+
editor,
|
|
81
|
+
theme,
|
|
82
|
+
editable,
|
|
83
|
+
onChange,
|
|
84
|
+
onSelectionChange,
|
|
85
|
+
renderEditor,
|
|
86
|
+
autoFocus,
|
|
87
|
+
portalElements,
|
|
88
|
+
className,
|
|
89
|
+
children,
|
|
90
|
+
ref,
|
|
91
|
+
...rest
|
|
92
|
+
} = props;
|
|
93
|
+
|
|
94
|
+
const outerContext = useBlockNoteContext();
|
|
95
|
+
const systemColorScheme = usePrefersColorScheme();
|
|
96
|
+
const scheme: 'light' | 'dark' =
|
|
97
|
+
theme ?? outerContext?.colorSchemePreference ??
|
|
98
|
+
(systemColorScheme === 'dark' ? 'dark' : 'light');
|
|
99
|
+
|
|
100
|
+
const [contentEditableProps, setContentEditableProps] = useState<Record<string, any> | undefined>(
|
|
101
|
+
undefined,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
useEditorChange((onChange ?? ignore) as Parameters<typeof useEditorChange>[0], editor);
|
|
105
|
+
useEditorSelectionChange(onSelectionChange ?? ignore, editor);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
const portalElement = editor.portalElement;
|
|
109
|
+
if (!portalElement) {
|
|
110
|
+
throw new Error('Portal element not found');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
portalElement.className = className ? `bn-root ${scheme} ${className}` : `bn-root ${scheme}`;
|
|
114
|
+
portalElement.setAttribute('data-color-scheme', scheme);
|
|
115
|
+
}, [editor, scheme, className]);
|
|
116
|
+
|
|
117
|
+
const portalTargetOption = portalElements?.default;
|
|
118
|
+
const portalTarget = useMemo(() => resolvePortalTarget(portalTargetOption), [portalTargetOption]);
|
|
119
|
+
|
|
120
|
+
const contextValue = useMemo<BlockNoteContextValue<B, I, S>>(
|
|
121
|
+
() => ({
|
|
122
|
+
...outerContext,
|
|
123
|
+
editor,
|
|
124
|
+
colorSchemePreference: scheme,
|
|
125
|
+
setContentEditableProps,
|
|
126
|
+
}),
|
|
127
|
+
[outerContext, editor, scheme, setContentEditableProps],
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const settings = useMemo<ViewEditorSettings>(
|
|
131
|
+
() => ({ autoFocus, editable, portalTarget, contentEditableProps }),
|
|
132
|
+
[autoFocus, editable, portalTarget, contentEditableProps],
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
<BlockNoteContext value={contextValue}>
|
|
136
|
+
<ViewEditorContext value={settings}>
|
|
137
|
+
<div
|
|
138
|
+
{...rest}
|
|
139
|
+
class={['bn-root bn-container', scheme, className]}
|
|
140
|
+
data-color-scheme={scheme}
|
|
141
|
+
ref={ref ?? null}
|
|
142
|
+
>
|
|
143
|
+
@if (renderEditor !== false) {
|
|
144
|
+
<BlockNoteViewEditor>{children}</BlockNoteViewEditor>
|
|
145
|
+
} @else {
|
|
146
|
+
<>
|
|
147
|
+
{children}
|
|
148
|
+
</>
|
|
149
|
+
}
|
|
150
|
+
</div>
|
|
151
|
+
</ViewEditorContext>
|
|
152
|
+
</BlockNoteContext>
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** The editable surface. Renders only inside a `BlockNoteViewRaw`. */
|
|
156
|
+
export function BlockNoteViewEditor(props: { children?: OctaneNode }) @{
|
|
157
|
+
const settings = useContext(ViewEditorContext);
|
|
158
|
+
const editor = useBlockNoteContext()?.editor;
|
|
159
|
+
|
|
160
|
+
if (!settings || !editor) {
|
|
161
|
+
throw new Error('BlockNoteViewEditor must be rendered inside a BlockNoteViewRaw component');
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const { autoFocus, editable, portalTarget, contentEditableProps } = settings;
|
|
165
|
+
|
|
166
|
+
// A new callback identity detaches the old ref (unmount) and attaches the new
|
|
167
|
+
// one (mount). Remounting on `editable` changes is deliberate: TipTap drops the
|
|
168
|
+
// host's tabIndex when editability flips on a mounted view.
|
|
169
|
+
const mountEditor = useCallback((element: HTMLDivElement | null) => {
|
|
170
|
+
editor.isEditable = editable !== false;
|
|
171
|
+
if (element) {
|
|
172
|
+
editor.mount(element, { portalTarget });
|
|
173
|
+
} else {
|
|
174
|
+
editor.unmount();
|
|
175
|
+
}
|
|
176
|
+
}, [editable, editor, portalTarget]);
|
|
177
|
+
|
|
178
|
+
<>
|
|
179
|
+
<div
|
|
180
|
+
aria-autocomplete="list"
|
|
181
|
+
aria-haspopup="listbox"
|
|
182
|
+
data-bn-autofocus={autoFocus}
|
|
183
|
+
{...contentEditableProps}
|
|
184
|
+
ref={mountEditor}
|
|
185
|
+
/>
|
|
186
|
+
{props.children}
|
|
187
|
+
</>
|
|
188
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
import type {
|
|
3
|
+
BlockNoteEditor,
|
|
4
|
+
BlockNoteSchema,
|
|
5
|
+
BlockSchema,
|
|
6
|
+
DefaultBlockSchema,
|
|
7
|
+
DefaultInlineContentSchema,
|
|
8
|
+
DefaultStyleSchema,
|
|
9
|
+
InlineContentSchema,
|
|
10
|
+
StyleSchema,
|
|
11
|
+
} from '@blocknote/core';
|
|
12
|
+
|
|
13
|
+
import { useBlockNoteContext } from '../BlockNoteContext';
|
|
14
|
+
import { splitSlot } from '../internal';
|
|
15
|
+
|
|
16
|
+
/** Read the editor from the nearest `BlockNoteViewRaw` or `BlockNoteContext`. */
|
|
17
|
+
export function useBlockNoteEditor<
|
|
18
|
+
B extends BlockSchema = DefaultBlockSchema,
|
|
19
|
+
I extends InlineContentSchema = DefaultInlineContentSchema,
|
|
20
|
+
S extends StyleSchema = DefaultStyleSchema,
|
|
21
|
+
>(schema?: BlockNoteSchema<B, I, S>): BlockNoteEditor<B, I, S>;
|
|
22
|
+
export function useBlockNoteEditor(...args: unknown[]): BlockNoteEditor<any, any, any> {
|
|
23
|
+
const [userArgs] = splitSlot(args);
|
|
24
|
+
const context = useBlockNoteContext(userArgs[0] as BlockNoteSchema<any, any, any> | undefined);
|
|
25
|
+
|
|
26
|
+
if (!context?.editor) {
|
|
27
|
+
throw new Error(
|
|
28
|
+
'useBlockNoteEditor was called outside of a BlockNoteContext provider or BlockNoteViewRaw component',
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return context.editor;
|
|
33
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
import {
|
|
3
|
+
BlockNoteEditor,
|
|
4
|
+
type BlockNoteEditorOptions,
|
|
5
|
+
type CustomBlockNoteSchema,
|
|
6
|
+
type DefaultBlockSchema,
|
|
7
|
+
type DefaultInlineContentSchema,
|
|
8
|
+
type DefaultStyleSchema,
|
|
9
|
+
} from '@blocknote/core';
|
|
10
|
+
import { useMemo } from 'octane';
|
|
11
|
+
|
|
12
|
+
import { splitSlot, subSlot } from '../internal';
|
|
13
|
+
|
|
14
|
+
type DependencyList = readonly unknown[];
|
|
15
|
+
|
|
16
|
+
const noDeps: DependencyList = [];
|
|
17
|
+
|
|
18
|
+
/** Create a BlockNote editor once, or again whenever `deps` change. */
|
|
19
|
+
export function useCreateBlockNote<
|
|
20
|
+
Options extends Partial<BlockNoteEditorOptions<any, any, any>> | undefined,
|
|
21
|
+
>(
|
|
22
|
+
options?: Options,
|
|
23
|
+
deps?: DependencyList,
|
|
24
|
+
): Options extends { schema: CustomBlockNoteSchema<infer B, infer I, infer S> }
|
|
25
|
+
? BlockNoteEditor<B, I, S>
|
|
26
|
+
: BlockNoteEditor<DefaultBlockSchema, DefaultInlineContentSchema, DefaultStyleSchema>;
|
|
27
|
+
export function useCreateBlockNote(...args: unknown[]): BlockNoteEditor<any, any, any> {
|
|
28
|
+
const [userArgs, slot] = splitSlot(args);
|
|
29
|
+
const options = userArgs[0] as Partial<BlockNoteEditorOptions<any, any, any>> | undefined;
|
|
30
|
+
const deps = (userArgs[1] as DependencyList | undefined) ?? noDeps;
|
|
31
|
+
|
|
32
|
+
return useMemo(
|
|
33
|
+
() => BlockNoteEditor.create(options ?? {}),
|
|
34
|
+
deps as unknown[],
|
|
35
|
+
subSlot(slot, 'create'),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
import type { BlockNoteEditor } from '@blocknote/core';
|
|
3
|
+
import { useEffect } from 'octane';
|
|
4
|
+
|
|
5
|
+
import { useBlockNoteContext } from '../BlockNoteContext';
|
|
6
|
+
import { splitSlot, subSlot } from '../internal';
|
|
7
|
+
|
|
8
|
+
type ChangeCallback = Parameters<BlockNoteEditor<any, any, any>['onChange']>[0];
|
|
9
|
+
|
|
10
|
+
/** Subscribe to content changes of `editor`, or of the context editor when omitted. */
|
|
11
|
+
export function useEditorChange(
|
|
12
|
+
callback: ChangeCallback,
|
|
13
|
+
editor?: BlockNoteEditor<any, any, any>,
|
|
14
|
+
): void;
|
|
15
|
+
export function useEditorChange(...args: unknown[]): void {
|
|
16
|
+
const [userArgs, slot] = splitSlot(args);
|
|
17
|
+
const callback = userArgs[0] as ChangeCallback;
|
|
18
|
+
const context = useBlockNoteContext();
|
|
19
|
+
const editor = (userArgs[1] as BlockNoteEditor<any, any, any> | undefined) ?? context?.editor;
|
|
20
|
+
|
|
21
|
+
if (!editor) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'useEditorChange was called outside of a BlockNoteContext provider or BlockNoteViewRaw component',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
useEffect(() => editor.onChange(callback), [editor, callback], subSlot(slot, 'subscribe'));
|
|
28
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
import type { BlockNoteEditor } from '@blocknote/core';
|
|
3
|
+
import { useEffect } from 'octane';
|
|
4
|
+
|
|
5
|
+
import { useBlockNoteContext } from '../BlockNoteContext';
|
|
6
|
+
import { splitSlot, subSlot } from '../internal';
|
|
7
|
+
|
|
8
|
+
/** Subscribe to selection changes of `editor`, or of the context editor when omitted. */
|
|
9
|
+
export function useEditorSelectionChange(
|
|
10
|
+
callback: () => void,
|
|
11
|
+
editor?: BlockNoteEditor<any, any, any>,
|
|
12
|
+
includeSelectionChangedByRemote?: boolean,
|
|
13
|
+
): void;
|
|
14
|
+
export function useEditorSelectionChange(...args: unknown[]): void {
|
|
15
|
+
const [userArgs, slot] = splitSlot(args);
|
|
16
|
+
const callback = userArgs[0] as () => void;
|
|
17
|
+
const includeSelectionChangedByRemote = userArgs[2] as boolean | undefined;
|
|
18
|
+
const context = useBlockNoteContext();
|
|
19
|
+
const editor = (userArgs[1] as BlockNoteEditor<any, any, any> | undefined) ?? context?.editor;
|
|
20
|
+
|
|
21
|
+
if (!editor) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
'useEditorSelectionChange was called outside of a BlockNoteContext provider or BlockNoteViewRaw component',
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
useEffect(
|
|
28
|
+
() => editor.onSelectionChange(callback, includeSelectionChangedByRemote),
|
|
29
|
+
[editor, callback, includeSelectionChangedByRemote],
|
|
30
|
+
subSlot(slot, 'subscribe'),
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
import { useSyncExternalStore } from 'octane';
|
|
3
|
+
|
|
4
|
+
import { splitSlot, subSlot } from '../internal';
|
|
5
|
+
|
|
6
|
+
export type ColorSchemePreference = 'dark' | 'light' | 'no-preference';
|
|
7
|
+
|
|
8
|
+
const darkQuery = '(prefers-color-scheme: dark)';
|
|
9
|
+
const lightQuery = '(prefers-color-scheme: light)';
|
|
10
|
+
|
|
11
|
+
function readPreference(): ColorSchemePreference {
|
|
12
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
13
|
+
return 'no-preference';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (window.matchMedia(darkQuery).matches) {
|
|
17
|
+
return 'dark';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return window.matchMedia(lightQuery).matches ? 'light' : 'no-preference';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function subscribe(onChange: () => void): () => void {
|
|
24
|
+
if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') {
|
|
25
|
+
return () => {};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const queries = [window.matchMedia(darkQuery), window.matchMedia(lightQuery)];
|
|
29
|
+
for (const query of queries) {
|
|
30
|
+
query.addEventListener('change', onChange);
|
|
31
|
+
}
|
|
32
|
+
return () => {
|
|
33
|
+
for (const query of queries) {
|
|
34
|
+
query.removeEventListener('change', onChange);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// The server cannot see the media query. Hydration renders this snapshot too,
|
|
40
|
+
// so server HTML and the first client render agree; the store then re-reads
|
|
41
|
+
// the live preference after hydration.
|
|
42
|
+
const serverPreference = (): ColorSchemePreference => 'no-preference';
|
|
43
|
+
|
|
44
|
+
/** Track the system `prefers-color-scheme` media query. */
|
|
45
|
+
export function usePrefersColorScheme(): ColorSchemePreference;
|
|
46
|
+
export function usePrefersColorScheme(...args: unknown[]): ColorSchemePreference {
|
|
47
|
+
const [, slot] = splitSlot(args);
|
|
48
|
+
return useSyncExternalStore(
|
|
49
|
+
subscribe,
|
|
50
|
+
readPreference,
|
|
51
|
+
serverPreference,
|
|
52
|
+
subSlot(slot, 'preference'),
|
|
53
|
+
);
|
|
54
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
export {
|
|
3
|
+
BlockNoteContext,
|
|
4
|
+
useBlockNoteContext,
|
|
5
|
+
type BlockNoteContextValue,
|
|
6
|
+
} from './BlockNoteContext';
|
|
7
|
+
export {
|
|
8
|
+
BlockNoteViewRaw,
|
|
9
|
+
BlockNoteViewEditor,
|
|
10
|
+
type BlockNoteViewProps,
|
|
11
|
+
} from './BlockNoteViewRaw.tsrx';
|
|
12
|
+
export type { PortalElementsMap, PortalTarget } from './portalTarget';
|
|
13
|
+
export { useCreateBlockNote } from './hooks/useCreateBlockNote';
|
|
14
|
+
export { useBlockNoteEditor } from './hooks/useBlockNoteEditor';
|
|
15
|
+
export { useEditorChange } from './hooks/useEditorChange';
|
|
16
|
+
export { useEditorSelectionChange } from './hooks/useEditorSelectionChange';
|
|
17
|
+
export { usePrefersColorScheme } from './hooks/usePrefersColorScheme';
|
package/src/internal.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
// Slot mechanics for @octanejs/blocknote's plain-`.ts` hooks. The Octane
|
|
3
|
+
// compiler appends a per-call-site Symbol as the final argument of every
|
|
4
|
+
// custom-hook call; these helpers separate it from the public arguments and
|
|
5
|
+
// derive one stable sub-slot per base hook a binding hook composes.
|
|
6
|
+
|
|
7
|
+
import { createSubSlot } from 'octane';
|
|
8
|
+
|
|
9
|
+
export const subSlot = createSubSlot({
|
|
10
|
+
tagPrefix: ':@octanejs/blocknote:',
|
|
11
|
+
slotlessPrefix: '@octanejs/blocknote:',
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
/** Split a compiler-owned trailing slot from a custom hook's user arguments. */
|
|
15
|
+
export function splitSlot(args: readonly unknown[]): [readonly unknown[], symbol | undefined] {
|
|
16
|
+
const tail = args.at(-1);
|
|
17
|
+
return typeof tail === 'symbol' ? [args.slice(0, -1), tail] : [args, undefined];
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// Independently authored Octane adapter for the public @blocknote/react 0.53.0 API.
|
|
2
|
+
/** Where floating UI portals mount: an element, a CSS selector, or `null` for `document.body`. */
|
|
3
|
+
export type PortalTarget = HTMLElement | string | null;
|
|
4
|
+
|
|
5
|
+
export type PortalElementsMap = {
|
|
6
|
+
default?: PortalTarget;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Resolve a public portal target to the element `editor.mount` expects.
|
|
11
|
+
* `undefined` yields `null`, which lets `mount` pick its own default.
|
|
12
|
+
*/
|
|
13
|
+
export function resolvePortalTarget(target: PortalTarget | undefined): HTMLElement | null {
|
|
14
|
+
if (target === undefined || typeof document === 'undefined') {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (target === null) {
|
|
19
|
+
return document.body;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (typeof target !== 'string') {
|
|
23
|
+
return target;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const element = document.querySelector<HTMLElement>(target);
|
|
27
|
+
if (element === null) {
|
|
28
|
+
console.warn(
|
|
29
|
+
`@octanejs/blocknote: portal target "${target}" matched no element; using the default portal target.`,
|
|
30
|
+
);
|
|
31
|
+
}
|
|
32
|
+
return element;
|
|
33
|
+
}
|