From cf3c90cb6529579188a24f2d8608ecce094421d7 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Wed, 15 Jan 2025 13:08:47 +0100 Subject: [PATCH 1/9] feat: creating multiline text entry markdown widget --- .../multiline_text_entry_markdown_widget.dart | 63 ++++++++++ .../widgets/rich_text/voices_rich_text.dart | 114 +++++++++++------- .../voices_date_time_text_field.dart | 1 + .../tiles/document_builder_section_tile.dart | 10 +- 4 files changed, 142 insertions(+), 46 deletions(-) create mode 100644 catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart new file mode 100644 index 00000000000..4c74206f9a0 --- /dev/null +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart @@ -0,0 +1,63 @@ +import 'package:catalyst_voices/common/ext/document_property_ext.dart'; +import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_quill/flutter_quill.dart' as quill; + +class MultilineTextEntryMarkdownWidget extends StatefulWidget { + final DocumentProperty property; + final ValueChanged onChanged; + final bool isEditMode; + final bool isRequired; + + const MultilineTextEntryMarkdownWidget({ + super.key, + required this.property, + required this.onChanged, + required this.isEditMode, + required this.isRequired, + }); + + @override + State createState() => + _MultilineTextEntryMarkdownWidgetState(); +} + +class _MultilineTextEntryMarkdownWidgetState + extends State { + late VoicesRichTextEditModeController _editModeController; + late VoicesRichTextController _controller; + + String get _description => widget.property.formattedDescription; + int? get _maxLength => widget.property.schema.strLengthRange?.max; + + @override + void initState() { + super.initState(); + + _controller = VoicesRichTextController( + document: quill.Document(), + selection: const TextSelection.collapsed(offset: 0), + ); + _editModeController = VoicesRichTextEditModeController(widget.isEditMode); + } + + @override + void didUpdateWidget(covariant MultilineTextEntryMarkdownWidget oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.isEditMode != oldWidget.isEditMode) { + _editModeController.value = widget.isEditMode; + } + } + + @override + Widget build(BuildContext context) { + return VoicesRichText( + controller: _controller, + editModeController: _editModeController, + title: _description, + charsLimit: _maxLength, + ); + } +} diff --git a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart index a8b4aeab9cb..b3cc3e2948f 100644 --- a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart @@ -136,6 +136,11 @@ class _VoicesRichTextState extends State { old?.removeListener(_onEditModeControllerChanged); current?.addListener(_onEditModeControllerChanged); } + + // if (widget.editModeController?.value != + // oldWidget.editModeController?.value) { + // _toggleEditMode(); + // } } @override @@ -160,15 +165,17 @@ class _VoicesRichTextState extends State { final charsLimit = widget.charsLimit; return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, children: [ - Padding( - padding: const EdgeInsets.only(left: 24, top: 20, bottom: 20), - child: _TopBar( - title: widget.title, - isEditMode: _effectiveEditModeController.value, - onToggleEditMode: _toggleEditMode, - ), - ), + // Padding( + // padding: const EdgeInsets.only(left: 24, top: 20, bottom: 20), + // child: _TopBar( + // title: widget.title, + // isEditMode: _effectiveEditModeController.value, + // onToggleEditMode: _toggleEditMode, + // ), + // ), Offstage( offstage: !_effectiveEditModeController.value, child: Padding( @@ -176,6 +183,13 @@ class _VoicesRichTextState extends State { child: _Toolbar(controller: _effectiveController), ), ), + if (widget.title.isNotEmpty) ...[ + Text( + widget.title, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 8), + ], _EditorDecoration( isEditMode: _effectiveEditModeController.value, child: _Editor( @@ -191,25 +205,25 @@ class _VoicesRichTextState extends State { charsLimit: charsLimit, ), ), - const SizedBox(height: 16), - Offstage( - offstage: !_effectiveEditModeController.value, - child: _Footer( - onSave: _saveDocument, - ), - ), - if (!_effectiveEditModeController.value) const SizedBox(height: 24), + // const SizedBox(height: 16), + // Offstage( + // offstage: !_effectiveEditModeController.value, + // child: _Footer( + // onSave: _saveDocument, + // ), + // ), + // if (!_effectiveEditModeController.value) const SizedBox(height: 24), ], ); } - Future _toggleEditMode() async { - if (!_effectiveEditModeController.value) { - if (!widget.canEditDocumentGetter(_effectiveController.document)) { - widget.onEditBlocked?.call(); - return; - } - } + void _toggleEditMode() { + // if (!_effectiveEditModeController.value) { + // if (!widget.canEditDocumentGetter(_effectiveController.document)) { + // widget.onEditBlocked?.call(); + // return; + // } + // } if (_effectiveEditModeController.value) { _stopEdit(); @@ -247,7 +261,7 @@ class _VoicesRichTextState extends State { } } - void _onEditModeControllerChanged() { + Future _onEditModeControllerChanged() async { setState(() { _effectiveFocusNode.canRequestFocus = _effectiveEditModeController.value; }); @@ -294,30 +308,28 @@ class _EditorDecoration extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 24), - // TODO(jakub): enable after implementing https://github.com/input-output-hk/catalyst-voices/issues/846 - // child: ResizableBoxParent( - // minHeight: 470, - // resizableVertically: true, - // resizableHorizontally: false, - child: DecoratedBox( - decoration: BoxDecoration( - color: isEditMode - ? Theme.of(context).colors.onSurfaceNeutralOpaqueLv1 - : Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, - border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, - ), - borderRadius: BorderRadius.circular(8), - ), - child: IgnorePointer( - ignoring: !isEditMode, - child: child, + return + // TODO(jakub): enable after implementing https://github.com/input-output-hk/catalyst-voices/issues/846 + // child: ResizableBoxParent( + // minHeight: 470, + // resizableVertically: true, + // resizableHorizontally: false, + DecoratedBox( + decoration: BoxDecoration( + color: isEditMode + ? Theme.of(context).colors.onSurfaceNeutralOpaqueLv1 + : Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, + border: Border.all( + color: Theme.of(context).colorScheme.outlineVariant, ), + borderRadius: BorderRadius.circular(8), + ), + child: IgnorePointer( + ignoring: !isEditMode, + child: child, ), - // ), ); + // ), } } @@ -334,6 +346,8 @@ class _Editor extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final textTheme = theme.textTheme; return QuillEditor( controller: controller, focusNode: focusNode, @@ -341,6 +355,16 @@ class _Editor extends StatelessWidget { configurations: QuillEditorConfigurations( padding: const EdgeInsets.all(16), placeholder: context.l10n.placeholderRichText, + customStyles: DefaultStyles( + placeHolder: DefaultTextBlockStyle( + textTheme.bodyLarge?.copyWith(color: theme.colors.textDisabled) ?? + DefaultTextStyle.of(context).style, + HorizontalSpacing.zero, + VerticalSpacing.zero, + VerticalSpacing.zero, + null, + ), + ), embedBuilders: CatalystPlatform.isWeb ? FlutterQuillEmbeds.editorWebBuilders() : FlutterQuillEmbeds.editorBuilders(), diff --git a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart index b1ad08e0a86..5fb72e87e57 100644 --- a/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart +++ b/catalyst_voices/apps/voices/lib/widgets/text_field/voices_date_time_text_field.dart @@ -52,6 +52,7 @@ class VoicesDateTimeTextField extends StatelessWidget { validator: validator, decoration: VoicesTextFieldDecoration( suffixIcon: ExcludeFocus(child: suffixIcon), + showStatusSuffixIcon: false, fillColor: theme.colors.elevationsOnSurfaceNeutralLv1Grey, filled: true, enabledBorder: OutlineInputBorder( diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart index 87cb6fe62d9..66cecf62c0a 100644 --- a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart @@ -1,5 +1,6 @@ import 'package:catalyst_voices/widgets/document_builder/agreement_confirmation_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/document_token_value_widget.dart'; +import 'package:catalyst_voices/widgets/document_builder/multiline_text_entry_markdown_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_dropdown_selection_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_grouped_tag_selector_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_line_https_url_widget.dart.dart'; @@ -213,7 +214,6 @@ class _PropertyBuilder extends StatelessWidget { ); case SingleLineTextEntryDefinition(): case MultiLineTextEntryDefinition(): - case MultiLineTextEntryMarkdownDefinition(): case MultiSelectDefinition(): case SingleLineTextEntryListDefinition(): case MultiLineTextEntryListMarkdownDefinition(): @@ -281,6 +281,14 @@ class _PropertyBuilder extends StatelessWidget { isEditMode: isEditMode, isRequired: castProperty.schema.isRequired, ); + case MultiLineTextEntryMarkdownDefinition(): + final castProperty = definition.castProperty(property); + return MultilineTextEntryMarkdownWidget( + property: castProperty, + onChanged: onChanged, + isEditMode: isEditMode, + isRequired: castProperty.schema.isRequired, + ); } } } From 5dc8c2df7479e8de68d7fce36d0d1d88c4ad3af2 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Thu, 16 Jan 2025 12:22:49 +0100 Subject: [PATCH 2/9] feat: markdown to quill editor --- .../multiline_text_entry_markdown_widget.dart | 152 +++++++- .../widgets/rich_text/voices_rich_text.dart | 365 +++--------------- .../rich_text/voices_rich_text_test.dart | 12 +- .../examples/voices_rich_text_example.dart | 6 +- 4 files changed, 208 insertions(+), 327 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart index 4c74206f9a0..431a667a14e 100644 --- a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart @@ -1,8 +1,14 @@ +import 'dart:async'; + +import 'package:catalyst_voices/common/codecs/markdown_codec.dart'; import 'package:catalyst_voices/common/ext/document_property_ext.dart'; import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart'; +import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' as quill; +import 'package:flutter_quill/flutter_quill.dart'; class MultilineTextEntryMarkdownWidget extends StatefulWidget { final DocumentProperty property; @@ -25,21 +31,30 @@ class MultilineTextEntryMarkdownWidget extends StatefulWidget { class _MultilineTextEntryMarkdownWidgetState extends State { - late VoicesRichTextEditModeController _editModeController; - late VoicesRichTextController _controller; + late final VoicesRichTextController _controller; + late final VoicesRichTextFocusNode _focus; + late final ScrollController _scrollController; + + final MarkdownEncoder _encoder = const MarkdownEncoder(); + final MarkdownDecoder _decoder = const MarkdownDecoder(); + + quill.Document? _observedDocument; + StreamSubscription? _documentChangeSub; + quill.Document? _preEditDocument; String get _description => widget.property.formattedDescription; int? get _maxLength => widget.property.schema.strLengthRange?.max; + String? get _value => widget.property.value; @override void initState() { super.initState(); - _controller = VoicesRichTextController( - document: quill.Document(), - selection: const TextSelection.collapsed(offset: 0), - ); - _editModeController = VoicesRichTextEditModeController(widget.isEditMode); + _handleInitialValue(); + _controller.addListener(_onControllerChanged); + + _focus = VoicesRichTextFocusNode(); + _scrollController = ScrollController(); } @override @@ -47,17 +62,136 @@ class _MultilineTextEntryMarkdownWidgetState super.didUpdateWidget(oldWidget); if (widget.isEditMode != oldWidget.isEditMode) { - _editModeController.value = widget.isEditMode; + _focus.toggleFocus(enabled: widget.isEditMode); + _controller.readOnly = !widget.isEditMode; + _toggleEditMode(); + } + + if (widget.property.value != oldWidget.property.value) { + _handleInitialValue(); } } + @override + void dispose() { + _controller.dispose(); + _focus.dispose(); + _scrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return VoicesRichText( controller: _controller, - editModeController: _editModeController, + enabled: widget.isEditMode, title: _description, + focusNode: _focus, + scrollController: _scrollController, charsLimit: _maxLength, ); } + + VoicesTextFieldValidationResult _validate(String? value) { + final result = widget.property.schema.validatePropertyValue(value); + if (result.isValid) { + return const VoicesTextFieldValidationResult.success(); + } else { + final localized = LocalizedDocumentValidationResult.from(result); + return VoicesTextFieldValidationResult.error(localized.message(context)); + } + } + + void _handleInitialValue() { + if (_value != null) { + final input = MarkdownData(_value!); + final delta = _decoder.convert(input); + _controller = VoicesRichTextController( + document: quill.Document.fromDelta(delta), + selection: const TextSelection.collapsed(offset: 0), + ); + } else { + _controller = VoicesRichTextController( + document: quill.Document(), + selection: const TextSelection.collapsed(offset: 0), + ); + } + } + + void _onControllerChanged() { + if (_observedDocument != _controller.document) { + _updateObservedDocument(); + } + } + + void _updateObservedDocument() { + _observedDocument = _controller.document; + unawaited(_documentChangeSub?.cancel()); + _documentChangeSub = _observedDocument?.changes.listen(_onDocumentChanged); + } + + void _onDocumentChanged(DocChange change) { + _validate(_convertToString()); + if (change.change.last.data != '\n') { + _notifyChangeListener(); + } + } + + void _notifyChangeListener() { + final delta = _controller.document.toDelta(); + final markdownData = _encoder.convert(delta); + widget.onChanged( + DocumentChange( + nodeId: widget.property.schema.nodeId, + value: markdownData.data, + ), + ); + } + + String _convertToString() { + final delta = _controller.document.toDelta(); + final markdownData = _encoder.convert(delta); + return markdownData.data; + } + + void _toggleEditMode() { + _controller.readOnly = !widget.isEditMode; + if (widget.isEditMode) { + _startEdit(); + } else { + _stopEdit(); + } + } + + void _startEdit() { + final currentDocument = _controller.document; + _preEditDocument = quill.Document.fromDelta(currentDocument.toDelta()); + } + + void _stopEdit() { + final preEditDocument = _preEditDocument; + _preEditDocument = null; + + if (preEditDocument != null) { + _controller.document = preEditDocument; + } + } +} + +class VoicesRichTextFocusNode extends FocusNode { + bool _disableFocus = true; + + // Can't request focus when disabled + @override + bool get canRequestFocus => !_disableFocus; + + @override + bool get hasFocus => !_disableFocus && (hasPrimaryFocus); + + void allowFocus() => _disableFocus = false; + void disableFocus() => _disableFocus = true; + + void toggleFocus({required bool enabled}) { + enabled ? allowFocus() : disableFocus(); + } } diff --git a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart index b3cc3e2948f..44c33435359 100644 --- a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart @@ -1,7 +1,3 @@ -import 'dart:async'; - -import 'package:catalyst_voices/widgets/buttons/voices_filled_button.dart'; -import 'package:catalyst_voices/widgets/buttons/voices_text_button.dart'; import 'package:catalyst_voices/widgets/rich_text/voices_rich_text_limit.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; @@ -11,10 +7,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_quill_extensions/flutter_quill_extensions.dart'; -typedef CanEditDocumentGetter = bool Function(Document document); - -bool _alwaysAllowEdit(Document document) => true; - final class VoicesRichTextController extends QuillController { VoicesRichTextController({ required super.document, @@ -22,287 +14,84 @@ final class VoicesRichTextController extends QuillController { }); } -final class VoicesRichTextEditModeController extends ValueNotifier { - //ignore: avoid_positional_boolean_parameters - VoicesRichTextEditModeController([super.value = false]); -} - -/// A component for rich text writing -/// using Quill under the hood -/// https://pub.dev/packages/flutter_quill -class VoicesRichText extends StatefulWidget { +class VoicesRichText extends StatelessWidget { + final VoicesRichTextController controller; + final bool enabled; final String title; - final VoicesRichTextController? controller; - final VoicesRichTextEditModeController? editModeController; - final FocusNode? focusNode; + final FocusNode focusNode; + final ScrollController scrollController; final int? charsLimit; - final CanEditDocumentGetter canEditDocumentGetter; - final VoidCallback? onEditBlocked; - final ValueChanged? onSaved; - const VoicesRichText({ super.key, - this.title = '', - this.controller, - this.editModeController, - this.focusNode, + required this.controller, + required this.enabled, + required this.title, + required this.focusNode, + required this.scrollController, this.charsLimit, - this.canEditDocumentGetter = _alwaysAllowEdit, - this.onEditBlocked, - this.onSaved, }); - @override - State createState() => _VoicesRichTextState(); -} - -class _VoicesRichTextState extends State { - VoicesRichTextController? _controller; - - VoicesRichTextController get _effectiveController { - return widget.controller ?? - (_controller ??= VoicesRichTextController( - document: Document(), - selection: const TextSelection.collapsed(offset: 0), - )); - } - - VoicesRichTextEditModeController? _editModeController; - - VoicesRichTextEditModeController get _effectiveEditModeController { - return widget.editModeController ?? - (_editModeController ??= VoicesRichTextEditModeController()); - } - - FocusNode? _focusNode; - - FocusNode get _effectiveFocusNode { - return widget.focusNode ?? - (_focusNode ??= FocusNode( - canRequestFocus: _effectiveEditModeController.value, - )); - } - - ScrollController? _scrollController; - - ScrollController get _effectiveScrollController { - return (_scrollController ??= ScrollController()); - } - - Document? _observedDocument; - StreamSubscription? _documentChangeSub; - - Document? _preEditDocument; - - @override - void initState() { - super.initState(); - - _effectiveController.addListener(_onControllerChanged); - _effectiveEditModeController.addListener(_onEditModeControllerChanged); - - _updateObservedDocument(); - } - - @override - void didUpdateWidget(covariant VoicesRichText oldWidget) { - super.didUpdateWidget(oldWidget); - - if (widget.controller == null && oldWidget.controller != null) { - _controller = VoicesRichTextController( - document: oldWidget.controller!.document, - selection: oldWidget.controller!.selection, - ); - } else if (widget.controller != null && oldWidget.controller == null) { - _controller?.removeListener(_onControllerChanged); - _controller?.dispose(); - _controller = null; - } - - if (widget.controller != oldWidget.controller) { - final old = oldWidget.controller ?? _controller; - final current = widget.controller ?? _controller; - - old?.removeListener(_onControllerChanged); - current?.addListener(_onControllerChanged); - - _updateObservedDocument(); - } - - if (widget.editModeController != oldWidget.editModeController) { - final old = oldWidget.editModeController ?? _editModeController; - final current = widget.editModeController ?? _editModeController; - - old?.removeListener(_onEditModeControllerChanged); - current?.addListener(_onEditModeControllerChanged); - } - - // if (widget.editModeController?.value != - // oldWidget.editModeController?.value) { - // _toggleEditMode(); - // } - } - - @override - void dispose() { - _controller?.dispose(); - _controller = null; - - _editModeController?.dispose(); - _editModeController = null; - - _focusNode?.dispose(); - _focusNode = null; - - _scrollController?.dispose(); - _scrollController = null; - - super.dispose(); - } - @override Widget build(BuildContext context) { - final charsLimit = widget.charsLimit; - return Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - // Padding( - // padding: const EdgeInsets.only(left: 24, top: 20, bottom: 20), - // child: _TopBar( - // title: widget.title, - // isEditMode: _effectiveEditModeController.value, - // onToggleEditMode: _toggleEditMode, - // ), - // ), Offstage( - offstage: !_effectiveEditModeController.value, - child: Padding( - padding: const EdgeInsets.only(bottom: 16), - child: _Toolbar(controller: _effectiveController), + offstage: !enabled, + child: _Toolbar( + controller: controller, ), ), - if (widget.title.isNotEmpty) ...[ - Text( - widget.title, - style: Theme.of(context).textTheme.titleSmall, - ), - const SizedBox(height: 8), - ], + _Title(title: title), _EditorDecoration( - isEditMode: _effectiveEditModeController.value, + isEditMode: enabled, + isInvalid: false, child: _Editor( - controller: _effectiveController, - focusNode: _effectiveFocusNode, - scrollController: _effectiveScrollController, + controller: controller, + focusNode: focusNode, + scrollController: scrollController, ), ), Offstage( offstage: charsLimit == null, child: VoicesRichTextLimit( - document: _effectiveController.document, + document: controller.document, charsLimit: charsLimit, ), ), - // const SizedBox(height: 16), - // Offstage( - // offstage: !_effectiveEditModeController.value, - // child: _Footer( - // onSave: _saveDocument, - // ), - // ), - // if (!_effectiveEditModeController.value) const SizedBox(height: 24), ], ); } +} - void _toggleEditMode() { - // if (!_effectiveEditModeController.value) { - // if (!widget.canEditDocumentGetter(_effectiveController.document)) { - // widget.onEditBlocked?.call(); - // return; - // } - // } - - if (_effectiveEditModeController.value) { - _stopEdit(); - } else { - _startEdit(); - } - } - - void _saveDocument() { - _preEditDocument = null; - _effectiveEditModeController.value = false; - - widget.onSaved?.call(_effectiveController.document); - } - - void _startEdit() { - final currentDocument = _effectiveController.document; - _preEditDocument = Document.fromDelta(currentDocument.toDelta()); - _effectiveEditModeController.value = true; - } - - void _stopEdit() { - final preEditDocument = _preEditDocument; - _preEditDocument = null; - _effectiveEditModeController.value = false; - - if (preEditDocument != null) { - _effectiveController.document = preEditDocument; - } - } - - void _onControllerChanged() { - if (_observedDocument != _effectiveController.document) { - _updateObservedDocument(); - } - } - - Future _onEditModeControllerChanged() async { - setState(() { - _effectiveFocusNode.canRequestFocus = _effectiveEditModeController.value; - }); - } - - void _onDocumentChanged(DocChange change) { - _enforceChatLimit(); - } - - void _updateObservedDocument() { - _observedDocument = _effectiveController.document; - unawaited(_documentChangeSub?.cancel()); - _documentChangeSub = _observedDocument?.changes.listen(_onDocumentChanged); - } - - void _enforceChatLimit() { - final charsLimit = widget.charsLimit; - if (charsLimit != null) { - _clipDocument(charsLimit); - } - } +class _Title extends StatelessWidget { + const _Title({ + required this.title, + }); - void _clipDocument(int limit) { - final documentLength = _effectiveController.document.length; - final latestIndex = limit - 1; + final String title; - _effectiveController.replaceText( - latestIndex, - documentLength - limit, - '', - TextSelection.collapsed(offset: latestIndex), + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 12), + child: Text( + title, + style: Theme.of(context).textTheme.titleSmall, + ), ); } } class _EditorDecoration extends StatelessWidget { final bool isEditMode; + final bool isInvalid; final Widget child; const _EditorDecoration({ required this.isEditMode, + required this.isInvalid, required this.child, }); @@ -310,27 +99,36 @@ class _EditorDecoration extends StatelessWidget { Widget build(BuildContext context) { return // TODO(jakub): enable after implementing https://github.com/input-output-hk/catalyst-voices/issues/846 - // child: ResizableBoxParent( + // ResizableBoxParent( // minHeight: 470, // resizableVertically: true, // resizableHorizontally: false, + // child: DecoratedBox( decoration: BoxDecoration( color: isEditMode ? Theme.of(context).colors.onSurfaceNeutralOpaqueLv1 : Theme.of(context).colors.elevationsOnSurfaceNeutralLv1White, border: Border.all( - color: Theme.of(context).colorScheme.outlineVariant, + color: _getBorderColor(context), + width: 2, ), borderRadius: BorderRadius.circular(8), ), - child: IgnorePointer( - ignoring: !isEditMode, - child: child, - ), + child: child, ); // ), } + + Color _getBorderColor(BuildContext context) { + if (!isEditMode) { + return Theme.of(context).colorScheme.outlineVariant; + } else { + return isInvalid + ? Theme.of(context).colorScheme.error + : Theme.of(context).colorScheme.outlineVariant; + } + } } class _Editor extends StatelessWidget { @@ -373,30 +171,6 @@ class _Editor extends StatelessWidget { } } -class _Footer extends StatelessWidget { - final VoidCallback? onSave; - - const _Footer({ - this.onSave, - }); - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, - ), - alignment: Alignment.centerRight, - color: Theme.of(context).colors.onSurfaceNeutralOpaqueLv1, - child: VoicesFilledButton( - onTap: onSave, - child: Text(context.l10n.saveButtonText.toUpperCase()), - ), - ); - } -} - class _Toolbar extends StatelessWidget { final QuillController controller; @@ -520,43 +294,6 @@ class _ToolbarIconButton extends StatelessWidget { } } -class _TopBar extends StatelessWidget { - final String title; - final bool isEditMode; - final VoidCallback? onToggleEditMode; - - const _TopBar({ - required this.title, - required this.isEditMode, - this.onToggleEditMode, - }); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - Expanded( - child: Text( - title, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - const SizedBox(width: 16), - VoicesTextButton( - onTap: onToggleEditMode, - child: Text( - isEditMode - ? context.l10n.cancelButtonText - : context.l10n.editButtonText, - style: Theme.of(context).textTheme.labelSmall, - ), - ), - const SizedBox(width: 16), - ], - ); - } -} - extension on QuillController { bool get isHeaderSelected { return getSelectionStyle().attributes.containsKey('header'); diff --git a/catalyst_voices/apps/voices/test/widgets/rich_text/voices_rich_text_test.dart b/catalyst_voices/apps/voices/test/widgets/rich_text/voices_rich_text_test.dart index 0465b94d3be..4bb376bf625 100644 --- a/catalyst_voices/apps/voices/test/widgets/rich_text/voices_rich_text_test.dart +++ b/catalyst_voices/apps/voices/test/widgets/rich_text/voices_rich_text_test.dart @@ -1,4 +1,5 @@ import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -8,7 +9,16 @@ void main() { group(VoicesRichText, () { testWidgets('renders correctly', (tester) async { // Given - const widget = VoicesRichText(); + final widget = VoicesRichText( + controller: VoicesRichTextController( + document: Document(), + selection: const TextSelection.collapsed(offset: 0), + ), + title: 'title', + enabled: true, + focusNode: FocusNode(), + scrollController: ScrollController(), + ); // When await tester.pumpApp(widget); diff --git a/catalyst_voices/utilities/uikit_example/lib/examples/voices_rich_text_example.dart b/catalyst_voices/utilities/uikit_example/lib/examples/voices_rich_text_example.dart index ab837a6dc35..4966e50f9bc 100644 --- a/catalyst_voices/utilities/uikit_example/lib/examples/voices_rich_text_example.dart +++ b/catalyst_voices/utilities/uikit_example/lib/examples/voices_rich_text_example.dart @@ -1,5 +1,3 @@ -import 'dart:developer'; - import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; import 'package:flutter/material.dart'; @@ -42,7 +40,9 @@ class _VoicesRichTextExampleState extends State { title: 'Rich text', controller: _controller, charsLimit: 800, - onSaved: (document) => log('Saved document: $document'), + enabled: true, + focusNode: FocusNode(), + scrollController: ScrollController(), ), ), ); From 54d70b1b76496144aa1e16a5b724d75ef412a746 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 17 Jan 2025 09:37:06 +0100 Subject: [PATCH 3/9] feat: adding validation to VoicesRichText --- .../multiline_text_entry_markdown_widget.dart | 28 ++---- .../widgets/rich_text/voices_rich_text.dart | 91 ++++++++++--------- .../rich_text/voices_rich_text_limit.dart | 5 +- .../tiles/document_builder_section_tile.dart | 2 +- 4 files changed, 63 insertions(+), 63 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart index 431a667a14e..c1a3eb4868e 100644 --- a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart @@ -3,9 +3,7 @@ import 'dart:async'; import 'package:catalyst_voices/common/codecs/markdown_codec.dart'; import 'package:catalyst_voices/common/ext/document_property_ext.dart'; import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart'; -import 'package:catalyst_voices/widgets/text_field/voices_text_field.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; -import 'package:catalyst_voices_view_models/catalyst_voices_view_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' as quill; import 'package:flutter_quill/flutter_quill.dart'; @@ -89,17 +87,13 @@ class _MultilineTextEntryMarkdownWidgetState focusNode: _focus, scrollController: _scrollController, charsLimit: _maxLength, - ); - } + validator: (val) { + return null; - VoicesTextFieldValidationResult _validate(String? value) { - final result = widget.property.schema.validatePropertyValue(value); - if (result.isValid) { - return const VoicesTextFieldValidationResult.success(); - } else { - final localized = LocalizedDocumentValidationResult.from(result); - return VoicesTextFieldValidationResult.error(localized.message(context)); - } + // TODO(LynxxLynx): implement validator when we got answer how to validate + // formatted document against maxLength + }, + ); } void _handleInitialValue() { @@ -131,7 +125,6 @@ class _MultilineTextEntryMarkdownWidgetState } void _onDocumentChanged(DocChange change) { - _validate(_convertToString()); if (change.change.last.data != '\n') { _notifyChangeListener(); } @@ -148,12 +141,6 @@ class _MultilineTextEntryMarkdownWidgetState ); } - String _convertToString() { - final delta = _controller.document.toDelta(); - final markdownData = _encoder.convert(delta); - return markdownData.data; - } - void _toggleEditMode() { _controller.readOnly = !widget.isEditMode; if (widget.isEditMode) { @@ -178,6 +165,9 @@ class _MultilineTextEntryMarkdownWidgetState } } +/// This focus helps to interact with [VoicesRichText] widget +/// When widget is not in edit mode this focus allows user to interact with +/// links and other elements that are inside of the textfield.. class VoicesRichTextFocusNode extends FocusNode { bool _disableFocus = true; diff --git a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart index 44c33435359..9d12af1ea15 100644 --- a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart @@ -14,55 +14,58 @@ final class VoicesRichTextController extends QuillController { }); } -class VoicesRichText extends StatelessWidget { +class VoicesRichText extends FormField { final VoicesRichTextController controller; - final bool enabled; final String title; final FocusNode focusNode; final ScrollController scrollController; final int? charsLimit; - const VoicesRichText({ + + VoicesRichText({ super.key, + super.enabled, + super.autovalidateMode = AutovalidateMode.always, required this.controller, - required this.enabled, required this.title, required this.focusNode, required this.scrollController, this.charsLimit, - }); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Offstage( - offstage: !enabled, - child: _Toolbar( - controller: controller, - ), - ), - _Title(title: title), - _EditorDecoration( - isEditMode: enabled, - isInvalid: false, - child: _Editor( - controller: controller, - focusNode: focusNode, - scrollController: scrollController, - ), - ), - Offstage( - offstage: charsLimit == null, - child: VoicesRichTextLimit( - document: controller.document, - charsLimit: charsLimit, - ), - ), - ], - ); - } + super.validator, + }) : super( + builder: (field) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Offstage( + offstage: !enabled, + child: _Toolbar( + controller: controller, + ), + ), + _Title(title: title), + _EditorDecoration( + isEditMode: enabled, + isInvalid: field.hasError, + focusNode: focusNode, + child: _Editor( + controller: controller, + focusNode: focusNode, + scrollController: scrollController, + ), + ), + Offstage( + offstage: charsLimit == null, + child: VoicesRichTextLimit( + document: controller.document, + charsLimit: charsLimit, + errorMessage: field.errorText, + ), + ), + ], + ); + }, + ); } class _Title extends StatelessWidget { @@ -87,11 +90,13 @@ class _Title extends StatelessWidget { class _EditorDecoration extends StatelessWidget { final bool isEditMode; final bool isInvalid; + final FocusNode focusNode; final Widget child; const _EditorDecoration({ required this.isEditMode, required this.isInvalid, + required this.focusNode, required this.child, }); @@ -124,9 +129,13 @@ class _EditorDecoration extends StatelessWidget { if (!isEditMode) { return Theme.of(context).colorScheme.outlineVariant; } else { - return isInvalid - ? Theme.of(context).colorScheme.error - : Theme.of(context).colorScheme.outlineVariant; + if (isInvalid) { + return Theme.of(context).colorScheme.error; + } + if (focusNode.hasFocus) { + return Theme.of(context).colorScheme.primary; + } + return Theme.of(context).colorScheme.outlineVariant; } } } diff --git a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text_limit.dart b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text_limit.dart index 5153c124815..304057cab58 100644 --- a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text_limit.dart +++ b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text_limit.dart @@ -1,17 +1,18 @@ import 'dart:async'; -import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; class VoicesRichTextLimit extends StatefulWidget { final Document document; final int? charsLimit; + final String? errorMessage; const VoicesRichTextLimit({ super.key, required this.document, this.charsLimit, + this.errorMessage, }); @override @@ -52,7 +53,7 @@ class _VoicesRichTextLimitState extends State { children: [ Expanded( child: Text( - context.l10n.supportingTextLabelText, + widget.errorMessage ?? '', style: Theme.of(context).textTheme.bodySmall, ), ), diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart index d2df7ec129c..0ee13f19d83 100644 --- a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart @@ -1,7 +1,7 @@ import 'package:catalyst_voices/widgets/document_builder/agreement_confirmation_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/document_token_value_widget.dart'; -import 'package:catalyst_voices/widgets/document_builder/simple_text_entry_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/multiline_text_entry_markdown_widget.dart'; +import 'package:catalyst_voices/widgets/document_builder/simple_text_entry_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_dropdown_selection_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_grouped_tag_selector_widget.dart'; import 'package:catalyst_voices/widgets/document_builder/single_line_https_url_widget.dart.dart'; From 681157ca82594117456208fce6955ff4ac4443b4 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 17 Jan 2025 09:44:59 +0100 Subject: [PATCH 4/9] fix: format --- .../multiline_text_entry_markdown_widget.dart | 4 ++-- .../lib/widgets/tiles/document_builder_section_tile.dart | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart index c1a3eb4868e..4a7d3a6dffa 100644 --- a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart @@ -90,8 +90,8 @@ class _MultilineTextEntryMarkdownWidgetState validator: (val) { return null; - // TODO(LynxxLynx): implement validator when we got answer how to validate - // formatted document against maxLength + // TODO(LynxxLynx): implement validator when we got answer how to + // validate formatted document against maxLength }, ); } diff --git a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart index 8fcc3541ba6..2f78ac58bc2 100644 --- a/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart +++ b/catalyst_voices/apps/voices/lib/widgets/tiles/document_builder_section_tile.dart @@ -1,4 +1,3 @@ - import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; @@ -276,22 +275,24 @@ class _PropertyBuilder extends StatelessWidget { onChanged: onChanged, );*/ case YesNoChoiceDefinition(): - /*final castProperty = definition.castProperty(property); + /*final castProperty = definition.castProperty(property); return YesNoChoiceWidget( property: castProperty, onChanged: onChanged, isEditMode: isEditMode, isRequired: castProperty.schema.isRequired, - ); + );*/ + case MultiLineTextEntryMarkdownDefinition(): + /*final castProperty = definition.castProperty(property); final castProperty = definition.castProperty(property); return MultilineTextEntryMarkdownWidget( property: castProperty, onChanged: onChanged, isEditMode: isEditMode, isRequired: castProperty.schema.isRequired, - ); );*/ + // TODO(dtscalac): uncomment tiles when casting works. return Text('${definition.runtimeType} casting problem'); } From b14844818e38dcd8152c20c4a8d1f2374416f319 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 17 Jan 2025 10:17:03 +0100 Subject: [PATCH 5/9] fix: remove unsessacary markdowncodec objects --- .../multiline_text_entry_markdown_widget.dart | 28 +++++++++---------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart index 4a7d3a6dffa..c7fc455eed6 100644 --- a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart @@ -6,7 +6,6 @@ import 'package:catalyst_voices/widgets/rich_text/voices_rich_text.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart' as quill; -import 'package:flutter_quill/flutter_quill.dart'; class MultilineTextEntryMarkdownWidget extends StatefulWidget { final DocumentProperty property; @@ -33,11 +32,8 @@ class _MultilineTextEntryMarkdownWidgetState late final VoicesRichTextFocusNode _focus; late final ScrollController _scrollController; - final MarkdownEncoder _encoder = const MarkdownEncoder(); - final MarkdownDecoder _decoder = const MarkdownDecoder(); - quill.Document? _observedDocument; - StreamSubscription? _documentChangeSub; + StreamSubscription? _documentChangeSub; quill.Document? _preEditDocument; String get _description => widget.property.formattedDescription; @@ -48,7 +44,7 @@ class _MultilineTextEntryMarkdownWidgetState void initState() { super.initState(); - _handleInitialValue(); + _buildController(value: _value); _controller.addListener(_onControllerChanged); _focus = VoicesRichTextFocusNode(); @@ -66,7 +62,7 @@ class _MultilineTextEntryMarkdownWidgetState } if (widget.property.value != oldWidget.property.value) { - _handleInitialValue(); + _buildController(value: _value); } } @@ -96,16 +92,18 @@ class _MultilineTextEntryMarkdownWidgetState ); } - void _handleInitialValue() { - if (_value != null) { - final input = MarkdownData(_value!); - final delta = _decoder.convert(input); - _controller = VoicesRichTextController( + VoicesRichTextController _buildController({ + String? value, + }) { + if (value != null) { + final input = MarkdownData(value); + final delta = markdown.encoder.convert(input); + return VoicesRichTextController( document: quill.Document.fromDelta(delta), selection: const TextSelection.collapsed(offset: 0), ); } else { - _controller = VoicesRichTextController( + return VoicesRichTextController( document: quill.Document(), selection: const TextSelection.collapsed(offset: 0), ); @@ -124,7 +122,7 @@ class _MultilineTextEntryMarkdownWidgetState _documentChangeSub = _observedDocument?.changes.listen(_onDocumentChanged); } - void _onDocumentChanged(DocChange change) { + void _onDocumentChanged(quill.DocChange change) { if (change.change.last.data != '\n') { _notifyChangeListener(); } @@ -132,7 +130,7 @@ class _MultilineTextEntryMarkdownWidgetState void _notifyChangeListener() { final delta = _controller.document.toDelta(); - final markdownData = _encoder.convert(delta); + final markdownData = markdown.decoder.convert(delta); widget.onChanged( DocumentChange( nodeId: widget.property.schema.nodeId, From dd2d1a414807b5c8acc3ebe8d640bcd08b59e284 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 17 Jan 2025 14:12:16 +0100 Subject: [PATCH 6/9] fix: controller assigning in initState --- .../multiline_text_entry_markdown_widget.dart | 4 ++-- .../apps/voices/lib/widgets/rich_text/voices_rich_text.dart | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart index c7fc455eed6..748fcafaab8 100644 --- a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart @@ -44,7 +44,7 @@ class _MultilineTextEntryMarkdownWidgetState void initState() { super.initState(); - _buildController(value: _value); + _controller = _buildController(value: _value); _controller.addListener(_onControllerChanged); _focus = VoicesRichTextFocusNode(); @@ -62,7 +62,7 @@ class _MultilineTextEntryMarkdownWidgetState } if (widget.property.value != oldWidget.property.value) { - _buildController(value: _value); + _controller = _buildController(value: _value); } } diff --git a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart index 9d12af1ea15..673667abab6 100644 --- a/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart +++ b/catalyst_voices/apps/voices/lib/widgets/rich_text/voices_rich_text.dart @@ -69,12 +69,12 @@ class VoicesRichText extends FormField { } class _Title extends StatelessWidget { + final String title; + const _Title({ required this.title, }); - final String title; - @override Widget build(BuildContext context) { return Padding( From 0efffb372c0f54f43f3357eeb251bad3e15f323a Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 17 Jan 2025 14:23:13 +0100 Subject: [PATCH 7/9] fix: deleting final keyword from text controller --- .../multiline_text_entry_markdown_widget.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart index 748fcafaab8..3695bd57d4e 100644 --- a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart @@ -28,7 +28,7 @@ class MultilineTextEntryMarkdownWidget extends StatefulWidget { class _MultilineTextEntryMarkdownWidgetState extends State { - late final VoicesRichTextController _controller; + late VoicesRichTextController _controller; late final VoicesRichTextFocusNode _focus; late final ScrollController _scrollController; @@ -44,7 +44,7 @@ class _MultilineTextEntryMarkdownWidgetState void initState() { super.initState(); - _controller = _buildController(value: _value); + _controller = _buildController(value: _value); _controller.addListener(_onControllerChanged); _focus = VoicesRichTextFocusNode(); From bd4fd7f385897c6d552f6a047b0d90e4d388a41d Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 17 Jan 2025 14:35:24 +0100 Subject: [PATCH 8/9] fix: adding remove/add listener when didUpdateWidget was called --- .../document_builder/multiline_text_entry_markdown_widget.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart index 3695bd57d4e..92585dfbe64 100644 --- a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart @@ -62,7 +62,9 @@ class _MultilineTextEntryMarkdownWidgetState } if (widget.property.value != oldWidget.property.value) { + _controller.removeListener(_onControllerChanged); _controller = _buildController(value: _value); + _controller.addListener(_onControllerChanged); } } From 99d03a7bd54d7abcd2c097a5522edf7ac0c409f6 Mon Sep 17 00:00:00 2001 From: Ryszard Schossler Date: Fri, 17 Jan 2025 14:59:00 +0100 Subject: [PATCH 9/9] fix: dispose controller --- .../document_builder/multiline_text_entry_markdown_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart index 92585dfbe64..e8532173c66 100644 --- a/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart +++ b/catalyst_voices/apps/voices/lib/widgets/document_builder/multiline_text_entry_markdown_widget.dart @@ -62,7 +62,7 @@ class _MultilineTextEntryMarkdownWidgetState } if (widget.property.value != oldWidget.property.value) { - _controller.removeListener(_onControllerChanged); + _controller.dispose(); _controller = _buildController(value: _value); _controller.addListener(_onControllerChanged); }