Skip to content

Commit 6d375ea

Browse files
xerialclaude
andauthored
feature: Add Clipboard API support to uni-dom (#404)
## Summary - Add `Clipboard` object for reading and writing to the system clipboard - Provides both async (Future-based) and reactive (Rx-based) APIs - Includes event handlers for copy/cut/paste interception - Falls back to `execCommand` for older browsers ## Features ```scala import wvlet.uni.dom.all.* // Write text to clipboard Clipboard.writeText("Hello, World!").foreach(_ => println("Copied!")) // Read text from clipboard Clipboard.readText().foreach(text => println(s"Pasted: ${text}")) // One-click copy button button( Clipboard.copyOnClick("Copy this text", onSuccess = () => showToast("Copied!")), "Copy" ) // Dynamic copy (evaluates at click time) button( Clipboard.copyOnClickDynamic(() => inputRef.current.map(_.value).getOrElse("")), "Copy Input" ) // Intercept paste events input( Clipboard.onPaste { text => println(s"User pasted: ${text}") } ) // Customize what gets copied div( Clipboard.copyAs(() => "Custom formatted text"), "Select and copy me" ) // Check browser support if Clipboard.isSupported then // Use modern API else // Fallback behavior ``` ## API | Method | Description | |--------|-------------| | `writeText(text)` | Write text to clipboard (Future) | | `readText()` | Read text from clipboard (Future) | | `writeTextRx(text)` | Write text, returns Rx[Boolean] | | `onCopy(handler)` | Handle copy events | | `onCut(handler)` | Handle cut events | | `onPaste(handler)` | Handle paste events | | `copyAs(getText)` | Customize copied content | | `cutAs(getText)` | Customize cut content | | `copyOnClick(text)` | Copy on click | | `copyOnClickDynamic(getText)` | Copy dynamic text on click | | `isSupported` | Check if Clipboard API is available | ## Test plan - [x] All 257 tests pass (17 new clipboard tests) - [x] Compilation successful - [x] Code formatted with scalafmtAll 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent ceeb28e commit 6d375ea

File tree

3 files changed

+453
-0
lines changed

3 files changed

+453
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/*
2+
* Licensed under the Apache License, Version 2.0 (the "License");
3+
* you may not use this file except in compliance with the License.
4+
* You may obtain a copy of the License at
5+
*
6+
* http://www.apache.org/licenses/LICENSE-2.0
7+
*
8+
* Unless required by applicable law or agreed to in writing, software
9+
* distributed under the License is distributed on an "AS IS" BASIS,
10+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
* See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package wvlet.uni.dom
15+
16+
import org.scalajs.dom
17+
import wvlet.uni.test.UniTest
18+
import wvlet.uni.dom.all.*
19+
import wvlet.uni.dom.all.given
20+
import wvlet.uni.rx.Rx
21+
22+
import scala.concurrent.Future
23+
24+
class ClipboardTest extends UniTest:
25+
26+
test("Clipboard.isSupported returns Boolean"):
27+
val supported = Clipboard.isSupported
28+
supported shouldMatch { case _: Boolean =>
29+
}
30+
31+
test("Clipboard.writeText returns Future[Unit]"):
32+
val result = Clipboard.writeText("test")
33+
result shouldMatch { case _: Future[?] =>
34+
}
35+
36+
test("Clipboard.readText returns Future[String]"):
37+
val result = Clipboard.readText()
38+
result shouldMatch { case _: Future[?] =>
39+
}
40+
41+
test("Clipboard.writeTextRx returns Rx[Option[Boolean]]"):
42+
val result = Clipboard.writeTextRx("test")
43+
result shouldMatch { case _: Rx[?] =>
44+
}
45+
46+
test("Clipboard.onCopy returns DomNode"):
47+
val node = Clipboard.onCopy(_ => ())
48+
node shouldMatch { case _: DomNode =>
49+
}
50+
51+
test("Clipboard.onCut returns DomNode"):
52+
val node = Clipboard.onCut(_ => ())
53+
node shouldMatch { case _: DomNode =>
54+
}
55+
56+
test("Clipboard.onPaste returns DomNode"):
57+
val node = Clipboard.onPaste(_ => ())
58+
node shouldMatch { case _: DomNode =>
59+
}
60+
61+
test("Clipboard.onPasteEvent returns DomNode"):
62+
val node = Clipboard.onPasteEvent(_ => ())
63+
node shouldMatch { case _: DomNode =>
64+
}
65+
66+
test("Clipboard.copyAs returns DomNode"):
67+
val node = Clipboard.copyAs(() => "text")
68+
node shouldMatch { case _: DomNode =>
69+
}
70+
71+
test("Clipboard.cutAs returns DomNode"):
72+
val node = Clipboard.cutAs(() => "text")
73+
node shouldMatch { case _: DomNode =>
74+
}
75+
76+
test("Clipboard.copyOnClick returns DomNode"):
77+
val node = Clipboard.copyOnClick("text")
78+
node shouldMatch { case _: DomNode =>
79+
}
80+
81+
test("Clipboard.copyOnClick with callbacks returns DomNode"):
82+
var success = false
83+
val node = Clipboard.copyOnClick("text", onSuccess = () => success = true, onFailure = _ => ())
84+
node shouldMatch { case _: DomNode =>
85+
}
86+
87+
test("Clipboard.copyOnClickDynamic returns DomNode"):
88+
val node = Clipboard.copyOnClickDynamic(() => "dynamic text")
89+
node shouldMatch { case _: DomNode =>
90+
}
91+
92+
test("Clipboard.copyOnClickDynamic with callbacks returns DomNode"):
93+
var success = false
94+
val node = Clipboard.copyOnClickDynamic(
95+
() => "dynamic text",
96+
onSuccess = () => success = true,
97+
onFailure = _ => ()
98+
)
99+
node shouldMatch { case _: DomNode =>
100+
}
101+
102+
test("Clipboard handlers can be attached to elements"):
103+
val elem = div(
104+
Clipboard.onCopy(text => ()),
105+
Clipboard.onCut(text => ()),
106+
Clipboard.onPaste(text => ()),
107+
input(placeholder -> "Test input")
108+
)
109+
elem shouldMatch { case _: RxElement =>
110+
}
111+
112+
test("Clipboard.copyOnClick can be attached to button"):
113+
val elem = button(Clipboard.copyOnClick("Hello, World!"), "Copy")
114+
elem shouldMatch { case _: RxElement =>
115+
}
116+
117+
test("Clipboard event handlers render correctly"):
118+
val elem = div(Clipboard.onPaste(_ => ()))
119+
val (node, cancel) = DomRenderer.createNode(elem)
120+
121+
node match
122+
case e: dom.Element =>
123+
// The element should exist
124+
e.tagName.toLowerCase shouldBe "div"
125+
case _ =>
126+
fail("Expected Element")
127+
128+
cancel.cancel
129+
130+
end ClipboardTest

0 commit comments

Comments
 (0)