dj-paste — Paste event handling
On this page
dj-paste fires a server event when the user pastes content into a bound element. The client extracts structured payload from the ClipboardEvent — plain text, rich HTML, and any attached files — and sends it as a single handler call. No dj-hook needed.
Every chat app, rich-text editor, and data-import UI needs paste handling. dj-paste is the complete clipboard-to-server pipeline.
Quick start
<textarea dj-paste="handle_paste" placeholder="Paste anything..."></textarea>
from djust import LiveView
from djust.decorators import event_handler
class ChatView(LiveView):
template_name = "chat/room.html"
@event_handler
def handle_paste(self, text: str = "", html: str = "",
has_files: bool = False, files: list = None,
**kwargs):
self.last_paste_preview = text[:200]
if has_files:
self.put_flash("info", "Received %d file(s) — uploading..." % len(files))
What gets sent to the handler
Every dj-paste event call carries four structured params:
| Param | Type | Content |
|---|---|---|
text | str | clipboardData.getData('text/plain') — always a string, even if the clipboard was empty. |
html | str | clipboardData.getData('text/html') — rich paste from Word, Google Docs, a web page, etc. Empty when the source was plain text. |
has_files | bool | True if clipboardData.files was non-empty (i.e. the user copied an image or a file). |
files | list[dict] | File metadata: {"name", "type", "size"}. The actual bytes are NOT in this dict — see Pasting files → uploads. |
The client also forwards any positional args from the attribute syntax:
<textarea dj-paste="handle_paste('chat', 42)"></textarea>
Positional args bind to the handler's leading named parameters, ahead of the paste params. They are not collected into kwargs. Declare them first, or they overwrite text and html:
@event_handler
def handle_paste(self, room: str = "", room_id: int = 0,
text: str = "", html: str = "",
has_files: bool = False, files: list = None,
**kwargs):
...
Pasting files → uploads
By default dj-paste only sends metadata about pasted files. To actually upload the bytes, combine dj-paste with dj-upload on the same element — the client routes the clipboard FileList through the existing upload pipeline (the same path file inputs and drag-drop use):
<div dj-paste="handle_paste" dj-upload="chat_image">
Paste an image here.
</div>
from djust.uploads import UploadMixin
class ChatView(UploadMixin, LiveView):
def mount(self, request, **kwargs):
self.allow_upload(
"chat_image",
accept=".png,.jpg,.jpeg,.gif,.webp",
max_file_size=5 * 1024 * 1024,
)
@event_handler
def handle_paste(self, text: str = "", has_files: bool = False, **kwargs):
if has_files:
# Files arrive via the upload pipeline — nothing more to do here.
return
self.draft = text
A dj-upload element that is not a file input accepts pastes only. For drag-and-drop, add dj-upload-drop="chat_image" as well.
Pasted files fire the same djust:upload:error / djust:upload:progress events as file-input uploads, so progress indicators and size-limit errors Just Work.
Suppressing the native paste
By default, dj-paste lets the browser's own paste happen too — the pasted text ends up in the textarea as usual, and your handler sees it via the event. That's the right behaviour for most editors and rich-text fields.
When you want to intercept the paste completely — for example when you're routing image paste to an upload slot and do not want the image's data URL dumped into a <div contenteditable> — add dj-paste-suppress:
<div dj-paste="handle_paste"
dj-paste-suppress
dj-upload="chat_image"
contenteditable>
Paste an image here.
</div>
Current limitation: dj-paste-suppress does not suppress a paste that contains files routed to dj-upload, which is the case shown above. The client calls preventDefault() only after it has queued the files for upload, and by then the browser has already performed the native paste. Suppression works for pastes that contain no files.
Combining with dj-confirm and dj-lock
dj-paste participates in the standard interaction pipeline:
dj-confirm— show a confirmation dialog before firing the event (useful for "are you sure you want to paste from clipboard?" privacy prompts).dj-lock— skip the event while an earlier handler is still running.
<textarea dj-paste="handle_paste"
dj-confirm="Import clipboard contents?"
dj-lock>
</textarea>
When to reach for dj-paste vs dj-input
dj-inputfires on every keystroke. The server sees the current value of the field. If a user pastes into adj-input-bound textarea, you get the post-paste value as a single update — no paste metadata, no file support.dj-pastefires once per paste. You receive text + HTML + file metadata. The native paste still happens (unless suppressed), so you can pairdj-pastewithdj-inputon the same element without conflict:
<textarea dj-input="set_draft"
dj-paste="handle_rich_paste"
placeholder="Type or paste..."></textarea>
Use dj-input for "keep the server copy of this field in sync" and dj-paste for "react specifically when something was pasted" (image upload, language detection on a code snippet, CSV parsing, markdown preview).
Edge cases
- Missing
clipboardData— very old browsers. The handler bails silently and the native paste still happens. getData('text/html')throws — some browsers block HTML reads from untrusted contexts.dj-pastecatches the error and sendshtml="".- Empty paste —
textandhtmlare empty strings,has_files=False,files=[]. The handler still fires; you can use it as a "user attempted paste" signal. - Multiple files — every file in
clipboardData.filesappears in thefileslist; the upload pipeline uploads them in sequence ifdj-uploadis set.
See also
- File uploads — the
dj-uploadpipeline thatdj-pasteroutes files through. - Event handlers — the
@event_handlerdecorator reference.