flutter_blocknote_editor 0.0.19
flutter_blocknote_editor: ^0.0.19 copied to clipboard
A Flutter package (beta) that embeds BlockNoteJS inside a WebView with bidirectional communication, transaction batching, and undo/redo safety.
Flutter BlockNote Editor #
⚠️ BETA VERSION WARNING
This is a beta version of the Flutter BlockNote Editor package. We've just started working on it, and there may be bugs and incomplete features. We're actively working on fixes and improvements. Please report any issues you encounter.
A Flutter package that embeds BlockNoteJS inside a WebView with bidirectional communication, transaction batching, and undo/redo safety.
BlockNote is an open source Block-Based rich text editor. This Flutter package brings BlockNote's modern text editing experience to your Flutter apps.
Homepage - Documentation - Examples
What We Support #
Features #
- ✅ WebView Integration: Embeds BlockNoteJS editor in a WebView for iOS and Android
- ✅ Bidirectional Communication: Type-safe message protocol between Flutter and JavaScript
- ✅ Transaction Batching: Efficient batching system prevents excessive Flutter rebuilds (configurable debounce duration)
- ✅ Undo/Redo Safety: Undo/redo operations remain entirely within the JavaScript editor
- ✅ Offline Support: JavaScript bundle embedded locally via assets
- ✅ Programmatic Control:
BlockNoteControllerfor on-demand document retrieval and editor operations - ✅ Custom Themes: Customize editor colors, fonts, and styling
- ✅ Custom Toolbar: Configure formatting toolbar buttons
- ✅ Custom Slash Commands: Add custom slash menu items
- ✅ Custom Schemas: Register custom blocks, inline content, and styles
- ✅ Read-only Mode: Toggle editor between editable and read-only states
- ✅ Document Loading: Load initial documents with blocks and content
- ✅ Transaction Handling: Receive and process editor changes via transactions
Platform Support #
- ✅ iOS (via
flutter_inappwebview) - ✅ Android (via
flutter_inappwebview) - ❌ Web (not yet supported - WebView limitations)
How It Works #
The package embeds BlockNoteJS inside a Flutter WebView and establishes bidirectional communication through a message channel. Here's the architecture:
graph LR
A[Flutter App] -->|Messages| B[WebView]
B -->|Messages| A
B -->|BlockNoteJS| C[BlockNote Editor]
C -->|Changes| B
B -->|Transactions| A
A -->|Document Updates| B
B -->|Load Document| C
Key Components #
- BlockNoteEditor Widget: Main Flutter widget that embeds the WebView
- BlockNoteController: Provides programmatic access to the editor (get document, load document, flush transactions, etc.)
- JS Bridge: Handles message protocol between Flutter and JavaScript
- Transaction Batcher: Batches transactions to prevent excessive rebuilds
- Document Models: Type-safe Dart models for documents, blocks, and transactions
Transaction Batching #
Transactions are automatically batched to prevent excessive Flutter rebuilds:
- Default batch window: 400ms (configurable via
transactionDebounceDuration) - Immediate flush triggers:
- Paste operations
- Delete block operations
- App background
- Widget disposal
Undo/Redo Safety #
The library follows strict undo/redo safety rules:
- Undo/redo operations are NEVER triggered from Flutter. They remain entirely within the JavaScript BlockNote editor.
- Flutter NEVER re-applies transactions into the editor. Once a transaction is emitted from JavaScript, it is considered final.
- On reload: Load the full document, reset the editor once, then resume streaming transactions. This ensures undo/redo history is preserved.
Installation #
Add this package to your pubspec.yaml:
dependencies:
flutter_blocknote_editor:
path: ../path/to/blocknotejs
Then run:
flutter pub get
Setup #
The package uses a local HTTP server to serve the BlockNote editor assets. To allow the WebView to load content from localhost, you need to configure platform-specific security settings.
Android Setup #
Android blocks cleartext (HTTP) traffic by default. To allow localhost HTTP connections, you need to add a network security configuration.
- Create a network security configuration file at
android/app/src/main/res/xml/network_security_config.xml:
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Allow cleartext traffic for localhost only -->
<!-- This is safe because localhost traffic never leaves the device -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">localhost</domain>
<domain includeSubdomains="true">127.0.0.1</domain>
<domain includeSubdomains="true">10.0.2.2</domain>
</domain-config>
</network-security-config>
- Update your AndroidManifest.xml to reference this configuration. Add
android:networkSecurityConfig="@xml/network_security_config"to the<application>tag:
<application
android:label="your_app_name"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:networkSecurityConfig="@xml/network_security_config">
<!-- ... rest of your application configuration ... -->
</application>
iOS Setup #
iOS requires App Transport Security (ATS) configuration to allow HTTP connections to localhost.
Update your Info.plist (typically at ios/Runner/Info.plist) to add the following configuration:
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsLocalNetworking</key>
<true/>
<key>NSExceptionDomains</key>
<dict>
<key>localhost</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
<key>127.0.0.1</key>
<dict>
<key>NSExceptionAllowsInsecureHTTPLoads</key>
<true/>
</dict>
</dict>
</dict>
Note: These configurations only allow cleartext traffic for localhost, which is safe since localhost traffic never leaves the device. This does not affect the security of your app's external network connections.
Usage #
Keyboard and WebView (Scaffold) #
For smooth keyboard behavior (correct bottom padding and no scroll jump when typing), disable the Scaffold’s resize-to-avoid-bottom-inset so the WebView handles keyboard height itself:
Scaffold(
resizeToAvoidBottomInset: false, // Let the WebView handle keyboard padding
body: BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
onReady: (controller) {},
),
)
If you leave resizeToAvoidBottomInset at its default (true), the scaffold will resize when the keyboard opens, which can conflict with the WebView’s own padding and cause a ~40–60px gap or the content jumping when you type.
BlockNoteEditor parameters #
| Parameter | Required | Description |
|---|---|---|
initialDocument |
Yes | The initial document to load into the editor. |
onTransactions |
No | Callback invoked when new transactions are available (batched). |
onReady |
No | Callback invoked when the editor is ready; receives BlockNoteController. |
readOnly |
No | Whether the editor is read-only. Default: false. |
debugLogging |
No | Enable debug logging. Default: false. |
localhostUrl |
No | URL for serving built assets (e.g. Vite preview). If null, uses bundled assets. |
theme |
No | Theme configuration for colors, fonts, and styling. |
toolbarConfig |
No | Toolbar configuration for formatting toolbar buttons. |
slashCommandConfig |
No | Slash command configuration for slash menu items. |
schemaConfig |
No | Schema configuration for custom blocks, inline content, or styles. |
schemaConfigs |
No | Schema configurations keyed by identifier; use with activeSchemaId. |
activeSchemaId |
No | Active schema identifier used with schemaConfigs. |
customJavaScript |
No | Custom JavaScript to run in the editor context. |
customJavaScriptAssetPaths |
No | Asset paths for custom JavaScript files. |
customCss |
No | Custom CSS to inject (e.g. @font-face). |
customCssAssetPaths |
No | Asset paths for custom CSS files. |
onToolbarPopupRequest |
No | Callback when a toolbar popup is clicked (e.g. color, block type). |
transactionDebounceDuration |
No | Debounce duration for batching transactions. Default: 400 ms. |
onLinkTapped |
No | Callback when a link is tapped in the editor (URL passed). |
extraBottomPadding |
No | Extra bottom padding in logical pixels (added to keyboard height). Default: 0. |
loading |
No | Show loading skeleton overlay. Default: false. |
skeletonBackgroundColor |
No | Custom background color for the loading skeleton. |
skeletonShimmerColor |
No | Custom color for the loading skeleton shimmer. |
Basic Usage #
Simple editor with empty document:
import 'package:flutter_blocknote_editor/flutter_blocknote_editor.dart';
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
onReady: (controller) {
print('Editor is ready');
},
)
Handling Transactions #
Receive editor changes via transactions:
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
onTransactions: (transactions) {
for (final transaction in transactions) {
print('Transaction base version: ${transaction.baseVersion}');
for (final op in transaction.operations) {
print('Operation: ${op.operation}, Block ID: ${op.blockId}');
}
}
},
onReady: (controller) {
print('Editor initialized');
},
)
Read-only Mode #
Toggle between editable and read-only states:
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
readOnly: true, // Set to true for read-only mode
onReady: (controller) {
print('Editor is ready');
},
)
Loading Initial Document #
Load a document with existing content:
BlockNoteEditor(
initialDocument: BlockNoteDocument(
blocks: [
BlockNoteBlock(
id: 'block1',
type: BlockNoteBlockType.paragraph,
content: BlockNoteBlockContent.inline(
content: [
BlockNoteInlineContent.text(
text: 'Hello, BlockNote!',
styles: {'bold': true},
),
],
),
),
BlockNoteBlock(
id: 'block2',
type: BlockNoteBlockType.heading,
props: {
'level': 1,
},
content: BlockNoteBlockContent.inline(
content: [
BlockNoteInlineContent.text(text: 'This is a heading'),
],
),
),
],
),
onTransactions: (transactions) {
// Handle transactions
},
onReady: (controller) {
print('Editor initialized with content');
},
)
Custom Theme #
Apply custom colors, fonts, and styling:
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
theme: BlockNoteTheme(
colors: BlockNoteColorScheme(
editor: const BlockNoteColorPair(
text: Color(0xFF222222),
background: Color(0xFFFFFFFF),
),
menu: const BlockNoteColorPair(
text: Color(0xFFFFFFFF),
background: Color(0xFF000000),
),
tooltip: const BlockNoteColorPair(
text: Color(0xFFFFFFFF),
background: Color(0xFF333333),
),
hovered: const BlockNoteColorPair(
text: Color(0xFFFFFFFF),
background: Color(0xFF444444),
),
selected: const BlockNoteColorPair(
text: Color(0xFFFFFFFF),
background: Color(0xFF555555),
),
shadow: const Color(0xFF000000),
border: const Color(0xFFCCCCCC),
),
borderRadius: 8,
font: const BlockNoteFontConfig(
family: "'Georgia', 'Times New Roman', serif",
),
),
onReady: (controller) {
print('Editor with custom theme is ready');
},
)
Custom Toolbar #
Customize formatting toolbar buttons:
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
toolbarConfig: BlockNoteToolbarConfig(
buttons: [
const BlockNoteToolbarButton(
type: BlockNoteToolbarButtonType.blockTypeSelect,
),
const BlockNoteToolbarButton(
type: BlockNoteToolbarButtonType.basicTextStyleButton,
basicTextStyle: BlockNoteBasicTextStyle.bold,
),
const BlockNoteToolbarButton(
type: BlockNoteToolbarButtonType.basicTextStyleButton,
basicTextStyle: BlockNoteBasicTextStyle.italic,
),
const BlockNoteToolbarButton(
type: BlockNoteToolbarButtonType.basicTextStyleButton,
basicTextStyle: BlockNoteBasicTextStyle.underline,
),
const BlockNoteToolbarButton(
type: BlockNoteToolbarButtonType.colorStyleButton,
),
const BlockNoteToolbarButton(
type: BlockNoteToolbarButtonType.createLinkButton,
),
],
),
onReady: (controller) {
print('Editor with custom toolbar is ready');
},
)
Custom Slash Commands #
Add custom slash menu items:
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
slashCommandConfig: BlockNoteSlashCommandConfig(
items: [
BlockNoteSlashCommandItem(
title: 'Insert Hello World',
onItemClick:
"editor.insertBlocks([{type: 'paragraph', content: [{type: 'text', text: 'Hello World', styles: {bold: true}}]}], editor.getTextCursorPosition().block, 'after');",
aliases: ['helloworld', 'hw'],
group: 'Custom',
subtext: 'Inserts a "Hello World" paragraph',
),
],
),
onReady: (controller) {
print('Editor with custom slash commands is ready');
},
)
Custom Schemas (Custom Blocks) #
Register custom blocks with JavaScript and enable them via schema config:
final customBlockScript = '''
(function() {
const { createReactBlockSpec, React, registerBlockNoteCustomBlocks } =
window.BlockNoteCustomSchema;
const AgendaItemBlock = createReactBlockSpec(
{
type: 'agenda_item',
content: 'none',
propSchema: {
title: { default: '' },
},
},
{
render: ({ block }) => {
return React.createElement(
'div',
{ className: 'agenda-item' },
block.props.title || 'Agenda Item'
);
},
toExternalHTML: ({ block }) => {
const element = document.createElement('div');
element.setAttribute('data-block-type', block.type);
element.textContent = block.props.title || '';
return { dom: element };
},
}
);
registerBlockNoteCustomBlocks({
agenda_item: AgendaItemBlock,
});
})();
''';
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
customJavaScript: customBlockScript,
schemaConfig: {
'blockSpecs': ['agenda_item'],
},
onReady: (controller) {
print('Editor with custom schema is ready');
},
)
The schemaConfig map also supports inlineContentSpecs and styleSpecs with
the same array format to enable registered inline content and styles.
BlockNoteController #
Use BlockNoteController for programmatic editor operations:
BlockNoteController? _controller;
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
onReady: (controller) {
_controller = controller;
print('Editor and controller are ready');
},
)
// Later, get the full document on-demand
Future<void> saveDocument() async {
if (_controller != null && _controller!.isReady) {
try {
final document = await _controller!.getDocument();
// Save document to storage, send to server, etc.
print('Document has ${document.blocks.length} blocks');
} catch (e) {
print('Error getting document: $e');
}
}
}
// Load a document programmatically
Future<void> loadSavedDocument(BlockNoteDocument document) async {
if (_controller != null && _controller!.isReady) {
await _controller!.loadDocument(document);
}
}
// Flush pending transactions immediately
Future<void> flushTransactions() async {
if (_controller != null && _controller!.isReady) {
await _controller!.flush();
}
}
Transaction Debounce Duration #
Customize the transaction batching window:
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
transactionDebounceDuration: const Duration(milliseconds: 200), // Faster updates
onTransactions: (transactions) {
// Handle transactions
},
onReady: () {
print('Editor is ready');
},
)
The default debounce duration is 400ms. Shorter durations provide more frequent updates but may impact performance with large documents.
Debug Logging #
Enable debug logging for development:
BlockNoteEditor(
initialDocument: BlockNoteDocument.empty(),
debugLogging: true, // Enable debug logging
onReady: (controller) {
print('Editor is ready');
},
)
Complete Example #
Here's a complete example with state management:
import 'package:flutter/material.dart';
import 'package:flutter_blocknote_editor/flutter_blocknote_editor.dart';
class EditorPage extends StatefulWidget {
const EditorPage({super.key});
@override
State<EditorPage> createState() => _EditorPageState();
}
class _EditorPageState extends State<EditorPage> {
bool _isReady = false;
bool _readOnly = false;
BlockNoteDocument _document = BlockNoteDocument.empty();
final List<BlockNoteTransaction> _transactions = [];
BlockNoteController? _controller;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('BlockNote Editor'),
actions: [
IconButton(
icon: Icon(_readOnly ? Icons.lock : Icons.lock_open),
onPressed: () {
setState(() {
_readOnly = !_readOnly;
});
},
tooltip: _readOnly ? 'Enable editing' : 'Disable editing',
),
IconButton(
icon: const Icon(Icons.save),
onPressed: _saveDocument,
tooltip: 'Save document',
),
],
),
body: Column(
children: [
if (_isReady)
Container(
padding: const EdgeInsets.all(8),
color: Colors.green[100],
child: const Text('Editor is ready'),
),
Expanded(
child: BlockNoteEditor(
initialDocument: _document,
onReady: (controller) {
setState(() {
_controller = controller;
_isReady = true;
});
},
onTransactions: (transactions) {
setState(() {
_transactions.addAll(transactions);
});
},
readOnly: _readOnly,
),
),
],
),
);
}
Future<void> _saveDocument() async {
if (_controller != null && _controller!.isReady) {
try {
final document = await _controller!.getDocument();
// Save document to storage, send to server, etc.
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Document saved with ${document.blocks.length} blocks'),
),
);
}
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Error saving document: $e'),
),
);
}
}
}
}
}
Known Issues #
Beta Version Limitations #
This is a beta version, and we're actively working on fixes and improvements. You may encounter:
- Bugs and unexpected behavior
- Incomplete features
- Performance issues with very large documents
- Platform-specific issues
Please report any issues you encounter so we can fix them.
BlockNoteJS Known Bugs #
BlockNoteJS itself has known bugs that may affect this package. You can check the BlockNoteJS GitHub Issues for a complete list.
Critical Bug: Issue #2122 - On iOS Safari, when selecting text near the top of the editor, two overlapping menus appear (the native iOS text selection menu and the BlockNote toolbar). This makes it difficult or impossible to access the BlockNote toolbar features on mobile devices. This is a known issue in BlockNoteJS and is being tracked upstream.
Credits #
This Flutter package wraps BlockNote, an open source Block-Based rich text editor.
BlockNote builds directly on two awesome projects:
- Prosemirror by Marijn Haverbeke
- Tiptap
BlockNote is built as part of TypeCell and is sponsored by the NLNet foundation.
License #
This project is licensed under the MIT License.
BlockNote itself is licensed under the MPL-2.0 license, which allows you to use BlockNote in commercial (and closed-source) applications.
Contributing #
Contributions are welcome! Please ensure all code complies with analysis_options.yaml and run dart analyze before submitting.
When contributing:
- Follow the existing code style
- Add tests for new features
- Update documentation as needed
- Run
dart fix --applyanddart analyzebefore committing