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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
use std::collections::HashSet;

use serde::{Deserialize, Serialize};

use yew::prelude::*;

use yew_agent::{use_bridge, HandlerId, Public, UseBridgeHandle, Worker, WorkerLink};

/// Modal actions.
pub enum ModalMsg {
    Open,
    Close,
    CloseFromAgent(ModalCloseMsg),
}

#[derive(Clone, Debug, Properties, PartialEq)]
pub struct ModalProps {
    /// The ID of this modal, used for triggering close events from other parts of the app.
    pub id: String,
    /// The content of the `"modal-content"` element.
    #[prop_or_default]
    pub children: Children,
    /// The contents of the modal trigger, typically a button or the like.
    #[prop_or_default]
    pub trigger: Html,
    #[prop_or_default]
    pub classes: Classes,
}

/// A classic modal overlay, in which you can include any content you want.
///
/// [https://bulma.io/documentation/components/modal/](https://bulma.io/documentation/components/modal/)
///
/// See the docs on the `ModalCloser` agent to be able to close your modal instance from anywhere
/// in your app for maximum flexibility.
#[function_component(Modal)]
pub fn modal(props: &ModalProps) -> Html {
    let is_active = use_state(|| false);

    let mut class = Classes::from("modal");

    class.push(props.classes.clone());

    let (opencb, closecb) = if *is_active {
        class.push("is-active");

        let is_active = is_active.clone();

        (Callback::noop(), Callback::from(move |_| is_active.set(false)))
    } else {
        let is_active = is_active.clone();

        (Callback::from(move |_| is_active.set(true)), Callback::noop())
    };

    {
        let id = props.id.clone();

        let _bridge: UseBridgeHandle<ModalCloser> = use_bridge(move |response: ModalCloseMsg| {
            if response.0 == id {
                is_active.set(false);
            } else {
            }
        });
    }

    html! {
        <>
        <div onclick={opencb}>
            {props.trigger.clone()}
        </div>
        <div id={props.id.clone()} {class}>
            <div class="modal-background" onclick={closecb.clone()}></div>
            <div class="modal-content">
                {props.children.clone()}
            </div>
            <button class="modal-close is-large" aria-label="close" onclick={closecb}></button>
        </div>
        </>
    }
}

//////////////////////////////////////////////////////////////////////////////

#[derive(Clone, Debug, Properties, PartialEq)]
pub struct ModalCardProps {
    /// The ID of this modal, used for triggering close events from other parts of the app.
    pub id: String,
    /// The title of this modal.
    pub title: String,
    /// The content to be placed in the `modal-card-body` not including the modal-card-header /
    /// modal-card-title, which is handled by the `modal_title` prop.
    #[prop_or_default]
    pub body: Html,
    /// The content to be placed in the `modal-card-footer`.
    #[prop_or_default]
    pub footer: Html,
    /// The contents of the modal trigger, typically a button or the like.
    #[prop_or_default]
    pub trigger: Html,
    #[prop_or_default]
    pub classes: Classes,
}

/// A classic modal with a header, body, and footer section.
///
/// [https://bulma.io/documentation/components/modal/](https://bulma.io/documentation/components/modal/)
///
/// See the docs on the `ModalCloser` agent to be able to close your modal instance from anywhere
/// in your app for maximum flexibility.
#[function_component(ModalCard)]
pub fn modal_card(props: &ModalCardProps) -> Html {
    let is_active = use_state(|| false);

    let mut class = Classes::from("modal");
    class.push(props.classes.clone());

    let (opencb, closecb) = if *is_active {
        class.push("is-active");

        let is_active = is_active.clone();

        (Callback::noop(), Callback::from(move |_| is_active.set(false)))
    } else {
        let is_active = is_active.clone();

        (Callback::from(move |_| is_active.set(true)), Callback::noop())
    };

    {
        let id = props.id.clone();

        let _bridge: UseBridgeHandle<ModalCloser> = use_bridge(move |response: ModalCloseMsg| {
            if response.0 == id {
                is_active.set(false);
            } else {
            }
        });
    }

    html! {
    <>
        <div onclick={opencb}>
            {props.trigger.clone()}
        </div>
        <div id={props.id.clone()} {class}>
            <div class="modal-background" onclick={closecb.clone()}></div>
            <div class="modal-card">
                <header class="modal-card-head">
                    <p class="modal-card-title">{props.title.clone()}</p>
                    <button class="delete" aria-label="close" onclick={closecb.clone()}></button>
                </header>
                <section class="modal-card-body">
                    {props.body.clone()}
                </section>
                <footer class="modal-card-foot">
                    {props.footer.clone()}
                </footer>
            </div>
            <button class="modal-close is-large" aria-label="close" onclick={closecb}></button>
        </div>
    </>
    }
}

//////////////////////////////////////////////////////////////////////////////

/// A request to close a modal instance by ID.
///
/// The ID provided in this message must match the ID of the modal which is to be closed, else
/// the message will be ignored.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct ModalCloseMsg(pub String);

/// An agent used for being able to close `Modal` & `ModalCard` instances by ID.
///
/// If custom modal closing functionality is need for your modal instance, the following
/// pattern is recommended.
///
/// First, in your component which is using this modal, configure a `ModalCloser` dispatcher.
/// ```rust
/// use yew::agent::Dispatcher;
/// use yew::prelude::*;
/// // .. snip ..
/// fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
///     let bridge = ModalCloser::dispatcher();
///     Self { link, props, bridge }
/// }
/// ```
///
/// Next, in your component's `view` method, setup a callback to handle your component's close
/// event. ```rust
/// let closer = self.link.callback(|_| ModalCloseMsg("modal-0".into()));
/// // ... snip ...
/// <ModalCard
///     id="modal-0"
///     // ... snip ...
///     footer=html!{
///         <Button onclick=Some(closer)>{"Close"}</Button>
///     }
/// />
/// ```
///
/// Finally, in your component's `update` method, send the `ModalCloseMsg` over to the agent which
/// will forward the message to the modal to cause it to close.
/// ```rust
/// fn update(&mut self, msg: Self::Message) -> ShouldRender {
///     self.bridge.send(msg);
///     true
/// }
/// ```
///
/// This pattern allows you to communicate with a modal by its given ID, allowing
/// you to close the modal from anywhere in your application.
pub struct ModalCloser {
    link: WorkerLink<Self>,
    subscribers: HashSet<HandlerId>,
}

impl Worker for ModalCloser {
    type Input = ModalCloseMsg;
    type Message = ();
    // The agent receives requests to close modals by ID.
    type Output = ModalCloseMsg;
    type Reach = Public<ModalCloser>;

    // The agent forwards the input to all registered modals.

    fn create(link: WorkerLink<Self>) -> Self {
        Self { link, subscribers: HashSet::new() }
    }

    fn update(&mut self, _: Self::Message) {}

    fn handle_input(&mut self, msg: Self::Input, _: HandlerId) {
        for cmp in self.subscribers.iter() {
            self.link.respond(*cmp, msg.clone());
        }
    }

    fn connected(&mut self, id: HandlerId) {
        self.subscribers.insert(id);
    }

    fn disconnected(&mut self, id: HandlerId) {
        self.subscribers.remove(&id);
    }
}