diff --git a/packages/fleather/lib/src/widgets/controller.dart b/packages/fleather/lib/src/widgets/controller.dart index 67f649a3..eec341d8 100644 --- a/packages/fleather/lib/src/widgets/controller.dart +++ b/packages/fleather/lib/src/widgets/controller.dart @@ -9,7 +9,9 @@ import '../../util.dart'; import 'autoformats.dart'; import 'history.dart'; -/// List of style keys which can be toggled for insertion +/// @docImport 'checkbox.dart'; + +// List of style keys which can be toggled for insertion List _toggleableStyleKeys = [ ParchmentAttribute.bold.key, ParchmentAttribute.italic.key, @@ -34,7 +36,7 @@ class FleatherController extends ChangeNotifier { ParchmentDocument _document; - /// Doument managed by this controller. + /// Document managed by this controller. ParchmentDocument get document => _document; // A list of changes applied to this doc. The changes could be undone or redone. @@ -206,7 +208,16 @@ class FleatherController extends ChangeNotifier { return true; } - void formatText(int index, int length, ParchmentAttribute attribute) { + /// Update format of [length] characters in the document starting at [index] + /// with the provided [attribute]. + /// + /// If [notify] is `true`, the controller will notify widgets to update + /// accordingly; otherwise widgets will not update, this is useful when + /// we do not want a widget to update the document without triggering a + /// rebuild if the editor (e.g.: [FleatherCheckbox] toggling should not cause + /// scrolling to cursor). + void formatText(int index, int length, ParchmentAttribute attribute, + {bool notify = true}) { final change = document.format(index, length, attribute); // _lastChangeSource = ChangeSource.local; const source = ChangeSource.local; @@ -227,7 +238,7 @@ class FleatherController extends ChangeNotifier { _updateSelectionSilent(adjustedSelection, source: source); } _updateHistory(); - notifyListeners(); + if (notify) notifyListeners(); } /// Formats current selection with [attribute]. diff --git a/packages/fleather/lib/src/widgets/editable_text_block.dart b/packages/fleather/lib/src/widgets/editable_text_block.dart index d4d4ec86..348148cb 100644 --- a/packages/fleather/lib/src/widgets/editable_text_block.dart +++ b/packages/fleather/lib/src/widgets/editable_text_block.dart @@ -256,7 +256,7 @@ class EditableTextBlock extends StatelessWidget { void _toggle(LineNode node, bool checked) { final attr = checked ? ParchmentAttribute.checked : ParchmentAttribute.checked.unset; - controller.formatText(node.documentOffset, 0, attr); + controller.formatText(node.documentOffset, 0, attr, notify: false); } } @@ -347,7 +347,7 @@ class _BulletPoint extends StatelessWidget { } } -class _CheckboxPoint extends StatelessWidget { +class _CheckboxPoint extends StatefulWidget { const _CheckboxPoint({ required this.value, required this.enabled, @@ -358,6 +358,21 @@ class _CheckboxPoint extends StatelessWidget { final bool enabled; final ValueChanged onChanged; + @override + State<_CheckboxPoint> createState() => _CheckboxPointState(); +} + +class _CheckboxPointState extends State<_CheckboxPoint> { + late bool value = widget.value; + + @override + void didUpdateWidget(covariant _CheckboxPoint oldWidget) { + super.didUpdateWidget(oldWidget); + if (value != widget.value) { + setState(() => value = widget.value); + } + } + @override Widget build(BuildContext context) { return Container( @@ -365,7 +380,12 @@ class _CheckboxPoint extends StatelessWidget { padding: const EdgeInsetsDirectional.only(top: 2.0, end: 12.0), child: FleatherCheckbox( value: value, - onChanged: enabled ? (_) => onChanged(!value) : null, + onChanged: widget.enabled + ? (_) { + widget.onChanged(!value); + setState(() => value = !value); + } + : null, ), ); } diff --git a/packages/fleather/test/widgets/editable_text_test.dart b/packages/fleather/test/widgets/editable_text_test.dart index c80bc67a..e20a9667 100644 --- a/packages/fleather/test/widgets/editable_text_test.dart +++ b/packages/fleather/test/widgets/editable_text_test.dart @@ -95,6 +95,29 @@ void main() { Operation.insert('\n', {'block': 'cl', 'checked': true})); }); + testWidgets('check list toggle', (tester) async { + const textPrecedingCheckBox = 'some text\n'; + final delta = Delta() + ..insert(textPrecedingCheckBox) + ..insert('an item') + ..insert('\n', {'block': 'cl'}); + final editor = EditorSandBox( + tester: tester, document: ParchmentDocument.fromDelta(delta)); + await editor.pump(); + expect(find.byType(FleatherCheckbox), findsOneWidget); + await editor.updateSelection(base: 0, extent: 0); + + editor.controller.addListener(() { + fail('Controller should not notify when checkbox is toggled'); + }); + await tester.tap(find.byType(FleatherCheckbox)); + await tester.pumpAndSettle(throttleDuration); + expect(editor.document.toDelta().last, + Operation.insert('\n', {'block': 'cl', 'checked': true})); + expect(editor.controller.selection, + const TextSelection.collapsed(offset: 0)); + }); + testWidgets('bullet list', (tester) async { final delta = Delta() ..insert('an item')