-
Notifications
You must be signed in to change notification settings - Fork 249
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.documentNode;
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. We know that there
// isn't any text, but displaying the standard text
// component gives us the standard height for a line
// of paragraph text. This avoids 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 size of components within a document.
Typically, the widget returned from a ComponentBuilder
should attach itself directly to the documentComponentKey
. However, 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
descendant.
Regardless which 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
with other DocumentComponent
s.