Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 54 additions & 15 deletions lib/web_ui/lib/src/engine/clipboard.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,26 +7,51 @@ part of engine;
/// Handles clipboard related platform messages.
class ClipboardMessageHandler {
/// Helper to handle copy to clipboard functionality.
final CopyToClipboardStrategy _copyToClipboardStrategy =
CopyToClipboardStrategy();
CopyToClipboardStrategy _copyToClipboardStrategy = CopyToClipboardStrategy();

/// Helper to handle copy to clipboard functionality.
final PasteFromClipboardStrategy _pasteFromClipboardStrategy =
PasteFromClipboardStrategy _pasteFromClipboardStrategy =
PasteFromClipboardStrategy();

/// Handles the platform message which stores the given text to the clipboard.
void setDataMethodCall(MethodCall methodCall) {
_copyToClipboardStrategy.setData(methodCall.arguments['text']);
void setDataMethodCall(
MethodCall methodCall, ui.PlatformMessageResponseCallback callback) {
const MethodCodec codec = JSONMethodCodec();
_copyToClipboardStrategy
.setData(methodCall.arguments['text'])
.then((bool success) {
if (success) {
callback(codec.encodeSuccessEnvelope(true));
} else {
callback(codec.encodeErrorEnvelope(
code: 'copy_fail', message: 'Clipboard.setData failed'));
}
}).catchError((_) {
callback(codec.encodeErrorEnvelope(
code: 'copy_fail', message: 'Clipboard.setData failed'));
});
}

/// Handles the platform message which retrieves text data from the clipboard.
void getDataMethodCall(ui.PlatformMessageResponseCallback callback) {
const MethodCodec codec = JSONMethodCodec();
_pasteFromClipboardStrategy.getData().then((String data) {
const MethodCodec codec = JSONMethodCodec();
final Map<String, dynamic> map = {'text': data};
callback(codec.encodeSuccessEnvelope(map));
}).catchError(
(error) => print('Could not get text from clipboard: $error'));
}).catchError((error) {
print('Could not get text from clipboard: $error');
callback(codec.encodeErrorEnvelope(
code: 'paste_fail', message: 'Clipboard.getData failed'));
});
}

/// Methods used by tests.
set pasteFromClipboardStrategy(PasteFromClipboardStrategy strategy) {
_pasteFromClipboardStrategy = strategy;
}

set copyToClipboardStrategy(CopyToClipboardStrategy strategy) {
_copyToClipboardStrategy = strategy;
}
}

Expand All @@ -42,7 +67,11 @@ abstract class CopyToClipboardStrategy {
}

/// Places the text onto the browser Clipboard.
void setData(String text);
///
/// Returns `true` for a successful action.
///
/// Returns `false` for an uncessful action or when there is an excaption.
Future<bool> setData(String text);
}

/// Provides functionality for reading text from clipboard.
Expand All @@ -67,10 +96,14 @@ abstract class PasteFromClipboardStrategy {
/// See: https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API
class ClipboardAPICopyStrategy implements CopyToClipboardStrategy {
@override
void setData(String text) {
html.window.navigator.clipboard
.writeText(text)
.catchError((error) => print('Could not copy text: $error'));
Future<bool> setData(String text) async {
try {
await html.window.navigator.clipboard.writeText(text);
} catch (e) {
print('copy is not successful ${e.message}');
return Future.value(false);
}
return Future.value(true);
}
}

Expand All @@ -90,15 +123,20 @@ class ClipboardAPIPasteStrategy implements PasteFromClipboardStrategy {
/// Provides a fallback strategy for browsers which does not support ClipboardAPI.
class ExecCommandCopyStrategy implements CopyToClipboardStrategy {
@override
void setData(String text) {
Future<bool> setData(String text) {
return Future.value(_setDataSync(text));
}

bool _setDataSync(String text) {
// Copy content to clipboard with execCommand.
// See: https://developers.google.com/web/updates/2015/04/cut-and-copy-commands
final html.TextAreaElement tempTextArea = _appendTemporaryTextArea();
tempTextArea.value = text;
tempTextArea.focus();
tempTextArea.select();
bool result = false;
try {
final bool result = html.document.execCommand('copy');
result = html.document.execCommand('copy');
if (!result) {
print('copy is not successful');
}
Expand All @@ -107,6 +145,7 @@ class ExecCommandCopyStrategy implements CopyToClipboardStrategy {
} finally {
_removeTemporaryTextArea(tempTextArea);
}
return result;
}

html.TextAreaElement _appendTemporaryTextArea() {
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/window.dart
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ class EngineWindow extends ui.Window {
// There are no default system sounds on web.
return;
case 'Clipboard.setData':
ClipboardMessageHandler().setDataMethodCall(decoded);
ClipboardMessageHandler().setDataMethodCall(decoded, callback);
return;
case 'Clipboard.getData':
ClipboardMessageHandler().getDataMethodCall(callback);
Expand Down
97 changes: 97 additions & 0 deletions lib/web_ui/test/clipboard_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:typed_data';

import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/src/engine.dart';

Future<void> main() async {
await ui.webOnlyInitializeTestDomRenderer();
group('message handler', () {
const String testText = 'test text';

final Future<bool> success = Future.value(true);
final Future<bool> failure = Future.value(false);
final Future<String> pasteTest = Future.value(testText);

ClipboardMessageHandler clipboardMessageHandler;
ClipboardAPICopyStrategy clipboardAPICopyStrategy =
MockClipboardAPICopyStrategy();
ClipboardAPIPasteStrategy clipboardAPIPasteStrategy =
MockClipboardAPIPasteStrategy();

setUp(() {
clipboardMessageHandler = new ClipboardMessageHandler();
clipboardAPICopyStrategy = MockClipboardAPICopyStrategy();
clipboardAPIPasteStrategy = MockClipboardAPIPasteStrategy();
clipboardMessageHandler.copyToClipboardStrategy =
clipboardAPICopyStrategy;
clipboardMessageHandler.pasteFromClipboardStrategy =
clipboardAPIPasteStrategy;
});

test('set data successful', () async {
when(clipboardAPICopyStrategy.setData(testText))
.thenAnswer((_) => success);
const MethodCodec codec = JSONMethodCodec();
bool result = false;
ui.PlatformMessageResponseCallback callback = (ByteData data) {
result = codec.decodeEnvelope(data);
};

await clipboardMessageHandler.setDataMethodCall(
const MethodCall('Clipboard.setData', <String, dynamic>{
'text': testText,
}),
callback);

await expectLater(result, true);
});

test('set data error', () async {
when(clipboardAPICopyStrategy.setData(testText))
.thenAnswer((_) => failure);
const MethodCodec codec = JSONMethodCodec();
ByteData result;
ui.PlatformMessageResponseCallback callback = (ByteData data) {
result = data;
};

await clipboardMessageHandler.setDataMethodCall(
const MethodCall('Clipboard.setData', <String, dynamic>{
'text': testText,
}),
callback);

expect(() async {
codec.decodeEnvelope(result);
}, throwsA(TypeMatcher<PlatformException>()
.having((e) => e.code, 'code', equals('copy_fail'))));
});

test('get data successful', () async {
when(clipboardAPIPasteStrategy.getData())
.thenAnswer((_) => pasteTest);
const MethodCodec codec = JSONMethodCodec();
Map<String, dynamic> result;
ui.PlatformMessageResponseCallback callback = (ByteData data) {
result = codec.decodeEnvelope(data);
};

await clipboardMessageHandler.getDataMethodCall(callback);

await expectLater(result['text'], testText);
});
});
}

class MockClipboardAPICopyStrategy extends Mock
implements ClipboardAPICopyStrategy {}

class MockClipboardAPIPasteStrategy extends Mock
implements ClipboardAPIPasteStrategy {}