Search in Embeds

pull/2090/head
AtlasAutocode 8 months ago
parent 72ae847a11
commit 35c0b17b24
  1. 5
      lib/src/controller/quill_controller.dart
  2. 60
      lib/src/document/document.dart
  3. 14
      lib/src/editor/config/editor_configurations.dart
  4. 26
      lib/src/toolbar/buttons/search/search_dialog.dart

@ -60,7 +60,7 @@ class QuillController extends ChangeNotifier {
QuillEditorConfigurations get editorConfigurations => QuillEditorConfigurations get editorConfigurations =>
_editorConfigurations ?? const QuillEditorConfigurations(); _editorConfigurations ?? const QuillEditorConfigurations();
set editorConfigurations(QuillEditorConfigurations? value) => set editorConfigurations(QuillEditorConfigurations? value) =>
_editorConfigurations = value; _editorConfigurations = document.editorConfigurations = value;
/// Toolbar configurations /// Toolbar configurations
/// ///
@ -78,6 +78,7 @@ class QuillController extends ChangeNotifier {
set document(Document doc) { set document(Document doc) {
_document = doc; _document = doc;
_document.editorConfigurations = editorConfigurations;
// Prevent the selection from // Prevent the selection from
_selection = const TextSelection(baseOffset: 0, extentOffset: 0); _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. /// Get the text for the selected region and expand the content of Embedded objects.
_pastePlainText = document.getPlainText( _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. /// Get the internal representation so it can be pasted into a QuillEditor with style retained.
_pasteStyleAndEmbed = getAllIndividualSelectionStylesAndEmbed(); _pasteStyleAndEmbed = getAllIndividualSelectionStylesAndEmbed();

@ -250,10 +250,21 @@ class Document {
return (res.node as Line).collectAllStylesWithOffsets(res.offset, len); 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. /// 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); 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]. /// Returns [Line] located at specified character [offset].
@ -276,13 +287,16 @@ class Document {
bool caseSensitive = false, bool caseSensitive = false,
bool wholeWord = false, bool wholeWord = false,
}) { }) {
final searchEmbedContent = editorConfigurations.searchEmbedContent;
final matches = <int>[]; final matches = <int>[];
for (final node in _root.children) { for (final node in _root.children) {
if (node is Line) { if (node is Line) {
_searchLine(substring, caseSensitive, wholeWord, node, matches); _searchLine(substring, caseSensitive, wholeWord, searchEmbedContent,
node, matches);
} else if (node is Block) { } else if (node is Block) {
for (final line in Iterable.castFrom<dynamic, Line>(node.children)) { for (final line in Iterable.castFrom<dynamic, Line>(node.children)) {
_searchLine(substring, caseSensitive, wholeWord, line, matches); _searchLine(substring, caseSensitive, wholeWord, searchEmbedContent,
line, matches);
} }
} else { } else {
throw StateError('Unreachable.'); throw StateError('Unreachable.');
@ -295,6 +309,7 @@ class Document {
String substring, String substring,
bool caseSensitive, bool caseSensitive,
bool wholeWord, bool wholeWord,
bool searchEmbedContent,
Line line, Line line,
List<int> matches, List<int> matches,
) { ) {
@ -312,6 +327,43 @@ class Document {
} }
matches.add(index + line.documentOffset); 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 /// Given offset, find its leaf node in document

@ -57,6 +57,8 @@ class QuillEditorConfigurations extends Equatable {
this.enableMarkdownStyleConversion = true, this.enableMarkdownStyleConversion = true,
this.embedBuilders, this.embedBuilders,
this.unknownEmbedBuilder, this.unknownEmbedBuilder,
this.searchEmbedContent = true,
this.searchEmbedRawData = false,
this.linkActionPickerDelegate = defaultLinkActionPickerDelegate, this.linkActionPickerDelegate = defaultLinkActionPickerDelegate,
this.customStyleBuilder, this.customStyleBuilder,
this.customRecognizerBuilder, this.customRecognizerBuilder,
@ -281,6 +283,14 @@ class QuillEditorConfigurations extends Equatable {
final CustomStyleBuilder? customStyleBuilder; final CustomStyleBuilder? customStyleBuilder;
final CustomRecognizerBuilder? customRecognizerBuilder; 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 /// Delegate function responsible for showing menu with link actions on
/// mobile platforms (iOS, Android). /// mobile platforms (iOS, Android).
/// ///
@ -422,6 +432,8 @@ class QuillEditorConfigurations extends Equatable {
ValueChanged<String>? onLaunchUrl, ValueChanged<String>? onLaunchUrl,
Iterable<EmbedBuilder>? embedBuilders, Iterable<EmbedBuilder>? embedBuilders,
EmbedBuilder? unknownEmbedBuilder, EmbedBuilder? unknownEmbedBuilder,
bool? searchEmbedContent,
bool? searchEmbedRawData,
CustomStyleBuilder? customStyleBuilder, CustomStyleBuilder? customStyleBuilder,
CustomRecognizerBuilder? customRecognizerBuilder, CustomRecognizerBuilder? customRecognizerBuilder,
LinkActionPickerDelegate? linkActionPickerDelegate, LinkActionPickerDelegate? linkActionPickerDelegate,
@ -481,6 +493,8 @@ class QuillEditorConfigurations extends Equatable {
onLaunchUrl: onLaunchUrl ?? this.onLaunchUrl, onLaunchUrl: onLaunchUrl ?? this.onLaunchUrl,
embedBuilders: embedBuilders ?? this.embedBuilders, embedBuilders: embedBuilders ?? this.embedBuilders,
unknownEmbedBuilder: unknownEmbedBuilder ?? this.unknownEmbedBuilder, unknownEmbedBuilder: unknownEmbedBuilder ?? this.unknownEmbedBuilder,
searchEmbedContent: searchEmbedContent ?? this.searchEmbedContent,
searchEmbedRawData: searchEmbedRawData ?? this.searchEmbedRawData,
customStyleBuilder: customStyleBuilder ?? this.customStyleBuilder, customStyleBuilder: customStyleBuilder ?? this.customStyleBuilder,
customRecognizerBuilder: customRecognizerBuilder:
customRecognizerBuilder ?? this.customRecognizerBuilder, customRecognizerBuilder ?? this.customRecognizerBuilder,

@ -273,23 +273,39 @@ class QuillToolbarSearchDialogState extends State<QuillToolbarSearchDialog> {
return; return;
} }
setState(() { setState(() {
final currPos = _offsets.isNotEmpty ? _offsets[_index] : 0;
_offsets = widget.controller.document.search( _offsets = widget.controller.document.search(
_text, _text,
caseSensitive: _caseSensitive, caseSensitive: _caseSensitive,
wholeWord: _wholeWord, wholeWord: _wholeWord,
); );
_index = 0; _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() { 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( widget.controller.updateSelection(
TextSelection( TextSelection(
baseOffset: _offsets[_index], baseOffset: offset,
extentOffset: _offsets[_index] + _text.length, extentOffset: offset + len,
), ),
ChangeSource.local, ChangeSource.local,
); );

Loading…
Cancel
Save