rich_text_controller

Text editing controller highlighting words, prefixes and suffixes based on RegExps and Strings.

  • User callback for all matched highlights
  • User callback for tapped highlights
  • User callback for selected highlights
  • User callback for constructing complex InlineSpans
  • Deletes highlights on backspace
  • Exact word, prefix, postfix matching
  • Disable highlighting and callbacks dynamically

Installation

flutter pub add rich_text_controllerx

Quick Start

MatchTargetItem

MatchTargetItem defines words or paterns to match and the TextStyle for matched words or patterns.

  • The optional function MatchTargetItem.onSelected is invoked when a matched words is selected.
  • The optional function MatchTargetItem.onTapInto is invoked when a matched words is tapped into. The needed minimum difference in cursor position is two chars.
  • Combining MatchTargetItem.onSelected and MatchTargetItem.onTapInto might not be useful.

MatchTargetItem.matchString offers various String matching patterns:

enum StringMatch {
  any, // No word boundaries, also inside a String
  prefix, // The beginning of a String
  postfix, // The beginning of a String
  prefixComplete, // The beginning of a String and the complete word
  postfixComplete, // The end of a String and the complete word
  exact, // The exact String within its own word boundaries
}

MatchTargetItem.matchPayload allows to set a custom payload, accessible in MatchResultItem.target.matchPayload.

MatchTargetItem.inlineSpanBuilder allows to define a custom function returning the InlineSpan to show. Useful for displaying tooltips etc.

RichTextController

The optional function RichTextController.onMatch is invoked everytime the set of matched words change.

late final RichTextController controller = RichTextController(
    text:
        'Home sweet home! Do good homework and housework at home. The happyness of an ending.',
    sortTargetsByPatternLength: true,
    mergeOverlappingStyles: true,
    deleteOnBack: true,
    enableHighlight: true, // toggable at runtime
    enableOnTapInto: true, // toggable at runtime
    enableOnSelected: true, // toggable at runtime
    onMatch: (List<MatchResultItem> matches) {
      for (final MatchResultItem match in matches) {
        log(match.toString());
      }
    },
    targets: [
      // --------------------------------------------------------------
      // SINGLE STRING
      // --------------------------------------------------------------

      MatchTargetItem.string(
        'homework',
        style:
            const TextStyle(color: Colors.orange, fontWeight: FontWeight.bold),
        stringMatch: StringMatch.exact,
        caseSensitive: false,
        onSelected: (MatchResultItem match) => _onWordMatchSelected(match),
        onTapInto: (MatchResultItem match) => _onWordMatchTap(match),
      ),

      MatchTargetItem.string(
        'Home',
        style: const TextStyle(color: Colors.red),
        stringMatch: StringMatch.any,
        caseSensitive: false,
        onSelected: (MatchResultItem match) => _onWordMatchSelected(match),
        onTapInto: (MatchResultItem match) => _onWordMatchTap(match),
      ),

      // Prefix

      MatchTargetItem.string(
        'happy',
        style: const TextStyle(color: Colors.blue),
        stringMatch: StringMatch.prefix,
        caseSensitive: false,
        onSelected: (MatchResultItem match) => _onWordMatchSelected(match),
        onTapInto: (MatchResultItem match) => _onWordMatchTap(match),
      ),

      // Postfix

      MatchTargetItem.string(
        'ing',
        style: const TextStyle(color: Colors.blue),
        stringMatch: StringMatch.postfix,
        caseSensitive: false,
        onSelected: (MatchResultItem match) => _onWordMatchSelected(match),
        onTapInto: (MatchResultItem match) => _onWordMatchTap(match),
      ),

      // --------------------------------------------------------------
      // MULTIPLE STRINGS
      // --------------------------------------------------------------

      MatchTargetItem.strings(
        ['do', 'at'],
        style: const TextStyle(color: Colors.pink),
        onSelected: (MatchResultItem match) => _onWordMatchSelected(match),
        onTapInto: (MatchResultItem match) => _onWordMatchTap(match),
      ),

      // --------------------------------------------------------------
      // SINGLE REGEXP
      // --------------------------------------------------------------

      MatchTargetItem.regex(
        RegExp(r'sweet'),
        style: const TextStyle(color: Colors.indigo),
        onSelected: (MatchResultItem match) => _onWordMatchSelected(match),
        onTapInto: (MatchResultItem match) => _onWordMatchTap(match),
      ),

      // --------------------------------------------------------------
      // MUTIPLE REGEXPs
      // --------------------------------------------------------------

      MatchTargetItem.regexes(
        [RegExp(r'good'), RegExp(r'and')],
        style: const TextStyle(
          decoration: TextDecoration.underline,
          decorationStyle: TextDecorationStyle.wavy,
          decorationColor: Colors.indigo,
          decorationThickness: 3,
        ),
        onSelected: (MatchResultItem match) => _onWordMatchSelected(match),
        onTapInto: (MatchResultItem match) => _onWordMatchTap(match),
      ),
    ],
  );

// ...

TextField(controller: controller)

RichWrapper

RichWrapper(
  initialText: 'Good morning and good night at Github',
  targets: [
    MatchTargetItem.string('good',
        style: const TextStyle(color: Colors.red)),
    MatchTargetItem.strings(['morning', 'night'],
        style: const TextStyle(color: Colors.blue)),
    MatchTargetItem.regex(RegExp(r'and'),
        style: const TextStyle(color: Colors.orange)),
    MatchTargetItem.regexs([RegExp(r'at'), RegExp(r'Github')],
        style: const TextStyle(color: Colors.indigo)),
  ],
  child: (controller) => TextField(controller: controller),
)

InlineSpan-Builder

The inlineSpanBuilder is an optional custom builder. In the example below, it returns a WidgetSpan showing aTooltip.

If the inlineSpanBuilder is null, highlighting is done as per style definition in MatchTargetItem.

MatchTargetItem.strings(
  ['thing', 'it', 'there was', 'used to'],
  style: TextStyle(color: Colors.red),
  stringMatch: StringMatch.exact,
  ///
  /// A custom payload, may be `null`.
  ///
  matchPayload: WritingAidItem(
    'Vague words: Underspecified',
    'Not providing concrete information',
  ),
  ///
  /// InlineSpanBui8uilder returns a `WidgetSpan` with `Tooltip`.
  /// The `Tooltip` is based on the object defined in [matchPayload].
  ///
 inlineSpanBuilder: (
    String match,
    TextStyle matchStyle,
    WritingAidItem? matchPayload) {

    InlineSpan ret = TextSpan(text: match, style: matchStyle);

    if (matchPayload != null) {
      final TextSpan toolTipRichMessage = TextSpan(
          style: Theme.of(context)
              .textTheme
              .titleMedium!
              .copyWith(color: Colors.white),
          children: [
            TextSpan(
                text: match,
                style: const TextStyle(fontStyle: FontStyle.italic)),
            const TextSpan(text: '\n'),
            TextSpan(
                text: matchPayload.category,
                style: const TextStyle(fontWeight: FontWeight.bold)),
            const TextSpan(text: '\n'),
            TextSpan(
                text: matchPayload.description,
                style: const TextStyle(fontStyle: FontStyle.normal)),
          ]);

      ret = WidgetSpan(
          child: Tooltip(
              richMessage: toolTipRichMessage,
              child: Text(match, style: matchStyle)));
    }

    return ret;
  }
),

Example

See the example app in example/flutter_example for code.

Inspiration

This package is a complete rewrite of https://github.com/micazi/rich_text_controller.

License

This project is licensed under the MIT License - see the LICENSE.md file for details