From a1744ce86c7abbeab718c38d67a937eff805dd4a Mon Sep 17 00:00:00 2001 From: Cecile Tonglet Date: Sun, 25 Oct 2020 08:29:19 +0100 Subject: [PATCH] Tabs (#60) --- README.md | 4 +- yewprint-doc/build.rs | 2 + yewprint-doc/src/app.rs | 9 ++ yewprint-doc/src/lib.rs | 1 + yewprint-doc/src/tabs/example.rs | 148 +++++++++++++++++++++ yewprint-doc/src/tabs/mod.rs | 89 +++++++++++++ yewprint/src/lib.rs | 2 + yewprint/src/tabs.rs | 215 +++++++++++++++++++++++++++++++ 8 files changed, 468 insertions(+), 2 deletions(-) create mode 100644 yewprint-doc/src/tabs/example.rs create mode 100644 yewprint-doc/src/tabs/mod.rs create mode 100644 yewprint/src/tabs.rs diff --git a/README.md b/README.md index 9bd8973..5d7c0e8 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,8 @@ Roadmap - [ ] [ResizeSensor](https://blueprintjs.com/docs/#core/components/resize-sensor) - [ ] [Skeleton](https://blueprintjs.com/docs/#core/components/skeleton) - [ ] [Spinner](https://blueprintjs.com/docs/#core/components/spinner) - - [ ] [Tabs](https://blueprintjs.com/docs/#core/components/tabs) - - [ ] [Tag](https://blueprintjs.com/docs/#core/components/tag) + - [x] [Tabs](https://blueprintjs.com/docs/#core/components/tabs) + - [x] [Tag](https://blueprintjs.com/docs/#core/components/tag) - depends on: Text - [x] [Text](https://blueprintjs.com/docs/#core/components/text) - [x] [Tree](https://blueprintjs.com/docs/#core/components/tree) diff --git a/yewprint-doc/build.rs b/yewprint-doc/build.rs index 2d17e8d..083d3a5 100644 --- a/yewprint-doc/build.rs +++ b/yewprint-doc/build.rs @@ -39,6 +39,8 @@ fn main() { println!("cargo:rerun-if-changed={}", path.display()); } } + println!("cargo:rerun-if-changed=./src/lib.rs"); + println!("cargo:rerun-if-changed=build.rs"); } recursive( diff --git a/yewprint-doc/src/app.rs b/yewprint-doc/src/app.rs index 572ab67..a6661e5 100644 --- a/yewprint-doc/src/app.rs +++ b/yewprint-doc/src/app.rs @@ -7,6 +7,7 @@ use crate::divider::*; use crate::html_select::*; use crate::icon::*; use crate::progressbar::*; +use crate::tabs::*; use crate::tag::*; use crate::text::*; use crate::tree::*; @@ -167,6 +168,11 @@ impl Component for App { href="#switch" onclick=self.link.callback(|_| Msg::GoToMenu(DocMenu::Switch)) /> + html!(), DocMenu::Icon => html!(), DocMenu::ProgressBar => html!(), + DocMenu::Tabs => html!(), DocMenu::Tag => html!(), DocMenu::Menu => html!(), } @@ -250,6 +257,8 @@ pub enum DocMenu { ProgressBar, #[to = "/#switch"] Switch, + #[to = "/#tabs"] + Tabs, #[to = "/#tag"] Tag, #[to = "/#text"] diff --git a/yewprint-doc/src/lib.rs b/yewprint-doc/src/lib.rs index 131f0f4..7f0af07 100644 --- a/yewprint-doc/src/lib.rs +++ b/yewprint-doc/src/lib.rs @@ -10,6 +10,7 @@ mod html_select; mod icon; mod progressbar; mod switch; +mod tabs; mod tag; mod text; mod tree; diff --git a/yewprint-doc/src/tabs/example.rs b/yewprint-doc/src/tabs/example.rs new file mode 100644 index 0000000..3f931f3 --- /dev/null +++ b/yewprint-doc/src/tabs/example.rs @@ -0,0 +1,148 @@ +use yew::prelude::*; +use yewprint::{Tab, Tabs}; + +pub struct Example { + link: ComponentLink, + props: ExampleProps, + selected: Civilization, +} + +#[derive(Clone, PartialEq, Properties)] +pub struct ExampleProps { + pub animate: bool, + pub vertical: bool, +} + +impl Component for Example { + type Message = Civilization; + type Properties = ExampleProps; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + Example { + link, + props, + selected: Civilization::Minoan, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + if self.selected != msg { + self.selected = msg; + true + } else { + false + } + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + if self.props != props { + self.props = props; + true + } else { + false + } + } + + fn view(&self) -> Html { + html! { +
+ + animate=self.props.animate + vertical=self.props.vertical + selected_tab_id=self.selected + onchange=self.link.callback(|x| x) + tabs=vec![ + Tab { + disabled: false, + id: Civilization::Sumer, + panel: html! { <> + {"Sumer"} + {" is the earliest known civilization in the historical \ + region of southern Mesopotamia (now southern Iraq), \ + emerging during the Chalcolithic and early Bronze Ages \ + between the sixth and fifth millennium BCE. It is also \ + one of the first civilizations in the world, along with \ + Ancient Egypt, Norte Chico, Minoan civilization, Ancient \ + China, Mesoamerica and the Indus Valley. Living along \ + the valleys of the Tigris and Euphrates, Sumerian farmers \ + grew an abundance of grain and other crops, the surplus \ + from which enabled them to form urban settlements. \ + Prehistoric proto-writing dates back before 3000 BCE. The \ + earliest texts come from the cities of Uruk and Jemdet \ + Nasr, and date to between c. 3500 and c. 3000 BCE."} + }, + title: html!({"Sumer"}), + panel_class: None, + title_class: None, + }, + Tab { + disabled: false, + id: Civilization::Minoan, + panel: html! { <> + {"The "}{"Minoan civilization"} + {" was a Bronze Age Aegean civilization on the island \ + of Crete and other Aegean Islands, flourishing from c. \ + 3000 BC to c. 1450 BC until a late period of decline, \ + finally ending around 1100 BC. It represents the first \ + advanced civilization in Europe, leaving behind massive \ + building complexes, tools, artwork, writing systems, and \ + a massive network of trade. The civilization was \ + rediscovered at the beginning of the 20th century through \ + the work of British archaeologist Sir Arthur Evans. The \ + name \"Minoan\" derives from the mythical King Minos and \ + was coined by Evans, who identified the site at Knossos \ + with the labyrinth and the Minotaur. The Minoan \ + civilization has been described as the earliest of its \ + kind in Europe, and historian Will Durant called the \ + Minoans \"the first link in the European chain\"."} + }, + title: html!({"Minoan"}), + panel_class: None, + title_class: None, + }, + Tab { + disabled: true, + id: Civilization::AncientEgypt, + panel: html!(), + title: html!({"Ancient Egypt"}), + panel_class: None, + title_class: None, + }, + Tab { + disabled: false, + id: Civilization::IndusValley, + panel: html! { <> + {"The "}{"Indus Valley Civilisation (IVC)"} + {" was a Bronze Age civilisation in the northwestern \ + regions of South Asia, lasting from 3300 BCE to 1300 BCE, \ + and in its mature form from 2600 BCE to 1900 BCE. \ + Together with ancient Egypt and Mesopotamia, it was one \ + of three early civilisations of the Near East and South \ + Asia, and of the three, the most widespread, its sites \ + spanning an area stretching from northeast Afghanistan, \ + through much of Pakistan, and into western and \ + northwestern India. It flourished in the basins of the \ + Indus River, which flows through the length of Pakistan, \ + and along a system of perennial, mostly monsoon-fed, \ + rivers that once coursed in the vicinity of the seasonal \ + Ghaggar-Hakra river in northwest India and eastern \ + Pakistan."} + }, + title: html!("Indus Valley"), + panel_class: None, + title_class: None, + }, + ] + /> +
+ } + } +} + +#[derive(Clone, Copy, PartialEq, Hash)] +pub enum Civilization { + Sumer, + Minoan, + AncientEgypt, + IndusValley, +} diff --git a/yewprint-doc/src/tabs/mod.rs b/yewprint-doc/src/tabs/mod.rs new file mode 100644 index 0000000..62fd473 --- /dev/null +++ b/yewprint-doc/src/tabs/mod.rs @@ -0,0 +1,89 @@ +mod example; + +use crate::ExampleContainer; +use example::*; +use yew::prelude::*; +use yewprint::{Switch, H1, H5}; + +pub struct TabsDoc { + callback: Callback, + state: ExampleProps, +} + +impl Component for TabsDoc { + type Message = ExampleProps; + type Properties = (); + + fn create(_: Self::Properties, link: ComponentLink) -> Self { + TabsDoc { + callback: link.callback(|x| x), + state: ExampleProps { + animate: true, + vertical: false, + }, + } + } + + fn update(&mut self, msg: Self::Message) -> ShouldRender { + self.state = msg; + true + } + + fn change(&mut self, _props: Self::Properties) -> ShouldRender { + true + } + + fn view(&self) -> Html { + let example_props = self.state.clone(); + let source = crate::include_raw_html!( + concat!(env!("OUT_DIR"), "/", file!(), ".html"), + "bp3-code-block" + ); + + html! { +
+

{"Tabs"}

+
+ + }) + > + + +
+
+ } + } +} + +crate::build_example_prop_component! { + TabsProps for ExampleProps => + fn view(&self) -> Html { + html! { +
+
{"Props"}
+ + +
+ } + } +} diff --git a/yewprint/src/lib.rs b/yewprint/src/lib.rs index db0fdbb..3553e01 100644 --- a/yewprint/src/lib.rs +++ b/yewprint/src/lib.rs @@ -10,6 +10,7 @@ mod icon; mod menu; mod progressbar; mod switch; +mod tabs; mod tag; mod text; #[cfg(feature = "tree")] @@ -29,6 +30,7 @@ pub use id_tree; pub use menu::*; pub use progressbar::*; pub use switch::*; +pub use tabs::*; pub use tag::*; pub use text::*; #[cfg(feature = "tree")] diff --git a/yewprint/src/tabs.rs b/yewprint/src/tabs.rs new file mode 100644 index 0000000..e05cf36 --- /dev/null +++ b/yewprint/src/tabs.rs @@ -0,0 +1,215 @@ +use crate::ConditionalClass; +use std::collections::{hash_map::DefaultHasher, HashMap}; +use std::hash::{Hash, Hasher}; +use web_sys::HtmlElement; +use yew::prelude::*; + +pub struct Tabs { + link: ComponentLink, + props: TabsProps, + tab_refs: HashMap, +} + +#[derive(Clone, PartialEq, Properties)] +pub struct TabsProps { + #[prop_or_default] + pub animate: bool, + #[prop_or_default] + pub default_selected_tab_id: Option, + #[prop_or_default] + pub id: String, + #[prop_or_default] + pub large: ConditionalClass, + #[prop_or_default] + pub render_active_panel_only: bool, + pub selected_tab_id: T, + #[prop_or_default] + pub vertical: ConditionalClass, + #[prop_or_default] + pub onchange: Callback, + #[prop_or_default] + pub class: String, + pub tabs: Vec>, +} + +impl Component for Tabs { + type Message = (); + type Properties = TabsProps; + + fn create(props: Self::Properties, link: ComponentLink) -> Self { + let tab_refs = props + .tabs + .iter() + .map(|x| { + let mut hasher = DefaultHasher::new(); + x.id.hash(&mut hasher); + let id = hasher.finish(); + (id, NodeRef::default()) + }) + .collect::>(); + + Tabs { + props, + tab_refs, + link, + } + } + + fn update(&mut self, _msg: Self::Message) -> ShouldRender { + true + } + + fn change(&mut self, props: Self::Properties) -> ShouldRender { + if self.props != props { + self.props = props; + true + } else { + false + } + } + + fn rendered(&mut self, first_render: bool) { + if first_render && self.props.animate { + self.link.send_message(()); + } + } + + fn view(&self) -> Html { + let tabs = self + .props + .tabs + .iter() + .map(|x| { + let mut hasher = DefaultHasher::new(); + x.id.hash(&mut hasher); + let id = hasher.finish(); + let title_id = format!("bp3-tab-title_{}_{}", self.props.id, id); + let panel_id = format!("bp3-tab-panel_{}_{}", self.props.id, id); + let selected = self.props.selected_tab_id == x.id; + (x, id, title_id, panel_id, selected) + }) + .collect::>(); + + html! { +
+
+ { + if self.props.animate { + let mut hasher = DefaultHasher::new(); + self.props.selected_tab_id.hash(&mut hasher); + let id = hasher.finish(); + + if let Some(element) = self.tab_refs[&id].cast::() + { + let indicator_style = format!( + "height: {}px; width: {}px; \ + transform: translateX({}px) translateY({}px);", + element.client_height(), + element.client_width(), + element.offset_left(), + element.offset_top(), + ); + + html! { +
+
+
+ } + } else { + html!() + } + } else { + html!() + } + } + { + tabs + .iter() + .map(|(props, id, title_id, panel_id, selected)| html! { + + }) + .collect::() + } +
+ { + tabs + .iter() + .filter(|(_, _, _, _, selected)| { + !self.props.render_active_panel_only || *selected + }) + .map(|(props, id, title_id, panel_id, selected)| html! { +
+ { props.panel.clone() } +
+ }) + .collect::() + } +
+ } + } +} + +#[derive(Clone, PartialEq)] +pub struct Tab { + pub disabled: bool, + pub id: T, + pub title: Html, + pub panel: Html, + pub title_class: Option, + pub panel_class: Option, +}