use std::{
cell::RefCell,
collections::HashMap,
pin::{pin, Pin},
task,
};
use futures::{channel::oneshot, Future};
use futures_signals::{
signal::SignalExt,
signal_vec::{self, SignalVec, SignalVecExt},
};
use paste::paste;
use pin_project::pin_project;
use silkenweb_base::document;
use silkenweb_signals_ext::value::SignalOrValue;
use wasm_bindgen::{JsCast, UnwrapThrowExt};
use crate::{
dom::{self, Dom, Dry, Wet},
event::{bubbling_events, GlobalEventCallback},
hydration::HydrationStats,
node::{
element::{
child_vec::{ChildVecHandle, ParentShared},
Const, Element, GenericElement,
},
Node,
},
HEAD_ID_ATTRIBUTE,
};
mod dry;
mod hydro;
mod wet;
#[must_use]
pub struct EventCallback(GlobalEventCallback<silkenweb_base::Document>);
impl EventCallback {
fn new<Event: JsCast>(name: &'static str, f: impl FnMut(Event) + 'static) -> Self {
Self(GlobalEventCallback::new(name, f))
}
pub fn perpetual(self) {
self.0.perpetual()
}
}
macro_rules! events{
($($name:ident: $typ:ty),* $(,)?) => { paste!{ $(
#[doc = "Add a `" $name "` event handler at the document level." ]
pub fn [< on_ $name >] (f: impl FnMut($typ) + 'static) -> EventCallback {
EventCallback::new(stringify!($name), f)
}
)*}}
}
pub fn on_dom_content_loaded(f: impl FnMut(web_sys::Event) + 'static) -> EventCallback {
EventCallback::new("DOMContentLoaded", f)
}
events! {
fullscreenchange: web_sys::Event,
fullscreenerror: web_sys::Event,
lostpointercapture: web_sys::PointerEvent,
pointerlockchange: web_sys::Event,
pointerlockerror: web_sys::Event,
readystatechange: web_sys::Event,
scroll: web_sys::Event,
scrollend: web_sys::Event,
selectionchange: web_sys::Event,
visibilitychange: web_sys::Event,
copy: web_sys::Event,
cut: web_sys::Event,
paste: web_sys::Event,
}
bubbling_events!();
pub trait Document: Dom + Sized {
type MountOutput;
type MountInHeadOutput;
fn mount(id: &str, element: impl Into<GenericElement<Self, Const>>) -> Self::MountOutput;
fn mount_in_head(id: &str, head: DocumentHead<Self>) -> Self::MountInHeadOutput;
fn unmount_all();
fn head_inner_html() -> String;
}
pub struct DocumentHead<D: Dom> {
child_vec: Pin<Box<dyn SignalVec<Item = GenericElement<D>>>>,
}
impl<D: Dom> Default for DocumentHead<D> {
fn default() -> Self {
Self::new()
}
}
impl<D: Dom> DocumentHead<D> {
pub fn new() -> Self {
let child_vec = Box::pin(signal_vec::always(Vec::new()));
Self { child_vec }
}
}
impl<D: Dom> DocumentHead<D> {
pub fn child(
self,
child: impl SignalOrValue<Item = impl Into<GenericElement<D>> + 'static>,
) -> Self {
self.optional_child(child.map(Some))
}
pub fn optional_child(
self,
child: impl SignalOrValue<Item = Option<impl Into<GenericElement<D>> + 'static>>,
) -> Self {
child.select(
|parent, child| {
if let Some(child) = child {
return parent.children_signal(signal_vec::always(vec![child]));
}
parent
},
|parent, child| {
let child_vec = child
.map(|child| child.into_iter().collect::<Vec<_>>())
.to_signal_vec();
parent.children_signal(child_vec)
},
self,
)
}
pub fn children<N>(self, children: impl IntoIterator<Item = N>) -> Self
where
N: Into<GenericElement<D>>,
{
self.children_signal(signal_vec::always(
children.into_iter().map(|child| child.into()).collect(),
))
}
pub fn children_signal<E>(mut self, children: impl SignalVec<Item = E> + 'static) -> Self
where
E: Into<GenericElement<D>>,
{
self.child_vec = self
.child_vec
.chain(children.map(|child| child.into()))
.boxed_local();
self
}
}
#[pin_project]
pub struct MountHydro(#[pin] oneshot::Receiver<HydrationStats>);
impl Future for MountHydro {
type Output = HydrationStats;
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
poll_receiver(self.project().0, cx)
}
}
#[pin_project]
pub struct MountHydroHead(#[pin] oneshot::Receiver<HydrationStats>);
impl Future for MountHydroHead {
type Output = HydrationStats;
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
poll_receiver(self.project().0, cx)
}
}
fn poll_receiver<T>(
receiver: Pin<&mut oneshot::Receiver<T>>,
cx: &mut task::Context<'_>,
) -> task::Poll<T> {
receiver.poll(cx).map(|r| {
r.unwrap_throw()
})
}
fn document_head() -> <Wet as dom::private::Dom>::Element {
<Wet as dom::private::Dom>::Element::from_element(document::head().unwrap_throw().into())
}
fn children_with_id<D: Dom>(head: DocumentHead<D>, id: &str) -> impl SignalVec<Item = Node<D>> {
head.child_vec.map({
let id = id.to_string();
move |child| child.attribute(HEAD_ID_ATTRIBUTE, id.clone()).into()
})
}
#[derive(Default)]
pub(crate) struct TaskLocal {
mounted_in_dry_head: RefCell<HashMap<String, ChildVecHandle<Dry, ParentShared>>>,
}
fn wet_insert_mounted(id: &str, element: GenericElement<Wet, Const>) {
let existing = WET_MOUNTED.with(|mounted| mounted.borrow_mut().insert(id.to_string(), element));
assert!(
existing.is_none(),
"Attempt to insert duplicate id ({id}) into document"
);
}
fn wet_unmount() {
for element in WET_MOUNTED.take().into_values() {
element.dom_element().remove()
}
}
struct MountedInHead<D: Dom>(RefCell<HashMap<String, ChildVecHandle<D, ParentShared>>>);
impl<D: Dom> MountedInHead<D> {
fn new() -> Self {
Self(RefCell::new(HashMap::new()))
}
fn mount(&self, id: &str, child_vec: ChildVecHandle<D, ParentShared>) {
let existing = self.0.borrow_mut().insert(id.to_string(), child_vec);
assert!(
existing.is_none(),
"Attempt to insert duplicate id ({id}) into head"
);
}
fn unmount_all(&self) {
for element in self.0.take().into_values() {
element.clear();
}
}
fn inner_html(&self) -> String {
let mut html = String::new();
for elem in self.0.borrow().values() {
html.push_str(&elem.inner_html());
}
html
}
}
thread_local! {
static WET_MOUNTED: RefCell<HashMap<String, GenericElement<Wet, Const>>> = RefCell::new(HashMap::new());
}