Skip to content

Commit

Permalink
feat(web): rich text paste from Clipboard using HTML (#2009)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
EchoEllet committed Sep 13, 2024
1 parent 1bdacd7 commit e51add5
Show file tree
Hide file tree
Showing 5 changed files with 165 additions and 70 deletions.
87 changes: 17 additions & 70 deletions lib/src/controller/quill_controller.dart
Original file line number Diff line number Diff line change
@@ -1,26 +1,28 @@
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';
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';
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);
Expand All @@ -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 =
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -472,6 +478,9 @@ class QuillController extends ChangeNotifier {
}

_isDisposed = true;
if (kIsWeb) {
closeWebPasteEvent();
}
super.dispose();
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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<bool> _pasteInternalImage() async {
final copiedImageUrl = _copiedImageUrl;
Expand Down Expand Up @@ -653,59 +653,6 @@ class QuillController extends ChangeNotifier {
return false;
}

/// Return true if can paste using HTML
Future<bool> _pasteHTML() async {
final clipboardService = ClipboardServiceProvider.instance;

Future<String?> 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<bool> _pasteMarkdown() async {
final clipboardService = ClipboardServiceProvider.instance;

Future<String?> 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,
Expand Down
93 changes: 93 additions & 0 deletions lib/src/controller/quill_controller_rich_paste.dart
Original file line number Diff line number Diff line change
@@ -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<bool> pasteHTML({String? html}) async {
final clipboardService = ClipboardServiceProvider.instance;

Future<String?> 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<bool> pasteMarkdown({String? markdown}) async {
final clipboardService = ClipboardServiceProvider.instance;

Future<String?> 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),
);
}
}
34 changes: 34 additions & 0 deletions lib/src/controller/web/quill_controller_web_real.dart
Original file line number Diff line number Diff line change
@@ -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;
}
}
20 changes: 20 additions & 0 deletions lib/src/controller/web/quill_controller_web_stub.dart
Original file line number Diff line number Diff line change
@@ -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.',
);
}
}
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down

0 comments on commit e51add5

Please sign in to comment.