mirror of
https://github.com/DungeonPaper/dungeon-paper-app.git
synced 2026-05-17 17:58:11 +00:00
677 lines
22 KiB
Dart
677 lines
22 KiB
Dart
import 'package:dungeon_paper/app/data/services/intl_service.dart';
|
|
import 'package:dungeon_paper/app/widgets/atoms/menu_button.dart';
|
|
import 'package:dungeon_paper/core/utils/builder_utils.dart';
|
|
import 'package:dungeon_paper/core/utils/markdown_styles.dart';
|
|
import 'package:dungeon_paper/i18n.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:flutter_markdown/flutter_markdown.dart';
|
|
import 'package:intl/intl.dart' as intl;
|
|
import 'package:url_launcher/url_launcher.dart';
|
|
|
|
class RichTextField extends StatelessWidget {
|
|
RichTextField({
|
|
super.key,
|
|
this.controller,
|
|
this.label,
|
|
this.rich = true,
|
|
this.text = '',
|
|
this.hintText,
|
|
this.initialValue,
|
|
this.focusNode,
|
|
this.decoration = const InputDecoration(),
|
|
this.keyboardType,
|
|
this.textCapitalization = TextCapitalization.none,
|
|
this.textInputAction,
|
|
this.style,
|
|
this.strutStyle,
|
|
this.textDirection,
|
|
this.textAlign = TextAlign.start,
|
|
this.textAlignVertical,
|
|
this.autofocus = false,
|
|
this.readOnly = false,
|
|
// this.toolbarOptions,
|
|
this.showCursor,
|
|
this.obscuringCharacter = '•',
|
|
this.obscureText = false,
|
|
this.autocorrect = true,
|
|
this.smartDashesType,
|
|
this.smartQuotesType,
|
|
this.enableSuggestions = true,
|
|
this.maxLengthEnforcement,
|
|
this.maxLines = 1,
|
|
this.minLines,
|
|
this.expands = false,
|
|
this.maxLength,
|
|
this.onChanged,
|
|
this.onTap,
|
|
this.onEditingComplete,
|
|
this.onFieldSubmitted,
|
|
this.onSaved,
|
|
this.validator,
|
|
this.inputFormatters,
|
|
this.enabled,
|
|
this.cursorWidth = 2.0,
|
|
this.cursorHeight,
|
|
this.cursorRadius,
|
|
this.cursorColor,
|
|
this.keyboardAppearance,
|
|
this.scrollPadding = const EdgeInsets.all(20.0),
|
|
this.enableInteractiveSelection,
|
|
this.selectionControls,
|
|
this.buildCounter,
|
|
this.scrollPhysics,
|
|
this.autofillHints,
|
|
this.autovalidateMode,
|
|
this.scrollController,
|
|
this.restorationId,
|
|
this.enableIMEPersonalizedLearning = true,
|
|
//
|
|
this.customButtons,
|
|
});
|
|
|
|
final String? label;
|
|
final String text;
|
|
final String? hintText;
|
|
final bool rich;
|
|
final TextEditingController? controller;
|
|
|
|
final String? initialValue;
|
|
final FocusNode? focusNode;
|
|
final InputDecoration? decoration;
|
|
final TextInputType? keyboardType;
|
|
final TextCapitalization textCapitalization;
|
|
final TextInputAction? textInputAction;
|
|
final TextStyle? style;
|
|
final StrutStyle? strutStyle;
|
|
final TextDirection? textDirection;
|
|
final TextAlign textAlign;
|
|
final TextAlignVertical? textAlignVertical;
|
|
final bool autofocus;
|
|
final bool readOnly;
|
|
// final ToolbarOptions? toolbarOptions;
|
|
final bool? showCursor;
|
|
final String obscuringCharacter;
|
|
final bool obscureText;
|
|
final bool autocorrect;
|
|
final SmartDashesType? smartDashesType;
|
|
final SmartQuotesType? smartQuotesType;
|
|
final bool enableSuggestions;
|
|
final MaxLengthEnforcement? maxLengthEnforcement;
|
|
final int? maxLines;
|
|
final int? minLines;
|
|
final bool expands;
|
|
final int? maxLength;
|
|
final ValueChanged<String>? onChanged;
|
|
final GestureTapCallback? onTap;
|
|
final VoidCallback? onEditingComplete;
|
|
final ValueChanged<String>? onFieldSubmitted;
|
|
final FormFieldSetter<String>? onSaved;
|
|
final FormFieldValidator<String>? validator;
|
|
final List<TextInputFormatter>? inputFormatters;
|
|
final bool? enabled;
|
|
final double cursorWidth;
|
|
final double? cursorHeight;
|
|
final Radius? cursorRadius;
|
|
final Color? cursorColor;
|
|
final Brightness? keyboardAppearance;
|
|
final EdgeInsets scrollPadding;
|
|
final bool? enableInteractiveSelection;
|
|
final TextSelectionControls? selectionControls;
|
|
final InputCounterWidgetBuilder? buildCounter;
|
|
final ScrollPhysics? scrollPhysics;
|
|
final Iterable<String>? autofillHints;
|
|
final AutovalidateMode? autovalidateMode;
|
|
final ScrollController? scrollController;
|
|
final String? restorationId;
|
|
final bool enableIMEPersonalizedLearning;
|
|
final _defaultController = TextEditingController();
|
|
TextEditingController get _controller => controller ?? _defaultController;
|
|
final List<RichButton>? customButtons;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!rich) {
|
|
return _buildInput(context);
|
|
}
|
|
|
|
return Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildRichControls(context),
|
|
_buildInput(context),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildInput(BuildContext context) {
|
|
final effectiveLabel = label != null ? Text(label!) : null;
|
|
|
|
return TextFormField(
|
|
controller: controller,
|
|
initialValue: initialValue,
|
|
focusNode: focusNode,
|
|
decoration: decoration?.copyWith(
|
|
hintText: hintText,
|
|
label: effectiveLabel,
|
|
) ??
|
|
InputDecoration(
|
|
filled: true,
|
|
hintText: hintText,
|
|
label: effectiveLabel,
|
|
),
|
|
keyboardType: keyboardType,
|
|
textCapitalization: textCapitalization,
|
|
textInputAction: textInputAction,
|
|
style: style,
|
|
strutStyle: strutStyle,
|
|
textDirection: textDirection,
|
|
textAlign: textAlign,
|
|
textAlignVertical: textAlignVertical,
|
|
autofocus: autofocus,
|
|
readOnly: readOnly,
|
|
// toolbarOptions: toolbarOptions,
|
|
showCursor: showCursor,
|
|
obscuringCharacter: obscuringCharacter,
|
|
obscureText: obscureText,
|
|
autocorrect: autocorrect,
|
|
smartDashesType: smartDashesType,
|
|
smartQuotesType: smartQuotesType,
|
|
enableSuggestions: enableSuggestions,
|
|
maxLengthEnforcement: maxLengthEnforcement,
|
|
maxLines: maxLines,
|
|
minLines: minLines,
|
|
expands: expands,
|
|
maxLength: maxLength,
|
|
onChanged: onChanged,
|
|
onTap: onTap,
|
|
onEditingComplete: onEditingComplete,
|
|
onFieldSubmitted: onFieldSubmitted,
|
|
onSaved: onSaved,
|
|
validator: validator,
|
|
inputFormatters: inputFormatters,
|
|
enabled: enabled,
|
|
cursorWidth: cursorWidth,
|
|
cursorHeight: cursorHeight,
|
|
cursorRadius: cursorRadius,
|
|
cursorColor: cursorColor,
|
|
keyboardAppearance: keyboardAppearance,
|
|
scrollPadding: scrollPadding,
|
|
enableInteractiveSelection: enableInteractiveSelection,
|
|
selectionControls: selectionControls,
|
|
buildCounter: buildCounter,
|
|
scrollPhysics: scrollPhysics,
|
|
autofillHints: autofillHints,
|
|
autovalidateMode: autovalidateMode,
|
|
scrollController: scrollController,
|
|
restorationId: restorationId,
|
|
enableIMEPersonalizedLearning: enableIMEPersonalizedLearning,
|
|
);
|
|
}
|
|
|
|
SizedBox _buildRichControls(BuildContext context) {
|
|
final mdTheme = MarkdownStyles.of(context);
|
|
const divider = Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8), child: VerticalDivider());
|
|
const thinDivider = Padding(
|
|
padding: EdgeInsets.symmetric(vertical: 8),
|
|
child: VerticalDivider(width: 4),
|
|
);
|
|
final builder = ItemBuilder.lazyChildren(
|
|
children: [
|
|
() => _RichButton(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
icon: const Icon(Icons.preview_outlined),
|
|
tooltip: tr.richText.preview,
|
|
onTap: () => _openPreview(context),
|
|
),
|
|
() => _RichButton(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
icon: const Icon(Icons.help),
|
|
tooltip: tr.richText.help,
|
|
onTap: () => launchUrl(
|
|
Uri.parse('https://www.markdownguide.org/basic-syntax')),
|
|
),
|
|
if (customButtons?.isNotEmpty == true) () => thinDivider,
|
|
if (customButtons?.isNotEmpty == true)
|
|
...customButtons!.map(
|
|
(button) => () => button.buildButton(context, _controller),
|
|
),
|
|
() => thinDivider,
|
|
() => RichButton(
|
|
icon: Icons.format_bold,
|
|
tooltip: tr.richText.bold,
|
|
defaultContent: () => '**bold**',
|
|
prefix: () => '**',
|
|
behavior: RichButtonTextBehavior.wrap,
|
|
selectionStartOffset: 2,
|
|
selectionEndOffset: -2,
|
|
).buildButton(context, _controller),
|
|
() => RichButton(
|
|
icon: Icons.format_italic,
|
|
tooltip: tr.richText.italic,
|
|
defaultContent: () => '*italic*',
|
|
prefix: () => '*',
|
|
behavior: RichButtonTextBehavior.wrap,
|
|
selectionStartOffset: 1,
|
|
selectionEndOffset: -1,
|
|
).buildButton(context, _controller),
|
|
() => RichButton.dropdown(
|
|
icon: Icons.format_size,
|
|
tooltip: tr.richText.headings,
|
|
actions: List.generate(6, (i) => i + 1)
|
|
.map(
|
|
(i) => RichButtonAction.dropdownItem(
|
|
text: Text(
|
|
tr.richText.heading(i),
|
|
style: {
|
|
'h1': mdTheme.h1,
|
|
'h2': mdTheme.h2,
|
|
'h3': mdTheme.h3,
|
|
'h4': mdTheme.h4,
|
|
'h5': mdTheme.h5,
|
|
'h6': mdTheme.h6,
|
|
}['h$i']!,
|
|
),
|
|
behavior: RichButtonTextBehavior.wrap,
|
|
defaultContent: () =>
|
|
'\n${List.filled(i, "#").join("")} ${tr.richText.heading(i)}\n',
|
|
prefix: () => '\n${List.filled(i, "#").join("")} ',
|
|
suffix: () => '\n',
|
|
selectionStartOffset: 2 + i,
|
|
selectionEndOffset: -1,
|
|
),
|
|
)
|
|
.toList(),
|
|
).buildButton(context, _controller),
|
|
() => divider,
|
|
() => RichButton(
|
|
icon: Icons.format_list_bulleted,
|
|
tooltip: tr.richText.bulletList,
|
|
defaultContent: () => '\n- ',
|
|
prefix: () => '\n- ',
|
|
behavior: RichButtonTextBehavior.prefix,
|
|
).buildButton(context, _controller),
|
|
() => RichButton(
|
|
icon: Icons.format_list_numbered,
|
|
tooltip: tr.richText.numberedList,
|
|
defaultContent: () => '\n1. ',
|
|
prefix: () => '\n1. ',
|
|
behavior: RichButtonTextBehavior.prefix,
|
|
).buildButton(context, _controller),
|
|
() => RichButton(
|
|
icon: Icons.check_box_outline_blank,
|
|
tooltip: tr.richText.checkList.unchecked,
|
|
defaultContent: () => '\n- [ ] ',
|
|
prefix: () => '\n- [ ] ',
|
|
selectionStartOffset: 7,
|
|
behavior: RichButtonTextBehavior.prefix,
|
|
).buildButton(context, _controller),
|
|
() => RichButton(
|
|
icon: Icons.check_box_outlined,
|
|
tooltip: tr.richText.checkList.checked,
|
|
defaultContent: () => '\n- [x] ',
|
|
prefix: () => '\n- [x] ',
|
|
selectionStartOffset: 7,
|
|
behavior: RichButtonTextBehavior.prefix,
|
|
).buildButton(context, _controller),
|
|
() => divider,
|
|
() => RichButton(
|
|
icon: Icons.link,
|
|
tooltip: tr.richText.url,
|
|
defaultContent: () => '[text](url)',
|
|
prefix: () => '[',
|
|
suffix: () => '](url)',
|
|
selectionStartOffset: 7,
|
|
selectionEndOffset: -1,
|
|
behavior: RichButtonTextBehavior.wrap,
|
|
).buildButton(context, _controller),
|
|
() => RichButton(
|
|
icon: Icons.image,
|
|
tooltip: tr.richText.imageURL,
|
|
defaultContent: () => '',
|
|
prefix: () => '![alt][',
|
|
suffix: () => ']',
|
|
selectionStartOffset: 7,
|
|
selectionEndOffset: -1,
|
|
behavior: RichButtonTextBehavior.wrap,
|
|
).buildButton(context, _controller),
|
|
() => RichButton(
|
|
icon: Icons.table_chart_outlined,
|
|
tooltip: tr.richText.table,
|
|
defaultContent: () => '| ${tr.richText.header(1)} '
|
|
'| ${tr.richText.header(2)} '
|
|
'|\n|---|---|\n'
|
|
'| ${tr.richText.cell(1)} '
|
|
'| ${tr.richText.cell(2)} |',
|
|
prefix: () => '| ${tr.richText.header(' ')}|\n|---|\n| ',
|
|
suffix: () => ' |',
|
|
selectionStartOffset: 2,
|
|
selectionEndOffset: -43,
|
|
behavior: RichButtonTextBehavior.wrap,
|
|
).buildButton(context, _controller),
|
|
() => divider,
|
|
() => RichButton(
|
|
icon: Icons.calendar_today,
|
|
tooltip: tr.richText.date,
|
|
prefix: () => _getFormattedDate(DateTime.now()),
|
|
defaultContent: () => _getFormattedDate(DateTime.now()),
|
|
behavior: RichButtonTextBehavior.prefix,
|
|
).buildButton(context, _controller),
|
|
() => RichButton(
|
|
icon: Icons.access_time_outlined,
|
|
tooltip: tr.richText.date,
|
|
prefix: () => _getFormattedTime(DateTime.now()),
|
|
behavior: RichButtonTextBehavior.prefix,
|
|
defaultContent: () => _getFormattedTime(DateTime.now()),
|
|
).buildButton(context, _controller),
|
|
],
|
|
);
|
|
return SizedBox(
|
|
height: 40,
|
|
child: ListView.builder(
|
|
scrollDirection: Axis.horizontal,
|
|
shrinkWrap: true,
|
|
itemBuilder: builder.itemBuilder,
|
|
itemCount: builder.itemCount,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _openPreview(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => MarkdownPreviewDialog(text: _controller.text),
|
|
);
|
|
}
|
|
|
|
String _getFormattedDate(DateTime date) =>
|
|
intl.DateFormat(IntlService.dateFormat).format(date);
|
|
|
|
String _getFormattedTime(DateTime date) =>
|
|
intl.DateFormat(IntlService.timeFormat).format(date);
|
|
}
|
|
|
|
class _RichButton extends StatelessWidget {
|
|
const _RichButton({
|
|
required this.icon,
|
|
required this.tooltip,
|
|
this.onTap,
|
|
this.color,
|
|
});
|
|
|
|
final Widget icon;
|
|
final String tooltip;
|
|
final void Function()? onTap;
|
|
final Color? color;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
var child = IconTheme.merge(data: IconThemeData(color: color), child: icon);
|
|
return SizedBox(
|
|
width: 40,
|
|
height: 40,
|
|
child: Tooltip(
|
|
message: tooltip,
|
|
child: onTap != null
|
|
? InkWell(
|
|
splashColor: Theme.of(context).splashColor,
|
|
borderRadius: BorderRadius.circular(4),
|
|
onTap: onTap,
|
|
child: child,
|
|
)
|
|
: child,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class MarkdownPreviewDialog extends StatelessWidget {
|
|
const MarkdownPreviewDialog({
|
|
super.key,
|
|
required this.text,
|
|
});
|
|
|
|
final String text;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return OrientationBuilder(builder: (context, orient) {
|
|
return AlertDialog(
|
|
title: Text(tr.richText.markdownPreview),
|
|
content: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
minWidth: MediaQuery.of(context).size.width - 100,
|
|
maxWidth: MediaQuery.of(context).size.width - 100,
|
|
maxHeight: MediaQuery.of(context).size.height - 100,
|
|
minHeight: 10,
|
|
),
|
|
child: Markdown(
|
|
data: text.trim().isNotEmpty ? text : tr.generic.noDescription,
|
|
padding: const EdgeInsets.all(0),
|
|
onTapLink: (text, href, title) => launchUrl(Uri.parse(href!)),
|
|
shrinkWrap: true,
|
|
styleSheet: MarkdownStyles.of(context),
|
|
),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
}
|
|
|
|
class RichButtonAction {
|
|
final Widget text;
|
|
final String Function() defaultContent;
|
|
final String Function() prefix;
|
|
final String? Function()? suffix;
|
|
final int? selectionStartOffset;
|
|
final int? selectionEndOffset;
|
|
final RichButtonTextBehavior behavior;
|
|
|
|
RichButtonAction({
|
|
required this.defaultContent,
|
|
required this.prefix,
|
|
this.suffix,
|
|
this.selectionStartOffset,
|
|
this.selectionEndOffset,
|
|
this.behavior = RichButtonTextBehavior.auto,
|
|
}) : text = const Text('');
|
|
|
|
RichButtonAction.dropdownItem({
|
|
required this.text,
|
|
required this.defaultContent,
|
|
required this.prefix,
|
|
this.suffix,
|
|
this.selectionStartOffset,
|
|
this.selectionEndOffset,
|
|
this.behavior = RichButtonTextBehavior.auto,
|
|
});
|
|
}
|
|
|
|
enum RichButtonTextBehavior { auto, prefix, suffix, wrap }
|
|
|
|
class RichButton {
|
|
final IconData icon;
|
|
final String tooltip;
|
|
final List<RichButtonAction> actions;
|
|
final Color? color;
|
|
final RichButtonTextBehavior behavior;
|
|
|
|
RichButton.dropdown({
|
|
required this.icon,
|
|
required this.tooltip,
|
|
required this.actions,
|
|
this.behavior = RichButtonTextBehavior.auto,
|
|
this.color,
|
|
});
|
|
|
|
RichButton({
|
|
required this.icon,
|
|
required this.tooltip,
|
|
this.color,
|
|
required String Function() defaultContent,
|
|
required String Function() prefix,
|
|
this.behavior = RichButtonTextBehavior.auto,
|
|
String? Function()? suffix,
|
|
int? selectionStartOffset,
|
|
int? selectionEndOffset,
|
|
}) : actions = [
|
|
RichButtonAction(
|
|
defaultContent: defaultContent,
|
|
prefix: prefix,
|
|
suffix: suffix,
|
|
selectionStartOffset: selectionStartOffset,
|
|
selectionEndOffset: selectionEndOffset,
|
|
)
|
|
];
|
|
|
|
bool get isSingle => actions.length == 1;
|
|
|
|
RichButtonAction get singleAction => actions.first;
|
|
|
|
Widget buildButton(BuildContext context, TextEditingController controller) {
|
|
if (isSingle) {
|
|
return _RichButton(
|
|
icon: Icon(icon),
|
|
tooltip: tooltip,
|
|
onTap: _wrapOrAppendCb(
|
|
controller,
|
|
singleAction.defaultContent(),
|
|
singleAction.prefix(),
|
|
singleAction.suffix?.call(),
|
|
singleAction.selectionStartOffset,
|
|
singleAction.selectionEndOffset,
|
|
),
|
|
);
|
|
}
|
|
return MenuButton(
|
|
items: actions.map(
|
|
(action) => MenuEntry(
|
|
label: action.text,
|
|
value: action.defaultContent,
|
|
onSelect: _wrapOrAppendCb(
|
|
controller,
|
|
action.defaultContent(),
|
|
action.prefix(),
|
|
action.suffix?.call(),
|
|
action.selectionStartOffset,
|
|
action.selectionEndOffset,
|
|
),
|
|
),
|
|
),
|
|
child: _RichButton(
|
|
icon: Icon(icon),
|
|
tooltip: tooltip,
|
|
),
|
|
);
|
|
}
|
|
|
|
void _wrapCursorWith(TextEditingController controller, String prefix,
|
|
[String? suffix]) {
|
|
if (controller.selection.isValid) {
|
|
// has selection - wrap current cursor positions
|
|
final selection = controller.selection.copyWith(
|
|
baseOffset: controller.selection.baseOffset + prefix.length,
|
|
extentOffset: controller.selection.extentOffset + prefix.length,
|
|
);
|
|
controller.text = [
|
|
if (controller.selection.start > 0)
|
|
controller.text.substring(0, controller.selection.start),
|
|
prefix,
|
|
controller.text
|
|
.substring(controller.selection.start, controller.selection.end),
|
|
suffix ?? prefix,
|
|
if (controller.selection.end < controller.text.length)
|
|
controller.text
|
|
.substring(controller.selection.end, controller.text.length),
|
|
].join('');
|
|
try {
|
|
controller.selection = selection;
|
|
} catch (e) {
|
|
// don't crash when selection is invalid
|
|
}
|
|
}
|
|
}
|
|
|
|
void _insertAtCursor(TextEditingController controller, String text,
|
|
[int? selectionStartOffset, int? selectionEndOffset]) {
|
|
if (controller.selection.isValid) {
|
|
// has cursor - append at cursor position
|
|
final selection = controller.selection.copyWith(
|
|
baseOffset:
|
|
controller.selection.baseOffset + (selectionStartOffset ?? 0),
|
|
extentOffset: controller.selection.extentOffset +
|
|
text.length +
|
|
(selectionEndOffset ?? 0),
|
|
);
|
|
controller.text = [
|
|
controller.text.substring(0, controller.selection.start),
|
|
text,
|
|
controller.text
|
|
.substring(controller.selection.start, controller.text.length),
|
|
].join('');
|
|
try {
|
|
controller.selection = selection;
|
|
} catch (e) {
|
|
// don't crash when selection is invalid
|
|
}
|
|
} else {
|
|
// no cursor - append to end of text
|
|
final selection = controller.selection.copyWith(
|
|
baseOffset: selectionStartOffset ?? 0,
|
|
extentOffset: text.length + (selectionEndOffset ?? 0),
|
|
);
|
|
controller.text += text;
|
|
try {
|
|
controller.selection = selection;
|
|
} catch (e) {
|
|
// don't crash when selection is invalid
|
|
}
|
|
}
|
|
}
|
|
|
|
void Function() _wrapOrAppendCb(
|
|
TextEditingController controller, String defaultContent, String prefix,
|
|
[String? suffix, int? selectionStartOffset, int? selectionEndOffset]) {
|
|
final isAuto = behavior == RichButtonTextBehavior.auto;
|
|
final isWrap = behavior == RichButtonTextBehavior.wrap;
|
|
final hasPrefix = isWrap ||
|
|
(prefix.isNotEmpty && isAuto) ||
|
|
behavior == RichButtonTextBehavior.prefix;
|
|
final hasSuffix = isWrap ||
|
|
(suffix?.isNotEmpty == true && isAuto) ||
|
|
behavior == RichButtonTextBehavior.suffix;
|
|
suffix ??= prefix;
|
|
// final shouldWrap = (isAuto && !controller.selection.isCollapsed)
|
|
|
|
// if text is empty, or selection starts directly after newline - remove prefix newlines
|
|
// if (controller.text.trim().isEmpty ||
|
|
// (controller.selection.isValid &&
|
|
// controller.text.substring(
|
|
// max(controller.selection.start - 1, 0), controller.selection.start) ==
|
|
// '\n')) {
|
|
// final originalText = text;
|
|
// text = text.trimLeft();
|
|
// prefix = prefix.trimLeft();
|
|
// // _suffix = _suffix.trimLeft();
|
|
// if (originalText.startsWith('\n') && selectionStartOffset != null) {
|
|
// selectionStartOffset -= 1;
|
|
// }
|
|
// if (originalText.endsWith('\n') && selectionEndOffset != null) {
|
|
// selectionEndOffset += 1;
|
|
// }
|
|
// }
|
|
return () {
|
|
if (!controller.selection.isCollapsed) {
|
|
_wrapCursorWith(
|
|
controller, hasPrefix ? prefix : '', hasSuffix ? suffix : '');
|
|
} else {
|
|
_insertAtCursor(controller, defaultContent, selectionStartOffset,
|
|
selectionEndOffset);
|
|
}
|
|
};
|
|
}
|
|
}
|