singerdmx / flutter-quill

Rich text editor for Flutter
https://pub.dev/packages/flutter_quill
MIT License
2.58k stars 832 forks source link

QuillEditor freezes when adding content or giving a spacebar #1837

Closed softwaredementor closed 2 months ago

softwaredementor commented 6 months ago

Is there an existing issue for this?

Flutter Quill version

9.3.6, 9.3.7, 9.3.8, 9.3.9

No response

Steps to reproduce

Tested on Edge, Chrome and Flutter WebApp version of the app

Step 1: Reference below code to create a form with single toolbar and multiple QuillEditor widgets.

Step 2: When I click on "+Add more details" button the first row is added. I click on the quilleditor, get the focus and I can type some content in it. This is ok

Step 3: When I click on "+Add more details" again to add second row. I click on the second added QuillEditor, get the focus and can see the same content of first quilleditor (because first and second quill editors share the same quillcontroller). I can see the print command of print("add more pressed"); This is also ok

Step 4: But when I type a space button in the second QuillEditor, my whole application freezes. I cannot type in the second Quilleditor at all nor interact with any of my widget.

I checked in ver 9.3.6, 09.3.7, 9.3.8 all seem to have same issues. Some isse related to Quilleditor focus or edit exists and user is unable to type

Expected results

  1. I should be able to type in the QuillEditor text when the focus is on without any issues.
  2. I should be able to interact with my web app widgets

Actual results

  1. Not able to any content in the second QuillEditor. Rarely I am able to type in second QuillEditor and add widgets (reproducible steps not identified for successful steps yet)
  2. I am not able to interact with my web app widgets as the whole application freezes with no errors or exceptions

Code sample

Code sample ```dart import 'package:admin/models/Resume.dart'; import 'package:flutter/material.dart'; import 'package:flutter_quill/flutter_quill.dart'; import 'package:provider/provider.dart'; class DynamicAdditionWidget extends StatefulWidget { const DynamicAdditionWidget({super.key}); @override State createState() => _DynamicAdditionWidgetState(); } class _DynamicAdditionWidgetState extends State { String sectionName = "Information"; List widgets = []; List editorHeader = []; final List _quillControllers = [QuillController.basic()]; ScrollController _scrollController = ScrollController(); @override void initState() { super.initState(); // TODO: implement initState } @override void didChangeDependencies() { // TODO: implement didChangeDependencies sectionName = Provider.of(context).ActiveSection; editorHeader = []; editorHeader.add( HeaderWidget( quillControllers: _quillControllers, sectionName: sectionName, ), ); super.didChangeDependencies(); } @override Widget build(BuildContext context) { return Expanded( flex: 1, child: Container( decoration: BoxDecoration( border: Border.all( color: Color.fromARGB(255, 60, 67, 72), // Border color width: 1.0, // Border width ), ), child: Container( child: Column( mainAxisAlignment: MainAxisAlignment.start, children: [ ...editorHeader, Flexible( child: Scrollbar( thumbVisibility: true, controller: _scrollController, child: ReorderableListView.builder( onReorder: (oldIndex, newIndex) { setState(() { if (newIndex > oldIndex) { newIndex = newIndex - 1; } final item = widgets.removeAt(oldIndex); widgets.insert(newIndex, item); }); }, scrollController: _scrollController, itemCount: widgets.length, itemBuilder: (context, index) { return Column( key: ValueKey(widgets[ index]), // Use a ValueKey to ensure correct reordering children: [ Container( padding: EdgeInsets.only( left: 12, top: 12, ), child: Row( children: [ // Your content here widgets[index], Padding( padding: const EdgeInsets.only(top: 24.0), child: Column( mainAxisAlignment: MainAxisAlignment.end, children: [ IconButton( icon: Icon( Icons.close_sharp, size: 15, color: const Color.fromARGB( 255, 214, 95, 107), ), onPressed: () { setState(() { widgets.removeAt(index); }); }, ), ], ), ), ], ), ), ], ); }, ), ), ), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded( flex: 11, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ ElevatedButton( child: Text( "+ Add more details", style: TextStyle( color: Theme.of(context).primaryColor, ), ), onPressed: () { print("add more pressed"); setState(() { widgets.add( Expanded( child: Column( children: [ Row( children: [ Expanded( child: Container( decoration: BoxDecoration( border: Border.all( color: Colors.grey[300]!), borderRadius: BorderRadius.circular(2.0), ), child: QuillEditor.basic( configurations: QuillEditorConfigurations( placeholder: "Compose your text here ...", controller: _quillControllers[0], // autoFocus: true, readOnly: false, showCursor: true, scrollable: true, padding: EdgeInsets.symmetric( vertical: 10.0, horizontal: 8.0, ), sharedConfigurations: const QuillSharedConfigurations( locale: Locale('en'), ), ), ), ), ), ], ), ], ), ), ); }); }, ), ], ), ), Expanded( flex: 8, child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ ElevatedButton( onPressed: () { // Add save functionality }, child: Text("Save"), ), SizedBox(width: 8.0), OutlinedButton( onPressed: () { // Add cancel functionality _quillControllers[0].clear(); }, child: Text("Cancel"), ), ], ), ), ], ) ], ), ), ), ); } } class HeaderWidget extends StatelessWidget { const HeaderWidget({ super.key, required List quillControllers, required this.sectionName, }) : _quillControllers = quillControllers; final List _quillControllers; final String sectionName; @override Widget build(BuildContext context) { return Column( children: [ Container( decoration: BoxDecoration( border: Border( top: BorderSide( color: Colors.grey[300]!, width: 1.0, ), ), ), child: QuillToolbar.simple( configurations: QuillSimpleToolbarConfigurations( // fontFamilyValues: , controller: _quillControllers[0], showSmallButton: true, showCodeBlock: false, showSearchButton: false, showQuote: true, showAlignmentButtons: true, showHeaderStyle: true, toolbarSectionSpacing: .5, toolbarSize: .5, sharedConfigurations: const QuillSharedConfigurations( locale: Locale('en'), ), ), ), ), Container( width: double.maxFinite, padding: EdgeInsets.all(4), color: Colors.amber, child: Text( sectionName, style: TextStyle( fontSize: 18, fontWeight: FontWeight.bold, ), ), ), ], ); } }```

Screenshots or Video

editor_stuck_infinitely

Logs

Launching lib\main.dart on Edge in debug mode... This app is linked to the debug service: ws://127.0.0.1:58130/U-cSlB8U0sA=/ws Debug service listening on ws://127.0.0.1:58130/U-cSlB8U0sA=/ws Connecting to VM Service at ws://127.0.0.1:58130/U-cSlB8U0sA=/ws 2 add more pressed

Logs ```console [Paste your logs here] ```
softwaredementor commented 6 months ago

editor_stuck_infinitely

Observe that in the second editor after the cursor nothing is typed or shown on screen. The whole application froze

kairan77 commented 6 months ago

It should not freeze.

Using _quillControllers to manage a 'list' of quill controllers is problematic, especially when you are only ever using the first element in the list. Calling widgets.add() and set state is pure evil, of course it won't work. In Flutter, manage values, don't manage widgets. Widgets should be managed by flutter's rendering engine 98% of the time, the other 2% are extremely hard to justify even at the expert level.

Encapsulate value of content of each section in something like a Section value object like Section{int index, String content, etc}, or just String if you only ever care about the content.

Then declares a PropertyValueNotifier<List

> sections in your top level widget state, to hold the list of section values.

Design a widget SectionEditor which takes in a section and internally wrap a quill editor inside.

Let SectionEditor manage its own scroll controller, quillController, and focusNode and configuration (ie. declare those objects in its State as private fields).

What you want to have is probably a ValueNotifier<QuillController?> which sends notification to your HeaderWidget to rebuild itself upon active section change. In the parent widget which holds the listViewBuilder which in turn holds the list of SectionEditors, have a ValueNotifier<QuillController?> activeController, Modify SectionEditor to also takes in a callback function which update the active quill controller to the controller of the active SectionEditor,

Pass the activeController to your HeaderWidget

lastly, you may want to pass a Key to the ToolBar derived from the content or index of your activeQuillController, if you are finding the state of the ToolBar is not updating when activeSection is changed.

Of course you should hook active section change call back with some sort of gesture detection or button click in your SectionEditor.

Irrelevant to the problem at hand but may be an improvement to your code anyway:

Ditch Provider if possible especially when the object you are passing is not that deep in the stack, why not pass ActiveSection directly from your calling widget to the current widget?

Favor ValueNotifier over setState

PropertyValueNotifier, is just ValueNotifier with its notifylistener method exposed, if you are not familiar with its purpose or usage just google it.

AtlasAutocode commented 2 months ago

because first and second quill editors share the same quillcontroller

Each editor must have its own controller. Similarly, there should be one toolbar for each editor/controller - set toolbar visibility based on which editor has the focus.

Can we close this issue?

softwaredementor commented 1 month ago

I have devised alternate solution. you can close this issue