diff --git a/lib/src/models/documents/attribute.dart b/lib/src/models/documents/attribute.dart index 365991a0..deea5a15 100644 --- a/lib/src/models/documents/attribute.dart +++ b/lib/src/models/documents/attribute.dart @@ -116,7 +116,9 @@ class Attribute extends Equatable { static const VideoAttribute video = VideoAttribute(null); - static final Set inlineKeys = { + static final registeredAttributeKeys = Set.unmodifiable(_registry.keys); + + static final inlineKeys = Set.unmodifiable({ Attribute.bold.key, Attribute.subscript.key, Attribute.superscript.key, @@ -128,7 +130,17 @@ class Attribute extends Equatable { Attribute.color.key, Attribute.background.key, Attribute.placeholder.key, - }; + Attribute.font.key, + Attribute.size.key, + Attribute.inlineCode.key, + }); + + static final ignoreKeys = Set.unmodifiable({ + Attribute.width.key, + Attribute.height.key, + Attribute.style.key, + Attribute.token.key, + }); static final Set blockKeys = LinkedHashSet.of({ Attribute.header.key, diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 6b2a05b6..7ea7050d 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -556,26 +556,47 @@ class PreserveInlineStylesRule extends InsertRule { return null; } - final itr = DeltaIterator(document.toDelta()); + final documentDelta = document.toDelta(); + final itr = DeltaIterator(documentDelta); var prev = itr.skip(len == 0 ? index : index + 1); if (prev == null || prev.data is! String) return null; - if ((prev.data as String).endsWith('\n')) { - if (prev.attributes != null) { - for (final key in prev.attributes!.keys) { - if (!Attribute.inlineKeys.contains(key)) { - return null; + /// Trap for simple insertions at start of line + if (len == 0) { + final prevData = prev.data as String; + if (prevData.endsWith('\n')) { + /// If current line is empty get attributes from a prior line + final currLine = itr.next(); + final currData = currLine.data as String?; + if (currData != null && (currData.isEmpty || currData[0] == '\n')) { + if (prevData.trimRight().isEmpty) { + final back = + DeltaIterator(documentDelta).skip(index - prevData.length); + if (back != null && back.data is String) { + prev = back; + } } + } else { + prev = currLine; } } - prev = itr - .next(); // at the start of a line, apply the style for the current line and not the style for the preceding line } - final attributes = prev.attributes; + final attributes = {}; + if (prev.attributes != null) { + for (final entry in prev.attributes!.entries) { + if (Attribute.inlineKeys.contains(entry.key)) { + attributes[entry.key] = entry.value; + } + } + } + if (attributes.isEmpty) { + return null; + } + final text = data; - if (attributes == null || !attributes.containsKey(Attribute.link.key)) { + if (attributes.isEmpty || !attributes.containsKey(Attribute.link.key)) { return Delta() ..retain(index + (len ?? 0)) ..insert(text, attributes); diff --git a/lib/src/widgets/raw_editor/raw_editor_actions.dart b/lib/src/widgets/raw_editor/raw_editor_actions.dart index a343f03f..eaa14635 100644 --- a/lib/src/widgets/raw_editor/raw_editor_actions.dart +++ b/lib/src/widgets/raw_editor/raw_editor_actions.dart @@ -460,6 +460,7 @@ class QuillEditorOpenSearchAction extends ContextAction { ); } await showDialog( + barrierColor: Colors.transparent, context: context, builder: (_) => FlutterQuillLocalizationsWidget( child: QuillToolbarSearchDialog( diff --git a/test/utils/attributes_test.dart b/test/utils/attributes_test.dart new file mode 100644 index 00000000..d0c99074 --- /dev/null +++ b/test/utils/attributes_test.dart @@ -0,0 +1,47 @@ +import 'package:flutter_quill/flutter_quill.dart'; +import 'package:test/test.dart'; + +void main() { + /// Attributes are assigned an AttributeScope to define how they are used. + /// Collections of Attribute keys are used to allow quick iteration by type of scope. + group('collections of keys', () { + test('unmodifiable inlineKeys', () { + expect(() => Attribute.inlineKeys.add('value'), + throwsA(const TypeMatcher())); + }); + + /// All registered attributes should be listed in collections of keys. + test('collections of keys', () { + final all = {}..addAll(Attribute.registeredAttributeKeys); + for (final key in Attribute.inlineKeys) { + expect(all.remove(key), true); + } + for (final key in Attribute.blockKeys) { + expect(all.remove(key), true); + } + for (final key in Attribute.embedKeys) { + expect(all.remove(key), true); + } + for (final key in Attribute.ignoreKeys) { + expect(all.remove(key), true); + } + expect(all, {}); + }); + + /// verify collections contain the correct AttributeScope. + test('collections of scope', () { + for (final key in Attribute.inlineKeys) { + expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.inline); + } + for (final key in Attribute.blockKeys) { + expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.block); + } + for (final key in Attribute.embedKeys) { + expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.embeds); + } + for (final key in Attribute.ignoreKeys) { + expect(Attribute.fromKeyValue(key, null)!.scope, AttributeScope.ignore); + } + }); + }); +}