Skip to content

Commit e51add5

Browse files
authored
feat(web): rich text paste from Clipboard using HTML (singerdmx#2009)
* feat(web): support rich text paste from Clipboard using HTML * chore: moving the comment of internal usage only in quill_controller_rich_paste.dart to the start of the file
1 parent 1bdacd7 commit e51add5

File tree

5 files changed

+165
-70
lines changed

5 files changed

+165
-70
lines changed

lib/src/controller/quill_controller.dart

Lines changed: 17 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,28 @@
11
import 'dart:math' as math;
22

3+
import 'package:flutter/foundation.dart' show kIsWeb;
34
import 'package:flutter/services.dart' show ClipboardData, Clipboard;
45
import 'package:flutter/widgets.dart';
5-
import 'package:html/parser.dart' as html_parser;
66
import 'package:meta/meta.dart' show experimental;
77

88
import '../../quill_delta.dart';
99
import '../common/structs/image_url.dart';
1010
import '../common/structs/offset_value.dart';
1111
import '../common/utils/embeds.dart';
1212
import '../delta/delta_diff.dart';
13-
import '../delta/delta_x.dart';
1413
import '../document/attribute.dart';
1514
import '../document/document.dart';
1615
import '../document/nodes/embeddable.dart';
1716
import '../document/nodes/leaf.dart';
1817
import '../document/structs/doc_change.dart';
1918
import '../document/style.dart';
2019
import '../editor/config/editor_configurations.dart';
21-
import '../editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart';
2220
import '../toolbar/config/simple_toolbar_configurations.dart';
2321
import 'quill_controller_configurations.dart';
22+
import 'quill_controller_rich_paste.dart';
23+
24+
import 'web/quill_controller_web_stub.dart'
25+
if (dart.library.html) 'web/quill_controller_web_real.dart';
2426

2527
typedef ReplaceTextCallback = bool Function(int index, int len, Object? data);
2628
typedef DeleteCallback = void Function(int cursorPosition, bool forward);
@@ -38,7 +40,11 @@ class QuillController extends ChangeNotifier {
3840
this.readOnly = false,
3941
this.editorFocusNode,
4042
}) : _document = document,
41-
_selection = selection;
43+
_selection = selection {
44+
if (kIsWeb) {
45+
initializeWebPasteEvent();
46+
}
47+
}
4248

4349
factory QuillController.basic(
4450
{QuillControllerConfigurations configurations =
@@ -132,8 +138,8 @@ class QuillController extends ChangeNotifier {
132138

133139
bool ignoreFocusOnTextChange = false;
134140

135-
/// Skip requestKeyboard being called in
136-
/// RawEditorState#_didChangeTextEditingValue
141+
/// Skip requestKeyboard being called
142+
/// in [QuillRawEditorState._didChangeTextEditingValue]
137143
bool skipRequestKeyboard = false;
138144

139145
/// True when this [QuillController] instance has been disposed.
@@ -472,6 +478,9 @@ class QuillController extends ChangeNotifier {
472478
}
473479

474480
_isDisposed = true;
481+
if (kIsWeb) {
482+
closeWebPasteEvent();
483+
}
475484
super.dispose();
476485
}
477486

@@ -565,13 +574,13 @@ class QuillController extends ChangeNotifier {
565574
return true;
566575
}
567576

568-
final pasteUsingHtmlSuccess = await _pasteHTML();
577+
final pasteUsingHtmlSuccess = await pasteHTML();
569578
if (pasteUsingHtmlSuccess) {
570579
updateEditor?.call();
571580
return true;
572581
}
573582

574-
final pasteUsingMarkdownSuccess = await _pasteMarkdown();
583+
final pasteUsingMarkdownSuccess = await pasteMarkdown();
575584
if (pasteUsingMarkdownSuccess) {
576585
updateEditor?.call();
577586
return true;
@@ -616,15 +625,6 @@ class QuillController extends ChangeNotifier {
616625
return false;
617626
}
618627

619-
void _pasteUsingDelta(Delta deltaFromClipboard) {
620-
replaceText(
621-
selection.start,
622-
selection.end - selection.start,
623-
deltaFromClipboard,
624-
TextSelection.collapsed(offset: selection.end),
625-
);
626-
}
627-
628628
/// Return true if can paste internal image
629629
Future<bool> _pasteInternalImage() async {
630630
final copiedImageUrl = _copiedImageUrl;
@@ -653,59 +653,6 @@ class QuillController extends ChangeNotifier {
653653
return false;
654654
}
655655

656-
/// Return true if can paste using HTML
657-
Future<bool> _pasteHTML() async {
658-
final clipboardService = ClipboardServiceProvider.instance;
659-
660-
Future<String?> getHTML() async {
661-
if (await clipboardService.canProvideHtmlTextFromFile()) {
662-
return await clipboardService.getHtmlTextFromFile();
663-
}
664-
if (await clipboardService.canProvideHtmlText()) {
665-
return await clipboardService.getHtmlText();
666-
}
667-
return null;
668-
}
669-
670-
final htmlText = await getHTML();
671-
if (htmlText != null) {
672-
final htmlBody = html_parser.parse(htmlText).body?.outerHtml;
673-
// ignore: deprecated_member_use_from_same_package
674-
final deltaFromClipboard = DeltaX.fromHtml(htmlBody ?? htmlText);
675-
676-
_pasteUsingDelta(deltaFromClipboard);
677-
678-
return true;
679-
}
680-
return false;
681-
}
682-
683-
/// Return true if can paste using Markdown
684-
Future<bool> _pasteMarkdown() async {
685-
final clipboardService = ClipboardServiceProvider.instance;
686-
687-
Future<String?> getMarkdown() async {
688-
if (await clipboardService.canProvideMarkdownTextFromFile()) {
689-
return await clipboardService.getMarkdownTextFromFile();
690-
}
691-
if (await clipboardService.canProvideMarkdownText()) {
692-
return await clipboardService.getMarkdownText();
693-
}
694-
return null;
695-
}
696-
697-
final markdownText = await getMarkdown();
698-
if (markdownText != null) {
699-
// ignore: deprecated_member_use_from_same_package
700-
final deltaFromClipboard = DeltaX.fromMarkdown(markdownText);
701-
702-
_pasteUsingDelta(deltaFromClipboard);
703-
704-
return true;
705-
}
706-
return false;
707-
}
708-
709656
void replaceTextWithEmbeds(
710657
int index,
711658
int len,
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
// This file should not be exported as the APIs in it are meant for internal usage only
2+
3+
import 'package:flutter/widgets.dart' show TextSelection;
4+
import 'package:html/parser.dart' as html_parser;
5+
6+
import '../../quill_delta.dart';
7+
import '../delta/delta_x.dart';
8+
import '../editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart';
9+
import 'quill_controller.dart';
10+
11+
extension QuillControllerRichPaste on QuillController {
12+
/// Paste the HTML into the document from [html] if not null, otherwise
13+
/// will read it from the Clipboard in case the [ClipboardServiceProvider.instance]
14+
/// support it on the current platform.
15+
///
16+
/// The argument [html] allow to override the HTML that's being pasted,
17+
/// mainly to support pasting HTML on the web in [_webPasteEventSubscription].
18+
///
19+
/// Return `true` if can paste or have pasted using HTML.
20+
Future<bool> pasteHTML({String? html}) async {
21+
final clipboardService = ClipboardServiceProvider.instance;
22+
23+
Future<String?> getHTML() async {
24+
if (html != null) {
25+
return html;
26+
}
27+
if (await clipboardService.canProvideHtmlTextFromFile()) {
28+
return await clipboardService.getHtmlTextFromFile();
29+
}
30+
if (await clipboardService.canProvideHtmlText()) {
31+
return await clipboardService.getHtmlText();
32+
}
33+
return null;
34+
}
35+
36+
final htmlText = await getHTML();
37+
if (htmlText != null) {
38+
final htmlBody = html_parser.parse(htmlText).body?.outerHtml;
39+
// ignore: deprecated_member_use_from_same_package
40+
final deltaFromClipboard = DeltaX.fromHtml(htmlBody ?? htmlText);
41+
42+
_pasteUsingDelta(deltaFromClipboard);
43+
44+
return true;
45+
}
46+
return false;
47+
}
48+
49+
// Paste the Markdown into the document from [markdown] if not null, otherwise
50+
/// will read it from the Clipboard in case the [ClipboardServiceProvider.instance]
51+
/// support it on the current platform.
52+
///
53+
/// The argument [markdown] allow to override the Markdown that's being pasted,
54+
/// mainly to support pasting Markdown on the web in [_webPasteEventSubscription].
55+
///
56+
/// Return `true` if can paste or have pasted using Markdown.
57+
Future<bool> pasteMarkdown({String? markdown}) async {
58+
final clipboardService = ClipboardServiceProvider.instance;
59+
60+
Future<String?> getMarkdown() async {
61+
if (markdown != null) {
62+
return markdown;
63+
}
64+
if (await clipboardService.canProvideMarkdownTextFromFile()) {
65+
return await clipboardService.getMarkdownTextFromFile();
66+
}
67+
if (await clipboardService.canProvideMarkdownText()) {
68+
return await clipboardService.getMarkdownText();
69+
}
70+
return null;
71+
}
72+
73+
final markdownText = await getMarkdown();
74+
if (markdownText != null) {
75+
// ignore: deprecated_member_use_from_same_package
76+
final deltaFromClipboard = DeltaX.fromMarkdown(markdownText);
77+
78+
_pasteUsingDelta(deltaFromClipboard);
79+
80+
return true;
81+
}
82+
return false;
83+
}
84+
85+
void _pasteUsingDelta(Delta deltaFromClipboard) {
86+
replaceText(
87+
selection.start,
88+
selection.end - selection.start,
89+
deltaFromClipboard,
90+
TextSelection.collapsed(offset: selection.end),
91+
);
92+
}
93+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// This file should not be exported as the APIs in it are meant for internal usage only
2+
3+
import 'dart:async' show StreamSubscription;
4+
5+
import 'package:web/web.dart';
6+
7+
import '../quill_controller.dart';
8+
import '../quill_controller_rich_paste.dart';
9+
10+
/// Paste event for the web.
11+
///
12+
/// Will be `null` for non-web platforms.
13+
///
14+
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event
15+
StreamSubscription? _webPasteEventSubscription;
16+
17+
extension QuillControllerWeb on QuillController {
18+
void initializeWebPasteEvent() {
19+
_webPasteEventSubscription =
20+
EventStreamProviders.pasteEvent.forTarget(window.document).listen((e) {
21+
// TODO: See if we can support markdown paste
22+
final html = e.clipboardData?.getData('text/html');
23+
if (html == null) {
24+
return;
25+
}
26+
pasteHTML(html: html);
27+
});
28+
}
29+
30+
void closeWebPasteEvent() {
31+
_webPasteEventSubscription?.cancel();
32+
_webPasteEventSubscription = null;
33+
}
34+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// This file should not be exported as the APIs in it are meant for internal usage only
2+
3+
import '../quill_controller.dart';
4+
5+
// This is a mock implementation to compile the app on non-web platforms.
6+
// The real implementation is quill_controller_web_real.dart
7+
8+
extension QuillControllerWeb on QuillController {
9+
void initializeWebPasteEvent() {
10+
throw UnsupportedError(
11+
'The initializeWebPasteEvent() method should be called only on web.',
12+
);
13+
}
14+
15+
void closeWebPasteEvent() {
16+
throw UnsupportedError(
17+
'The closeWebPasteEvent() method should be called only on web.',
18+
);
19+
}
20+
}

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ dependencies:
5050
equatable: ^2.0.5
5151
meta: ^1.10.0
5252
html: ^0.15.4
53+
web: ^1.0.0
5354

5455
flutter_colorpicker: ^1.1.0
5556

0 commit comments

Comments
 (0)