From 858d61a7a331298ea72e717dc870eb889adc1101 Mon Sep 17 00:00:00 2001 From: AtlasAutocode <165201146+AtlasAutocode@users.noreply.github.com> Date: Sun, 30 Jun 2024 15:31:54 -0600 Subject: [PATCH] Fix style settings (#1962) * Value setting Stateful toolbar buttons derive from base class * Rename base class as QuillToolbarBaseValueButton * Removed deprecated functions * Move clipboard actions to QuillController * Fix: collectAllIndividualStylesAndEmbed for result span * Add: Clipboard toolbar buttons * Add: test for QuillController clipboard Dart Formatted * Translation Justify * Translation alignJustify * Fix: Translation en-US * Fix style settings --------- Co-authored-by: Douglas Ward --- .../lib/screens/quill/my_quill_editor.dart | 19 ++--- lib/src/models/documents/document.dart | 34 ++++++-- .../widgets/raw_editor/raw_editor_state.dart | 8 ++ test/utils/document_test.dart | 85 +++++++++++++++++++ 4 files changed, 127 insertions(+), 19 deletions(-) create mode 100644 test/utils/document_test.dart diff --git a/example/lib/screens/quill/my_quill_editor.dart b/example/lib/screens/quill/my_quill_editor.dart index 254057d7..d50f75d8 100644 --- a/example/lib/screens/quill/my_quill_editor.dart +++ b/example/lib/screens/quill/my_quill_editor.dart @@ -30,6 +30,7 @@ class MyQuillEditor extends StatelessWidget { @override Widget build(BuildContext context) { + final defaultTextStyle = DefaultTextStyle.of(context); return QuillEditor( scrollController: scrollController, focusNode: focusNode, @@ -43,27 +44,21 @@ class MyQuillEditor extends StatelessWidget { useTextColorForDot: true, ), ), - customStyles: const DefaultStyles( + customStyles: DefaultStyles( h1: DefaultTextBlockStyle( - TextStyle( + defaultTextStyle.style.copyWith( fontSize: 32, height: 1.15, fontWeight: FontWeight.w300, ), - VerticalSpacing(16, 0), - VerticalSpacing(0, 0), + const VerticalSpacing(16, 0), + const VerticalSpacing(0, 0), null, ), - sizeSmall: TextStyle(fontSize: 9), - subscript: TextStyle( - fontFeatures: [FontFeature.subscripts()], - ), - superscript: TextStyle( - fontFeatures: [FontFeature.superscripts()], - ), + sizeSmall: defaultTextStyle.style.copyWith(fontSize: 9), ), scrollable: true, - placeholder: 'Start writting your notes...', + placeholder: 'Start writing your notes...', padding: const EdgeInsets.all(16), onImagePaste: (imageBytes) async { if (isWeb()) { diff --git a/lib/src/models/documents/document.dart b/lib/src/models/documents/document.dart index 99a72e7a..b46e6707 100644 --- a/lib/src/models/documents/document.dart +++ b/lib/src/models/documents/document.dart @@ -177,26 +177,46 @@ class Document { /// Only attributes applied to all characters within this range are /// included in the result. + /// Special case of no-selection at start of empty line: gets inline style(s) from preceding non-empty line. Style collectStyle(int index, int len) { - final res = queryChild(index); - Style rangeStyle; + var res = queryChild(index); if (len > 0) { return (res.node as Line).collectStyle(res.offset, len); } + // if (res.offset == 0) { - return rangeStyle = (res.node as Line).collectStyle(res.offset, len); + final current = (res.node as Line).collectStyle(0, 0); + // + while ((res.node as Line).length == 1 && index > 0) { + res = queryChild(--index); + } + // + final style = (res.node as Line).collectStyle(res.offset, 0); + final remove = {}; + for (final attr in style.attributes.values) { + if (!Attribute.inlineKeys.contains(attr.key)) { + if (!current.containsKey(attr.key)) { + remove.add(attr); + } + } + } + if (remove.isNotEmpty) { + return style.removeAll(remove); + } + return style; } - rangeStyle = (res.node as Line).collectStyle(res.offset - 1, len); - final linkAttribute = rangeStyle.attributes[Attribute.link.key]; + // + final style = (res.node as Line).collectStyle(res.offset - 1, 0); + final linkAttribute = style.attributes[Attribute.link.key]; if ((linkAttribute != null) && (linkAttribute.value != (res.node as Line) .collectStyle(res.offset, len) .attributes[Attribute.link.key] ?.value)) { - return rangeStyle.removeAll({linkAttribute}); + return style.removeAll({linkAttribute}); } - return rangeStyle; + return style; } /// Returns all styles and Embed for each node within selection diff --git a/lib/src/widgets/raw_editor/raw_editor_state.dart b/lib/src/widgets/raw_editor/raw_editor_state.dart index 1f2b7ff4..ccd91fb5 100644 --- a/lib/src/widgets/raw_editor/raw_editor_state.dart +++ b/lib/src/widgets/raw_editor/raw_editor_state.dart @@ -875,6 +875,14 @@ class QuillRawEditorState extends EditorState ..formatSelection(attribute) // Remove the added newline. ..replaceText(controller.selection.baseOffset + 1, 1, '', null); + // + final style = + controller.document.collectStyle(controller.selection.baseOffset, 0); + if (style.isNotEmpty) { + for (final attr in style.values) { + controller.formatSelection(attr); + } + } } void _handleSelectionChanged( diff --git a/test/utils/document_test.dart b/test/utils/document_test.dart new file mode 100644 index 00000000..c672e74d --- /dev/null +++ b/test/utils/document_test.dart @@ -0,0 +1,85 @@ +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:flutter_quill/quill_delta.dart'; +import 'package:test/test.dart'; + +void main() { + group('collectStyle', () { + /// Enter key inserts newline as plain text without inline styles. + /// collectStyle needs to retrieve style of preceding line + test('Simulate double enter key at end', () { + final delta = Delta() + ..insert('data\n') + ..insert('second\n', {'bold': true}) + ..insert('\n\nplain\n'); + final document = Document.fromDelta(delta); + // + expect(document.getPlainText(0, document.length), + 'data\nsecond\n\n\nplain\n'); + expect(document.length, 20); + // + expect('data\n', document.getPlainText(0, 5)); + for (var index = 0; index < 5; index++) { + expect(const Style(), document.collectStyle(index, 0)); + } + // + expect('second\n', document.getPlainText(5, 7)); + for (var index = 5; index < 12; index++) { + expect(const Style.attr({'bold': Attribute.bold}), + document.collectStyle(index, 0)); + } + // + expect('\n\n', document.getPlainText(12, 2)); + for (var index = 12; index < 14; index++) { + expect(const Style.attr({'bold': Attribute.bold}), + document.collectStyle(index, 0)); + } + // + for (var index = 14; index < document.length; index++) { + expect(const Style(), document.collectStyle(index, 0)); + } + }); + + test('No selection', () { + final delta = Delta() + ..insert('plain\n') + ..insert('bold\n', {'bold': true}) + ..insert('italic\n', {'italic': true}); + final document = Document.fromDelta(delta); + // + expect( + document.getPlainText(0, document.length), 'plain\nbold\nitalic\n'); + expect(document.length, 18); + // + for (var index = 0; index < 6; index++) { + expect(const Style(), document.collectStyle(index, 0)); + } + // + for (var index = 6; index < 11; index++) { + expect(const Style.attr({'bold': Attribute.bold}), + document.collectStyle(index, 0)); + } + // + for (var index = 11; index < document.length; index++) { + expect(const Style.attr({'italic': Attribute.italic}), + document.collectStyle(index, 0)); + } + }); + + test('Selection', () { + final delta = Delta() + ..insert('data\n') + ..insert('second\n', {'bold': true}); + final document = Document.fromDelta(delta); + // + expect(const Style(), document.collectStyle(0, 4)); + expect(const Style(), document.collectStyle(1, 3)); + // + expect(const Style.attr({'bold': Attribute.bold}), + document.collectStyle(5, 3)); + expect(const Style.attr({'bold': Attribute.bold}), + document.collectStyle(8, 3)); + // + expect(const Style(), document.collectStyle(3, 3)); + }); + }); +}