1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
use crate::{js, js_fut, use_event_listener, use_supported, UseTimeoutFnReturn};
use default_struct_builder::DefaultBuilder;
use leptos::ev::{copy, cut};
use leptos::*;

/// Reactive [Clipboard API](https://developer.mozilla.org/en-US/docs/Web/API/Clipboard_API).
/// Provides the ability to respond to clipboard commands (cut, copy, and paste)
/// as well as to asynchronously read from and write to the system clipboard.
/// Access to the contents of the clipboard is gated behind the
/// [Permissions API](https://developer.mozilla.org/en-US/docs/Web/API/Permissions_API).
/// Without user permission, reading or altering the clipboard contents is not permitted.
///
/// ## Demo
///
/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_clipboard)
///
/// ## Usage
///
/// ```
/// # use leptos::*;
/// # use leptos_use::{use_clipboard, UseClipboardReturn};
/// #
/// # #[component]
/// # fn Demo() -> impl IntoView {
/// let UseClipboardReturn { is_supported, text, copied, copy } = use_clipboard();
///
/// view! {
///     <Show
///         when=move || is_supported.get()
///         fallback=move || view! { <p>Your browser does not support Clipboard API</p> }
///     >
///         <button on:click={
///             let copy = copy.clone();
///             move |_| copy("Hello!")
///         }>
///             <Show when=move || copied.get() fallback=move || "Copy">
///                 "Copied!"
///             </Show>
///         </button>
///     </Show>
/// }
/// # }
/// ```
///
/// ## Server-Side Rendering
///
/// On the server the returnd `text` signal will always be `None` and `copy` is a no-op.
pub fn use_clipboard() -> UseClipboardReturn<impl Fn(&str) + Clone> {
    use_clipboard_with_options(UseClipboardOptions::default())
}

/// Version of [`use_clipboard`] that takes a `UseClipboardOptions`. See [`use_clipboard`] for how to use.
pub fn use_clipboard_with_options(
    options: UseClipboardOptions,
) -> UseClipboardReturn<impl Fn(&str) + Clone> {
    let UseClipboardOptions {
        copied_reset_delay,
        read,
    } = options;

    let is_supported = use_supported(|| {
        js!("clipboard" in &window()
            .navigator())
    });

    let (text, set_text) = create_signal(None);
    let (copied, set_copied) = create_signal(false);

    let UseTimeoutFnReturn { start, .. } = crate::use_timeout_fn::use_timeout_fn(
        move |_: ()| {
            set_copied.set(false);
        },
        copied_reset_delay,
    );

    let update_text = move |_| {
        if is_supported.get() {
            spawn_local(async move {
                let clipboard = window().navigator().clipboard();
                if let Ok(text) = js_fut!(clipboard.read_text()).await {
                    set_text.set(text.as_string());
                }
            })
        }
    };

    if is_supported.get() && read {
        let _ = use_event_listener(window(), copy, update_text);
        let _ = use_event_listener(window(), cut, update_text);
    }

    let do_copy = {
        let start = start.clone();

        move |value: &str| {
            if is_supported.get() {
                let start = start.clone();
                let value = value.to_owned();

                spawn_local(async move {
                    let clipboard = window().navigator().clipboard();
                    if js_fut!(clipboard.write_text(&value)).await.is_ok() {
                        set_text.set(Some(value));
                        set_copied.set(true);
                        start(());
                    }
                });
            }
        }
    };

    UseClipboardReturn {
        is_supported,
        text: text.into(),
        copied: copied.into(),
        copy: do_copy,
    }
}

/// Options for [`use_clipboard_with_options`].
#[derive(DefaultBuilder)]
pub struct UseClipboardOptions {
    /// When `true` event handlers are added so that the returned signal `text` is updated whenever the clipboard changes.
    /// Defaults to `false`.
    ///
    /// > Please note that clipboard changes are only detected when copying or cutting text inside the same document.
    read: bool,

    /// After how many milliseconds after copying should the returned signal `copied` be set to `false`?
    /// Defaults to 1500.
    copied_reset_delay: f64,
}

impl Default for UseClipboardOptions {
    fn default() -> Self {
        Self {
            read: false,
            copied_reset_delay: 1500.0,
        }
    }
}

/// Return type of [`use_clipboard`].
pub struct UseClipboardReturn<CopyFn>
where
    CopyFn: Fn(&str) + Clone,
{
    /// Whether the Clipboard API is supported.
    pub is_supported: Signal<bool>,

    /// The current state of the clipboard.
    pub text: Signal<Option<String>>,

    /// `true` for [`UseClipboardOptions::copied_reset_delay`] milliseconds after copying.
    pub copied: Signal<bool>,

    /// Copy the given text to the clipboard.
    pub copy: CopyFn,
}