Skip to content

[SuperTextField] [web] Clipboard paste not working without Clipboard permissions #166

Open
@venkatd

Description

@venkatd

@matthew-carroll

If I try to paste in release mode deployed to prod, I get the following error when attempting to paste:

20:27:16.866 main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:6523 Uncaught PlatformException(paste_fail, Clipboard.getData failed, null, null)
    at Object.wrapException (https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:6523:17)
    at JSONMethodCodec0.decodeEnvelope$1 (https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:127035:17)
    at https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:127513:60
    at _wrapJsFunctionForAsync_closure.$protected (https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:9858:15)
    at _wrapJsFunctionForAsync_closure.call$2 (https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:72454:12)
    at _awaitOnObject_closure.call$1 (https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:72440:32)
    at _RootZone.runUnary$2$2 (https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:74744:18)
    at _Future__propagateToListeners_handleValueCallback.call$0 (https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:73403:51)
    at Object._Future__propagateToListeners (https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:10107:93)
    at _Future._completeWithValue$1 (https://beta.turtleos.com/main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:73223:9)
wrapException @ main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:6523
call$0 @ main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:74617
_microtaskLoop @ main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:10168
_startMicrotaskLoop @ main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:10174
call$1 @ main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:72316
invokeClosure @ main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:6726
(anonymous) @ main-b370b170139bc249346302b029c6166fd9dfd67d.dart.js:6745
20:35:10.345 

I have to enable the Clipboard API to get things working.

However, with the built-in TextField on web, no such permission is required. I tried reading through the Flutter source, but I'm not sure what kind of trickery they are doing to allow a regular paste operation.

One observation is that, during editing of a TextField, there's an invisible input that appears to be kept in sync with the editor. So entering cmd+v may end up pasting there.

Do you know someone we can confirm the TextField paste implementation with?

Activity

venkatd

venkatd commented on May 9, 2021

@venkatd
Author

@matthew-carroll I have found an approach that seems to work on Safari/Chrome at least.

There is a document.onpaste event in dart:html which doesn't require permission to access.

I think that it may be best to keep the editors agnostic of how copy/paste is handled. Meaning the core editor has no reference to the Clipboard class.

A function like controller.replaceSelection with a signature similar to what you have for AttributedText.insertString could be enough.

Some variations of how copy/paste could work:

  • In our app, we have add a listener to document.onPaste and call e.clipboardData.getData('text/plain'); to grab the contents. This requires no explicit permissions grants from the user unlike Clipboard.getData. I'm currently using a FocusNode to determine which control should handle the onPaste.
  • In MacOS, we would handle as you currently are. So cmd+v would call final clipboardData = await Clipboard.getData('text/plain');
  • Other apps might be OK with requesting the web Clipboard API permissions for additional benefits. Like detecting the contents of the clipboard and adapting the UI based on it.
  • In some scenarios, copy/paste may just be internal to the app. So they maintain their own in-memory buffer for the clipboard without using their own OS clipboard.

How would we would want to deal with platform specific behavior for now?

On one hand, I think we'd want super editor to be composable and unopinionated. On the other hand, it would be nice to have good defaults. (I.e., Flutter web has copy/paste work outside of the box.)

venkatd

venkatd commented on May 11, 2021

@venkatd
Author

@matthew-carroll as discussed, here's a gist of how copy/paste is working in our app.

I've removed and commented some parts which may be irrelevant for now.

Stream<ClipboardContent> onClipboardPaste() {
  return html.document.onPaste
      .map(_getClipboardContent)
      .where((content) => content.isNotEmpty);
}

ClipboardContent _getClipboardContent(html.ClipboardEvent e) {
  final clipboardData = e.clipboardData;

  if (clipboardData == null) return ClipboardContent.empty;

  // ClipboardContent is just a value object, it has no logic. Can remove it and return a String directly if you want.
  return ClipboardContent(
    text: _extractTextFrom(clipboardData),
      // LocalFile is our own internal cross-platform abstraction for files.
      // Can ignore all code for LocalFile for now if we just care about pasting text.
      // In our app, we also care about pasting images/files
    files: _extractFilesFrom(clipboardData).map(castToFile).toList(),
  );
}

String? _extractTextFrom(html.DataTransfer clipboardData) {
  final data = clipboardData.getData('text/plain');
  return data.isNotEmpty ? data : null;
}

List<html.File> _extractFilesFrom(html.DataTransfer clipboardData) {
  final files = <html.File>[];

  final items = clipboardData.items;
  if (items == null) return const [];
  final length = items.length ?? 0;

  for (var i = 0; i < length; i++) {
    final clipboardItem = items[i];
    if (clipboardItem.kind != 'file') continue;

    final clipboardHtmlFile = clipboardItem.getAsFile();
    if (clipboardHtmlFile == null) continue;

    files.add(clipboardHtmlFile);
  }

  return files;
}

LocalFile castToFile(Object platformFile) =>
    LocalHtmlFile(platformFile as html.File);
matthew-carroll

matthew-carroll commented on May 15, 2021

@matthew-carroll
Contributor

@venkatd when do you start listening for paste events? When the app starts?

venkatd

venkatd commented on May 16, 2021

@venkatd
Author

@matthew-carroll I do it in the initState of a widget, but I think on app start would probably work fine.

added this to the v0.2.0 milestone on Jun 5, 2021
modified the milestone: v0.2.0 on Feb 3, 2022
removed their assignment
on Feb 3, 2022
removed this from the v0.2.0 milestone on Feb 3, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

      Participants

      @venkatd@matthew-carroll

      Issue actions

        [SuperTextField] [web] Clipboard paste not working without Clipboard permissions · Issue #166 · superlistapp/super_editor