controller.dart 5.5 KB

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