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 '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; /// Local copy of editor configurations enables fail-safe setting from editor _initState method QuillEditorConfigurations? _editorConfigurations; QuillEditorConfigurations? get editorConfigurations => configurations.editorConfigurations ?? _editorConfigurations; set editorConfigurations(QuillEditorConfigurations? value) => _editorConfigurations = 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 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 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