Browse Source

Inline toggle style feature (#169)

jolancornevin 5 years ago
parent
commit
7f102b9943

+ 41
- 1
packages/zefyr/lib/src/widgets/controller.dart View File

44
   /// Source of the last text or selection change.
44
   /// Source of the last text or selection change.
45
   ChangeSource get lastChangeSource => _lastChangeSource;
45
   ChangeSource get lastChangeSource => _lastChangeSource;
46
 
46
 
47
+  /// Store any styles attribute that got toggled by the tap of a button
48
+  /// and that has not been applied yet.
49
+  /// It gets reseted after each format action within the [document].
50
+  NotusStyle get toggledStyles => _toggledStyles;
51
+  NotusStyle _toggledStyles = new NotusStyle();
52
+
47
   /// Updates selection with specified [value].
53
   /// Updates selection with specified [value].
48
   ///
54
   ///
49
   /// [value] and [source] cannot be `null`.
55
   /// [value] and [source] cannot be `null`.
104
   /// Resulting change is registered as produced by user action, e.g.
110
   /// Resulting change is registered as produced by user action, e.g.
105
   /// using [ChangeSource.local].
111
   /// using [ChangeSource.local].
106
   ///
112
   ///
113
+  /// It also applies the toggledStyle if needed. And then it resets it
114
+  /// in any cases as we don't want to keep it except on inserts.
115
+  ///
107
   /// Optionally updates selection if provided.
116
   /// Optionally updates selection if provided.
108
   void replaceText(int index, int length, String text,
117
   void replaceText(int index, int length, String text,
109
       {TextSelection selection}) {
118
       {TextSelection selection}) {
111
 
120
 
112
     if (length > 0 || text.isNotEmpty) {
121
     if (length > 0 || text.isNotEmpty) {
113
       delta = document.replace(index, length, text);
122
       delta = document.replace(index, length, text);
123
+      // If the delta is a classical insert operation and we have toggled
124
+      // some style, then we apply it to our document.
125
+      if (delta != null &&
126
+          toggledStyles.isNotEmpty &&
127
+          delta.length == 2 &&
128
+          delta[1].isInsert) {
129
+        // Apply it.
130
+        Delta retainDelta = new Delta()
131
+          ..retain(index)
132
+          ..retain(1, toggledStyles.toJson());
133
+        document.compose(retainDelta, ChangeSource.local);
134
+      }
114
     }
135
     }
115
 
136
 
137
+    // Always reset it after any user action, even if it has not been applied.
138
+    _toggledStyles = new NotusStyle();
139
+
116
     if (selection != null) {
140
     if (selection != null) {
117
       if (delta == null) {
141
       if (delta == null) {
118
         _updateSelectionSilent(selection, source: ChangeSource.local);
142
         _updateSelectionSilent(selection, source: ChangeSource.local);
140
   void formatText(int index, int length, NotusAttribute attribute) {
164
   void formatText(int index, int length, NotusAttribute attribute) {
141
     final change = document.format(index, length, attribute);
165
     final change = document.format(index, length, attribute);
142
     _lastChangeSource = ChangeSource.local;
166
     _lastChangeSource = ChangeSource.local;
167
+
168
+    if (length == 0 &&
169
+        (attribute.key == NotusAttribute.bold.key ||
170
+            attribute.key == NotusAttribute.italic.key)) {
171
+      // Add the attribute to our toggledStyle. It will be used later upon insertion.
172
+      _toggledStyles = toggledStyles.put(attribute);
173
+    }
174
+
143
     // Transform selection against the composed change and give priority to
175
     // Transform selection against the composed change and give priority to
144
     // the change. This is needed in cases when format operation actually
176
     // the change. This is needed in cases when format operation actually
145
     // inserts data into the document (e.g. embeds).
177
     // inserts data into the document (e.g. embeds).
160
     formatText(index, length, attribute);
192
     formatText(index, length, attribute);
161
   }
193
   }
162
 
194
 
195
+  /// Returns style of specified text range.
196
+  ///
197
+  /// If nothing is selected but we've toggled an attribute,
198
+  ///  we also merge those in our style before returning.
163
   NotusStyle getSelectionStyle() {
199
   NotusStyle getSelectionStyle() {
164
     int start = _selection.start;
200
     int start = _selection.start;
165
     int length = _selection.end - start;
201
     int length = _selection.end - start;
166
-    return _document.collectStyle(start, length);
202
+    var lineStyle = _document.collectStyle(start, length);
203
+
204
+    lineStyle = lineStyle.mergeAll(toggledStyles);
205
+
206
+    return lineStyle;
167
   }
207
   }
168
 
208
 
169
   TextEditingValue get plainTextEditingValue {
209
   TextEditingValue get plainTextEditingValue {

+ 57
- 0
packages/zefyr/test/widgets/controller_test.dart View File

90
       expect(controller.lastChangeSource, ChangeSource.local);
90
       expect(controller.lastChangeSource, ChangeSource.local);
91
     });
91
     });
92
 
92
 
93
+    test('formatText with toggled style enabled', () {
94
+      bool notified = false;
95
+      controller.addListener(() {
96
+        notified = true;
97
+      });
98
+      controller.replaceText(0, 0, 'Words');
99
+      controller.formatText(2, 0, NotusAttribute.bold);
100
+      // Test that doing nothing does reset the toggledStyle.
101
+      controller.replaceText(2, 0, '');
102
+      controller.replaceText(2, 0, 'n');
103
+      controller.formatText(3, 0, NotusAttribute.bold);
104
+      controller.replaceText(3, 0, 'B');
105
+      expect(notified, isTrue);
106
+
107
+      expect(
108
+        controller.document.toDelta(),
109
+        new Delta()
110
+          ..insert('Won')
111
+          ..insert('B', NotusAttribute.bold.toJson())
112
+          ..insert('rds')
113
+          ..insert('\n'),
114
+      );
115
+      expect(controller.lastChangeSource, ChangeSource.local);
116
+    });
117
+
118
+    test('insert text with toggled style unset', () {
119
+      bool notified = false;
120
+      controller.addListener(() {
121
+        notified = true;
122
+      });
123
+      controller.replaceText(0, 0, 'Words');
124
+      controller.formatText(1, 0, NotusAttribute.bold);
125
+      controller.replaceText(1, 0, 'B');
126
+      controller.formatText(2, 0, NotusAttribute.bold.unset);
127
+      controller.replaceText(2, 0, 'u');
128
+
129
+      expect(notified, isTrue);
130
+      expect(
131
+        controller.document.toDelta(),
132
+        new Delta()
133
+          ..insert('W')
134
+          ..insert('B', NotusAttribute.bold.toJson())
135
+          ..insert('uords')
136
+          ..insert('\n'),
137
+      );
138
+      expect(controller.lastChangeSource, ChangeSource.local);
139
+    });
140
+
93
     test('formatSelection', () {
141
     test('formatSelection', () {
94
       bool notified = false;
142
       bool notified = false;
95
       controller.addListener(() {
143
       controller.addListener(() {
113
       var result = controller.getSelectionStyle();
161
       var result = controller.getSelectionStyle();
114
       expect(result.values, [NotusAttribute.bold]);
162
       expect(result.values, [NotusAttribute.bold]);
115
     });
163
     });
164
+
165
+    test('getSelectionStyle with toggled style', () {
166
+      var selection = new TextSelection.collapsed(offset: 3);
167
+      controller.replaceText(0, 0, 'Words', selection: selection);
168
+      controller.formatText(3, 0, NotusAttribute.bold);
169
+
170
+      var result = controller.getSelectionStyle();
171
+      expect(result.values, [NotusAttribute.bold]);
172
+    });
116
   });
173
   });
117
 }
174
 }