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:
- By calling
navigator.clipboard.read()
(Clipboard Read method) - 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 acontenteditable
or<textarea>
element. It is triggered when the user pastes content into the element viaCtrl/Cmd + V
orright-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:
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:
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
:
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:
<div class="flex gap-1">
<span>or paste</span>
<div
class="underline"
contenteditable
@paste="onPaste"
>
here
</div>
</div>
![](/pasting-files-into-browser/paste-area.png)
To solve the interactivity issues, I added a keyup
event listener hack:
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:
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) andpaste
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 tocontenteditable
elements, so it requires some mitigations in order to deliver good usability.
Other notes
Secure context
Clipboard API is only available in secure contexts.