-
Notifications
You must be signed in to change notification settings - Fork 243
How to: custom UI component
Super Editor ships with a number of built-in UI components to render things like paragraphs, list items, images, and horizontal rules.
You may want a different visual representation for an existing type of content, or you may want to add more functionality to an existing component, or you may want to provide a component for a new type of content. All of these goals are achieved by defining a new component Widget
and building that Widget
in a new ComponentBuilder
function. This guide describes how to do that.
Let's say that you want to introduce a custom component that shows hint text for the very first paragraph when there is no content in a document. These are the steps you might take.
Add a custom ComponentBuilder
to the top of the list of ComponentBuilder
s given to your Editor
(you'll implement the ComponentBuilder
in the next step).
Editor.custom(
editor: DocumentEditor(document: doc),
componentBuilders: [
firstParagraphHintComponentBuilder,
...defaultComponentBuilders,
],
);
In this example, an Editor
widget is built with a custom list of componentBuilder
s. This list of ComponentBuilder
s is responsible for building every possible widget that might appear in the DocumentLayout
.
firstParagraphHintComponentBuilder
is the new ComponentBuilder
that is added to display hint text when the document is empty.
Notice that a list of defaultComponentBuilders
are added after firstParagraphHintComponentBuilder
. The defaultComponentBuilders
are the standard component builders for the Editor
. If you don't include the defaultComponentBuilders
in the list then the Editor
will not know how to build all of the standard visual components.
A ComponentBuilder
function is responsible for creating a Widget
that renders a given piece of content. For example, by default, a TextComponent
is used to render a ParagraphNode
.
For every piece of content in a Document
, the list of ComponentBuilder
s is asked to provide a Widget
. The first ComponentBuilder
in the list to return a Widget
is the builder that's used for that content. In other words, all of the ComponentBuilder
s are in a priority list.
Due to the ComponentBuilder
priority list, whenever a builder is asked to build a Widget
for content or a situation that doesn't apply, the builder needs to return null
. Therefore, a good first step when building a ComponentBuilder
is to filter out all the situations that don't apply.
Widget firstParagraphHintComponentBuilder(ComponentContext componentContext) {
// We only care about paragraphs.
final paragraphNode = componentContext.currentNode;
if (paragraphNode is! ParagraphNode) {
return null;
}
// We only care about the situation where the Document is empty. In this case
// a Document is "empty" when there is only a single ParagraphNode.
if (componentContext.document.nodes.length > 1) {
return null;
}
// We only care about the situation where the first ParagraphNode is empty.
if (paragraphNode.text.text.isNotEmpty()) {
return null;
}
// We only want to show a hint component if the first ParagraphNode isn't
// selected, i.e., doesn't have the caret.
final hasCaret = componentContext.nodeSelection != null ? componentContext.nodeSelection.isExtent : false;
if (hasCaret) {
return null;
}
// TODO: create the widget
}
This particular example has a lot of conditions that need to be met before choosing to build a Widget
. If any of the conditions fail, and the builder returns null
, the editor moves on to the next builder until eventually a Widget
is returned.
Once all of the conditions are met, a Widget
needs to be built and returned.
Instantiate and return a new Widget
.
The TextWithHintComponent
returned in this example is implemented in a later step.
Widget firstParagraphHintComponentBuilder(ComponentContext componentContext) {
// Existing situation conditionals are omitted for brevity...
// Create and return a new TextWithHintComponent to render
// what we want.
return TextWithHintComponent(
documentComponentKey: componentContext.componentKey,
text: paragraphNode.text,
styleBuilder: componentContext.extensions[textStylesExtensionKey],
metadata: paragraphNode.metadata,
hintText: 'Enter your content...',
textAlign: textAlign,
);
}
A new TextWithHintComponent
is instantiated and returned. This component is rendered like any other widget within the editor.
Typically, a custom ComponentBuilder
is defined for the purpose of rendering a new type of Widget
within the DocumentLayout
.
The following is one possible implementation of TextWithHintComponent
, achieving hint text in the first paragraph of an empty document.
class TextWithHintComponent extends StatelessWidget {
const TextWithHintComponent({
Key key,
@required this.documentComponentKey,
@required this.text,
@required this.styleBuilder,
this.metadata = const {},
@required this.hintText,
this.textAlign,
}) : super(key: key);
final GlobalKey documentComponentKey;
final AttributedText text;
final AttributionStyleBuilder styleBuilder;
final Map<String, dynamic> metadata;
final String hintText;
final TextAlign textAlign;
@override
Widget build(BuildContext context) {
// The hint text alignment needs to match the alignment of
// the content that will appear in this paragraph. Look up
// the preference from the node's metadata.
TextAlign textAlign = TextAlign.left;
final textAlignName = metadata['textAlign'];
switch (textAlignName) {
case 'left':
textAlign = TextAlign.left;
break;
case 'center':
textAlign = TextAlign.center;
break;
case 'right':
textAlign = TextAlign.right;
break;
case 'justify':
textAlign = TextAlign.justify;
break;
}
return MouseRegion(
// We want a text style cursor to appear when the user hovers
// over any area within the first line of the paragraph.
cursor: SystemMouseCursors.text,
child: Stack(
children: [
// Display the hint text.
Text(
hintText,
textAlign: textAlign,
style: styleBuilder({blockType}).copyWith(
color: const Color(0xFFC3C1C1),
),
),
// Display a standard text component. Even though there
// is no text, by definition, displaying the standard
// text component gives us the standard height for a line
// of paragraph text. This avoid a jarring change in height
// when the first character is entered.
Positioned.fill(
child: TextComponent(
key: documentComponentKey,
text: blockLevelText,
textAlign: textAlign,
textStyleBuilder: styleBuilder,
),
),
],
),
);
}
}
The most important thing about building a Widget
as a document component is correctly handling the documentComponentKey
. The editor uses documentComponentKey
s to locate the position and sizes of components within a document.
Typically, the widget returned from a ComponentBuilder
should attach itself to the documentComponentKey
. In this example, because an existing component is being wrapped by other widgets, and those widgets don't change the visual size of the component, the documentComponentKey
is given to the TextComponent
.
Regardless of whichever widget receives the documentComponentKey
, that widget must be a StatefulWidget
and its State
object must implement DocumentComponent
. The DocumentComponent
API includes all the functionality that every visual component must implement to play nicely within a DocumentLayout
that contains other DocumentComponent
s.