diff --git a/lib/src/controller/quill_controller.dart b/lib/src/controller/quill_controller.dart index e5b8e320a..ed8695784 100644 --- a/lib/src/controller/quill_controller.dart +++ b/lib/src/controller/quill_controller.dart @@ -1,8 +1,8 @@ import 'dart:math' as math; +import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/services.dart' show ClipboardData, Clipboard; import 'package:flutter/widgets.dart'; -import 'package:html/parser.dart' as html_parser; import 'package:meta/meta.dart' show experimental; import '../../quill_delta.dart'; @@ -10,7 +10,6 @@ import '../common/structs/image_url.dart'; import '../common/structs/offset_value.dart'; import '../common/utils/embeds.dart'; import '../delta/delta_diff.dart'; -import '../delta/delta_x.dart'; import '../document/attribute.dart'; import '../document/document.dart'; import '../document/nodes/embeddable.dart'; @@ -18,9 +17,12 @@ import '../document/nodes/leaf.dart'; import '../document/structs/doc_change.dart'; import '../document/style.dart'; import '../editor/config/editor_configurations.dart'; -import '../editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart'; import '../toolbar/config/simple_toolbar_configurations.dart'; import 'quill_controller_configurations.dart'; +import 'quill_controller_rich_paste.dart'; + +import 'web/quill_controller_web_stub.dart' + if (dart.library.html) 'web/quill_controller_web_real.dart'; typedef ReplaceTextCallback = bool Function(int index, int len, Object? data); typedef DeleteCallback = void Function(int cursorPosition, bool forward); @@ -38,7 +40,11 @@ class QuillController extends ChangeNotifier { this.readOnly = false, this.editorFocusNode, }) : _document = document, - _selection = selection; + _selection = selection { + if (kIsWeb) { + initializeWebPasteEvent(); + } + } factory QuillController.basic( {QuillControllerConfigurations configurations = @@ -132,8 +138,8 @@ class QuillController extends ChangeNotifier { bool ignoreFocusOnTextChange = false; - /// Skip requestKeyboard being called in - /// RawEditorState#_didChangeTextEditingValue + /// Skip requestKeyboard being called + /// in [QuillRawEditorState._didChangeTextEditingValue] bool skipRequestKeyboard = false; /// True when this [QuillController] instance has been disposed. @@ -472,6 +478,9 @@ class QuillController extends ChangeNotifier { } _isDisposed = true; + if (kIsWeb) { + closeWebPasteEvent(); + } super.dispose(); } @@ -565,13 +574,13 @@ class QuillController extends ChangeNotifier { return true; } - final pasteUsingHtmlSuccess = await _pasteHTML(); + final pasteUsingHtmlSuccess = await pasteHTML(); if (pasteUsingHtmlSuccess) { updateEditor?.call(); return true; } - final pasteUsingMarkdownSuccess = await _pasteMarkdown(); + final pasteUsingMarkdownSuccess = await pasteMarkdown(); if (pasteUsingMarkdownSuccess) { updateEditor?.call(); return true; @@ -616,15 +625,6 @@ class QuillController extends ChangeNotifier { return false; } - void _pasteUsingDelta(Delta deltaFromClipboard) { - replaceText( - selection.start, - selection.end - selection.start, - deltaFromClipboard, - TextSelection.collapsed(offset: selection.end), - ); - } - /// Return true if can paste internal image Future _pasteInternalImage() async { final copiedImageUrl = _copiedImageUrl; @@ -653,59 +653,6 @@ class QuillController extends ChangeNotifier { return false; } - /// Return true if can paste using HTML - Future _pasteHTML() async { - final clipboardService = ClipboardServiceProvider.instance; - - Future getHTML() async { - if (await clipboardService.canProvideHtmlTextFromFile()) { - return await clipboardService.getHtmlTextFromFile(); - } - if (await clipboardService.canProvideHtmlText()) { - return await clipboardService.getHtmlText(); - } - return null; - } - - final htmlText = await getHTML(); - if (htmlText != null) { - final htmlBody = html_parser.parse(htmlText).body?.outerHtml; - // ignore: deprecated_member_use_from_same_package - final deltaFromClipboard = DeltaX.fromHtml(htmlBody ?? htmlText); - - _pasteUsingDelta(deltaFromClipboard); - - return true; - } - return false; - } - - /// Return true if can paste using Markdown - Future _pasteMarkdown() async { - final clipboardService = ClipboardServiceProvider.instance; - - Future getMarkdown() async { - if (await clipboardService.canProvideMarkdownTextFromFile()) { - return await clipboardService.getMarkdownTextFromFile(); - } - if (await clipboardService.canProvideMarkdownText()) { - return await clipboardService.getMarkdownText(); - } - return null; - } - - final markdownText = await getMarkdown(); - if (markdownText != null) { - // ignore: deprecated_member_use_from_same_package - final deltaFromClipboard = DeltaX.fromMarkdown(markdownText); - - _pasteUsingDelta(deltaFromClipboard); - - return true; - } - return false; - } - void replaceTextWithEmbeds( int index, int len, diff --git a/lib/src/controller/quill_controller_rich_paste.dart b/lib/src/controller/quill_controller_rich_paste.dart new file mode 100644 index 000000000..ed1ca318c --- /dev/null +++ b/lib/src/controller/quill_controller_rich_paste.dart @@ -0,0 +1,93 @@ +// This file should not be exported as the APIs in it are meant for internal usage only + +import 'package:flutter/widgets.dart' show TextSelection; +import 'package:html/parser.dart' as html_parser; + +import '../../quill_delta.dart'; +import '../delta/delta_x.dart'; +import '../editor_toolbar_controller_shared/clipboard/clipboard_service_provider.dart'; +import 'quill_controller.dart'; + +extension QuillControllerRichPaste on QuillController { + /// Paste the HTML into the document from [html] if not null, otherwise + /// will read it from the Clipboard in case the [ClipboardServiceProvider.instance] + /// support it on the current platform. + /// + /// The argument [html] allow to override the HTML that's being pasted, + /// mainly to support pasting HTML on the web in [_webPasteEventSubscription]. + /// + /// Return `true` if can paste or have pasted using HTML. + Future pasteHTML({String? html}) async { + final clipboardService = ClipboardServiceProvider.instance; + + Future getHTML() async { + if (html != null) { + return html; + } + if (await clipboardService.canProvideHtmlTextFromFile()) { + return await clipboardService.getHtmlTextFromFile(); + } + if (await clipboardService.canProvideHtmlText()) { + return await clipboardService.getHtmlText(); + } + return null; + } + + final htmlText = await getHTML(); + if (htmlText != null) { + final htmlBody = html_parser.parse(htmlText).body?.outerHtml; + // ignore: deprecated_member_use_from_same_package + final deltaFromClipboard = DeltaX.fromHtml(htmlBody ?? htmlText); + + _pasteUsingDelta(deltaFromClipboard); + + return true; + } + return false; + } + + // Paste the Markdown into the document from [markdown] if not null, otherwise + /// will read it from the Clipboard in case the [ClipboardServiceProvider.instance] + /// support it on the current platform. + /// + /// The argument [markdown] allow to override the Markdown that's being pasted, + /// mainly to support pasting Markdown on the web in [_webPasteEventSubscription]. + /// + /// Return `true` if can paste or have pasted using Markdown. + Future pasteMarkdown({String? markdown}) async { + final clipboardService = ClipboardServiceProvider.instance; + + Future getMarkdown() async { + if (markdown != null) { + return markdown; + } + if (await clipboardService.canProvideMarkdownTextFromFile()) { + return await clipboardService.getMarkdownTextFromFile(); + } + if (await clipboardService.canProvideMarkdownText()) { + return await clipboardService.getMarkdownText(); + } + return null; + } + + final markdownText = await getMarkdown(); + if (markdownText != null) { + // ignore: deprecated_member_use_from_same_package + final deltaFromClipboard = DeltaX.fromMarkdown(markdownText); + + _pasteUsingDelta(deltaFromClipboard); + + return true; + } + return false; + } + + void _pasteUsingDelta(Delta deltaFromClipboard) { + replaceText( + selection.start, + selection.end - selection.start, + deltaFromClipboard, + TextSelection.collapsed(offset: selection.end), + ); + } +} diff --git a/lib/src/controller/web/quill_controller_web_real.dart b/lib/src/controller/web/quill_controller_web_real.dart new file mode 100644 index 000000000..b745f0ac2 --- /dev/null +++ b/lib/src/controller/web/quill_controller_web_real.dart @@ -0,0 +1,34 @@ +// This file should not be exported as the APIs in it are meant for internal usage only + +import 'dart:async' show StreamSubscription; + +import 'package:web/web.dart'; + +import '../quill_controller.dart'; +import '../quill_controller_rich_paste.dart'; + +/// Paste event for the web. +/// +/// Will be `null` for non-web platforms. +/// +/// See: https://developer.mozilla.org/en-US/docs/Web/API/Element/paste_event +StreamSubscription? _webPasteEventSubscription; + +extension QuillControllerWeb on QuillController { + void initializeWebPasteEvent() { + _webPasteEventSubscription = + EventStreamProviders.pasteEvent.forTarget(window.document).listen((e) { + // TODO: See if we can support markdown paste + final html = e.clipboardData?.getData('text/html'); + if (html == null) { + return; + } + pasteHTML(html: html); + }); + } + + void closeWebPasteEvent() { + _webPasteEventSubscription?.cancel(); + _webPasteEventSubscription = null; + } +} diff --git a/lib/src/controller/web/quill_controller_web_stub.dart b/lib/src/controller/web/quill_controller_web_stub.dart new file mode 100644 index 000000000..ef30a29a8 --- /dev/null +++ b/lib/src/controller/web/quill_controller_web_stub.dart @@ -0,0 +1,20 @@ +// This file should not be exported as the APIs in it are meant for internal usage only + +import '../quill_controller.dart'; + +// This is a mock implementation to compile the app on non-web platforms. +// The real implementation is quill_controller_web_real.dart + +extension QuillControllerWeb on QuillController { + void initializeWebPasteEvent() { + throw UnsupportedError( + 'The initializeWebPasteEvent() method should be called only on web.', + ); + } + + void closeWebPasteEvent() { + throw UnsupportedError( + 'The closeWebPasteEvent() method should be called only on web.', + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml index 5bc566caa..462346a9f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -50,6 +50,7 @@ dependencies: equatable: ^2.0.5 meta: ^1.10.0 html: ^0.15.4 + web: ^1.0.0 flutter_colorpicker: ^1.1.0