diff --git a/lib/src/controller/quill_controller.dart b/lib/src/controller/quill_controller.dart index 53ae646d..57e3ed42 100644 --- a/lib/src/controller/quill_controller.dart +++ b/lib/src/controller/quill_controller.dart @@ -60,7 +60,7 @@ class QuillController extends ChangeNotifier { QuillEditorConfigurations get editorConfigurations => _editorConfigurations ?? const QuillEditorConfigurations(); set editorConfigurations(QuillEditorConfigurations? value) => - _editorConfigurations = value; + _editorConfigurations = document.editorConfigurations = value; /// Toolbar configurations /// @@ -78,6 +78,7 @@ class QuillController extends ChangeNotifier { set document(Document doc) { _document = doc; + _document.editorConfigurations = editorConfigurations; // Prevent the selection from _selection = const TextSelection(baseOffset: 0, extentOffset: 0); @@ -520,7 +521,7 @@ class QuillController extends ChangeNotifier { /// Get the text for the selected region and expand the content of Embedded objects. _pastePlainText = document.getPlainText( - selection.start, selection.end - selection.start, editorConfigurations); + selection.start, selection.end - selection.start, true); /// Get the internal representation so it can be pasted into a QuillEditor with style retained. _pasteStyleAndEmbed = getAllIndividualSelectionStylesAndEmbed(); diff --git a/lib/src/document/document.dart b/lib/src/document/document.dart index 4b2267e2..364a8c7f 100644 --- a/lib/src/document/document.dart +++ b/lib/src/document/document.dart @@ -250,10 +250,21 @@ class Document { return (res.node as Line).collectAllStylesWithOffsets(res.offset, len); } + /// Editor configurations + /// + /// Caches configuration set in QuillController. + /// Allows access to embedBuilders and search configurations + QuillEditorConfigurations? _editorConfigurations; + QuillEditorConfigurations get editorConfigurations => + _editorConfigurations ?? const QuillEditorConfigurations(); + set editorConfigurations(QuillEditorConfigurations? value) => + _editorConfigurations = value; + /// Returns plain text within the specified text range. - String getPlainText(int index, int len, [QuillEditorConfigurations? config]) { + String getPlainText(int index, int len, [bool includeEmbeds = false]) { final res = queryChild(index); - return (res.node as Line).getPlainText(res.offset, len, config); + return (res.node as Line).getPlainText( + res.offset, len, includeEmbeds ? editorConfigurations : null); } /// Returns [Line] located at specified character [offset]. @@ -276,13 +287,16 @@ class Document { bool caseSensitive = false, bool wholeWord = false, }) { + final searchEmbedContent = editorConfigurations.searchEmbedContent; final matches = []; for (final node in _root.children) { if (node is Line) { - _searchLine(substring, caseSensitive, wholeWord, node, matches); + _searchLine(substring, caseSensitive, wholeWord, searchEmbedContent, + node, matches); } else if (node is Block) { for (final line in Iterable.castFrom(node.children)) { - _searchLine(substring, caseSensitive, wholeWord, line, matches); + _searchLine(substring, caseSensitive, wholeWord, searchEmbedContent, + line, matches); } } else { throw StateError('Unreachable.'); @@ -295,6 +309,7 @@ class Document { String substring, bool caseSensitive, bool wholeWord, + bool searchEmbedContent, Line line, List matches, ) { @@ -312,6 +327,43 @@ class Document { } matches.add(index + line.documentOffset); } + // + if (searchEmbedContent && line.hasEmbed) { + Node? node = line.children.first; + while (node != null) { + if (node is Embed) { + final ofs = node.offset; + EmbedBuilder? builder; + if (editorConfigurations.embedBuilders != null) { + // Find the builder for this embed + for (final b in editorConfigurations.embedBuilders!) { + if (b.key == node.value.type) { + builder = b; + break; + } + } + } + builder ??= editorConfigurations.unknownEmbedBuilder; + // Get searchable text for this embed + var embedText = builder?.toPlainText(node); + if (editorConfigurations.searchEmbedRawData == true && + (embedText == null || + embedText == Embed.kObjectReplacementCharacter)) { + embedText = node.value.data.toString(); + } + if (embedText?.contains(searchExpression) == true) { + final documentOffset = line.documentOffset + ofs; + final index = matches.indexWhere((e) => e > documentOffset); + if (index < 0) { + matches.add(documentOffset); + } else { + matches.insert(index, documentOffset); + } + } + } + node = node.next; + } + } } /// Given offset, find its leaf node in document diff --git a/lib/src/editor/config/editor_configurations.dart b/lib/src/editor/config/editor_configurations.dart index bbfdcd05..29925ff4 100644 --- a/lib/src/editor/config/editor_configurations.dart +++ b/lib/src/editor/config/editor_configurations.dart @@ -57,6 +57,8 @@ class QuillEditorConfigurations extends Equatable { this.enableMarkdownStyleConversion = true, this.embedBuilders, this.unknownEmbedBuilder, + this.searchEmbedContent = true, + this.searchEmbedRawData = false, this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, this.customStyleBuilder, this.customRecognizerBuilder, @@ -281,6 +283,14 @@ class QuillEditorConfigurations extends Equatable { final CustomStyleBuilder? customStyleBuilder; final CustomRecognizerBuilder? customRecognizerBuilder; + /// Search options for embed objects - enable [searchEmbedContent], disable [searchEmbedRawData] + /// + /// [searchEmbedContent] enables searching within embed objects using the [EmbedBuilder.toPlainText] override to provide the searchable text. Default is true. + /// [searchEmbedRawData] enables searching the raw data of the embed object if [EmbedBuilder.toPlainText] is not overridden. + /// Default is false since the raw data is not usually visible to a user and it will not be obvious why the embed object was selected. + final bool searchEmbedContent; + final bool searchEmbedRawData; + /// Delegate function responsible for showing menu with link actions on /// mobile platforms (iOS, Android). /// @@ -422,6 +432,8 @@ class QuillEditorConfigurations extends Equatable { ValueChanged? onLaunchUrl, Iterable? embedBuilders, EmbedBuilder? unknownEmbedBuilder, + bool? searchEmbedContent, + bool? searchEmbedRawData, CustomStyleBuilder? customStyleBuilder, CustomRecognizerBuilder? customRecognizerBuilder, LinkActionPickerDelegate? linkActionPickerDelegate, @@ -481,6 +493,8 @@ class QuillEditorConfigurations extends Equatable { onLaunchUrl: onLaunchUrl ?? this.onLaunchUrl, embedBuilders: embedBuilders ?? this.embedBuilders, unknownEmbedBuilder: unknownEmbedBuilder ?? this.unknownEmbedBuilder, + searchEmbedContent: searchEmbedContent ?? this.searchEmbedContent, + searchEmbedRawData: searchEmbedRawData ?? this.searchEmbedRawData, customStyleBuilder: customStyleBuilder ?? this.customStyleBuilder, customRecognizerBuilder: customRecognizerBuilder ?? this.customRecognizerBuilder, diff --git a/lib/src/toolbar/buttons/search/search_dialog.dart b/lib/src/toolbar/buttons/search/search_dialog.dart index 780ac380..d1ee9f63 100644 --- a/lib/src/toolbar/buttons/search/search_dialog.dart +++ b/lib/src/toolbar/buttons/search/search_dialog.dart @@ -273,23 +273,39 @@ class QuillToolbarSearchDialogState extends State { return; } setState(() { + final currPos = _offsets.isNotEmpty ? _offsets[_index] : 0; _offsets = widget.controller.document.search( _text, caseSensitive: _caseSensitive, wholeWord: _wholeWord, ); _index = 0; + if (_offsets.isNotEmpty) { + // Select the next hit position + for (var n = 0; n < _offsets.length; n++) { + if (_offsets[n] >= currPos) { + _index = n; + break; + } + } + _moveToPosition(); + } }); - if (_offsets.isNotEmpty) { - _moveToPosition(); - } } void _moveToPosition() { + final offset = _offsets[_index]; + var len = _text.length; + + /// Trap search hit within embed must only show selection of the embed + final leaf = widget.controller.queryNode(offset); + if (leaf is Embed) { + len = 1; + } widget.controller.updateSelection( TextSelection( - baseOffset: _offsets[_index], - extentOffset: _offsets[_index] + _text.length, + baseOffset: offset, + extentOffset: offset + len, ), ChangeSource.local, );