#![doc = include_str!("../LEPTOS.md")]
use crate::common::{
Color, Language, Size, Theme, Variant, base_code_style, base_content_style,
base_copy_trigger_style, base_lang_badge_style, base_title_style, themed_block_style,
themed_header_style, themed_pre_style,
};
use gloo_timers::future::TimeoutFuture;
use leptos::prelude::*;
#[component]
pub fn Code(
children: Children,
#[prop(default = Color::Default)]
color: Color,
#[prop(default = Variant::Subtle)]
variant: Variant,
#[prop(default = Size::Sm)]
size: Size,
#[prop(default = "")]
class: &'static str,
#[prop(default = "")]
style: &'static str,
#[prop(default = "")]
id: &'static str,
#[prop(default = "")]
aria_label: &'static str,
#[prop(default = "")]
data_testid: &'static str,
) -> impl IntoView {
let color_style = match variant {
Variant::Solid => color.to_solid_style(),
Variant::Subtle => color.to_subtle_style(),
Variant::Outline => color.to_outline_style(),
Variant::Surface => color.to_surface_style(),
Variant::Plain => "",
Variant::Custom(_) => "",
};
let full_style = format!(
"{} {} {} {}",
base_code_style(),
size.to_style(),
color_style,
style,
);
let element_class = format!(
"code {} {} {} {}",
variant.to_class(),
color.to_class(),
size.to_class(),
class,
);
view! {
<code
id=id
class=element_class
style=full_style
aria-label=aria_label
data-testid=data_testid
>
{children()}
</code>
}
}
#[derive(Clone, Debug, PartialEq, Copy)]
pub struct BlockContext {
pub code: &'static str,
pub language: Language,
pub theme: Theme,
}
#[component]
pub fn Header(
children: Children,
#[prop(default = "")]
class: &'static str,
#[prop(default = "")]
style: &'static str,
#[prop(default = "")]
id: &'static str,
) -> impl IntoView {
let theme = use_context::<BlockContext>()
.map(|c| c.theme)
.unwrap_or_default();
view! {
<div
id=id
class=format!("code-block__header {}", class)
style=format!("{} {}", themed_header_style(theme), style)
>
{children()}
</div>
}
}
#[component]
pub fn Title(
children: Children,
#[prop(default = "")]
class: &'static str,
#[prop(default = "")]
style: &'static str,
#[prop(default = "")]
id: &'static str,
) -> impl IntoView {
view! {
<span
id=id
class=format!("code-block__title {}", class)
style=format!("{} {}", base_title_style(), style)
>
{children()}
</span>
}
}
#[component]
pub fn CopyTrigger(
#[prop(default = "Copy")]
copy_label: &'static str,
#[prop(default = "Copied!")]
copied_label: &'static str,
#[prop(default = 2000)]
reset_ms: u32,
#[prop(default = "")]
class: &'static str,
#[prop(default = "")]
style: &'static str,
#[prop(default = "")]
id: &'static str,
#[prop(default = "Copy code to clipboard")]
aria_label: &'static str,
) -> impl IntoView {
let ctx = use_context::<BlockContext>().expect("CopyTrigger must be inside Block");
let copied = RwSignal::new(false);
let on_click = move |_| {
let code = ctx.code;
spawner(async move {
if let Some(window) = web_sys::window() {
let clipboard = window.navigator().clipboard();
let _ = wasm_bindgen_futures::JsFuture::from(clipboard.write_text(code)).await;
}
copied.set(true);
TimeoutFuture::new(reset_ms).await;
copied.set(false);
});
};
let label = move || {
if copied.get() {
copied_label
} else {
copy_label
}
};
let full_style = format!("{} {}", base_copy_trigger_style(), style);
view! {
<button
id=id
type="button"
class=format!("code-block__copy-trigger {}", class)
style=full_style
on:click=on_click
aria-label=aria_label
aria-live="polite"
>
{label}
</button>
}
}
#[component]
pub fn Content(
#[prop(optional)]
children: Option<Children>,
#[prop(default = "")]
class: &'static str,
#[prop(default = "")]
style: &'static str,
#[prop(default = "")]
id: &'static str,
#[prop(default = "")]
pre_class: &'static str,
#[prop(default = "")]
pre_style: &'static str,
) -> impl IntoView {
let ctx = use_context::<BlockContext>().expect("Content must be inside Block");
let full_style = format!("{} {}", base_content_style(), style);
let pre_full_style = format!("{} {}", themed_pre_style(ctx.theme), pre_style);
let hl = crate::common::highlight_code(ctx.code, ctx.language, ctx.theme);
view! {
<div
id=id
class=format!("code-block__content {}", class)
style=full_style
>
{match children {
Some(ch) => view! { {ch()} }.into_any(),
None => view! {
<pre
class=format!("code-block__pre {}", pre_class)
style=pre_full_style
tabindex="0"
>
<code
class=ctx.language.to_class()
inner_html=hl
/>
</pre>
}.into_any(),
}}
</div>
}
}
#[component]
pub fn LanguageBadge(
#[prop(default = "")]
class: &'static str,
#[prop(default = "")]
style: &'static str,
) -> impl IntoView {
let ctx = use_context::<BlockContext>().expect("LanguageBadge must be inside Block");
view! {
<span
class=format!("code-block__lang {}", class)
style=format!("{} {}", base_lang_badge_style(), style)
aria-label=format!("Language: {}", ctx.language.to_label())
>
{ctx.language.to_label()}
</span>
}
}
#[component]
pub fn Block(
code: &'static str,
#[prop(default = Language::Plain)]
language: Language,
#[prop(default = Theme::OneDark)]
theme: Theme,
#[prop(optional)]
children: Option<Children>,
#[prop(default = "")]
class: &'static str,
#[prop(default = "")]
style: &'static str,
#[prop(default = "")]
id: &'static str,
#[prop(default = "Code block")]
aria_label: &'static str,
#[prop(default = "")]
data_testid: &'static str,
) -> impl IntoView {
let ctx = BlockContext {
code,
language,
theme,
};
provide_context(ctx);
let full_style = format!("{} {}", themed_block_style(theme), style);
let block_class = format!("code-block {} {}", language.to_class(), class);
view! {
<figure
id=id
class=block_class
style=full_style
role="region"
aria-label=aria_label
data-testid=data_testid
>
{match children {
Some(ch) => view! { {ch()} }.into_any(),
None => view! { <Content /> }.into_any(),
}}
</figure>
}
}
fn spawner<F: std::future::Future<Output = ()> + 'static>(fut: F) {
wasm_bindgen_futures::spawn_local(fut);
}