split_yew/
lib.rs

1#![doc = include_str!("../README.md")]
2use std::fmt::Display;
3
4use regex::Regex;
5use wasm_bindgen::{__rt::IntoJsResult, prelude::*};
6use web_sys::{Element, HtmlElement};
7use yew::prelude::*;
8
9mod js;
10
11#[derive(Clone, Properties, PartialEq)]
12pub struct SplitProps {
13    /// Classes to apply to the split container element
14    #[prop_or_default]
15    pub class: Classes,
16
17    /// Initial sizes of each element
18    #[prop_or_default]
19    pub sizes: Option<Vec<f64>>,
20
21    /// Minimum size of all elements (if min_sizes is specified, this will be ignored)
22    #[prop_or_default]
23    pub min_size: Option<f64>,
24
25    /// Minimum size of each element
26    #[prop_or_default]
27    pub min_sizes: Option<Vec<f64>>,
28
29    /// Maximum size of all elements (if max_sizes is specified, this will be ignored)
30    #[prop_or_default]
31    pub max_size: Option<f64>,
32
33    /// Maximum size of each element
34    #[prop_or_default]
35    pub max_sizes: Option<Vec<f64>>,
36
37    /// Grow initial sizes to min_size (default: false)
38    #[prop_or_default]
39    pub expand_to_min: Option<bool>,
40
41    /// Gutter size in pixels (default: 10)
42    #[prop_or_default]
43    pub gutter_size: Option<f64>,
44
45    /// Gutter alignment between elements (default: GutterAlign::Center)
46    #[prop_or_default]
47    pub gutter_align: Option<GutterAlign>,
48
49    /// Snap to minimum size offset in pixels (default: 30)
50    #[prop_or_default]
51    pub snap_offset: Option<f64>,
52
53    /// Number of pixels to drag (default: 1)
54    #[prop_or_default]
55    pub drag_interval: Option<f64>,
56
57    /// Direction to split: horizontal or vertical (default: Direction::Horizontal)
58    #[prop_or_default]
59    pub direction: Option<Direction>,
60
61    /// Cursor to display while dragging (default: Cursor::ColResize)
62    #[prop_or_default]
63    pub cursor: Option<Cursor>,
64
65    /// Called to create each gutter element
66    #[prop_or(
67        Closure::<dyn Fn(js_sys::BigInt, String, Element) -> Element>::new(
68            |_index, direction, _pair_element| {
69                let gutter_element = web_sys::window()
70                    .expect_throw("No window")
71                    .document()
72                    .expect_throw("No document")
73                    .create_element("div")
74                    .expect_throw("Failed to create gutter div");
75
76                gutter_element.set_class_name(&format!("gutter gutter-{}", direction));
77                js_sys::Reflect::set(
78                    &gutter_element,
79                    &"__isSplitGutter".into(),
80                    &true.into(),
81                )
82                .expect_throw("Unable to set __isSplitGutter property");
83                gutter_element
84            },
85        )
86        .into_js_value()
87        .into()
88    )]
89    pub gutter: js_sys::Function,
90
91    /// Called to set the style of each element
92    #[prop_or_default]
93    pub element_style: Option<js_sys::Function>,
94
95    /// Called to set the style of the gutter
96    #[prop_or_default]
97    pub gutter_style: Option<js_sys::Function>,
98
99    /// Called on drag
100    #[prop_or_default]
101    pub on_drag: Option<js_sys::Function>,
102
103    /// Called on drag start
104    #[prop_or_default]
105    pub on_drag_start: Option<js_sys::Function>,
106
107    /// Called on drag end
108    #[prop_or_default]
109    pub on_drag_end: Option<js_sys::Function>,
110
111    #[prop_or_default]
112    pub collapsed: Option<usize>,
113
114    pub children: Children,
115}
116
117pub struct Split {
118    parent_ref: NodeRef,
119    split: Option<js::Split>,
120}
121
122impl Component for Split {
123    type Message = ();
124    type Properties = SplitProps;
125
126    fn rendered(&mut self, ctx: &Context<Self>, first_render: bool) {
127        if first_render {
128            let children = self
129                .parent_ref
130                .cast::<HtmlElement>()
131                .unwrap_throw()
132                .children();
133
134            let children = js_sys::Array::from(&children);
135            let options = ctx.props().make_options_object();
136            self.split = Some(js::Split::new(children, options));
137
138            if let Some(collapsed) = ctx.props().collapsed {
139                self.split.as_ref().unwrap_throw().collapse(collapsed);
140            }
141        }
142    }
143
144    fn create(_ctx: &Context<Self>) -> Self {
145        Self {
146            parent_ref: NodeRef::default(),
147            split: None,
148        }
149    }
150
151    fn view(&self, ctx: &Context<Self>) -> Html {
152        let SplitProps {
153            class, children, ..
154        } = ctx.props();
155
156        html! {
157            <div class={(*class).clone()} ref={self.parent_ref.clone()}>
158                { for children.iter() }
159            </div>
160        }
161    }
162
163    fn changed(&mut self, ctx: &Context<Self>, old_props: &Self::Properties) -> bool {
164        let SplitProps {
165            sizes,
166            min_size,
167            min_sizes,
168            collapsed,
169            ..
170        } = ctx.props();
171
172        let SplitProps {
173            min_size: old_min_size,
174            min_sizes: old_min_sizes,
175            sizes: old_sizes,
176            collapsed: old_collapsed,
177            ..
178        } = old_props;
179
180        let mut needs_recreate = ctx.props().other_props_changed(old_props);
181
182        if min_sizes.is_some() && old_min_sizes.is_some() {
183            let mut min_size_changed = false;
184
185            min_sizes
186                .as_ref()
187                .unwrap_throw()
188                .iter()
189                .enumerate()
190                .for_each(|(i, min_size_i)| {
191                    min_size_changed |= min_size_i
192                        != old_min_sizes.as_ref().unwrap_throw().get(i).expect_throw(
193                            "Cannot index min_sizes during update. Did the length change?",
194                        );
195                });
196
197            needs_recreate |= min_size_changed;
198        } else if min_sizes.is_some() || old_min_sizes.is_some() {
199            needs_recreate = true;
200        } else {
201            needs_recreate |= min_size != old_min_size;
202        }
203
204        if needs_recreate {
205            let options = ctx.props().make_options_object();
206
207            // This is done in the React version, not sure why exactly but I'm doing it as well
208            let cur_sizes = js_sys::Reflect::get(&options, &"sizes".into()).unwrap_throw();
209            if cur_sizes.is_falsy() {
210                js_sys::Reflect::set(
211                    &options,
212                    &"sizes".into(),
213                    &js::Split::get_sizes(self.split.as_ref().unwrap_throw()),
214                )
215                .unwrap_throw();
216            }
217
218            js::Split::destroy(self.split.as_ref().unwrap_throw(), true.into(), true.into());
219
220            // The old gutter creates new div elements, here we just want to get the divs already
221            // in the DOM and prepare them for the new split
222            let new_gutter: js_sys::Function =
223                Closure::<dyn Fn(js_sys::BigInt, String, Element) -> Element>::new(
224                    |_index, direction, pair_element: Element| {
225                        let gutter_el: Element = pair_element
226                            .previous_sibling()
227                            .map(|node| node.into_js_result().unwrap_throw())
228                            .unwrap_or(JsValue::UNDEFINED)
229                            .into();
230
231                        if direction == "horizontal" {
232                            gutter_el.set_class_name("gutter gutter-horizontal");
233                        } else {
234                            gutter_el.set_class_name("gutter gutter-vertical");
235                        }
236
237                        // We need to reset the styles otherwise the element will keep the
238                        // width/height that was assigned by the previous split. No need to manually
239                        // set the width/height ourselves as split.js will do that for us
240                        gutter_el
241                            .set_attribute("style", "")
242                            .expect_throw("Cannot reset gutter style on recreate");
243
244                        gutter_el
245                    },
246                )
247                .into_js_value()
248                .into();
249            js_sys::Reflect::set(&options, &"gutter".into(), &new_gutter).unwrap_throw();
250
251            let non_gutter_children = js_sys::Array::from(
252                &self
253                    .parent_ref
254                    .cast::<HtmlElement>()
255                    .expect_throw("No parent during update")
256                    .children(),
257            )
258            .filter(&mut |element, _, _| {
259                js_sys::Reflect::get(&element, &"__isSplitGutter".into())
260                    .unwrap_throw()
261                    .is_falsy()
262            });
263
264            // We need to reset height/width styles for all non-gutter children, otherwise they will
265            // remain with the width/height set during the previous split
266            let width_regex = Regex::new(r"width: .*;").unwrap_throw();
267            let height_regex = Regex::new(r"height: .*;").unwrap_throw();
268            for child in non_gutter_children.iter() {
269                let child: Element = child.into();
270                if let Some(style) = child.get_attribute("style") {
271                    let new_style = match ctx
272                        .props()
273                        .direction
274                        .as_ref()
275                        .expect_throw("No direction during update")
276                    {
277                        // width/height is inverted compared to direction as we need to reset the
278                        // current non-splitting direction
279                        Direction::Vertical => width_regex.replace_all(&style, ""),
280                        Direction::Horizontal => height_regex.replace_all(&style, ""),
281                    };
282                    child
283                        .set_attribute("style", &new_style)
284                        .expect_throw("Cannot reset child style on recreate");
285                }
286            }
287
288            self.split = Some(js::Split::new(non_gutter_children, options));
289        } else if sizes.is_some() {
290            let mut size_changed = false;
291
292            sizes
293                .as_ref()
294                .unwrap_throw()
295                .iter()
296                .enumerate()
297                .for_each(|(i, size_i)| {
298                    size_changed |= size_i
299                        != old_sizes.as_ref().unwrap_throw().get(i).expect_throw(
300                            "Cannot index sizes during update. Did the length change?",
301                        );
302                });
303
304            if size_changed {
305                let new_sizes = js_sys::Array::new();
306                for size in sizes.as_ref().unwrap_throw().iter() {
307                    new_sizes.push(&JsValue::from(*size));
308                }
309                js::Split::set_sizes(self.split.as_ref().unwrap_throw(), new_sizes);
310            }
311        }
312
313        if collapsed.is_some()
314            && (old_collapsed.is_some() && collapsed.unwrap_throw() != old_collapsed.unwrap_throw()
315                || needs_recreate)
316        {
317            js::Split::collapse(self.split.as_ref().unwrap_throw(), collapsed.unwrap_throw());
318        }
319
320        true
321    }
322
323    fn destroy(&mut self, _ctx: &Context<Self>) {
324        js::Split::destroy(
325            self.split.as_ref().unwrap_throw(),
326            false.into(),
327            false.into(),
328        );
329        self.split = None;
330    }
331}
332
333#[derive(Debug, Clone, PartialEq)]
334pub enum GutterAlign {
335    Start,
336    End,
337    Center,
338}
339
340impl Display for GutterAlign {
341    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342        match self {
343            GutterAlign::Start => write!(f, "start"),
344            GutterAlign::End => write!(f, "end"),
345            GutterAlign::Center => write!(f, "center"),
346        }
347    }
348}
349
350#[derive(Debug, Clone, PartialEq)]
351pub enum Direction {
352    Vertical,
353    Horizontal,
354}
355
356impl Display for Direction {
357    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
358        match self {
359            Direction::Vertical => write!(f, "vertical"),
360            Direction::Horizontal => write!(f, "horizontal"),
361        }
362    }
363}
364
365#[derive(Debug, Clone, PartialEq)]
366pub enum Cursor {
367    ColResize,
368    RowResize,
369}
370
371impl Display for Cursor {
372    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373        match self {
374            Cursor::ColResize => write!(f, "col-resize"),
375            Cursor::RowResize => write!(f, "row-resize"),
376        }
377    }
378}
379
380impl SplitProps {
381    fn other_props_changed(&self, old_props: &SplitProps) -> bool {
382        let SplitProps {
383            max_size,
384            expand_to_min,
385            gutter_size,
386            gutter_align,
387            snap_offset,
388            drag_interval,
389            direction,
390            cursor,
391            ..
392        } = self;
393
394        let SplitProps {
395            max_size: old_max_size,
396            expand_to_min: old_expand_to_min,
397            gutter_size: old_gutter_size,
398            gutter_align: old_gutter_align,
399            snap_offset: old_snap_offset,
400            drag_interval: old_drag_interval,
401            direction: old_direction,
402            cursor: old_cursor,
403            ..
404        } = old_props;
405
406        max_size != old_max_size
407            || expand_to_min != old_expand_to_min
408            || gutter_size != old_gutter_size
409            || gutter_align != old_gutter_align
410            || snap_offset != old_snap_offset
411            || drag_interval != old_drag_interval
412            || direction != old_direction
413            || cursor != old_cursor
414    }
415
416    fn set_option<T, F: Fn(&T) -> JsValue>(
417        options: &js_sys::Object,
418        key: &str,
419        value: &Option<T>,
420        f: F,
421    ) {
422        if value.is_some() {
423            js_sys::Reflect::set(
424                options,
425                &key.into(),
426                &value.as_ref().map_or(JsValue::UNDEFINED, f),
427            )
428            .unwrap_throw();
429        }
430    }
431
432    fn make_options_object(&self) -> js_sys::Object {
433        let SplitProps {
434            sizes,
435            min_size,
436            min_sizes,
437            max_size,
438            max_sizes,
439            expand_to_min,
440            gutter_size,
441            gutter_align,
442            snap_offset,
443            drag_interval,
444            direction,
445            cursor,
446            gutter,
447            element_style,
448            gutter_style,
449            on_drag,
450            on_drag_start,
451            on_drag_end,
452            ..
453        } = self;
454
455        let options = js_sys::Object::new();
456        let f64_vec_to_arr = |v: &Vec<f64>| {
457            let arr = js_sys::Array::new();
458            for val in v.iter() {
459                arr.push(&JsValue::from(*val));
460            }
461            arr.into()
462        };
463
464        fn val_to_js<T: Into<JsValue> + Clone>(v: &T) -> JsValue {
465            (*v).clone().into()
466        }
467
468        fn to_js_string<T: Display>(v: &T) -> JsValue {
469            v.to_string().into()
470        }
471
472        Self::set_option(&options, "sizes", sizes, f64_vec_to_arr);
473
474        if min_sizes.is_some() {
475            Self::set_option(&options, "minSize", min_sizes, f64_vec_to_arr);
476        } else if min_size.is_some() {
477            Self::set_option(&options, "minSize", min_size, val_to_js);
478        }
479
480        if max_sizes.is_some() {
481            Self::set_option(&options, "maxSize", max_sizes, f64_vec_to_arr);
482        } else if max_size.is_some() {
483            Self::set_option(&options, "maxSize", max_size, val_to_js);
484        }
485
486        Self::set_option(&options, "expandToMin", expand_to_min, val_to_js);
487        Self::set_option(&options, "gutterSize", gutter_size, val_to_js);
488        Self::set_option(&options, "gutterAlign", gutter_align, to_js_string);
489        Self::set_option(&options, "snapOffset", snap_offset, val_to_js);
490        Self::set_option(&options, "dragInterval", drag_interval, val_to_js);
491        Self::set_option(&options, "direction", direction, to_js_string);
492        Self::set_option(&options, "cursor", cursor, to_js_string);
493        Self::set_option(&options, "gutter", &Some(gutter), val_to_js);
494        Self::set_option(&options, "elementStyle", element_style, val_to_js);
495        Self::set_option(&options, "gutterStyle", gutter_style, val_to_js);
496        Self::set_option(&options, "onDrag", on_drag, val_to_js);
497        Self::set_option(&options, "onDragStart", on_drag_start, val_to_js);
498        Self::set_option(&options, "onDragEnd", on_drag_end, val_to_js);
499
500        options
501    }
502}