use crate::base;
use crate::history::{History, HistoryListener};
use crate::scope::{NavigationTarget, ScopeContext};
use crate::state::State;
use crate::target::Target;
use gloo_utils::window;
use std::borrow::Cow;
use std::fmt::Debug;
use std::rc::Rc;
use web_sys::Location;
use yew::html::IntoPropValue;
use yew::prelude::*;
#[derive(Clone, PartialEq)]
pub struct RouterContext<T>
where
T: Target,
{
pub(crate) base: Rc<String>,
pub(crate) scope: Rc<ScopeContext<T>>,
pub active_target: Option<T>,
}
impl<T> RouterContext<T>
where
T: Target,
{
pub fn push(&self, target: T) {
self.scope.push(target);
}
pub fn replace(&self, target: T) {
self.scope.replace(target);
}
pub fn push_with(&self, target: T, state: State) {
self.scope.push_with(target, state.0);
}
pub fn replace_with(&self, target: T, state: State) {
self.scope.replace_with(target, state.0);
}
pub fn render_target(&self, target: T) -> String {
self.scope.collect(target)
}
pub fn render_target_with(&self, target: T, state: impl IntoPropValue<State>) -> String {
let mut result = self.scope.collect(target);
let state = state.into_prop_value().0;
if state.is_null() || state.is_undefined() {
} else if let Some(value) = state.as_string() {
result.push('#');
result.push_str(&value);
} else if let Some(value) = js_sys::JSON::stringify(&state)
.ok()
.and_then(|s| s.as_string())
{
result.push('#');
result.push_str(&value);
}
result
}
pub fn is_same(&self, target: &T) -> bool {
match &self.active_target {
Some(current) => current == target,
None => false,
}
}
pub fn is_active(&self, target: &T, predicate: Option<&Callback<T, bool>>) -> bool {
match predicate {
Some(predicate) => self
.active_target
.clone()
.map(|target| predicate.emit(target))
.unwrap_or_default(),
None => self.is_same(target),
}
}
pub fn active(&self) -> &Option<T> {
&self.active_target
}
}
#[derive(Clone, Debug, PartialEq, Properties)]
pub struct RouterProps<T>
where
T: Target,
{
#[prop_or_default]
pub children: Children,
#[prop_or_default]
pub default: Option<T>,
#[prop_or_default]
pub base: Option<String>,
}
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum StackOperation {
Push,
Replace,
}
#[derive(Debug)]
#[doc(hidden)]
pub enum Msg<T: Target> {
RouteChanged,
ChangeTarget(NavigationTarget<T>, StackOperation),
}
pub struct Router<T: Target> {
_listener: HistoryListener,
target: Option<T>,
scope: Rc<ScopeContext<T>>,
router: RouterContext<T>,
base: Rc<String>,
}
impl<T> Component for Router<T>
where
T: Target + 'static,
{
type Message = Msg<T>;
type Properties = RouterProps<T>;
fn create(ctx: &Context<Self>) -> Self {
let cb = ctx.link().callback(|_| Msg::RouteChanged);
let base = Rc::new(
ctx.props()
.base
.clone()
.or_else(base::eval_base)
.unwrap_or_default(),
);
let target = Self::parse_location(&base, window().location())
.or_else(|| ctx.props().default.clone());
let listener = History::listener(move || {
cb.emit(window().location());
});
let (scope, router) = Self::build_context(base.clone(), &target, ctx);
Self {
_listener: listener,
target,
scope,
router,
base,
}
}
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
match msg {
Msg::RouteChanged => {
let target = Self::parse_location(&self.base, window().location())
.or_else(|| ctx.props().default.clone());
if target != self.target {
self.target = target;
self.sync_context(ctx);
return true;
}
}
Msg::ChangeTarget(target, operation) => {
let route = Self::render_target(&self.base, &target.target);
let _ = match operation {
StackOperation::Push => History::push_state(target.state, &route),
StackOperation::Replace => History::replace_state(target.state, &route),
};
ctx.link().send_message(Msg::RouteChanged)
}
}
false
}
fn changed(&mut self, ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
self.sync_context(ctx);
true
}
fn view(&self, ctx: &Context<Self>) -> Html {
let scope = self.scope.clone();
let router = self.router.clone();
html! (
<ContextProvider<ScopeContext<T>> context={(*scope).clone()}>
<ContextProvider<RouterContext<T >> context={router}>
{ for ctx.props().children.iter() }
</ContextProvider<RouterContext<T >>>
</ContextProvider<ScopeContext<T>>>
)
}
}
impl<T: Target> Router<T> {
fn render_target(base: &str, target: &T) -> String {
let path = target
.render_path()
.into_iter()
.map(|segment| urlencoding::encode(&segment).to_string())
.collect::<Vec<_>>()
.join("/");
format!("{base}/{path}",)
}
fn parse_location(base: &str, location: Location) -> Option<T> {
let path = location.pathname().unwrap_or_default();
if !path.starts_with(base) {
return None;
}
let (_, path) = path.split_at(base.len());
let path: Result<Vec<Cow<str>>, _> = path
.split('/')
.skip(1)
.map(urlencoding::decode)
.collect();
let path = match &path {
Ok(path) => path.iter().map(|s| s.as_ref()).collect::<Vec<_>>(),
Err(_) => return None,
};
T::parse_path(&path)
}
fn sync_context(&mut self, ctx: &Context<Self>) {
let (scope, router) = Self::build_context(self.base.clone(), &self.target, ctx);
self.scope = scope;
self.router = router;
}
fn build_context(
base: Rc<String>,
target: &Option<T>,
ctx: &Context<Self>,
) -> (Rc<ScopeContext<T>>, RouterContext<T>) {
let scope = Rc::new(ScopeContext {
upwards: ctx
.link()
.callback(|(target, operation)| Msg::ChangeTarget(target, operation)),
collect: {
let base = base.clone();
Callback::from(move |target| Self::render_target(&base, &target))
},
});
let router = RouterContext {
base,
scope: scope.clone(),
active_target: target.clone(),
};
(scope, router)
}
}
#[hook]
pub fn use_router<T>() -> Option<RouterContext<T>>
where
T: Target + 'static,
{
use_context()
}