dartlangeditorflutterflutter-appsflutter-examplesflutter-packageflutter-widgetquillquill-deltaquilljsreactquillrich-textrich-text-editorwysiwygwysiwyg-editor
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
754 lines
22 KiB
754 lines
22 KiB
import 'dart:math' as math; |
|
|
|
import 'package:flutter/services.dart' show ClipboardData, Clipboard; |
|
import 'package:flutter/widgets.dart'; |
|
import 'package:html/parser.dart' as html_parser; |
|
import 'package:meta/meta.dart' show experimental; |
|
|
|
import '../../quill_delta.dart'; |
|
import '../common/structs/image_url.dart'; |
|
import '../common/structs/offset_value.dart'; |
|
import '../common/utils/embeds.dart'; |
|
import '../delta/delta_diff.dart'; |
|
import '../delta/delta_x.dart'; |
|
import '../document/attribute.dart'; |
|
import '../document/document.dart'; |
|
import '../document/nodes/embeddable.dart'; |
|
import '../document/nodes/leaf.dart'; |
|
import '../document/structs/doc_change.dart'; |
|
import '../document/style.dart'; |
|
import '../editor/config/editor_configurations.dart'; |
|
import '../editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart'; |
|
import '../toolbar/config/simple_toolbar_configurations.dart'; |
|
import 'quill_controller_configurations.dart'; |
|
|
|
typedef ReplaceTextCallback = bool Function(int index, int len, Object? data); |
|
typedef DeleteCallback = void Function(int cursorPosition, bool forward); |
|
|
|
class QuillController extends ChangeNotifier { |
|
QuillController({ |
|
required Document document, |
|
required TextSelection selection, |
|
this.configurations = const QuillControllerConfigurations(), |
|
this.keepStyleOnNewLine = true, |
|
this.onReplaceText, |
|
this.onDelete, |
|
this.onSelectionCompleted, |
|
this.onSelectionChanged, |
|
this.readOnly = false, |
|
this.editorFocusNode, |
|
}) : _document = document, |
|
_selection = selection; |
|
|
|
factory QuillController.basic( |
|
{QuillControllerConfigurations configurations = |
|
const QuillControllerConfigurations(), |
|
FocusNode? editorFocusNode}) => |
|
QuillController( |
|
configurations: configurations, |
|
editorFocusNode: editorFocusNode, |
|
document: Document(), |
|
selection: const TextSelection.collapsed(offset: 0), |
|
); |
|
|
|
final QuillControllerConfigurations configurations; |
|
|
|
/// Editor configurations |
|
/// |
|
/// Caches configuration set in QuillEditor ctor. |
|
QuillEditorConfigurations? _editorConfigurations; |
|
QuillEditorConfigurations get editorConfigurations => |
|
_editorConfigurations ?? const QuillEditorConfigurations(); |
|
set editorConfigurations(QuillEditorConfigurations? value) => |
|
_editorConfigurations = value; |
|
|
|
/// Toolbar configurations |
|
/// |
|
/// Caches configuration set in QuillSimpleToolbar ctor. |
|
QuillSimpleToolbarConfigurations? _toolbarConfigurations; |
|
QuillSimpleToolbarConfigurations get toolbarConfigurations => |
|
_toolbarConfigurations ?? const QuillSimpleToolbarConfigurations(); |
|
set toolbarConfigurations(QuillSimpleToolbarConfigurations? value) => |
|
_toolbarConfigurations = value; |
|
|
|
/// Document managed by this controller. |
|
Document _document; |
|
|
|
Document get document => _document; |
|
|
|
set document(Document doc) { |
|
_document = doc; |
|
|
|
// Prevent the selection from |
|
_selection = const TextSelection(baseOffset: 0, extentOffset: 0); |
|
|
|
notifyListeners(); |
|
} |
|
|
|
@experimental |
|
void setContents( |
|
Delta delta, { |
|
ChangeSource changeSource = ChangeSource.local, |
|
}) { |
|
final newDocument = Document.fromDelta(delta); |
|
|
|
final change = DocChange(_document.toDelta(), delta, changeSource); |
|
newDocument.documentChangeObserver.add(change); |
|
newDocument.history.handleDocChange(change); |
|
|
|
_document = newDocument; |
|
notifyListeners(); |
|
} |
|
|
|
/// Tells whether to keep or reset the [toggledStyle] |
|
/// when user adds a new line. |
|
final bool keepStyleOnNewLine; |
|
|
|
/// Currently selected text within the [document]. |
|
TextSelection get selection => _selection; |
|
TextSelection _selection; |
|
|
|
/// Custom [replaceText] handler |
|
/// Return false to ignore the event |
|
ReplaceTextCallback? onReplaceText; |
|
|
|
/// Custom delete handler |
|
DeleteCallback? onDelete; |
|
|
|
void Function()? onSelectionCompleted; |
|
void Function(TextSelection textSelection)? onSelectionChanged; |
|
|
|
/// Store any styles attribute that got toggled by the tap of a button |
|
/// and that has not been applied yet. |
|
/// It gets reset after each format action within the [document]. |
|
Style toggledStyle = const Style(); |
|
|
|
bool ignoreFocusOnTextChange = false; |
|
|
|
/// Skip requestKeyboard being called in |
|
/// RawEditorState#_didChangeTextEditingValue |
|
bool skipRequestKeyboard = false; |
|
|
|
/// True when this [QuillController] instance has been disposed. |
|
/// |
|
/// A safety mechanism to ensure that listeners don't crash when adding, |
|
/// removing or listeners to this instance. |
|
bool _isDisposed = false; |
|
|
|
Stream<DocChange> get changes => document.changes; |
|
|
|
TextEditingValue get plainTextEditingValue => TextEditingValue( |
|
text: document.toPlainText(), |
|
selection: selection, |
|
); |
|
|
|
/// Only attributes applied to all characters within this range are |
|
/// included in the result. |
|
Style getSelectionStyle() { |
|
return document |
|
.collectStyle(selection.start, selection.end - selection.start) |
|
.mergeAll(toggledStyle); |
|
} |
|
|
|
// Increases or decreases the indent of the current selection by 1. |
|
void indentSelection(bool isIncrease) { |
|
if (selection.isCollapsed) { |
|
_indentSelectionFormat(isIncrease); |
|
} else { |
|
_indentSelectionEachLine(isIncrease); |
|
} |
|
} |
|
|
|
void _indentSelectionFormat(bool isIncrease) { |
|
final indent = getSelectionStyle().attributes[Attribute.indent.key]; |
|
if (indent == null) { |
|
if (isIncrease) { |
|
formatSelection(Attribute.indentL1); |
|
} |
|
return; |
|
} |
|
if (indent.value == 1 && !isIncrease) { |
|
formatSelection(Attribute.clone(Attribute.indentL1, null)); |
|
return; |
|
} |
|
if (isIncrease) { |
|
if (indent.value < 5) { |
|
formatSelection(Attribute.getIndentLevel(indent.value + 1)); |
|
} |
|
return; |
|
} |
|
formatSelection(Attribute.getIndentLevel(indent.value - 1)); |
|
} |
|
|
|
void _indentSelectionEachLine(bool isIncrease) { |
|
final styles = document.collectAllStylesWithOffset( |
|
selection.start, |
|
selection.end - selection.start, |
|
); |
|
for (final style in styles) { |
|
final indent = style.value.attributes[Attribute.indent.key]; |
|
final formatIndex = math.max(style.offset, selection.start); |
|
final formatLength = math.min( |
|
style.offset + (style.length ?? 0), |
|
selection.end, |
|
) - |
|
style.offset; |
|
Attribute? formatAttribute; |
|
if (indent == null) { |
|
if (isIncrease) { |
|
formatAttribute = Attribute.indentL1; |
|
} |
|
} else if (indent.value == 1 && !isIncrease) { |
|
formatAttribute = Attribute.clone(Attribute.indentL1, null); |
|
} else if (isIncrease) { |
|
if (indent.value < 5) { |
|
formatAttribute = Attribute.getIndentLevel(indent.value + 1); |
|
} |
|
} else { |
|
formatAttribute = Attribute.getIndentLevel(indent.value - 1); |
|
} |
|
if (formatAttribute != null) { |
|
document.format(formatIndex, formatLength, formatAttribute); |
|
} |
|
} |
|
notifyListeners(); |
|
} |
|
|
|
/// Returns all styles and Embed for each node within selection |
|
List<OffsetValue> getAllIndividualSelectionStylesAndEmbed() { |
|
final stylesAndEmbed = document.collectAllIndividualStyleAndEmbed( |
|
selection.start, selection.end - selection.start); |
|
return stylesAndEmbed; |
|
} |
|
|
|
/// Returns plain text for each node within selection |
|
String getPlainText() { |
|
final text = |
|
document.getPlainText(selection.start, selection.end - selection.start); |
|
return text; |
|
} |
|
|
|
/// Returns all styles for any character within the specified text range. |
|
List<Style> getAllSelectionStyles() { |
|
final styles = document.collectAllStyles( |
|
selection.start, selection.end - selection.start) |
|
..add(toggledStyle); |
|
return styles; |
|
} |
|
|
|
void undo() { |
|
final result = document.undo(); |
|
if (result.changed) { |
|
_handleHistoryChange(result.len); |
|
} |
|
} |
|
|
|
void _handleHistoryChange(int len) { |
|
updateSelection( |
|
TextSelection.collapsed( |
|
offset: len, |
|
), |
|
ChangeSource.local, |
|
); |
|
} |
|
|
|
void redo() { |
|
final result = document.redo(); |
|
if (result.changed) { |
|
_handleHistoryChange(result.len); |
|
} |
|
} |
|
|
|
bool get hasUndo => document.hasUndo; |
|
|
|
bool get hasRedo => document.hasRedo; |
|
|
|
/// clear editor |
|
void clear() { |
|
replaceText(0, plainTextEditingValue.text.length - 1, '', |
|
const TextSelection.collapsed(offset: 0)); |
|
} |
|
|
|
void replaceText( |
|
int index, |
|
int len, |
|
Object? data, |
|
TextSelection? textSelection, { |
|
bool ignoreFocus = false, |
|
bool shouldNotifyListeners = true, |
|
}) { |
|
assert(data is String || data is Embeddable || data is Delta); |
|
|
|
if (onReplaceText != null && !onReplaceText!(index, len, data)) { |
|
return; |
|
} |
|
|
|
Delta? delta; |
|
if (len > 0 || data is! String || data.isNotEmpty) { |
|
delta = document.replace(index, len, data); |
|
var shouldRetainDelta = toggledStyle.isNotEmpty && |
|
delta.isNotEmpty && |
|
delta.length <= 2 && |
|
delta.last.isInsert; |
|
if (shouldRetainDelta && |
|
toggledStyle.isNotEmpty && |
|
delta.length == 2 && |
|
delta.last.data == '\n') { |
|
// if all attributes are inline, shouldRetainDelta should be false |
|
final anyAttributeNotInline = |
|
toggledStyle.values.any((attr) => !attr.isInline); |
|
if (!anyAttributeNotInline) { |
|
shouldRetainDelta = false; |
|
} |
|
} |
|
if (shouldRetainDelta) { |
|
final retainDelta = Delta() |
|
..retain(index) |
|
..retain(data is String ? data.length : 1, toggledStyle.toJson()); |
|
document.compose(retainDelta, ChangeSource.local); |
|
} |
|
} |
|
|
|
if (textSelection != null) { |
|
if (delta == null || delta.isEmpty) { |
|
_updateSelection(textSelection); |
|
} else { |
|
final user = Delta() |
|
..retain(index) |
|
..insert(data) |
|
..delete(len); |
|
final positionDelta = getPositionDelta(user, delta); |
|
_updateSelection( |
|
textSelection.copyWith( |
|
baseOffset: textSelection.baseOffset + positionDelta, |
|
extentOffset: textSelection.extentOffset + positionDelta, |
|
), |
|
insertNewline: data == '\n'); |
|
} |
|
} |
|
|
|
if (ignoreFocus) { |
|
ignoreFocusOnTextChange = true; |
|
} |
|
if (shouldNotifyListeners) { |
|
notifyListeners(); |
|
} |
|
ignoreFocusOnTextChange = false; |
|
} |
|
|
|
/// Called in two cases: |
|
/// forward == false && textBefore.isEmpty |
|
/// forward == true && textAfter.isEmpty |
|
/// Android only |
|
/// see https://github.com/singerdmx/flutter-quill/discussions/514 |
|
void handleDelete(int cursorPosition, bool forward) => |
|
onDelete?.call(cursorPosition, forward); |
|
|
|
void formatTextStyle(int index, int len, Style style) { |
|
style.attributes.forEach((key, attr) { |
|
formatText(index, len, attr); |
|
}); |
|
} |
|
|
|
void formatText( |
|
int index, |
|
int len, |
|
Attribute? attribute, { |
|
bool shouldNotifyListeners = true, |
|
}) { |
|
if (len == 0 && |
|
attribute!.isInline && |
|
attribute.key != Attribute.link.key) { |
|
// Add the attribute to our toggledStyle. |
|
// It will be used later upon insertion. |
|
toggledStyle = toggledStyle.put(attribute); |
|
} |
|
|
|
final change = document.format(index, len, attribute); |
|
// Transform selection against the composed change and give priority to |
|
// the change. This is needed in cases when format operation actually |
|
// inserts data into the document (e.g. embeds). |
|
final adjustedSelection = selection.copyWith( |
|
baseOffset: change.transformPosition(selection.baseOffset), |
|
extentOffset: change.transformPosition(selection.extentOffset)); |
|
if (selection != adjustedSelection) { |
|
_updateSelection(adjustedSelection); |
|
} |
|
if (shouldNotifyListeners) { |
|
notifyListeners(); |
|
} |
|
} |
|
|
|
void formatSelection(Attribute? attribute, |
|
{bool shouldNotifyListeners = true}) { |
|
formatText( |
|
selection.start, |
|
selection.end - selection.start, |
|
attribute, |
|
shouldNotifyListeners: shouldNotifyListeners, |
|
); |
|
} |
|
|
|
void moveCursorToStart() { |
|
updateSelection( |
|
const TextSelection.collapsed(offset: 0), |
|
ChangeSource.local, |
|
); |
|
} |
|
|
|
void moveCursorToPosition(int position) { |
|
updateSelection( |
|
TextSelection.collapsed(offset: position), |
|
ChangeSource.local, |
|
); |
|
} |
|
|
|
void moveCursorToEnd() { |
|
updateSelection( |
|
TextSelection.collapsed(offset: plainTextEditingValue.text.length), |
|
ChangeSource.local, |
|
); |
|
} |
|
|
|
void updateSelection(TextSelection textSelection, ChangeSource source) { |
|
_updateSelection(textSelection); |
|
notifyListeners(); |
|
} |
|
|
|
void compose(Delta delta, TextSelection textSelection, ChangeSource source) { |
|
if (delta.isNotEmpty) { |
|
document.compose(delta, source); |
|
} |
|
|
|
textSelection = selection.copyWith( |
|
baseOffset: delta.transformPosition(selection.baseOffset, force: false), |
|
extentOffset: delta.transformPosition( |
|
selection.extentOffset, |
|
force: false, |
|
), |
|
); |
|
if (selection != textSelection) { |
|
_updateSelection(textSelection); |
|
} |
|
|
|
notifyListeners(); |
|
} |
|
|
|
@override |
|
void addListener(VoidCallback listener) { |
|
// By using `_isDisposed`, make sure that `addListener` won't be called on a |
|
// disposed `ChangeListener` |
|
if (!_isDisposed) { |
|
super.addListener(listener); |
|
} |
|
} |
|
|
|
@override |
|
void removeListener(VoidCallback listener) { |
|
// By using `_isDisposed`, make sure that `removeListener` won't be called |
|
// on a disposed `ChangeListener` |
|
if (!_isDisposed) { |
|
super.removeListener(listener); |
|
} |
|
} |
|
|
|
@override |
|
void dispose() { |
|
if (!_isDisposed) { |
|
document.close(); |
|
} |
|
|
|
_isDisposed = true; |
|
super.dispose(); |
|
} |
|
|
|
void _updateSelection(TextSelection textSelection, |
|
{bool insertNewline = false}) { |
|
_selection = textSelection; |
|
final end = document.length - 1; |
|
_selection = selection.copyWith( |
|
baseOffset: math.min(selection.baseOffset, end), |
|
extentOffset: math.min(selection.extentOffset, end)); |
|
if (keepStyleOnNewLine) { |
|
if (insertNewline && selection.start > 0) { |
|
final style = document.collectStyle(selection.start - 1, 0); |
|
final ignoredStyles = style.attributes.values.where( |
|
(s) => !s.isInline || s.key == Attribute.link.key, |
|
); |
|
toggledStyle = style.removeAll(ignoredStyles.toSet()); |
|
} else { |
|
toggledStyle = const Style(); |
|
} |
|
} else { |
|
toggledStyle = const Style(); |
|
} |
|
onSelectionChanged?.call(textSelection); |
|
} |
|
|
|
/// Given offset, find its leaf node in document |
|
Leaf? queryNode(int offset) { |
|
return document.querySegmentLeafNode(offset).leaf; |
|
} |
|
|
|
// Notify toolbar buttons directly with attributes |
|
Map<String, Attribute> toolbarButtonToggler = const {}; |
|
|
|
/// Clipboard caches last copy to allow paste with styles. Static to allow paste between multiple instances of editor. |
|
static String _pastePlainText = ''; |
|
static Delta _pasteDelta = Delta(); |
|
static List<OffsetValue> _pasteStyleAndEmbed = <OffsetValue>[]; |
|
|
|
String get pastePlainText => _pastePlainText; |
|
Delta get pasteDelta => _pasteDelta; |
|
List<OffsetValue> get pasteStyleAndEmbed => _pasteStyleAndEmbed; |
|
|
|
bool readOnly; |
|
|
|
/// Used to give focus to the editor following a toolbar action |
|
FocusNode? editorFocusNode; |
|
|
|
ImageUrl? _copiedImageUrl; |
|
ImageUrl? get copiedImageUrl => _copiedImageUrl; |
|
|
|
set copiedImageUrl(ImageUrl? value) { |
|
_copiedImageUrl = value; |
|
Clipboard.setData(const ClipboardData(text: '')); |
|
} |
|
|
|
bool clipboardSelection(bool copy) { |
|
copiedImageUrl = null; |
|
|
|
/// Get the text for the selected region and expand the content of Embedded objects. |
|
_pastePlainText = document.getPlainText( |
|
selection.start, selection.end - selection.start, editorConfigurations); |
|
|
|
/// Get the internal representation so it can be pasted into a QuillEditor with style retained. |
|
_pasteStyleAndEmbed = getAllIndividualSelectionStylesAndEmbed(); |
|
|
|
/// Get the deltas for the selection so they can be pasted into a QuillEditor with styles and embeds retained. |
|
_pasteDelta = document.toDelta().slice(selection.start, selection.end); |
|
|
|
if (!selection.isCollapsed) { |
|
Clipboard.setData(ClipboardData(text: _pastePlainText)); |
|
if (!copy) { |
|
if (readOnly) return false; |
|
final sel = selection; |
|
replaceText(sel.start, sel.end - sel.start, '', |
|
TextSelection.collapsed(offset: sel.start)); |
|
} |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
/// Returns whether paste operation was handled here. |
|
/// updateEditor is called if paste operation was successful. |
|
Future<bool> clipboardPaste({void Function()? updateEditor}) async { |
|
if (readOnly || !selection.isValid) return true; |
|
|
|
final pasteUsingInternalImageSuccess = await _pasteInternalImage(); |
|
if (pasteUsingInternalImageSuccess) { |
|
updateEditor?.call(); |
|
return true; |
|
} |
|
|
|
final pasteUsingHtmlSuccess = await _pasteHTML(); |
|
if (pasteUsingHtmlSuccess) { |
|
updateEditor?.call(); |
|
return true; |
|
} |
|
|
|
final pasteUsingMarkdownSuccess = await _pasteMarkdown(); |
|
if (pasteUsingMarkdownSuccess) { |
|
updateEditor?.call(); |
|
return true; |
|
} |
|
|
|
// Snapshot the input before using `await`. |
|
// See https://github.com/flutter/flutter/issues/11427 |
|
final plainTextClipboardData = |
|
await Clipboard.getData(Clipboard.kTextPlain); |
|
if (pasteUsingPlainOrDelta(plainTextClipboardData?.text)) { |
|
updateEditor?.call(); |
|
return true; |
|
} |
|
|
|
if (await configurations.onClipboardPaste?.call() == true) { |
|
updateEditor?.call(); |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
/// Internal method to allow unit testing |
|
bool pasteUsingPlainOrDelta(String? clipboardText) { |
|
if (clipboardText != null) { |
|
/// Internal copy-paste preserves styles and embeds |
|
if (clipboardText == _pastePlainText && |
|
_pastePlainText.isNotEmpty && |
|
_pasteDelta.isNotEmpty) { |
|
replaceText(selection.start, selection.end - selection.start, |
|
_pasteDelta, TextSelection.collapsed(offset: selection.end)); |
|
} else { |
|
replaceText( |
|
selection.start, |
|
selection.end - selection.start, |
|
clipboardText, |
|
TextSelection.collapsed( |
|
offset: selection.end + clipboardText.length)); |
|
} |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
void _pasteUsingDelta(Delta deltaFromClipboard) { |
|
replaceText( |
|
selection.start, |
|
selection.end - selection.start, |
|
deltaFromClipboard, |
|
TextSelection.collapsed(offset: selection.end), |
|
); |
|
} |
|
|
|
/// Return true if can paste internal image |
|
Future<bool> _pasteInternalImage() async { |
|
final copiedImageUrl = _copiedImageUrl; |
|
if (copiedImageUrl != null) { |
|
final index = selection.baseOffset; |
|
final length = selection.extentOffset - index; |
|
replaceText( |
|
index, |
|
length, |
|
BlockEmbed.image(copiedImageUrl.url), |
|
null, |
|
); |
|
if (copiedImageUrl.styleString.isNotEmpty) { |
|
formatText( |
|
getEmbedNode(this, index + 1).offset, |
|
1, |
|
StyleAttribute(copiedImageUrl.styleString), |
|
); |
|
} |
|
_copiedImageUrl = null; |
|
await Clipboard.setData( |
|
const ClipboardData(text: ''), |
|
); |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
/// Return true if can paste using HTML |
|
Future<bool> _pasteHTML() async { |
|
final clipboardService = ClipboardServiceProvider.instance; |
|
|
|
Future<String?> getHTML() async { |
|
if (await clipboardService.canProvideHtmlTextFromFile()) { |
|
return await clipboardService.getHtmlTextFromFile(); |
|
} |
|
if (await clipboardService.canProvideHtmlText()) { |
|
return await clipboardService.getHtmlText(); |
|
} |
|
return null; |
|
} |
|
|
|
final htmlText = await getHTML(); |
|
if (htmlText != null) { |
|
final htmlBody = html_parser.parse(htmlText).body?.outerHtml; |
|
// ignore: deprecated_member_use_from_same_package |
|
final deltaFromClipboard = DeltaX.fromHtml(htmlBody ?? htmlText); |
|
|
|
_pasteUsingDelta(deltaFromClipboard); |
|
|
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
/// Return true if can paste using Markdown |
|
Future<bool> _pasteMarkdown() async { |
|
final clipboardService = ClipboardServiceProvider.instance; |
|
|
|
Future<String?> getMarkdown() async { |
|
if (await clipboardService.canProvideMarkdownTextFromFile()) { |
|
return await clipboardService.getMarkdownTextFromFile(); |
|
} |
|
if (await clipboardService.canProvideMarkdownText()) { |
|
return await clipboardService.getMarkdownText(); |
|
} |
|
return null; |
|
} |
|
|
|
final markdownText = await getMarkdown(); |
|
if (markdownText != null) { |
|
// ignore: deprecated_member_use_from_same_package |
|
final deltaFromClipboard = DeltaX.fromMarkdown(markdownText); |
|
|
|
_pasteUsingDelta(deltaFromClipboard); |
|
|
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
void replaceTextWithEmbeds( |
|
int index, |
|
int len, |
|
String insertedText, |
|
TextSelection? textSelection, { |
|
bool ignoreFocus = false, |
|
bool shouldNotifyListeners = true, |
|
}) { |
|
final containsEmbed = |
|
insertedText.codeUnits.contains(Embed.kObjectReplacementInt); |
|
insertedText = |
|
containsEmbed ? _adjustInsertedText(insertedText) : insertedText; |
|
|
|
replaceText(index, len, insertedText, textSelection, |
|
ignoreFocus: ignoreFocus, shouldNotifyListeners: shouldNotifyListeners); |
|
|
|
_applyPasteStyleAndEmbed(insertedText, index, containsEmbed); |
|
} |
|
|
|
void _applyPasteStyleAndEmbed( |
|
String insertedText, int start, bool containsEmbed) { |
|
if (insertedText == pastePlainText && pastePlainText != '' || |
|
containsEmbed) { |
|
final pos = start; |
|
for (final p in pasteStyleAndEmbed) { |
|
final offset = p.offset; |
|
final styleAndEmbed = p.value; |
|
|
|
final local = pos + offset; |
|
if (styleAndEmbed is Embeddable) { |
|
replaceText(local, 0, styleAndEmbed, null); |
|
} else { |
|
final style = styleAndEmbed as Style; |
|
if (style.isInline) { |
|
formatTextStyle(local, p.length!, style); |
|
} else if (style.isBlock) { |
|
final node = document.queryChild(local).node; |
|
if (node != null && p.length == node.length - 1) { |
|
for (final attribute in style.values) { |
|
document.format(local, 0, attribute); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
String _adjustInsertedText(String text) { |
|
final sb = StringBuffer(); |
|
for (var i = 0; i < text.length; i++) { |
|
if (text.codeUnitAt(i) == Embed.kObjectReplacementInt) { |
|
continue; |
|
} |
|
sb.write(text[i]); |
|
} |
|
return sb.toString(); |
|
} |
|
}
|
|
|