417 lines
13 KiB
Dart
417 lines
13 KiB
Dart
import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle;
|
|
|
|
import 'package:flutter/gestures.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:linkfy_text/src/enum.dart';
|
|
import 'package:linkfy_text/src/model/link.dart';
|
|
import 'package:linkfy_text/src/utils/regex.dart';
|
|
|
|
/// Linkify [text] containing urls, emails or hashtag
|
|
class LinkifyText extends StatelessWidget {
|
|
const LinkifyText(this.text,
|
|
{this.textStyle,
|
|
this.linkStyle,
|
|
this.linkTypes,
|
|
this.onTap,
|
|
this.customLinkStyles,
|
|
this.strutStyle,
|
|
this.textAlign,
|
|
this.textDirection,
|
|
this.locale,
|
|
this.softWrap,
|
|
this.overflow,
|
|
this.textScaler,
|
|
this.maxLines,
|
|
this.semanticsLabel,
|
|
this.textWidthBasis,
|
|
this.linkifyListCallBack,
|
|
Key? key})
|
|
: super(key: key);
|
|
|
|
/// text to be linkified
|
|
final String text;
|
|
|
|
/// [textStyle] applied the text
|
|
final TextStyle? textStyle;
|
|
|
|
/// [textStyle] added to the formatted links in the text
|
|
final TextStyle? linkStyle;
|
|
|
|
/// called when a formatted link is pressed, it returns the link as a parameter
|
|
/// ```dart
|
|
/// LinkifyText("#helloWorld", onTap: (link) {
|
|
/// // do stuff with link
|
|
/// print("${link.value} hashtag was tapped");
|
|
/// });
|
|
/// ```
|
|
final void Function(Link)? onTap;
|
|
|
|
/// option to override the links to be formatted in the text, defaults to `[LinkType.url]`
|
|
/// so only urls are being linkified in the text
|
|
final List<LinkType>? linkTypes;
|
|
|
|
/// {@macro flutter.painting.textPainter.strutStyle}
|
|
final StrutStyle? strutStyle;
|
|
|
|
/// How the text should be aligned horizontally.
|
|
final TextAlign? textAlign;
|
|
|
|
/// The directionality of the text.
|
|
///
|
|
/// This decides how [textAlign] values like [TextAlign.start] and
|
|
/// [TextAlign.end] are interpreted.
|
|
///
|
|
/// This is also used to disambiguate how to render bidirectional text. For
|
|
/// example, if the [data] is an English phrase followed by a Hebrew phrase,
|
|
/// in a [TextDirection.ltr] context the English phrase will be on the left
|
|
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
|
|
/// context, the English phrase will be on the right and the Hebrew phrase on
|
|
/// its left.
|
|
///
|
|
/// Defaults to the ambient [Directionality], if any.
|
|
final TextDirection? textDirection;
|
|
|
|
/// Used to select a font when the same Unicode character can
|
|
/// be rendered differently, depending on the locale.
|
|
///
|
|
/// It's rarely necessary to set this property. By default its value
|
|
/// is inherited from the enclosing app with `Localizations.localeOf(context)`.
|
|
///
|
|
/// See [RenderParagraph.locale] for more information.
|
|
final Locale? locale;
|
|
|
|
/// Whether the text should break at soft line breaks.
|
|
///
|
|
/// If false, the glyphs in the text will be positioned as if there was unlimited horizontal space.
|
|
final bool? softWrap;
|
|
|
|
/// How visual overflow should be handled.
|
|
///
|
|
/// Defaults to retrieving the value from the nearest [DefaultTextStyle] ancestor.
|
|
final TextOverflow? overflow;
|
|
|
|
/// The number of font pixels for each logical pixel.
|
|
///
|
|
/// For example, if the text scale factor is 1.5, text will be 50% larger than
|
|
/// the specified font size.
|
|
///
|
|
/// The value given to the constructor as textScaleFactor. If null, will
|
|
/// use the [MediaQueryData.textScaler] obtained from the ambient
|
|
/// [MediaQuery], or 1.0 if there is no [MediaQuery] in scope.
|
|
final TextScaler? textScaler;
|
|
|
|
/// An optional maximum number of lines for the text to span, wrapping if necessary.
|
|
/// If the text exceeds the given number of lines, it will be truncated according
|
|
/// to [overflow].
|
|
///
|
|
/// If this is 1, text will not wrap. Otherwise, text will be wrapped at the
|
|
/// edge of the box.
|
|
///
|
|
/// If this is null, but there is an ambient [DefaultTextStyle] that specifies
|
|
/// an explicit number for its [DefaultTextStyle.maxLines], then the
|
|
/// [DefaultTextStyle] value will take precedence. You can use a [RichText]
|
|
/// widget directly to entirely override the [DefaultTextStyle].
|
|
final int? maxLines;
|
|
|
|
/// An alternative semantics label for this text.
|
|
///
|
|
/// If present, the semantics of this widget will contain this value instead
|
|
/// of the actual text. This will overwrite any of the semantics labels applied
|
|
/// directly to the [TextSpan]s.
|
|
///
|
|
/// This is useful for replacing abbreviations or shorthands with the full
|
|
/// text value:
|
|
///
|
|
/// ```dart
|
|
/// Text(r'$$', semanticsLabel: 'Double dollars')
|
|
/// ```
|
|
final String? semanticsLabel;
|
|
|
|
/// {@macro flutter.painting.textPainter.textWidthBasis}
|
|
final TextWidthBasis? textWidthBasis;
|
|
|
|
final Map<LinkType, TextStyle>? customLinkStyles;
|
|
|
|
final Map<String,String>? linkifyListCallBack;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Text.rich(
|
|
_linkify(
|
|
text: text,
|
|
linkStyle: linkStyle,
|
|
onTap: onTap,
|
|
linkTypes: linkTypes,
|
|
customLinkStyles: customLinkStyles,
|
|
mapReplace: linkifyListCallBack),
|
|
key: key,
|
|
style: textStyle,
|
|
strutStyle: strutStyle,
|
|
textAlign: textAlign,
|
|
textDirection: textDirection,
|
|
textScaler: textScaler,
|
|
textWidthBasis: textWidthBasis,
|
|
semanticsLabel: semanticsLabel,
|
|
softWrap: softWrap,
|
|
overflow: overflow,
|
|
maxLines: maxLines,
|
|
locale: locale,
|
|
);
|
|
}
|
|
}
|
|
|
|
class LinkifySelectableText extends StatelessWidget {
|
|
const LinkifySelectableText(
|
|
this.text, {
|
|
Key? key,
|
|
this.focusNode,
|
|
this.textStyle,
|
|
this.strutStyle,
|
|
this.textAlign,
|
|
this.textDirection,
|
|
this.textScaler,
|
|
this.autofocus = false,
|
|
this.minLines,
|
|
this.maxLines,
|
|
this.showCursor = false,
|
|
this.cursorWidth = 2.0,
|
|
this.cursorHeight,
|
|
this.cursorRadius,
|
|
this.cursorColor,
|
|
this.selectionHeightStyle = ui.BoxHeightStyle.tight,
|
|
this.selectionWidthStyle = ui.BoxWidthStyle.tight,
|
|
this.enableInteractiveSelection = true,
|
|
this.selectionControls,
|
|
this.dragStartBehavior = DragStartBehavior.start,
|
|
this.onTap,
|
|
this.scrollPhysics,
|
|
this.semanticsLabel,
|
|
this.textHeightBehavior,
|
|
this.textWidthBasis,
|
|
this.onSelectionChanged,
|
|
this.customLinkStyles,
|
|
this.linkStyle,
|
|
this.linkTypes,
|
|
this.contextMenuBuilder,
|
|
}) : super(key: key);
|
|
|
|
/// The text to display.
|
|
///
|
|
final String text;
|
|
|
|
/// Defines the focus for this widget.
|
|
///
|
|
/// Text is only selectable when widget is focused.
|
|
///
|
|
/// The [focusNode] is a long-lived object that's typically managed by a
|
|
/// [StatefulWidget] parent. See [FocusNode] for more information.
|
|
///
|
|
/// To give the focus to this widget, provide a [focusNode] and then
|
|
/// use the current [FocusScope] to request the focus:
|
|
///
|
|
/// ```dart
|
|
/// FocusScope.of(context).requestFocus(myFocusNode);
|
|
/// ```
|
|
///
|
|
/// This happens automatically when the widget is tapped.
|
|
///
|
|
/// To be notified when the widget gains or loses the focus, add a listener
|
|
/// to the [focusNode]:
|
|
///
|
|
/// ```dart
|
|
/// focusNode.addListener(() { print(myFocusNode.hasFocus); });
|
|
/// ```
|
|
///
|
|
/// If null, this widget will create its own [FocusNode] with
|
|
/// [FocusNode.skipTraversal] parameter set to `true`, which causes the widget
|
|
/// to be skipped over during focus traversal.
|
|
final FocusNode? focusNode;
|
|
|
|
/// The style to use for the text.
|
|
///
|
|
/// If null, defaults [DefaultTextStyle] of context.
|
|
final TextStyle? textStyle;
|
|
|
|
/// {@macro flutter.widgets.editableText.strutStyle}
|
|
final StrutStyle? strutStyle;
|
|
|
|
/// {@macro flutter.widgets.editableText.textAlign}
|
|
final TextAlign? textAlign;
|
|
|
|
/// {@macro flutter.widgets.editableText.textDirection}
|
|
final TextDirection? textDirection;
|
|
|
|
/// {@macro flutter.widgets.editableText.textScaler}
|
|
final TextScaler? textScaler;
|
|
|
|
/// {@macro flutter.widgets.editableText.autofocus}
|
|
final bool autofocus;
|
|
|
|
/// {@macro flutter.widgets.editableText.minLines}
|
|
final int? minLines;
|
|
|
|
/// {@macro flutter.widgets.editableText.maxLines}
|
|
final int? maxLines;
|
|
|
|
/// {@macro flutter.widgets.editableText.showCursor}
|
|
final bool showCursor;
|
|
|
|
/// {@macro flutter.widgets.editableText.cursorWidth}
|
|
final double cursorWidth;
|
|
|
|
/// {@macro flutter.widgets.editableText.cursorHeight}
|
|
final double? cursorHeight;
|
|
|
|
/// {@macro flutter.widgets.editableText.cursorRadius}
|
|
final Radius? cursorRadius;
|
|
|
|
/// The color to use when painting the cursor.
|
|
///
|
|
/// Defaults to the theme's `cursorColor` when null.
|
|
final Color? cursorColor;
|
|
|
|
/// Controls how tall the selection highlight boxes are computed to be.
|
|
///
|
|
/// See [ui.BoxHeightStyle] for details on available styles.
|
|
final ui.BoxHeightStyle selectionHeightStyle;
|
|
|
|
/// Controls how wide the selection highlight boxes are computed to be.
|
|
///
|
|
/// See [ui.BoxWidthStyle] for details on available styles.
|
|
final ui.BoxWidthStyle selectionWidthStyle;
|
|
|
|
/// {@macro flutter.widgets.editableText.enableInteractiveSelection}
|
|
final bool enableInteractiveSelection;
|
|
|
|
/// {@macro flutter.widgets.editableText.selectionControls}
|
|
final TextSelectionControls? selectionControls;
|
|
|
|
/// {@macro flutter.widgets.scrollable.dragStartBehavior}
|
|
final DragStartBehavior dragStartBehavior;
|
|
|
|
/// Configuration of toolbar options.
|
|
///
|
|
/// Paste and cut will be disabled regardless.
|
|
///
|
|
/// If not set, select all and copy will be enabled by default.
|
|
final Widget Function(BuildContext, EditableTextState)? contextMenuBuilder;
|
|
|
|
/// {@macro flutter.widgets.editableText.selectionEnabled}
|
|
bool get selectionEnabled => enableInteractiveSelection;
|
|
|
|
/// {@macro flutter.widgets.editableText.scrollPhysics}
|
|
final ScrollPhysics? scrollPhysics;
|
|
|
|
/// {@macro flutter.widgets.Text.semanticsLabel}
|
|
final String? semanticsLabel;
|
|
|
|
/// {@macro dart.ui.textHeightBehavior}
|
|
final TextHeightBehavior? textHeightBehavior;
|
|
|
|
/// {@macro flutter.painting.textPainter.textWidthBasis}
|
|
final TextWidthBasis? textWidthBasis;
|
|
|
|
/// {@macro flutter.widgets.editableText.onSelectionChanged}
|
|
final SelectionChangedCallback? onSelectionChanged;
|
|
|
|
final Map<LinkType, TextStyle>? customLinkStyles;
|
|
|
|
/// [textStyle] added to the formatted links in the text
|
|
final TextStyle? linkStyle;
|
|
|
|
/// called when a formatted link is pressed, it returns the link as a parameter
|
|
/// ```dart
|
|
/// LinkifyText("#helloWorld", onTap: (link) {
|
|
/// // do stuff with link
|
|
/// print("${link.value} hashtag was tapped");
|
|
/// });
|
|
/// ```
|
|
final void Function(Link)? onTap;
|
|
|
|
/// option to override the links to be formatted in the text, defaults to `[LinkType.url]`
|
|
/// so only urls are being linkified in the text
|
|
final List<LinkType>? linkTypes;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SelectableText.rich(
|
|
_linkify(
|
|
text: text,
|
|
linkStyle: linkStyle,
|
|
onTap: onTap,
|
|
linkTypes: linkTypes,
|
|
customLinkStyles: customLinkStyles,
|
|
),
|
|
key: key,
|
|
focusNode: focusNode,
|
|
style: textStyle,
|
|
strutStyle: strutStyle,
|
|
textAlign: textAlign,
|
|
textDirection: textDirection,
|
|
textScaler: textScaler,
|
|
showCursor: showCursor,
|
|
autofocus: autofocus,
|
|
contextMenuBuilder: contextMenuBuilder,
|
|
minLines: minLines,
|
|
maxLines: maxLines,
|
|
cursorWidth: cursorWidth,
|
|
cursorHeight: cursorHeight,
|
|
cursorRadius: cursorRadius,
|
|
cursorColor: cursorColor,
|
|
selectionHeightStyle: selectionHeightStyle,
|
|
selectionWidthStyle: selectionWidthStyle,
|
|
dragStartBehavior: dragStartBehavior,
|
|
enableInteractiveSelection: enableInteractiveSelection,
|
|
selectionControls: selectionControls,
|
|
scrollPhysics: scrollPhysics,
|
|
semanticsLabel: semanticsLabel,
|
|
textHeightBehavior: textHeightBehavior,
|
|
textWidthBasis: textWidthBasis,
|
|
onSelectionChanged: onSelectionChanged,
|
|
);
|
|
}
|
|
}
|
|
|
|
TextSpan _linkify({
|
|
String text = '',
|
|
TextStyle? linkStyle,
|
|
List<LinkType>? linkTypes,
|
|
Map<LinkType, TextStyle>? customLinkStyles,
|
|
Function(Link)? onTap,
|
|
Map<String,String>? mapReplace,
|
|
}) {
|
|
final _regExp = constructRegExpFromLinkType(linkTypes ?? [LinkType.url]);
|
|
|
|
// return the full text if there's no match or if empty
|
|
if (!_regExp.hasMatch(text) || text.isEmpty) return TextSpan(text: text);
|
|
|
|
final texts = text.split(_regExp);
|
|
final List<InlineSpan> spans = [];
|
|
final links = _regExp.allMatches(text).toList();
|
|
|
|
for (final text in texts) {
|
|
spans.add(TextSpan(
|
|
text: text,
|
|
));
|
|
if (links.isNotEmpty) {
|
|
final match = links.removeAt(0);
|
|
final link = Link.fromMatch(match);
|
|
// add the link
|
|
spans.add(
|
|
TextSpan(
|
|
text:( mapReplace?[link.value??'']?.isNotEmpty??false) ? (mapReplace?[link.value??''] ??'' ): link.value,
|
|
style: customLinkStyles?[link.type] ?? linkStyle,
|
|
recognizer: TapGestureRecognizer()
|
|
..onTap = () {
|
|
if (onTap != null) onTap(link);
|
|
},
|
|
),
|
|
);
|
|
|
|
}
|
|
}
|
|
|
|
|
|
return TextSpan(children: spans);
|
|
}
|