diff --git a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs
index 58377e9c1c..2563463d17 100644
--- a/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs
+++ b/frontend/src/app/main/ui/workspace/shapes/text/v3_editor.cljs
@@ -19,6 +19,7 @@
[app.main.ui.css-cursors :as cur]
[app.render-wasm.api :as wasm.api]
[app.render-wasm.text-editor :as text-editor]
+ [app.util.clipboard :as clipboard]
[app.util.dom :as dom]
[app.util.keyboard :as kbd]
[cuerdas.core :as str]
@@ -267,8 +268,13 @@
(when (text-editor/text-editor-has-focus?)
(dom/prevent-default event)
(when (text-editor/text-editor-has-selection?)
- (let [text (text-editor/text-editor-export-selection)]
- (.setData (.-clipboardData event) "text/plain" text))))))
+ (let [text (or (text-editor/text-editor-export-selection) "")
+ html (clipboard/plain-text->html text)
+ data (.-clipboardData event)]
+ ;; text/html matters on Windows: many apps prefer CF_HTML, and
+ ;; without it they can pick up the empty contenteditable `
`.
+ (.setData data "text/plain" text)
+ (.setData data "text/html" html))))))
on-cut
(mf/use-fn
@@ -276,9 +282,12 @@
(when (text-editor/text-editor-has-focus?)
(dom/prevent-default event)
(when (text-editor/text-editor-has-selection?)
- (let [text (text-editor/text-editor-export-selection)]
- (.setData (.-clipboardData event) "text/plain" (or text ""))
- (when (and text (seq text))
+ (let [text (or (text-editor/text-editor-export-selection) "")
+ html (clipboard/plain-text->html text)
+ data (.-clipboardData event)]
+ (.setData data "text/plain" text)
+ (.setData data "text/html" html)
+ (when (seq text)
(text-editor/text-editor-delete-backward)
(sync-wasm-text-editor-content!)
(wasm.api/request-render-preserving-target "text-cut"))))
diff --git a/frontend/src/app/util/clipboard.cljs b/frontend/src/app/util/clipboard.cljs
index 3bc1f1bba3..c2c92c9a76 100644
--- a/frontend/src/app/util/clipboard.cljs
+++ b/frontend/src/app/util/clipboard.cljs
@@ -22,6 +22,18 @@
#js {:decodeTransit t/decode-str
:allowHTMLPaste false})
+(defn plain-text->html
+ "Build a minimal text/html clipboard payload from plain text.
+
+ Windows apps often prefer CF_HTML over CF_UNICODETEXT; writing only
+ text/plain from a contenteditable copy handler can leave them with the
+ editor surface's empty/`
` fallback (a lone newline)."
+ [text]
+ (let [escaped (-> (or text "")
+ (dom/escape-html)
+ (str/replace "\n" "
"))]
+ (str "" escaped)))
+
(defn- from-data-transfer
"Get clipboard stream from DataTransfer instance"
([data-transfer]