-
Notifications
You must be signed in to change notification settings - Fork 234
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Update docs website with a guide about how to add a Popover Toolbar
- Loading branch information
1 parent
de71311
commit c624384
Showing
1 changed file
with
240 additions
and
0 deletions.
There are no files selected for viewing
240 changes: 240 additions & 0 deletions
240
doc/website/source/super-editor-guides/add-a-popover-toolbar.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,244 @@ | ||
--- | ||
title: Add a Popover Toolbar | ||
--- | ||
|
||
# Add a Popover Toolbar | ||
|
||
To display a Popover Toolbar, it is recomended to use an `OverlayPortal`. To do that, start by wraping `SuperEditor` with an `OverlayPortal` and give it a toolbar builder: | ||
|
||
```dart | ||
class MyApp extends StatefulWidget { | ||
State<MyApp> createState() => MyAppState(); | ||
} | ||
class MyAppState extends State<MyApp> { | ||
/// Controls the visibility of the toolbar. | ||
final _popoverToolbarController = OverlayPortalController(); | ||
@override | ||
Widget build(BuildContext context) { | ||
return OverlayPortal( | ||
controller: _popoverToolbarController, | ||
overlayChildBuilder: _buildPopoverToolbar, | ||
child: SuperEditor(), | ||
); | ||
} | ||
Widget _buildPopoverToolbar() { | ||
return const SizedBox(); | ||
} | ||
} | ||
``` | ||
|
||
# Showing the toolbar | ||
|
||
Usually, a Popover Toolbar is displayed when the user selects some content. To do that, listen for selection changes to show or hide the toolbar: | ||
|
||
```dart | ||
class MyAppState extends State<MyApp> { | ||
// ... | ||
late final MutableDocument _document; | ||
late final MutableDocumentComposer _composer; | ||
// ... | ||
@override | ||
void initState() { | ||
super.initState(); | ||
_document = MutableDocument.empty(); | ||
_composer = MutableDocumentComposer(); | ||
_composer.selectionNotifier.addListener(_hideOrShowToolbar); | ||
} | ||
void _hideOrShowToolbar() { | ||
final selection = _composer.selection; | ||
if (selection == null) { | ||
// Nothing is selected. We don't want to show a toolbar in this case. | ||
_popoverToolbarController.hide(); | ||
return; | ||
} | ||
if (selection.isCollapsed) { | ||
// We only want to show the toolbar when a span of text | ||
// is selected. Therefore, we ignore collapsed selections. | ||
_popoverToolbarController.hide(); | ||
return; | ||
} | ||
// We have an expanded selection. Show the toolbar. | ||
_popoverToolbarController.show(); | ||
} | ||
// ... | ||
} | ||
``` | ||
|
||
# Aligning the toolbar with the content | ||
|
||
By default, no alignment is enforced to the toolbar. To align it with the content, and make it follow the content, it is recomended to use the `follow_the_leader` package. | ||
|
||
Start by adding `follow_the_leader` to your dependencies in your `pubspec.yaml`. | ||
|
||
```yaml | ||
dependencies: | ||
follow_the_leader: latest_version | ||
``` | ||
|
||
Wrap `SuperEditor` with a `KeyedSubtree` widget to delimit the viewport area and assign a `GlobalKey` to it. This is used to prevent the toolbar from going off-screen. | ||
|
||
```dart | ||
class MyAppState extends State<MyApp> { | ||
// ... | ||
final GlobalKey _viewportKey = GlobalKey(); | ||
// ... | ||
@override | ||
Widget build(BuildContext context) { | ||
return OverlayPortal( | ||
// ... | ||
child: KeyedSubtree( | ||
key: _viewportKey, | ||
child: SuperEditor(), | ||
), | ||
); | ||
} | ||
} | ||
``` | ||
|
||
Create a `SelectionLayerLinks` instance and pass it to the `SuperEditor`. This object holds the links that make it possible to follow the content. | ||
|
||
```dart | ||
class MyAppState extends State<MyApp> { | ||
// ... | ||
final SelectionLayerLinks _selectionLayerLinks = SelectionLayerLinks(); | ||
// ... | ||
@override | ||
Widget build(BuildContext context) { | ||
return OverlayPortal( | ||
// ... | ||
child: KeyedSubtree( | ||
// ... | ||
child: SuperEditor( | ||
selectionLayerLinks: _selectionLayerLinks, | ||
), | ||
), | ||
); | ||
} | ||
} | ||
``` | ||
|
||
Create a `FollowerBoundary` to configure the boundary of the area where the toolbar is allowed to be. | ||
|
||
```dart | ||
class MyAppState extends State<MyApp> { | ||
// ... | ||
late FollowerBoundary _screenBoundary; | ||
// ... | ||
@override | ||
void didChangeDependencies() { | ||
super.didChangeDependencies(); | ||
// Confine the toolbar to the bounds of the widget attached | ||
// to the _viewportKey. | ||
_screenBoundary = WidgetFollowerBoundary( | ||
boundaryKey: _viewportKey, | ||
devicePixelRatio: MediaQuery.devicePixelRatioOf(context), | ||
); | ||
} | ||
} | ||
``` | ||
|
||
Create a `FollowerAligner` to configure how the toolbar should be aligned with the selected content. | ||
|
||
```dart | ||
class MyAppState extends State<MyApp> { | ||
// ... | ||
late final FollowerAligner _toolbarAligner; | ||
// ... | ||
@override | ||
void initState() { | ||
super.initState(); | ||
// Place the toolbar above the content by default. | ||
_toolbarAligner = CupertinoPopoverToolbarAligner(_viewportKey); | ||
}; | ||
// ... | ||
} | ||
``` | ||
|
||
Finally, wrap the toolbar with a `Follower` widget. | ||
|
||
```dart | ||
class MyAppState extends State<MyApp> { | ||
// ... | ||
Widget _buildPopoverToolbar() { | ||
return Follower.withAligner( | ||
// Make the toolbar follow the expanded selection. | ||
link: _selectionLayerLinks.expandedSelectionBoundsLink, | ||
// Configure how the toolbar is aligned to the content. | ||
aligner: _toolbarAligner, | ||
// Configure the boundary where the toolbar is allowed | ||
// to be displayed. | ||
boundary: _screenBoundary, | ||
showWhenUnlinked: false, | ||
child: _buildToolbarContent(), | ||
); | ||
} | ||
Widget _buildToolbarContent() { | ||
return const SizedBox(); | ||
} | ||
// ... | ||
} | ||
``` | ||
|
||
# Showing different toolbars depending on the content | ||
|
||
To show different toolbar depending on the content, check the type of the selected node and show/hide the appropriate toolbars. | ||
|
||
```dart | ||
void _hideOrShowToolbar() { | ||
// ... | ||
if (selection.base.nodeId != selection.extent.nodeId) { | ||
// Since we want to show different toolbars depending on the content, | ||
// we don't show the toolbar if more than one node is selected. | ||
_popoverToolbarController.hide(); | ||
_imageToolbarController.hide(); | ||
return; | ||
} | ||
// Grab the selected node to check its type. | ||
final selectedNode = _document.getNodeById(selection.extent.nodeId); | ||
if (selectedNode is ImageNode) { | ||
// The selected node is an image. Show the image toolbar and hide | ||
// the text toolbar. | ||
_popoverToolbarController.hide(); | ||
_imageToolbarController.show(); | ||
return; | ||
} | ||
// The currently selected node isn't an image. Hide the image toolbar | ||
// if it's visible. | ||
_imageToolbarController.hide(); | ||
if (selectedNode is TextNode) { | ||
// The selected node is a text node, e.g., a paragraph, a list item, | ||
// a task, etc. Show the text toolbar and hide the image toolbar. | ||
_imageToolbarController.show(); | ||
_popoverToolbarController.show(); | ||
return; | ||
} | ||
// The currently selected node isn't a text node. Hide the text toolbar | ||
// if it's visible. | ||
_popoverToolbarController.hide(); | ||
} | ||
``` |