-
-
Notifications
You must be signed in to change notification settings - Fork 564
feat: API design feedback #1756
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 6 commits
52ecefb
24517fb
536ea9d
a80259f
ac54f67
966692d
9684a7d
fc6ac01
daa62dd
aff3cbb
e066c85
329e5a7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
# Document Transforms | ||
|
||
A core part of the BlockNote API is the ability to make changes to the document, using BlockNote's core design of blocks, this is much easier to do programmatically than with other editors. | ||
|
||
We've done pretty well with our existing API, but there are a few things that could be improved. | ||
|
||
Referencing content within the document is either awkward or non-existent. Right now we essentially really only have an API for referencing blocks by their id with no further level of granularity. | ||
|
||
## Locations | ||
|
||
[Looking at Slate](https://docs.slatejs.org/concepts/03-locations) (highly recommend reading the docs), they have the concept of a `Location` which is a way to reference a specific point in the document, but it does not have to be so specific as positions, it has levels of granularity. | ||
|
||
This gives us a unified way to reference content within the document, allowing for much more granular editing. Take a look at the `Location.ts` file for more details around this. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to necessarily use the
I feel like Slate's type Location = {
id: string;
side?: "start" | "end";
offset?: number
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Inline content doesn't have an ID does it? And, I don't think it should, it'd be too granular and require wrapping things in marks everywhere. I'm willing to remove I don't think your proposal for a Using your language, I think we can achieve this with: /**
* A block id is a unique identifier for a block, it is a string.
*/
export type BlockId = string;
/**
* A block identifier is a unique identifier for a block, it is either a string, or can be object with an id property (out of convenience).
*/
export type BlockIdentifier = { id: BlockId } | BlockId;
/**
* A point has an id and an offset, it is used to identify a specific position within a block.
*/
export type Point = {
id: BlockId;
offset: number;
};
/**
* A range is a pair of points, it is used to identify a range of blocks within a document.
*/
export type Range = {
anchor: Point;
head: Point;
};
/**
* A location is an id, point, or range, it is used to identify positions within a document.
*/
export type Location = BlockId | Point | Range; There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah good point about ranges, missed that. I'm wondering if there's a use case for pointing to a specific inline content that we would miss by just having block + offset, maybe for extensions which work with links? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To point to a specific inline content, you could have a {id: 'abc', content:[{type:'link', text: 'ABC'}]}
const point = { id: 'abc', offset: 0 };
const range: { anchor: { id: 'abc', offset: 0 }, head: { id: 'abc', offset: 3 } } |
||
|
||
## Transforms separation of concerns | ||
|
||
Right now all transformation functions are defined directly on the `Editor` instance, this is not ideal because it only further muddles the API. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool. I think maybe this should be a separate work-item ("cleaning up There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Working on that 😉 |
||
|
||
Instead, we should have a separate `Transform` class which defines methods that operate on the editor's document to make changes to it. This will also be very useful for doing server-side transformations. | ||
|
||
```ts | ||
editor.transform.insertBlocks(ctx:{ | ||
at: Location, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it useful to insertblocks at a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Toss-up for |
||
blocks: Block | Block[] | ||
}); | ||
|
||
editor.transform.replaceBlocks(ctx: { | ||
at: Location, | ||
with: Block | Block[] | ||
}) | ||
|
||
editor.transform.replaceContent(ctx: { | ||
at: Location, | ||
with: InlineContent | InlineContent[] | ||
}) | ||
|
||
editor.transform.deleteContent(ctx: { | ||
at: Location, | ||
}) | ||
``` | ||
|
||
## References | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice, currently things like comments can't be represented in the BlockNote API, and indeed, this would enable us to separate them from the document data definitely feels like the "right approach" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I like this idea a lot, feels more general than ProseMirror's decorations. In theory it would also make it much nicer for our UI architecture, since instead of writing plugins you could just use references. E.g. for the formatting toolbar, you could listen to selection changes and create a reference pointing to the selection when you want to show it. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
🤯 what a great idea! If it were robust enough for that, this could simplify a ton of things! |
||
|
||
Currently, we do not have a good way to reference things about content within a document, except by block id. With Locations, we can be much more granular & more powerful. | ||
|
||
I think one application of this would be to introduce the concept of "references" to content within a document. For example, we currently do not store comments into the blocknote json, taking the position that it really is not part of the document, but rather metadata about the document. | ||
|
||
With references, we could store comments with references to the blocks that they are about. And map those references through changes if they are ever modified. For example, if someone commented on the text within block id `123`, and then the block was moved to a new location, we could update the comment to reference the new block id. | ||
|
||
So, this would allow a comment to still be a separate entity, but be able to "hydrate" within the editor and keep track of the content that it was about. | ||
|
||
```ts | ||
type Reference<Metadata extends Record<string, any>> = { | ||
to: Location, | ||
metadata: Metadata | ||
} | ||
|
||
type Comment = Reference<{ | ||
threadId: string | ||
}> | ||
|
||
editor.references.add(reference: Reference, onUpdate: (reference: Reference) => void); | ||
``` | ||
|
||
This is inspired by [bluesky's concept of facets for rich text content](https://docs.bsky.app/docs/advanced-guides/post-richtext) which is a great example of how to make block note content more inter-operable between different applications. |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -0,0 +1,49 @@ | ||||
# BlockNote Extension API | ||||
|
||||
It is definitely more designed by accident than intention, so let's put some thought into the sort of API we would want people to build extensions with. | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Before diving into technical details below, let's zoom back out and ask ourselves what an Extension actually is / should be used for. When should people use extensions? And what for? Why do we want to introduce the concept of an extension? Having some clarity to these questions will help us to both better explain the concept to users, and help us create a better API design There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An extension is a way to extend the editor with additional functionality, in a way that is integrated with the editor API. Examples of when to use an extension: You want to add behaviors to the editor that are reactive to the editor's current state such as:
These extensions have APIs of their own, which should be accessible across the UI, (e.g. a button may open a menu). We want to introduce the concept of an extension for 2 reasons:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Based on our discussion here, I think I've come around to something. I think that there are 2 separate needs here like you pointed out:
I'll propose the naming of: extension (low-level) vs. bundle (high-level) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Cool. I like the separation, I think when we work on this it will become clear if the separation will be needed and / or whether we need to expose both to consumers |
||||
|
||||
## Core Requirements | ||||
|
||||
What I identified was: | ||||
|
||||
- The ability to "eject" to the prosemirror API, if we don't provide something good enough. This is a core choice, and one I don't think we would ever need to walk back on. Fundamentally, we do not want to expose absolutely everything that Prosemirror can do, but also do not want to stop those with the know-how to actually get stuff done. | ||||
- A unified store API, to make consuming extension state homogenous, this will be discussed further below | ||||
- Life-cycle event handlers, by providing hooks for `create`, `mount`, and `unmount` we allow extensions to attach event handlers to DOM elements, and have a better general understanding of the current state of the editor. The `onCreate` handler will be very handy to guarantee access to the editor instance before anything is called. | ||||
- Editing event handlers, by providing hooks for `change`, `selectionChange`, `beforeChange`, and `transaction` we give extensions access to the fundamental primitives of the editor. | ||||
|
||||
## State Management | ||||
|
||||
What I had the most trepidation about was deciding on whether we should prescribe a state management library, and if so, which one. | ||||
|
||||
I think the answer is _yes_, we should prescribe a state management library, and I think the answer is @tanstack/store. | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the best way to validate this is to create a PR that migrates the AI extension from zustand to tanstack. Let's create a task for this? |
||||
|
||||
<details> | ||||
<summary>Why @tanstack/store? And not something else?</summary> | ||||
It comes with a few benefits: | ||||
|
||||
- It gives a single API for all state management, which is more convenient/consistent for consumers | ||||
|
||||
As for which library to use, I think we should use @tanstack/store. | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicate |
||||
|
||||
- The store is very simple, and can easily be re-implemented if needed | ||||
- There is already bindings for most major frameworks, but can be used without them | ||||
- It seems that anything tanstack does will be widely adopted so it should be a pretty safe bet | ||||
|
||||
What I had trouble with is there are a few different use-cases for state management, events like we have now aren't great because they put the burden on the consumer to manage the state. Or, they can emit an event (e.g. `update`), but then have to round-trip back to the extension to get the state (and somehow store it on their side again). | ||||
|
||||
Something like observables have a nicer API for this, but they are for pushing data (i.e. multiple readers), not for pulling it (i.e. any writers). They also have the same problem of putting the burden on the consumer. | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do you have an example of what you mean with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An Observable is a one-way stream, like an event emitter. From RXJS Observable: So, they still don't fix the current problem we have with events because they are still putting the burden on consumers. TanStack & Zustand are "push-pull" systems where you get a snapshot (i.e. a pull), and also be notified of changes (i.e. a push). There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah got it, coming from RX I think you're right. I was thinking about MobX observables which (if I'm not mistaken) are more like (deep) signals There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, overloaded term, mobx is based on signals since it is "push-pull" |
||||
|
||||
Signals are a nice middle ground, being that they are for both pushing & pulling data. The problem is that there are many implementations, and not super well-known in the React ecosystem. | ||||
|
||||
Zustand is a popular library, but allowing partial states makes it somewhat unsafe in TypeScript. | ||||
|
||||
Jotai is probably my second choice, but it makes it a bit awkward to update states because it relies on a separate store instance rather than the "atom" being able to update itself <https://jotai.org/docs/guides/using-store-outside-react>. | ||||
</details> | ||||
|
||||
## Exposing extension methods | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to expose them in a type-safe way imo. What do you think of the pattern with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I was leaning more towards something like Though this can get awkward if you don't have the consumer's editor instance. So we likely would have a helper similar to that. But, I wanna see how that shakes out type-wise There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I doubt this will work type-wise, but curious what you come up with! |
||||
|
||||
Not everything can be communicated through just state, so we need to be able to expose additional methods to the application. | ||||
|
||||
I propose that we have a `extensions` property on the `BlockNoteEditor` instance, which is a map of the extension key to the extension instance, but filtered out to only include non-blocknote methods (as defined by the `ExtensionMethods` type). | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm curious to see what you have in mind for how extensions should be defined too. Right now, to make something resembling an extension using the BlockNote API only, you generally listen to So I'm wondering if all we really need an extension to be is a state + listeners, sort of like this: function defineExtension<State extends Record<string, any>>({
initialState: State;
listeners?: Partial<{
onCreate: (editor: BlockNoteEditor) => State,
onContentChange: (editor: BlockNoteEditor) => State,
onSelectionChange: (editor: BlockNoteEditor) => State,
onDestroy: (editor: BlockNoteEditor) => State,
}>
}) {...} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Described in the TS file here: BlockNote/adr/BlockNoteExtension.ts Line 9 in 9684a7d
I also thought it'd be good to have hooks for these sort of event's, but Yousef pointed out they were already available on the editor instance. |
||||
|
||||
This will allow the application to access the extension methods, and also allows us to keep the extension instance private to the editor (type-wise). |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
# BlockNote Packages & Sub-Packages | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks good overall. What's your reasoning for leaning heavy into subpackages? I don't think this is very common, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Leaning heavy into sub-packages isn't super common for editors, because they normally split on the package level, and most editors are fairly old (i.e. pre-subpackages). I've always hated how many packages there are just to get an editor going. But, the main trade-off of splitting at the package level is that it makes the install size smaller and feels "composable", users can get into weird states (e.g. a user using a new version that depends on a core feature that didn't exist on their version). This sort of a approach, completely side-steps package version drift, while also not making the top-level API just a huge list of options. It also is nice for code-splitting and general "organization", we can treat sub-packages as a proper top-level package and design it's API to be more sensible than a huge export of just everything we have. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Cool, seems like this is the main reason. Note to self to summarize:
Also agree with the second point? I.e.: better to use subpackages vs namespaces? One benefit of namespaces I suppose would be that it's more discoverable (e.g.: when you do (note that with namespaces I mean that for example, instead of exporting all blocks top-level, we can export a "blocks" object which encapsulates the default blocks. i.e. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, I think a strict folder structure with namespaces is conceptually very similar. |
||
|
||
## Core | ||
|
||
The `@blocknote/core` package should contain everything needed to build a pure JS editor instance. It has no opinion on the UI layer, and is only concerned with the core editor functionality. | ||
|
||
It contains sub-packages for: | ||
|
||
- `@blocknote/core/blocks` - Contains the default blocks, inline content, styles and their implementations (excluding things like blockContainer, blockGroup, etc which are core to the editor but should not be part of the schema). | ||
- `@blocknote/core/blocks/header` - Contains the header block. | ||
- `@blocknote/core/blocks/code-block` - Contains the code block block. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. except the code-block will probably stay in a different package right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There is the |
||
- etc... | ||
- `@blocknote/core/extensions` - Re-exports the extension packages below. | ||
- `@blocknote/core/extensions/comments` - Contains the comments extension. | ||
- `@blocknote/core/exporter` - Contains the exporter package. | ||
- `@blocknote/core/exporter/html` - Contains the html exporter package. | ||
- `@blocknote/core/exporter/markdown` - Contains the markdown exporter package. | ||
- `@blocknote/core/server-util` - Contains the server util package. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not sure if There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worthwhile to look into, I should mention that the main reason to split to another package is around deps & install size (or being a very different concern of course) |
||
- `@blocknote/core/locales` - Contains the locales package. | ||
|
||
## React | ||
|
||
The `@blocknote/react` package should contain everything needed to build a React editor instance. It has no opinion on the core editor functionality, and is only concerned with the React UI layer. | ||
|
||
It contains sub-packages for: | ||
|
||
- `@blocknote/react/editor` - Contains the editor package. | ||
- `@blocknote/react/comments` - Contains the comments package. | ||
- `@blocknote/react/table-handles` - Contains the table handles package. | ||
- etc... (need to figure out what needs to be exported vs. available and how coupled this all is) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# BlockNote Schema | ||
|
||
Right now it is overly burdensome to have to pass around 3 different types to the editor, and it is also not very type-safe (when you just end up with `any` everywhere). | ||
|
||
The idea is to have a single type that is a union of the 3 types, and then make type predicates available to check if accessed properties are valid (and likely just assertions too). | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. would be great if we can get this to work! |
||
|
||
You'll see some of what I came up with in the `Schema.ts` file. | ||
|
||
You'll also notice that the default blocks, inline content, styles, and groups are all defined in the `@blocknote/core/blocks` package. This is assuming that we have already moved them to the blocknote API and out of the core package. | ||
|
||
## Groups | ||
|
||
In a somewhat similar vein, I think there might be use for having an indirection layer for referring to specific blocks, inline content, styles, etc. Reason being that it allows callers to refer to a group of things with a single identifier, and allow customizing that membership by just modifying that group. | ||
|
||
Examples include: | ||
|
||
- Keyboard shortcuts can refer to a group of blocks/inline-content/styles without modification to the handler | ||
- Relationships between blocks/inline-content/styles can be defined (e.g. allow for a todo block to only have todo item children) | ||
- Properties of blocks/inline-content/styles can be defined (e.g. adding `heading` to the `toggleable` group) | ||
|
||
This may or may not be useful, but it is a thought. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cool, let's keep in mind 👍 |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# BlockNote Configuration | ||
|
||
Editors are widely different across applications, often with users having opposing requirements. Most editors explode with complexity when trying to support all of these use cases. So, there needs to be a guide for the different ways to configure so that there is not just a single overwhelming list of options. | ||
|
||
Fundamentally, there are few different kinds of things to be configured: | ||
|
||
- **block-configuration**: The configuration for a specific kind of block | ||
- e.g. the `table` block might have toggles for enabling/disabling the header rows/columns | ||
- **schema-configuration**: The configuration of what blocks, inline content, and styles are available in the editor | ||
- e.g. whether to include the `table` block at all | ||
- **extension-level-configuration**: The configuration of the extension itself | ||
- e.g. what ydoc should the collaboration extension use | ||
- **extension-configuration**: The configuration of the extensions that are available in the editor | ||
- e.g. whether to add collaboration to the editor | ||
- **editor-view-configuration**: The configuration of the editor views | ||
- e.g. whether to show the sidebar, the toolbar, etc. | ||
- **editor-configuration**: The configuration of the editor itself | ||
- e.g. how to handle paste events | ||
|
||
These forms of configuration are not mutually exclusive, and can be combined in different ways. For example, knowing that the editor has collaboration enabled, might change the what the keybindings do for undo/redo. | ||
|
||
In an ideal world, these configurations would be made at the "lowest possible level", like configuring the number of levels of headings would be configured when composing the schema for the editor, rather than at the editor level. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. great principle 🙌 |
||
|
||
Configuration should be publicly accessible, so that mixed combinations can be created (i.e. different behaviors for an editor with or without collaboration). | ||
|
||
## TODO | ||
|
||
- Describe how you configure at the block level, then compose that into a schema, the compose that into an editor, and then compose that into a view. |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,155 @@ | ||||||||||||||||||||||||||||
# BlockNote Nested Blocks | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
There are two separate problems when it comes to "nested blocks" support in BlockNote: | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
- **nested-blocks** The first is the ability for a block to contain other blocks within it (e.g. a table cell having not just inline content, but actual other blocks inside it) | ||||||||||||||||||||||||||||
- **content-fields** The second is the ability for a block to contain multiple pieces of content within it (e.g. an alert block having a title & description field (which contain inline content)) | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Let's start with the first problem, nested blocks. By describing existing block relationships: | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
## Block with inline content | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
This is the simplest case, and the only one that is currently supported by BlockNote. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
```json | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Worth mentioning that the current inline content object shape is a mess. You have There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good thing to add, I don't have an answer for this atm. But feel free to write a proposal |
||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "block-type", | ||||||||||||||||||||||||||||
"content": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "text", | ||||||||||||||||||||||||||||
"text": "Hello, world!" | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
], | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
There is a 1:1 relationship between the block type and it's content. And, no restrictions of the inline content allowed within the block. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
> TODO come up with a description of the sorts of keybinds & cursor behavior that should be supported for this block type | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
## Block with nested blocks | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
This is a proposal for how to support nested blocks. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
```json | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is technically what we already support to some level, right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, modeled on the multi-column, but generic for others to implement |
||||||||||||||||||||||||||||
"type": "custom-block-type", | ||||||||||||||||||||||||||||
"props": { | ||||||||||||||||||||||||||||
"abc": 123 | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
"content": null, | ||||||||||||||||||||||||||||
"children": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "nested-block", | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||||||||||||||||||||||||
"content": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "block", | ||||||||||||||||||||||||||||
"content": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "text", | ||||||||||||||||||||||||||||
"text": "Hello, world!" | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
This would completely enclose all nested blocks within the `custom-block-type` block. And, works the same way as the multi-column example. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
> TODO come up with a description of the sorts of keybinds & cursor behavior that should be supported for this block type | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
## Block with structured inline content fields | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
This is a proposal for how to support multiple inline content fields within a block. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
```json | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "alert", | ||||||||||||||||||||||||||||
"content": null, | ||||||||||||||||||||||||||||
"children": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if this will work out. now suddenly, There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. or wait, alert-title and alert-content are considered blocks here as well? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yep, considered blocks. Would need to introduce the concept of a "nested block" which is not valid on it's own but only in the context of being part of another block. This is also shown with the table example below |
||||||||||||||||||||||||||||
"type": "alert-title", | ||||||||||||||||||||||||||||
"content": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "text", | ||||||||||||||||||||||||||||
"text": "Hello, world!" | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "alert-content", | ||||||||||||||||||||||||||||
"content": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "text", | ||||||||||||||||||||||||||||
"text": "Hello, world!" | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
``` | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
The core idea is that the `parent` block restricts what content is allowed within it's children. | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
> TODO come up with a description of the sorts of keybinds & cursor behavior that should be supported for this block type | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
## Examples | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
### Rebuilding tables with nested blocks | ||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
Using this new structure, we can rebuild tables if we had this new API: | ||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes sense, but 2 questions:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Correct. We'd have to have prosemirror enforce this, we can decide how best to let the consumer describe this structure. I proposed groups partially for this reason: BlockNote/adr/2025_06_13-schema.md Lines 11 to 21 in 52ecefb
Tables would be defined to only allow There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I see, so this is more of a change of concept of what a nested blocks actually is I suppose. Because right now, it's an inherent property of all blocks, that they can have nested blocks that are indented below. Whereas in this new design, nested blocks are a lot more flexible, and the indented nested blocks are just a generic implementation. Am I understanding that right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I tried to define my terms here: BlockNote/adr/2025_06_16-nested-blocks.md Lines 5 to 6 in a80259f
But, I see that "nested-blocks" is indeed overloaded, take it to mean "blocks as content", maybe. |
||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||
```json | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "table", | ||||||||||||||||||||||||||||
"content": null, | ||||||||||||||||||||||||||||
"props": { | ||||||||||||||||||||||||||||
"backgroundColor": "default", | ||||||||||||||||||||||||||||
"textColor": "default", | ||||||||||||||||||||||||||||
"columnWidths": [100, 100, 100], | ||||||||||||||||||||||||||||
"headerRows": 1, | ||||||||||||||||||||||||||||
"headerCols": 1, | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
"children": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "table-row", | ||||||||||||||||||||||||||||
"content": null, | ||||||||||||||||||||||||||||
"children": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "table-cell", | ||||||||||||||||||||||||||||
"content": null, | ||||||||||||||||||||||||||||
"children": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "paragraph", | ||||||||||||||||||||||||||||
"content": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "text", | ||||||||||||||||||||||||||||
"text": "Hello, world!" | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
}, | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "table-cell", | ||||||||||||||||||||||||||||
"content": null, | ||||||||||||||||||||||||||||
"children": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "paragraph", | ||||||||||||||||||||||||||||
"content": [ | ||||||||||||||||||||||||||||
{ | ||||||||||||||||||||||||||||
"type": "text", | ||||||||||||||||||||||||||||
"text": "Hello, world!" | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
] | ||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||
``` |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Overall, I think the concept of a
Location
is useful to have - but if we start implementing this I think we should have more sight on practical use-cases. i.e. which use-cases / APIs do we want to "unlock" with this?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
insertContentAt(at: Location, content: Block | PartialBlock | string)
getSelection(): {range: Range}
A number of the low-level Tiptap API uses for things like deleting some chars
This will also be very useful for server-side editing ops like rewriting a paragraph