-
-
Notifications
You must be signed in to change notification settings - Fork 645
feat: Major Extensions & UI Refactor #2143
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?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
2e2cb06 to
f449a1c
Compare
f449a1c to
b94e7ee
Compare
packages/core/src/extensions/FormattingToolbar/FormattingToolbarPlugin.ts
Outdated
Show resolved
Hide resolved
| // (this.options.editor.formattingToolbar?.shown || | ||
| // this.options.editor.linkToolbar?.shown || | ||
| // this.options.editor.filePanel?.shown) |
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.
Ideally this would not have to be aware of every menu that could be open. I'm not sure if this means that there should be a single "menu" extension which stores the current state of whatever menu is currently active? Or, if these menus should be setting keyboard handlers to capture the Tab key or otherwise not giving this event to the editor.
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.
this was here for accessibility reasons - but indeed, there needs to be a better / more scalable architecture
| // TODO defaults? | ||
| const placeholders = options.placeholders; |
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.
There were some defaults here that we should also apply
packages/core/src/extensions/SuggestionMenu/SuggestionPlugin.ts
Outdated
Show resolved
Hide resolved
…o just block ID string
Problem
Currently, there is not much consistency in how the editor's feature set is extended, both internally and externally. Things like UI extensions (formatting toolbar, side menu, etc) are baked directly into the editor, while the trailing block extension gets added as a TipTap extension, and the placeholder extension is a ProseMirror Plugin.
The BlockNote extension API was somewhat haphazardly created out of the need for a unified API to add features to the editor, such as keyboard shortcuts, input rules, and exposing the ProseMirror plugin API for more advanced use cases. We migrated this in piecemeal, supporting ProseMirror plugins, TipTap extensions & a stripped down BlockNote extension API, but we need more!
Solution
BlockNote Extensions API Refactor
We've made big improvements to the BlockNote extension API, making extensions much more scalable. In addition to all previous functionality (like adding keyboard extensions), extensions can now:
Pretty much all existing editor features have been converted into BlockNote extensions, and use the same API to be created, as well as added to/removed from the editor. This is a great change to the codebase quality, as the editor is a lot more modular thanks to this change, and there is now a clear way to extend the editor's functionality.
UI Extension Refactor
Additionally, the UI extensions have been partially rewritten (currently includes the formatting toolbar, link toolbar, and file panel) to take advantage of the new extension functionality. The states they keep have been minimised as much as possible, and many of their responsibilities have been moved to the React layer. This massively cuts down on the overall code and makes them much more understandable.
Speaking of the React layer, alongside refactoring the UI extensions in the editor core, the React components and hooks have also been overhauled. FloatingUI now handles much of what was previously done by the extensions, including:
Alongside that, a bunch of helper components and hooks have been created to streamline the code.
In-Depth Changes
Extensions API
Creating Extensions
To create a BlockNote extension, you use the
createExtensionfunction. It works similarly to the oldcreateBlockNoteExtensionfunction, and you can still add an extension key, keyboard shortcuts, input rules, TipTap extensions, and ProseMirror plugins similarly to before. Also like before, you can add your own custom properties to the extension:You can also now give your extensions configurable options by passing a function to
createExtensioninstead of an object:Additionally, there are now three additional properties that you can give your extensions:
store: A TanStack store which holds the extension's state. For example, a table of contents extension may keep a state containing a list of heading blocks in the editor, as well as their text content. This state can also be accessed and modified by other extensions.mount: A callback which runs when the editor gets mounted to the DOM. You can use it to setup event listeners and update the state.runsBefore: An array which holdskeys of other BlockNote extensions that the extension depends on. This ensures that an extension's state will be initialised before other extensions try to access it.Adding/Removing Extensions
You can still pass your BlockNote extensions to the
extensionsoption inuseCreateBlockNote:However, you can now also dynamically add/remove extensions after the editor is initialised using
editor.registerExtensionandeditor.unregisterExtension:Accessing Extensions
You can access extensions and their states using
editor.getExtension:React Components & Hooks
Editor Hooks
A universal
useEditorStatehook has been added to listen to changes in the editor state. It replaces existing hooks which read parts of the editor state, such asuseActiveStylesanduseSelectedBlocks.The
selectorallows you return specific properties from the editor state, or derive new properties from it. Whenever it's called (i.e. when the editor content of selection changes), it compares the new result with the old one. Only if the new result is different does it trigger a re-render. This works not only when the result is a primitive type (boolean, number, string, etc), but also if it's a plain object, i.e. an object without function properties.For example, here's how you would redefine the old
useActiveStyleshook:This will only trigger a re-render if any of the active styles have actually changed.
The
useEditorStatehook takes several other options:editor: The BlockNote editor to use. Only needed if used outside a.bn-containerelement.equalityFn: Function to check whether the new result of calling theselectoris different to the old result, in order to trigger a re-render. Uses deep equals by default, and only really needed of the returned object contains functions, or other niche cases where deep equals may throw false positives.on: Determines when theselectoris called. Calls on both editor selection and content ("all") changes by default, but can be changed to only listen for content ("change") or selection ("selection") changes.runsBefore: An array which holdskeys of other BlockNote extensions that the extension depends on. This ensures that an extension's state will be initialised before other extensions try to access it.Extension Hooks
When creating custom blocks or UI elements like menus & toolbars, it's likely you'll want to access editor extensions and their states. To do this, the
useExtensionanduseExtensionStatehooks have been created.The
useExtensionhook lets you access the properties of an extension in the editor, except:keyboardShortcutsinputRulestiptapExtensionsprosemirrorPluginsrunsBeforeIt also has the same
editoroption asuseEditorState:The
useExtensionStatehook lets you access the state of an extension in the editor:It also has the same
editorandselectoroptions asuseEditorState:Popover Components
The old
useUIElementPositioninghas been removed in favour of components which are easier to use and understand.The
BlockPopovercomponent is used to create a popover around a specific block, and is used by elements like the side menu as well as the file panel:The
PositionPopovercomponent is used to create a popover around a ProseMirror position range, and is used by elements like the formatting toolbar:The
GenericPopovercomponent is used to create a popover around DOM element or FloatingUI virtual element, and is used by both theBlockPopoveras well as thePositionPopover:All 3 components take
floatingUIOptions, which is an object that combines props from all the FloatingUI hooks used inGenericPopover, e.g.useFloatinganduseDismiss.Breaking Changes
@blocknote/coreBlockNoteExtensionclass are no longer supported. Use thecreateExtensionfunction instead.@blocknote/coreto@blocknote/core/extensions.@blocknote/coreto@blocknote/core/commnents.BlockNoteEditor, e.g.editor.formattingToolbar. Use thegetExtensionmethod instead, e.g.editor.getExtension(FormattingToolbarExtension).editor.openSuggestionMenuhas been removed. Useeditor.getExtension(SuggestionMenu).openSuggestionMenuinstead.editor.getForceSelectionVisible/editor.setForceSelectionVisiblehave been removed. Useeditor.getExtension(ShowSelectionExtension).store.state.enabled/editor.getExtension(ShowSelectionExtension).showSelectioninstead.editor.onCreatehas been removed. Useeditor.onMountinstead.insertOrUpdateBlockhas been renamed toinsertOrUpdateBlockForSlashMenu.editor.updateCollaborationUserInfohas been removed.@blocknote/reactuseEditorContentOrSelectionChangehas been removed. UseuseEditorStateinstead.useUIPluginStatehas been removed. UseuseExtensionoruseExtensionStateinstead.useUIElementPositioninghas been removed. UseBlockPopover,PositionPopover, orGenericPopoverinstead.useEditorForceUpdatehas been removed.@blocknote/xl-aiBlockPositionerhas been removed. UseBlockPopoverinstead.Testing
While this PR brings many changes to UI elements and their extensions, our UI testing setup is sadly not up to the standard we need to ensure nothing breaks when it gets merged. We could expand our suite of Playwright E2E tests, but decided not to as these have historically slow, flaky, and hard to debug. At some point, we will need to look at implementing a solid component testing setup to prevent UI regressions.
In the meantime though, we have compiled a thorough list of test cases for each default UI element, and manually ensured that each of them pass. The list can be found here:
Notion Testing Doc
The only exception are the table handles, which have very minor regressions.