controller.dart 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. import 'dart:math' as math;
  2. import 'package:flutter/material.dart';
  3. import 'package:tuple/tuple.dart';
  4. import '../model/quill_delta.dart';
  5. import '../util/delta_diff.dart';
  6. import '../model/document/attribute.dart';
  7. import '../model/document/document.dart';
  8. import '../model/document/style.dart';
  9. import '../model/document/node/embed.dart';
  10. class EditorController extends ChangeNotifier {
  11. final Document document;
  12. TextSelection selection;
  13. Style toggledStyle = Style();
  14. EditorController({
  15. required this.document,
  16. required this.selection,
  17. });
  18. // item1: Document state before [change].
  19. // item2: Change delta applied to the document.
  20. // item3: The source of this change.
  21. Stream<Tuple3<Delta, Delta, ChangeSource>> get changes => document.changes;
  22. TextEditingValue get plainTextEditingValue => TextEditingValue(
  23. text: document.toPlainText(),
  24. selection: selection,
  25. );
  26. Style getSelectionStyle() =>
  27. document.collectStyle(selection.start, selection.end - selection.start)
  28. ..mergeAll(toggledStyle);
  29. bool get hasUndo => document.hasUndo;
  30. bool get hasRedo => document.hasRedo;
  31. void undo() {
  32. final action = document.undo();
  33. if (action.item1) {
  34. _handleHistoryChange(action.item2);
  35. }
  36. }
  37. void redo() {
  38. final action = document.redo();
  39. if (action.item1) {
  40. _handleHistoryChange(action.item2);
  41. }
  42. }
  43. void save() {
  44. // no need to save, deprecated
  45. }
  46. @override
  47. void dispose() {
  48. document.close();
  49. super.dispose();
  50. }
  51. void updateSelection(TextSelection textSelection, ChangeSource source) {
  52. _updateSelection(textSelection, source);
  53. notifyListeners();
  54. }
  55. void formatSelection(Attribute? attribute) {
  56. formatText(selection.start, selection.end - selection.start, attribute);
  57. }
  58. void formatText(int index, int length, Attribute? attribute) {
  59. if (length == 0 &&
  60. attribute!.isInline &&
  61. attribute.key != Attribute.link.key) {
  62. toggledStyle = toggledStyle.put(attribute);
  63. }
  64. // final change =
  65. // document.format(index, length, LinkAttribute("www.baidu.com"));
  66. final change = document.format(index, length, attribute);
  67. final adjustedSelection = selection.copyWith(
  68. baseOffset: change.transformPosition(selection.baseOffset),
  69. extentOffset: change.transformPosition(selection.extentOffset),
  70. );
  71. if (selection != adjustedSelection) {
  72. _updateSelection(adjustedSelection, ChangeSource.LOCAL);
  73. }
  74. notifyListeners();
  75. }
  76. void replaceText(
  77. int index, int length, Object? data, TextSelection? textSelection) {
  78. assert(data is String || data is Embeddable);
  79. Delta? delta;
  80. if (length > 0 || data is! String || data.isNotEmpty) {
  81. delta = document.replace(index, length, data);
  82. var shouldRetainDelta = toggledStyle.isNotEmpty &&
  83. delta.isNotEmpty &&
  84. delta.length <= 2 &&
  85. delta.last.isInsert;
  86. if (shouldRetainDelta &&
  87. toggledStyle.isNotEmpty &&
  88. delta.length == 2 &&
  89. delta.last.data == '\n') {
  90. // if all attributes are inline, shouldRetainDelta should be false
  91. final anyAttributeNotInline =
  92. toggledStyle.values.any((attr) => !attr.isInline);
  93. shouldRetainDelta &= anyAttributeNotInline;
  94. }
  95. if (shouldRetainDelta) {
  96. final retainDelta = Delta()
  97. ..retain(index)
  98. ..retain(
  99. data is String ? data.length : 1,
  100. toggledStyle.toJson(),
  101. );
  102. document.compose(retainDelta, ChangeSource.LOCAL);
  103. }
  104. }
  105. toggledStyle = Style();
  106. if (textSelection != null) {
  107. if (delta == null || delta.isEmpty) {
  108. _updateSelection(textSelection, ChangeSource.LOCAL);
  109. } else {
  110. final user = Delta()
  111. ..retain(index)
  112. ..insert(data)
  113. ..delete(length);
  114. final positionDelta = getPositionDelta(user, delta);
  115. _updateSelection(
  116. textSelection.copyWith(
  117. baseOffset: textSelection.baseOffset + positionDelta,
  118. extentOffset: textSelection.extentOffset + positionDelta,
  119. ),
  120. ChangeSource.LOCAL);
  121. }
  122. }
  123. notifyListeners();
  124. }
  125. void compose(Delta delta, TextSelection textSelection, ChangeSource source) {
  126. if (delta.isNotEmpty) {
  127. document.compose(delta, source);
  128. }
  129. textSelection = selection.copyWith(
  130. baseOffset: delta.transformPosition(selection.baseOffset, force: false),
  131. extentOffset:
  132. delta.transformPosition(selection.extentOffset, force: false),
  133. );
  134. if (selection != textSelection) {
  135. _updateSelection(textSelection, source);
  136. }
  137. notifyListeners();
  138. }
  139. /* --------------------------------- Helper --------------------------------- */
  140. void _handleHistoryChange(int? length) {
  141. if (length != 0) {
  142. updateSelection(
  143. TextSelection.collapsed(offset: selection.baseOffset + length!),
  144. ChangeSource.LOCAL,
  145. );
  146. } else {
  147. // no need to move cursor
  148. notifyListeners();
  149. }
  150. }
  151. void _updateSelection(TextSelection textSelection, ChangeSource source) {
  152. selection = textSelection;
  153. final end = document.length - 1;
  154. selection = selection.copyWith(
  155. baseOffset: math.min(selection.baseOffset, end),
  156. extentOffset: math.min(selection.extentOffset, end),
  157. );
  158. }
  159. }