#![doc = include_str!("../YEW.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 wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local;
use yew::prelude::*;
#[derive(Properties, PartialEq, Clone)]
pub struct CodeProps {
#[prop_or_default]
pub children: Children,
#[prop_or_default]
pub color: Color,
#[prop_or_default]
pub variant: Variant,
#[prop_or_default]
pub size: Size,
#[prop_or("code")]
pub as_element: &'static str,
#[prop_or_default]
pub class: &'static str,
#[prop_or_default]
pub style: &'static str,
#[prop_or_default]
pub id: &'static str,
#[prop_or_default]
pub aria_label: &'static str,
#[prop_or_default]
pub data_testid: &'static str,
}
#[function_component(Code)]
pub fn code(props: &CodeProps) -> Html {
let color_style = match props.variant {
Variant::Solid => props.color.to_solid_style(),
Variant::Subtle => props.color.to_subtle_style(),
Variant::Outline => props.color.to_outline_style(),
Variant::Surface => props.color.to_surface_style(),
Variant::Plain => "",
Variant::Custom(_) => "",
};
let full_style = format!(
"{} {} {} {}",
base_code_style(),
props.size.to_style(),
color_style,
props.style,
);
let element_class = format!(
"code {} {} {} {}",
props.variant.to_class(),
props.color.to_class(),
props.size.to_class(),
props.class,
);
html! {
<code
id={props.id}
class={element_class}
style={full_style}
aria-label={props.aria_label}
data-testid={props.data_testid}
>
{ for props.children.iter() }
</code>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct HeaderProps {
#[prop_or_default]
pub children: Children,
#[prop_or_default]
pub class: &'static str,
#[prop_or_default]
pub style: &'static str,
#[prop_or_default]
pub id: &'static str,
}
#[function_component(Header)]
pub fn code_block_header(props: &HeaderProps) -> Html {
let theme = use_context::<BlockContext>()
.map(|c| c.theme)
.unwrap_or_default();
html! {
<div
id={props.id}
class={format!("code-block__header {}", props.class)}
style={format!("{} {}", themed_header_style(theme), props.style)}
>
{ for props.children.iter() }
</div>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct TitleProps {
#[prop_or_default]
pub children: Children,
#[prop_or_default]
pub class: &'static str,
#[prop_or_default]
pub style: &'static str,
#[prop_or_default]
pub id: &'static str,
}
#[function_component(Title)]
pub fn code_block_title(props: &TitleProps) -> Html {
html! {
<span
id={props.id}
class={format!("code-block__title {}", props.class)}
style={format!("{} {}", base_title_style(), props.style)}
>
{ for props.children.iter() }
</span>
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct BlockContext {
pub code: &'static str,
pub language: Language,
pub theme: Theme,
}
#[derive(Properties, PartialEq, Clone)]
pub struct CopyTriggerProps {
#[prop_or("Copy")]
pub copy_label: &'static str,
#[prop_or("Copied!")]
pub copied_label: &'static str,
#[prop_or(2000)]
pub reset_ms: u32,
#[prop_or_default]
pub class: &'static str,
#[prop_or_default]
pub style: &'static str,
#[prop_or_default]
pub id: &'static str,
#[prop_or("Copy code to clipboard")]
pub aria_label: &'static str,
}
#[function_component(CopyTrigger)]
pub fn code_block_copy_trigger(props: &CopyTriggerProps) -> Html {
let ctx = use_context::<BlockContext>().expect("CopyTrigger must be inside Block");
let copied = use_state(|| false);
let on_click = {
let code = ctx.code;
let copied = copied.clone();
let reset_ms = props.reset_ms;
Callback::from(move |_: MouseEvent| {
let copied = copied.clone();
let window = web_sys::window().unwrap();
let navigator = window.navigator();
let clipboard = navigator.clipboard();
let promise = clipboard.write_text(code);
spawn_local(async move {
let _ = wasm_bindgen_futures::JsFuture::from(promise).await;
copied.set(true);
let copied2 = copied.clone();
let closure = Closure::once(move || {
copied2.set(false);
});
window
.set_timeout_with_callback_and_timeout_and_arguments_0(
closure.as_ref().unchecked_ref(),
reset_ms as i32,
)
.ok();
closure.forget();
});
})
};
let label = if *copied {
props.copied_label
} else {
props.copy_label
};
let full_style = format!("{} {}", base_copy_trigger_style(), props.style);
html! {
<button
id={props.id}
type="button"
class={format!("code-block__copy-trigger {}", props.class)}
style={full_style}
onclick={on_click}
aria-label={props.aria_label}
aria-live="polite"
>
{label}
</button>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct ContentProps {
#[prop_or_default]
pub children: Children,
#[prop_or_default]
pub class: &'static str,
#[prop_or_default]
pub style: &'static str,
#[prop_or_default]
pub id: &'static str,
#[prop_or_default]
pub pre_class: &'static str,
#[prop_or_default]
pub pre_style: &'static str,
}
#[function_component(Content)]
pub fn code_block_content(props: &ContentProps) -> Html {
let ctx = use_context::<BlockContext>().expect("Content must be inside Block");
let has_children = props.children.iter().next().is_some();
let full_style = format!("{} {}", base_content_style(), props.style);
let pre_full_style = format!("{} {}", themed_pre_style(ctx.theme), props.pre_style);
html! {
<div
id={props.id}
class={format!("code-block__content {}", props.class)}
style={full_style}
>
if has_children {
{ for props.children.iter() }
} else {
<pre
class={format!("code-block__pre {}", props.pre_class)}
style={pre_full_style}
tabindex={"0"}
>
{ Html::from_html_unchecked(
format!(
"<code class=\"{}\">{}</code>",
ctx.language.to_class(),
crate::common::highlight_code(ctx.code, ctx.language, ctx.theme)
).into()
) }
</pre>
}
</div>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct LanguageBadgeProps {
#[prop_or_default]
pub class: &'static str,
#[prop_or_default]
pub style: &'static str,
}
#[function_component(LanguageBadge)]
pub fn code_block_language_badge(props: &LanguageBadgeProps) -> Html {
let ctx = use_context::<BlockContext>().expect("LanguageBadge must be inside Block");
html! {
<span
class={format!("code-block__lang {}", props.class)}
style={format!("{} {}", base_lang_badge_style(), props.style)}
aria-label={format!("Language: {}", ctx.language.to_label())}
>
{ctx.language.to_label()}
</span>
}
}
#[derive(Properties, PartialEq, Clone)]
pub struct BlockProps {
pub code: &'static str,
#[prop_or_default]
pub language: Language,
#[prop_or_default]
pub theme: Theme,
#[prop_or_default]
pub children: Children,
#[prop_or_default]
pub class: &'static str,
#[prop_or_default]
pub style: &'static str,
#[prop_or_default]
pub id: &'static str,
#[prop_or("Code block")]
pub aria_label: &'static str,
#[prop_or_default]
pub data_testid: &'static str,
}
#[function_component(Block)]
pub fn code_block(props: &BlockProps) -> Html {
let ctx = BlockContext {
code: props.code,
language: props.language,
theme: props.theme,
};
let has_children = props.children.iter().next().is_some();
let full_style = format!("{} {}", themed_block_style(props.theme), props.style);
let block_class = format!("code-block {} {}", props.language.to_class(), props.class);
html! {
<ContextProvider<BlockContext> context={ctx}>
<figure
id={props.id}
class={block_class}
style={full_style}
role="region"
aria-label={props.aria_label}
data-testid={props.data_testid}
>
if has_children {
{ for props.children.iter() }
} else {
<Content />
}
</figure>
</ContextProvider<BlockContext>>
}
}