diff --git a/client/lib/core/widgets/custom_text_field.dart b/client/lib/core/widgets/custom_text_field.dart index e5d204de5..f78c07cb5 100644 --- a/client/lib/core/widgets/custom_text_field.dart +++ b/client/lib/core/widgets/custom_text_field.dart @@ -4,6 +4,7 @@ import 'package:flutter/services.dart'; import 'package:client/styles/styles.dart'; import 'package:client/core/localization/localization_helper.dart'; import 'package:universal_html/js.dart' as universal_js; +import 'package:markdown_editor_plus/markdown_editor_plus.dart'; enum BorderType { none, @@ -190,6 +191,8 @@ class CustomTextField extends StatefulWidget { /// Allow for custom input formatters final TextInputFormatter? inputFormatters; + final bool markdownEditor; + const CustomTextField({ Key? key, this.padding = const EdgeInsets.only(top: 15), @@ -237,6 +240,7 @@ class CustomTextField extends StatefulWidget { this.suffixIcon, this.keyboardType = TextInputType.text, this.inputFormatters, + this.markdownEditor = false, }) : super(key: key); @override @@ -364,102 +368,147 @@ class _CustomTextFieldState extends State { ), child: Stack( children: [ - TextFormField( - onTap: () { - _unfocus(); - final localOnTap = widget.onTap; - if (localOnTap != null) { - localOnTap(); - } - }, - onChanged: (text) { - final onChanged = widget.onChanged; - if (onChanged != null) { - onChanged(text); - } - }, - onFieldSubmitted: (value) { - widget.onEditingComplete?.call(); - }, - focusNode: _focusNode, - textInputAction: TextInputAction.none, - controller: _controller, - style: widget.textStyle ?? context.theme.textTheme.bodyMedium, - onEditingComplete: widget.onEditingComplete, - // This is absolutely nuts, but this is needed for now in order to allow a unit test to succeed, - // while not having to specify max lines for every single usage πŸ™„ - maxLines: widget.maxLines == null - ? null - : !widget.minLines.compareTo(widget.maxLines!).isNegative - ? widget.minLines - : widget.maxLines, - minLines: widget.minLines, - obscureText: widget.obscureText, - cursorColor: - widget.cursorColor ?? context.theme.colorScheme.primary, - cursorHeight: 20, - autovalidateMode: widget.autovalidateMode, - maxLength: widget.maxLength, - buildCounter: ( - _, { - required currentLength, - required maxLength, - required isFocused, - }) => - maxLength != null && !widget.hideCounter - ? Container( - margin: EdgeInsets.only(left: 10), - alignment: widget.counterAlignment ?? - Alignment.centerRight, - child: isFocused - ? Text( - '$currentLength/$maxLength', - style: widget.counterStyle ?? - AppTextStyle.bodySmall, - ) - : SizedBox.square( - dimension: widget.counterStyle?.fontSize ?? - AppTextStyle.bodySmall.fontSize, - ), - ) - : null, - maxLengthEnforcement: widget.maxLengthEnforcement, - inputFormatters: [ - if (widget.isOnlyDigits) - FilteringTextInputFormatter.digitsOnly, - if (widget.numberThreshold != null) - NumberThresholdFormatter(widget.numberThreshold!) - else if (widget.inputFormatters != null) - widget.inputFormatters!, - ], - validator: widget.validator, - decoration: InputDecoration( - contentPadding: widget.contentPadding, - border: _getBorder(), - focusedBorder: _getFocusedBorder(), - enabledBorder: _getBorder(), - errorBorder: _getBorder(isError: true), - focusedErrorBorder: _getFocusedBorder(isError: true), - labelText: widget.labelText, - labelStyle: _buildLabelStyle(), - errorStyle: context.theme.textTheme.labelMedium! - .copyWith(color: context.theme.colorScheme.error), - prefixText: widget.prefixText, - prefixStyle: widget.textStyle, - alignLabelWithHint: true, - hintText: widget.hintText, - hintStyle: context.theme.textTheme.bodyMedium, - helperText: widget.helperText, - fillColor: widget.fillColor, - filled: widget.fillColor != null, - suffixIcon: widget.suffixIcon, + if (widget.markdownEditor) + MarkdownAutoPreview( + emojiConvert: true, + minLines: 4, + maxLines: 10, + writeOnly: true, + onChanged: (text) { + final onChanged = widget.onChanged; + if (onChanged != null) { + onChanged(text); + } + }, + toolbarBackground: Colors.transparent, + controller: _controller, + decoration: InputDecoration( + // make room with contentPadding for toolbar + contentPadding: EdgeInsets.fromLTRB( + widget.contentPadding?.left ?? 12, + (widget.contentPadding?.top ?? 12) + 110, + widget.contentPadding?.right ?? 12, + widget.contentPadding?.bottom ?? 12, + ), + border: _getBorder(), + focusedBorder: _getFocusedBorder(), + enabledBorder: _getBorder(), + errorBorder: _getBorder(isError: true), + focusedErrorBorder: _getFocusedBorder(isError: true), + labelText: widget.labelText, + labelStyle: _buildLabelStyle(), + errorStyle: context.theme.textTheme.labelMedium! + .copyWith(color: context.theme.colorScheme.error), + prefixText: widget.prefixText, + prefixStyle: widget.textStyle, + alignLabelWithHint: true, + hintText: widget.hintText, + hintStyle: context.theme.textTheme.bodyMedium, + helperText: widget.helperText, + fillColor: widget.fillColor, + filled: widget.fillColor != null, + suffixIcon: widget.suffixIcon, + ), + borderColor: _getBorderColor(), + ), + if (!widget.markdownEditor) + TextFormField( + onTap: () { + _unfocus(); + final localOnTap = widget.onTap; + if (localOnTap != null) { + localOnTap(); + } + }, + onChanged: (text) { + final onChanged = widget.onChanged; + if (onChanged != null) { + onChanged(text); + } + }, + onFieldSubmitted: (value) { + widget.onEditingComplete?.call(); + }, + focusNode: _focusNode, + textInputAction: TextInputAction.none, + controller: _controller, + style: widget.textStyle ?? context.theme.textTheme.bodyMedium, + onEditingComplete: widget.onEditingComplete, + // This is absolutely nuts, but this is needed for now in order to allow a unit test to succeed, + // while not having to specify max lines for every single usage πŸ™„ + maxLines: widget.maxLines == null + ? null + : !widget.minLines.compareTo(widget.maxLines!).isNegative + ? widget.minLines + : widget.maxLines, + minLines: widget.minLines, + obscureText: widget.obscureText, + cursorColor: + widget.cursorColor ?? context.theme.colorScheme.primary, + cursorHeight: 20, + autovalidateMode: widget.autovalidateMode, + maxLength: widget.maxLength, + buildCounter: ( + _, { + required currentLength, + required maxLength, + required isFocused, + }) => + maxLength != null && !widget.hideCounter + ? Container( + margin: EdgeInsets.only(left: 10), + alignment: widget.counterAlignment ?? + Alignment.centerRight, + child: isFocused + ? Text( + '$currentLength/$maxLength', + style: widget.counterStyle ?? + AppTextStyle.bodySmall, + ) + : SizedBox.square( + dimension: widget.counterStyle?.fontSize ?? + AppTextStyle.bodySmall.fontSize, + ), + ) + : null, + maxLengthEnforcement: widget.maxLengthEnforcement, + inputFormatters: [ + if (widget.isOnlyDigits) + FilteringTextInputFormatter.digitsOnly, + if (widget.numberThreshold != null) + NumberThresholdFormatter(widget.numberThreshold!) + else if (widget.inputFormatters != null) + widget.inputFormatters!, + ], + validator: widget.validator, + decoration: InputDecoration( + contentPadding: widget.contentPadding, + border: _getBorder(), + focusedBorder: _getFocusedBorder(), + enabledBorder: _getBorder(), + errorBorder: _getBorder(isError: true), + focusedErrorBorder: _getFocusedBorder(isError: true), + labelText: widget.labelText, + labelStyle: _buildLabelStyle(), + errorStyle: context.theme.textTheme.labelMedium! + .copyWith(color: context.theme.colorScheme.error), + prefixText: widget.prefixText, + prefixStyle: widget.textStyle, + alignLabelWithHint: true, + hintText: widget.hintText, + hintStyle: context.theme.textTheme.bodyMedium, + helperText: widget.helperText, + fillColor: widget.fillColor, + filled: widget.fillColor != null, + suffixIcon: widget.suffixIcon, + ), + autofocus: widget.autofocus, + readOnly: widget.readOnly, + enabled: !widget.readOnly, + keyboardType: widget.keyboardType, ), - autofocus: widget.autofocus, - readOnly: widget.readOnly, - enabled: !widget.readOnly, - keyboardType: widget.keyboardType, - ), - if (widget.isOptional && + if (!widget.markdownEditor && + widget.isOptional && !_focusNode.hasFocus && _controller.text.isEmpty) Align( diff --git a/client/lib/features/events/features/live_meeting/features/meeting_agenda/presentation/views/agenda_item_text.dart b/client/lib/features/events/features/live_meeting/features/meeting_agenda/presentation/views/agenda_item_text.dart index 3029757ab..6df1f6ceb 100644 --- a/client/lib/features/events/features/live_meeting/features/meeting_agenda/presentation/views/agenda_item_text.dart +++ b/client/lib/features/events/features/live_meeting/features/meeting_agenda/presentation/views/agenda_item_text.dart @@ -5,6 +5,7 @@ import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:client/features/events/features/live_meeting/features/meeting_agenda/data/models/agenda_item_text_data.dart'; import 'package:client/core/widgets/custom_text_field.dart'; import 'package:client/styles/styles.dart'; +import 'package:markdown_editor_plus/markdown_editor_plus.dart'; class AgendaItemText extends StatelessWidget { final bool isEditMode; @@ -37,6 +38,7 @@ class AgendaItemText extends StatelessWidget { SizedBox(height: 20), CustomTextField( initialValue: agendaItemTextData.content, + markdownEditor: true, labelText: 'Content', hintText: 'Keep it short! You don’t want people to spend time reading.', diff --git a/client/pubspec.lock b/client/pubspec.lock index d963d251a..f503e8018 100644 --- a/client/pubspec.lock +++ b/client/pubspec.lock @@ -361,6 +361,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + expandable: + dependency: transitive + description: + name: expandable + sha256: "9604d612d4d1146dafa96c6d8eec9c2ff0994658d6d09fed720ab788c7f5afc2" + url: "https://pub.dev" + source: hosted + version: "5.0.1" fake_async: dependency: transitive description: @@ -645,10 +653,10 @@ packages: dependency: "direct main" description: name: flutter_markdown - sha256: "04c4722cc36ec5af38acc38ece70d22d3c2123c61305d555750a091517bbe504" + sha256: "08fb8315236099ff8e90cb87bb2b935e0a724a3af1623000a9cec930468e0f27" url: "https://pub.dev" source: hosted - version: "0.6.23" + version: "0.7.7+1" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1046,6 +1054,15 @@ packages: url: "https://pub.dev" source: hosted version: "7.2.2" + markdown_editor_plus: + dependency: "direct main" + description: + path: "." + ref: main + resolved-ref: b6b78c6ebb63f09f1f02d84bfa4e9ae234c3828e + url: "https://github.com/dariusk/markdown_editor_plus.git" + source: git + version: "0.2.15" matcher: dependency: transitive description: @@ -1949,4 +1966,4 @@ packages: version: "2.0.2" sdks: dart: ">=3.4.1 <4.0.0" - flutter: ">=3.19.0" + flutter: ">=3.22.0" diff --git a/client/pubspec.yaml b/client/pubspec.yaml index 57c9cb922..28b20a282 100644 --- a/client/pubspec.yaml +++ b/client/pubspec.yaml @@ -6,6 +6,10 @@ environment: sdk: ">=2.17.0 <4.0.0" dependencies: + markdown_editor_plus: + git: + url: https://github.com/dariusk/markdown_editor_plus.git + ref: main json_annotation: ^4.9.0 crypto: ^3.0.2 duration: ^3.0.13 @@ -44,7 +48,7 @@ dependencies: responsive_builder: ^0.7.0 datetime_picker_formfield: ^2.0.1 flutter_form_builder: ^9.2.1 - flutter_markdown: ^0.6.15 + flutter_markdown: ^0.7.2+1 ntp: ^2.0.0 transparent_image: ^2.0.1 timezone: ^0.9.2