-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy patheditor_api.dart
More file actions
381 lines (324 loc) · 13.1 KB
/
editor_api.dart
File metadata and controls
381 lines (324 loc) · 13.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
import 'dart:convert';
import 'dart:io';
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
import 'package:image/image.dart' as img;
import 'editor.dart';
import 'models.dart';
/// API to control the `HtmlEditor`.
///
/// Get access to this API either by waiting for the `HtmlEditor.onCreated()`
/// callback or by accessing the `HtmlEditorState` with a
/// `GlobalKey<HtmlEditorState>`.
class HtmlEditorApi {
/// Creates a new HTML editor api
HtmlEditorApi(HtmlEditorState htmlEditorState)
: _htmlEditorState = htmlEditorState;
late InAppWebViewController _webViewController;
final HtmlEditorState _htmlEditorState;
/// The document's background color, defaults to `null`
String? _documentBackgroundColor;
/// The document's foreground color, defaults to `null`
String? _documentForegroundColor;
/// The web view controller allows direct interactions
// ignore: unnecessary_getters_setters
InAppWebViewController get webViewController => _webViewController;
set webViewController(InAppWebViewController value) {
_webViewController = value;
//TODO wait for InAppWebView project to approve this
//value.onImeCommitContent = _onImeCommitContent;
}
// void _onImeCommitContent(String mimeType, Uint8List data) {
// // print('HtmlEditor: onImeCommitContent: received $mimeType');
// insertImageData(data, mimeType);
// }
/// Define any custom CSS styles, replacing the existing styles.
///
/// Also compare [customStyles].
String get styles => _htmlEditorState.styles;
set styles(String value) => _htmlEditorState.styles = value;
/// Define any custom CSS styles, amending the default styles
///
/// Also compare [styles].
String get customStyles => _htmlEditorState.styles;
//TODO if called several times, this will change the custom styles each time
set customStyles(String value) => _htmlEditorState.styles += value;
/// Callback to be informed when the API can be used fully.
void Function()? onReady;
/// Callback to be informed when the format settings have been changed
void Function(FormatSettings)? onFormatSettingsChanged;
/// Callback to be informed when the align settings have been changed
void Function(ElementAlign)? onAlignSettingsChanged;
/// Callback to be informed when the font size has been changed
void Function(FontSize)? onFontSizeChanged;
/// Callback to be informed when the font family has been changed
void Function(SafeFont?)? onFontFamilyChanged;
final List<void Function(ColorSetting)> _colorChangedSettings = [];
/// Callback to be informed when the color settings have been changed
set onColorChanged(void Function(ColorSetting)? value) {
if (value != null && !_colorChangedSettings.contains(value)) {
_colorChangedSettings.add(value);
}
}
/// Getter for color changes callback
void Function(ColorSetting)? get onColorChanged {
if (_colorChangedSettings.isEmpty) {
return null;
}
return _callOnColorChanged;
}
/// Callback to be informed when the link settings have been changed.
///
/// Getting a `null` value as the current link settings means that
/// no link is currently selected
void Function(LinkSettings?)? onLinkSettingsChanged;
void _callOnColorChanged(ColorSetting colorSetting) {
for (final callback in _colorChangedSettings) {
callback(colorSetting);
}
}
/// Removes the focus from the editor
Future<void> unfocus(BuildContext context) async {
await _webViewController.clearFocus();
FocusScope.of(context).unfocus();
return SystemChannels.textInput.invokeMethod('TextInput.hide');
}
/// Formats the current text to be bold
Future<void> formatBold() => _execCommand('"bold"');
/// Formats the current text to be italic
Future<void> formatItalic() => _execCommand('"italic"');
/// Formats the current text to be underlined
Future<void> formatUnderline() => _execCommand('"underline"');
/// Formats the current text to be strike through
Future<void> formatStrikeThrough() => _execCommand('"strikeThrough"');
/// Inserts an ordered list at the current position
Future<void> insertOrderedList() => _execCommand('"insertOrderedList"');
/// Inserts an unordered list at the current position
Future<void> insertUnorderedList() => _execCommand('"insertUnorderedList"');
/// Formats the current paragraph to align left
Future<void> formatAlignLeft() => _execCommand('"justifyLeft"');
/// Formats the current paragraph to align right
Future<void> formatAlignRight() => _execCommand('"justifyRight"');
/// Formats the current paragraph to center
Future<void> formatAlignCenter() => _execCommand('"justifyCenter"');
/// Formats the current paragraph to justify
Future<void> formatAlignJustify() => _execCommand('"justifyFull"');
/// Sets the [size] of the selected text
Future<void> setFontSize(FontSize size) =>
_execCommand('"fontSize", false, ${size.index + 1}');
/// Sets the [font] of the selected text
Future<void> setFont(SafeFont font) => setFontFamily(font.name);
/// Sets the [fontFamilyName] of the selected text
Future<void> setFontFamily(String fontFamilyName) =>
_execCommand('"fontName", false, "$fontFamilyName"');
/// Inserts the [html] code at the insertion point (replaces selection).
Future<void> insertHtml(String html) async {
final insertHtml = html.replaceAll('"', r'\"');
await _execCommand('"insertHTML", false, "$insertHtml"');
return _htmlEditorState.onDocumentChanged();
}
/// Inserts the given plain [text] at the insertion point
/// (replaces selection).
Future<void> insertText(String text) async {
await _execCommand('"insertText", false, "$text"');
return _htmlEditorState.onDocumentChanged();
}
/// Inserts a link to [href] at the current position (replaces selection).
///
/// Optionally specify the user-visible [text], by default the [href]
/// will be user visible.
///
/// You can define a link [target] such as `'_blank'`,
/// by default no target will be defined.
Future<void> insertLink(String href, {String? text, String? target}) {
final buffer = StringBuffer()
..write('<a href="')
..write(href)
..write('"');
if (target != null) {
buffer
..write(' target="')
..write(target)
..write('"');
}
buffer
..write('>')
..write(text ?? href)
..write('</a>');
final html = buffer.toString();
return insertHtml(html);
}
/// Converts the given [file] with the specified [mimeType] into
/// image data and inserts it into the editor.
///
/// Optionally set the given [maxWidth] for the decoded image.
Future<void> insertImageFile(File file, String mimeType,
{int? maxWidth}) async {
final data = await file.readAsBytes();
return insertImageData(data, mimeType, maxWidth: maxWidth);
}
/// Inserts the given image [data] with the specified [mimeType]
/// into the editor.
///
/// Optionally set the given [maxWidth] for the decoded image.
Future<void> insertImageData(Uint8List data, String mimeType,
{int? maxWidth}) async {
if (maxWidth != null) {
final image = img.decodeImage(data);
if (image == null) {
return;
}
if (image.width > maxWidth) {
final copy = img.copyResize(image, width: maxWidth);
// ignore: parameter_assignments
data = img.encodePng(copy);
// ignore: parameter_assignments
mimeType = 'image/png';
}
}
final base64Data = base64Encode(data);
return insertHtml(
'<img src="data:$mimeType;base64,$base64Data" style="max-width: 100%" />');
}
String _toHex(Color color) {
final buffer = StringBuffer();
_appendHex(color.red, buffer);
_appendHex(color.green, buffer);
_appendHex(color.blue, buffer);
return buffer.toString();
}
void _appendHex(int value, StringBuffer buffer) {
final text = value.toRadixString(16);
if (text.length < 2) {
buffer.write('0');
}
buffer.write(text);
}
String _getColor(Color color, double opacity) {
if (opacity < 1.0) {
return 'rgba(${color.red},${color.green},${color.blue},$opacity)';
}
return '#${_toHex(color)}';
}
/// Sets the given [color] as the current foreground / text color.
///
/// Optionally specify the [opacity] being between `1.0` (fully opaque)
/// and `0.0` (fully transparent).
Future<void> setColorTextForeground(Color color,
{double opacity = 1.0}) async {
final colorText = _getColor(color, opacity);
return _execCommand('"foreColor", false, "$colorText"');
}
/// Sets the given [color] as the current text background color.
///
/// Optionally specify the [opacity] being between `1.0` (fully opaque) and
/// `0.0` (fully transparent).
Future<void> setColorTextBackground(Color color,
{double opacity = 1.0}) async {
final colorText = _getColor(color, opacity);
return _execCommand('"backColor", false, "$colorText"');
}
/// Sets the document's background color
Future<void> setColorDocumentBackground(Color color) async {
final colorText = _getColor(color, 1.0);
_documentBackgroundColor = colorText;
return _webViewController.evaluateJavascript(
source: 'document.body.style.backgroundColor="$colorText";');
}
/// Sets the document's foreground color
Future<void> setColorDocumentForeground(Color color) async {
final colorText = _getColor(color, 1.0);
_documentForegroundColor = colorText;
return _webViewController.evaluateJavascript(
source: 'document.body.style.color="$colorText";');
}
Future<void> _execCommand(String command) async {
await _webViewController.evaluateJavascript(
source: 'document.execCommand($command);');
}
/// Retrieves the edited text as HTML
///
/// Compare [getFullHtml()] to the complete HTML document's text.
Future<String> getText() async {
final innerHtml = await _webViewController.evaluateJavascript(
source: 'document.getElementById("editor").innerHTML;');
return innerHtml;
}
/// Retrieves the edited text within a complete HTML document.
///
/// Optionally specify the [content] if you have previously called [getText]
/// for other reasons.
///
/// Compare [getText] to retrieve only the edited HTML text.
Future<String> getFullHtml({String? content}) async {
content ??= await getText();
final bodyStyle =
(_documentBackgroundColor != null && _documentForegroundColor != null)
? ' style="color: $_documentForegroundColor;'
'background-color: $_documentBackgroundColor;"'
: _documentForegroundColor != null
? ' style="color: $_documentForegroundColor;"'
: _documentBackgroundColor != null
? ' style="background-color: $_documentBackgroundColor;"'
: '';
final styles = _htmlEditorState.styles.replaceFirst('''#editor {
min-height: ==minHeight==px;
}''', '');
return '''<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="content-type" content="text/html;charset="utf-8">
<style>$styles</style>
</head>
<body$bodyStyle>$content</body>
</html>''';
}
/// Retrieves the currently selected text.
Future<String?> getSelectedText() async {
final text = await _webViewController.evaluateJavascript(
source: '''document.getSelection().getRangeAt(0).toString();''');
if (text.isEmpty || text == 'null') {
return null;
}
return _removeQuotes(text);
}
String _removeQuotes(String text) {
if (text.length > 1 && text.startsWith('"') && text.endsWith('"')) {
return text.substring(1, text.length - 1);
}
return text;
}
/// Stores the current selection and retrieves the selected text.
///
/// Compare [restoreSelectionRange]
Future<String> storeSelectionRange() async {
final text = await _webViewController.evaluateJavascript(
source: 'storeSelectionRange();');
return _removeQuotes(text);
}
/// Restores the previously stored selection range
///
/// Compare [storeSelectionRange]
Future<void> restoreSelectionRange() =>
_webViewController.evaluateJavascript(source: 'restoreSelectionRange();');
/// Replaces all text parts [from] with the replacement [replace]
/// and returns the updated text.
Future<String> replaceAll(String from, String replace) async {
final text = (await getText()).replaceAll(from, replace);
await setText(text);
return text;
}
/// Sets the given text, replacing the previous text completely
Future<void> setText(String text) {
final html = _htmlEditorState.generateHtmlDocument(text);
return _webViewController.loadData(data: html);
}
/// Selects the HTML DOM node at the current position fully.
Future<void> selectCurrentNode() =>
_webViewController.evaluateJavascript(source: 'selectNode();');
/// Updates the currently selected link with the url [href] and [text].
Future<void> editCurrentLink(String href, String text) => _webViewController
.evaluateJavascript(source: "editLink('$href', '$text');");
}