Skip to content

Pasting files into browser

Context

Sometimes, I want to send screenshots or photos from my Ubuntu laptop to my iPhone, and vice-versa, before transfer it to the final destination. Sometimes, I also need to send text snippets or URLs between devices.

Reasoning

I need to send between devices because I have different services logged in on different devices. For example, my laptop is logged in to more work-related services and editing tools, while my phone logged in to more personal-related services (like chat apps). Some apps are also not available on Ubuntu, some apps are not available on iOS.

A common use case is where I take a screenshot on laptop, send it to phone, then send it to friends via a personal chat app.

In such use cases, I would like to "copy, send, then forget" rather than "copy, send, then clean up".

Thus, I added a Paste file feature into Fileber.

This post

In this post, I'm going to describe my journey of exploring different methods and designs in implementing such a Paste file feature that lets users quickly copy and send files.

Approaches

I use Clipboard API as I believe it is the modern approach to the clipboard functions.

I found that the Clipboard API provides 2 methods to access the clipboard:

  1. By calling navigator.clipboard.read() (Clipboard Read method)
  2. By listening to the paste ClipboardEvent

Initially, I thought the 2 methods have the same clipboard functionalities and are only differ in their interface/trigger.

  • navigator.clipboard.read() can be programmatically triggered from a click event.
  • paste ClipboardEvent can be listened on a contenteditable or <textarea> element. It is triggered when the user pastes content into the element via Ctrl/Cmd + V or right-click + Paste.

Thus, my first choice was to implement a "Paste" button with navigator.clipboard.read(), because a button would be much more discoverable and usable (compared to hotkeys or right-click), especially for mobile users.

Clipboard Read method

However, I soon realize that the Clipboard Read method supports pasting certain file types only.

Given this (Vue) code:

jsx
import { Button } from 'primevue'

async function onPasteClick() {
  items = await navigator.clipboard.read()

  console.log(JSON.stringify(items.map(i => i.types), undefined, 2))

  items.forEach(item => {
    item.types.forEach(type => {
      item.getType(type).then(blob => {
        blob.text().then(text => console.log(type, text.slice(0, 300)))
      })
    })
  })
}

// template code
<Button
  icon="pi pi-copy"
  label="Paste"
  variant="outlined"
  @click="onPasteClick"
/>

I saw this:

As shown above, when I tried to paste a jpg file, navigator.clipboard.read() only returned a single entry of text/plain content. When I tried to print that content to the console, I saw that the content is actually the file system URI of the file.

💡 Upon some research, I found some resources suggesting that Clipboard API only supports a limited number of content types:

  • text/plain
  • text/html
  • image/png
References

I went ahead and experimented more.

Text contents

When I copied this highlighted text

As expected, Clipboard Read API returned 2 content types: text/plain and text/html

💡 So the Clipboard API can read both plain text and rich text contents at the same time.

For Fileber, I was thinking I would send only the plain text content and discard the rich text content.
  • Sending both is probably not desirable, most of the time.
  • Asking the user to choose would be inconvenient, especially for less technical users who might not know the difference between plain text and rich text
  • In most cases, at least for me, I only need to send plain text snippets. In case I need to send html text, I could put it in an .html file.

PNG contents

Next, I tested with .png files:

💡 To my surprise, even when copying .png files, it still read only the file system URI of the file, similar to when I copied the .jpg files!
After some further research and experimentation, I found that Clipboard API only reads image/png content when I do copy on the displayed image instead of on the image file.

I was trying on Chrome 132.0.6834.110, but Firefox 134.0.2 also yielded the same behavior:

Showstopper

🚫 While I'm not sure whether this behavior is specific to Ubuntu 24.04 or not, the above limitations are a showstopper for me when using the Clipboard Read method. It would make the Paste feature too limited and confusing.

"paste" ClipboardEvent

So I made another attempt, this time using the paste ClipboardEvent.

Functionality

First, I needed to validate whether the paste ClipboardEvent can read more types of clipboard contents or not. So I added contenteditable and a onPaste event listener to the outer-most div of the page:

jsx
function onPaste(e: ClipboardEvent) {
  console.log(e.clipboardData?.types)
}

// template code
<div
  contenteditable
  @paste="onPaste"
>
</div>

💡 It recognized the pasted content as Files!

I went ahead and added more code to onPaste:

jsx
function onPaste(e: ClipboardEvent) {
  const { clipboardData } = e
  if (!clipboardData) return

  clipboardData.types.forEach(type => {
    if (type === 'Files') {
      for (let i = 0; i < clipboardData.files.length; i++) {
        console.log(`File ${i}`, clipboardData.files.item(i).name, clipboardData.files.item(i).type)
      }
    } else {
      const data = clipboardData.getData(type)
      console.log(type, data)
    }
  })
}

and tested again

It could recognize all files!

The feature looked like this after integrating with the file transfer function:

Usability

🛠️ Now that I had validated that the paste ClipboardEvent satisfies the functional requirement, it was time to deal with its usability drawbacks:

  • Discoverability: It is hard for a user to discover that Fileber supports pasting files.
  • Aesthetics: You may notice that my browser put red swirly underline under the Fileber header because it detects a "spelling error" in the "editable content".
  • Interactivity:
    • contenteditable makes it possible for the user to edit anything within the element. This is obviously not desirable.
    • The feature is not usable on mobile devices because those devices can neither input hotkeys nor open the context menu via "right-click" (at least not without an external input device).

To solve the discoverability and aesthetics issues, I decided to make an explicit "paste area" in the UI:

jsx
<div class="flex gap-1">
  <span>or paste</span>
  <div
    class="underline"
    contenteditable
    @paste="onPaste"
  >
    here
  </div>
</div>

To solve the interactivity issues, I added a keyup event listener hack:

jsx
function onPasteAreaKeyup(e: KeyboardEvent) {
  // NOTE: Why not using e.preventDefault() instead? Because it would block `Ctrl/Cmd + V` action
  (e.target as HTMLElement).innerHTML = 'here'
}

// template code
<div class="flex gap-1">
  <span>or paste</span>
  <div
    class="underline"
    contenteditable
    @keyup="onPasteAreaKeyup"
    @paste="onPaste"
  >
    here
  </div>
</div>

However, I noticed that, on my iPhone, it was still very hard to trigger the context menu (in order to trigger "Paste"). Often, I would press and hold to trigger the context menu, but pressing and holding on the word here is more likely to move the cursor than to open the context menu.

Example

So I made the browser select the whole here text when it is clicked, so that it is easier to press and hold on mobile devices:

jsx
function onPasteAreaClick(e: MouseEvent) {
  const el = (e.target as HTMLElement)
  const range = document.createRange()
  range.selectNodeContents(el)
  const selection = window.getSelection()
  selection?.removeAllRanges()
  selection?.addRange(range)
  e.preventDefault()
}

// template code
<div class="flex gap-1">
  <span>or paste</span>
  <div
    class="underline"
    contenteditable
    @keyup="onPasteAreaKeyup"
    @click="onPasteAreaClick"
    @paste="onPaste"
  >
    here
  </div>
</div>

p.s. I also added a drop event handler so that desktop users can also drag-and-drop files onto anywhere in the "or paste here" text.

Security

The usability has become acceptable to me. I could copy and paste files into Fileber with enough convenience.
However, I noticed that perhaps it was "too easy" to paste files into Fileber, which could lead to users mistakenly sending sensitive files.

Thus, I added a preview dialog, and now the feature looks like this:

Conclusion

  • navigator.clipboard.read() (Clipboard Read method) and paste ClipboardEvent are actually quite different
    • Different interface
    • Different trigger
    • Different capability in reading clipboard contents
  • navigator.clipboard.read() can be triggered programmatically, but is very limited in terms of readable clipboard contents.
  • paste ClipboardEvent can read all clipboard contents, but its interactivity is limited to contenteditable elements, so it requires some mitigations in order to deliver good usability.

Other notes

Secure context

Clipboard API is only available in secure contexts.