patternfly_yew/components/
clipboard.rs1use crate::icon::*;
3use crate::prelude::TextInput;
4use crate::prelude::*;
5use gloo_timers::callback::Timeout;
6use wasm_bindgen::prelude::*;
7use web_sys::{Element, HtmlInputElement};
8use yew::prelude::*;
9
10#[derive(Clone, PartialEq, Properties)]
12pub struct ClipboardProperties {
13 #[prop_or_default]
14 pub value: String,
15 #[prop_or_default]
16 pub readonly: bool,
17 #[prop_or_default]
18 pub code: bool,
19 #[prop_or_default]
20 pub variant: ClipboardVariant,
21 #[prop_or_default]
22 pub name: String,
23 #[prop_or_default]
24 pub id: String,
25}
26
27#[derive(Clone, Default, PartialEq, Eq, Debug)]
28pub enum ClipboardVariant {
29 #[default]
31 Default,
32 Inline,
34 Expandable,
36 Expanded,
38}
39
40impl ClipboardVariant {
41 pub fn is_expandable(&self) -> bool {
42 matches!(self, Self::Expandable | Self::Expanded)
43 }
44
45 pub fn is_inline(&self) -> bool {
46 matches!(self, Self::Inline)
47 }
48}
49
50#[doc(hidden)]
51#[derive(Clone, Debug)]
52pub enum Msg {
53 Copy,
54 Copied,
55 Failed(&'static str),
56 Reset,
57 ToggleExpand,
58 Sync,
60}
61
62const DEFAULT_MESSAGE: &str = "Copy to clipboard";
63const FAILED_MESSAGE: &str = "Failed to copy";
64const OK_MESSAGE: &str = "Copied!";
65
66pub struct Clipboard {
76 message: &'static str,
77 task: Option<Timeout>,
78 expanded: bool,
79 value: Option<String>,
81 text_ref: NodeRef,
82 details_ref: NodeRef,
83}
84
85impl Component for Clipboard {
86 type Message = Msg;
87 type Properties = ClipboardProperties;
88
89 fn create(ctx: &Context<Self>) -> Self {
90 let expanded = matches!(ctx.props().variant, ClipboardVariant::Expanded);
91
92 Self {
93 message: DEFAULT_MESSAGE,
94 task: None,
95 expanded,
96 value: None,
97 text_ref: NodeRef::default(),
98 details_ref: NodeRef::default(),
99 }
100 }
101
102 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
103 match msg {
104 Msg::Copy => {
105 self.do_copy(ctx);
106 }
107 Msg::Copied => {
108 self.trigger_message(ctx, OK_MESSAGE);
109 }
110 Msg::Failed(msg) => {
111 self.trigger_message(ctx, msg);
112 }
113 Msg::Reset => {
114 self.message = DEFAULT_MESSAGE;
115 self.task.take();
116 }
117 Msg::ToggleExpand => {
118 self.expanded = !self.expanded;
119 }
120 Msg::Sync => {
121 self.sync_from_edit(ctx);
122 return false;
123 }
124 }
125 true
126 }
127
128 fn view(&self, ctx: &Context<Self>) -> Html {
129 let mut classes = Classes::from("pf-v5-c-clipboard-copy");
130
131 if self.expanded {
132 classes.push("pf-m-expanded");
133 }
134 if ctx.props().variant.is_inline() {
135 classes.push("pf-m-inline");
136 }
137
138 let value = self.value(ctx);
139
140 html! {
141 <div class={classes}>
142 { match ctx.props().variant {
143 ClipboardVariant::Inline => {
144 html!{
145 <>
146 if ctx.props().code {
147 <code name={ctx.props().name.clone()} id={ctx.props().id.clone()} class="pf-v5-c-clipboard-copy__text pf-m-code">{value}</code>
148 } else {
149 <span name={ctx.props().name.clone()} id={ctx.props().id.clone()} class="pf-v5-c-clipboard-copy__text">{value}</span>
150 }
151 <span class="pf-v5-c-clipboard-copy__actions">
152 <span class="pf-v5-c-clipboard-copy__actions-item">
153 <Tooltip text={self.message}>
154 <Button aria_label="Copy to clipboard" variant={ButtonVariant::Plain} icon={Icon::Copy} onclick={ctx.link().callback(|_|Msg::Copy)}/>
155 </Tooltip>
156 </span>
157 </span>
158 </>
159 }
160 },
161 _ => {
162 html!{
163 <>
164 <div class="pf-v5-c-clipboard-copy__group">
165 { self.expander(ctx) }
166 <TextInput
167 r#ref={self.text_ref.clone()}
168 readonly={ctx.props().readonly | self.expanded}
169 value={value}
170 name={ctx.props().name.clone()}
171 id={ctx.props().id.clone()}
172 oninput={ctx.link().callback(|_|Msg::Sync)}
173 />
174 <Tooltip text={self.message}>
175 <Button aria_label="Copy to clipboard" variant={ButtonVariant::Control} icon={Icon::Copy} onclick={ctx.link().callback(|_|Msg::Copy)}/>
176 </Tooltip>
177 </div>
178 { self.expanded(ctx) }
179 </>
180 }
181 }
182 }}
183 </div>
184 }
185 }
186}
187
188impl Clipboard {
189 fn value(&self, ctx: &Context<Self>) -> String {
190 self.value
191 .clone()
192 .unwrap_or_else(|| ctx.props().value.clone())
193 }
194
195 fn trigger_message(&mut self, ctx: &Context<Self>, msg: &'static str) {
196 self.message = msg;
197 self.task = Some({
198 let link = ctx.link().clone();
199 Timeout::new(2_000, move || {
200 link.send_message(Msg::Reset);
201 })
202 });
203 }
204
205 fn do_copy(&self, ctx: &Context<Self>) {
206 let s = self.value(ctx);
207
208 let ok: Callback<()> = ctx.link().callback(|_| Msg::Copied);
209 let err: Callback<&'static str> = ctx.link().callback(Msg::Failed);
210
211 wasm_bindgen_futures::spawn_local(async move {
212 match copy_to_clipboard(s).await {
213 Ok(_) => ok.emit(()),
214 Err(_) => err.emit(FAILED_MESSAGE),
215 };
216 });
217 }
218
219 fn expander(&self, ctx: &Context<Self>) -> Html {
220 if !ctx.props().variant.is_expandable() {
221 return Default::default();
222 }
223
224 let onclick = ctx.link().callback(|_| Msg::ToggleExpand);
225
226 html! (
227 <Button
228 expanded={self.expanded}
229 variant={ButtonVariant::Control}
230 onclick={onclick}>
231 <div class="pf-v5-c-clipboard-copy__toggle-icon">
232 { Icon::AngleRight }
233 </div>
234 </Button>
235 )
236 }
237
238 fn expanded(&self, ctx: &Context<Self>) -> Html {
239 if !self.expanded {
240 return Default::default();
241 }
242
243 let value = self.value(ctx);
244
245 html! {
246 <div
247 ref={self.details_ref.clone()}
248 class="pf-v5-c-clipboard-copy__expandable-content"
249 contenteditable={(!ctx.props().readonly).to_string()}
250 oninput={ctx.link().callback(|_|Msg::Sync)}
251 >
252
253 if ctx.props().code {
254 <pre>{ value }</pre>
255 } else {
256 { value }
257 }
258
259 </div>
260 }
261 }
262
263 fn sync_from_edit(&mut self, ctx: &Context<Self>) {
265 if ctx.props().readonly || ctx.props().variant.is_inline() {
266 return;
267 }
268
269 let value = if self.expanded {
270 let ele: Option<Element> = self.details_ref.cast::<Element>();
272 ele.and_then(|ele| ele.text_content())
273 .unwrap_or_else(|| "".into())
274 } else {
275 let ele: Option<HtmlInputElement> = self.text_ref.cast::<HtmlInputElement>();
277 ele.map(|ele| ele.value()).unwrap_or_else(|| "".into())
278 };
279
280 log::debug!("New value: {}", value);
281
282 match self.expanded {
284 true => {
285 if let Some(ele) = self.text_ref.cast::<HtmlInputElement>() {
286 ele.set_value(&value);
287 }
288 }
289 false => {
290 if let Some(ele) = self.details_ref.cast::<Element>() {
291 ele.set_text_content(Some(&value));
292 }
293 }
294 }
295
296 self.value = Some(value);
299 }
300}
301
302#[wasm_bindgen(inline_js=r#"
303export function copy_to_clipboard(value) {
304 try {
305 return window.navigator.clipboard.writeText(value);
306 } catch(e) {
307 console.log(e);
308 return Promise.reject(e)
309 }
310}
311"#)]
312#[rustfmt::skip] extern "C" {
314 #[wasm_bindgen(catch)]
315 async fn copy_to_clipboard(value: String) -> Result<(), JsValue>;
316}