use super::HandlerId;
use crate::template::{self, ComponentId};
use std::{cell::RefCell, collections::VecDeque, rc::Rc};
use wasm_bindgen::{JsCast, prelude::*};
use web_sys::{Document, Element, EventTarget, NodeList};
#[wasm_bindgen(module = "/src/dom/strings.js")]
extern "C" {
#[wasm_bindgen(js_name = setTextIfChanged)]
pub(super) fn set_text_if_changed(node: &web_sys::Text, value: &str);
#[wasm_bindgen(js_name = setIntegerTextIfChanged)]
pub(super) fn set_integer_text_if_changed(node: &web_sys::Text, value: f64);
#[wasm_bindgen(catch, js_name = setIntegerAttribute)]
pub(super) fn set_integer_attribute(
node: &Element,
name: &str,
value: f64,
) -> Result<(), JsValue>;
#[wasm_bindgen(catch, js_name = serverRows)]
fn server_rows_native(
container: &Element,
name: &JsValue,
encoded: &str,
count: u32,
complete: bool,
) -> Result<js_sys::Array, JsValue>;
#[wasm_bindgen(js_name = listenBundleOk)]
fn listen_bundle_ok(
nodes: &JsValue,
index: u32,
name: &JsValue,
dispatch: &JsValue,
slot: u32,
generation: u32,
) -> bool;
#[wasm_bindgen(js_name = unlistenBundleOk)]
fn unlisten_bundle_ok(nodes: &JsValue, index: u32, name: &JsValue, slot: u32) -> bool;
#[wasm_bindgen(js_name = unlistenOk)]
fn unlisten_ok(target: &EventTarget, name: &JsValue, slot: u32) -> bool;
#[wasm_bindgen(js_name = takeRemovalFailure)]
fn take_removal_failure() -> JsValue;
#[wasm_bindgen(js_name = insertBeforeOk)]
fn insert_before_ok(
parent: &Element,
node: &web_sys::Node,
anchor: Option<&web_sys::Node>,
) -> bool;
#[wasm_bindgen(js_name = listenOk)]
fn listen_ok(
target: &EventTarget,
name: &JsValue,
dispatch: &JsValue,
slot: u32,
generation: u32,
) -> bool;
}
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = Element, js_name = Element)]
type StringElement;
#[wasm_bindgen(method, structural, js_name = getAttribute)]
fn attribute(this: &StringElement, name: &JsValue) -> Option<String>;
#[wasm_bindgen(method, structural, js_name = getAttribute)]
fn attribute_value(this: &StringElement, name: &JsValue) -> JsValue;
#[wasm_bindgen(method, structural, catch, js_name = setAttribute)]
fn set_attribute_value(
this: &StringElement,
name: &JsValue,
value: &JsValue,
) -> Result<(), JsValue>;
#[wasm_bindgen(extends = Document, js_name = Document)]
type StringDocument;
#[wasm_bindgen(method, structural, catch, js_name = querySelectorAll)]
fn query(this: &StringDocument, selector: &JsValue) -> Result<NodeList, JsValue>;
}
#[derive(Clone, Copy)]
pub(super) enum Name {
#[cfg_attr(
not(feature = "islands"),
expect(
dead_code,
reason = "component attributes are read during island hydration"
)
)]
Component,
Version,
Element,
Key,
Click,
Input,
Change,
Instance,
}
thread_local! {
static NAMES: [JsValue; 8] = [
template::COMPONENT_ATTRIBUTE,
template::VERSION_ATTRIBUTE,
template::ELEMENT_ATTRIBUTE,
"data-fusor-key", "click", "input", "change", template::INSTANCE_ATTRIBUTE,
].map(JsValue::from_str);
}
fn with_name<R>(name: Name, call: impl FnOnce(&JsValue) -> R) -> R {
NAMES.with(|names| call(&names[name as usize]))
}
fn string_element(element: &Element) -> &StringElement {
element.unchecked_ref()
}
pub(super) fn server_rows(
container: &Element,
encoded: &str,
count: u32,
complete: bool,
) -> Result<js_sys::Array, JsValue> {
with_name(Name::Key, |name| {
server_rows_native(container, name, encoded, count, complete)
})
}
pub(super) fn attribute(element: &Element, name: Name) -> Option<String> {
with_name(name, |name| string_element(element).attribute(name))
}
pub(super) enum EventName {
Cached(Name),
Owned(JsValue),
}
impl From<&str> for EventName {
fn from(name: &str) -> Self {
match name {
"click" => Self::Cached(Name::Click),
"input" => Self::Cached(Name::Input),
"change" => Self::Cached(Name::Change),
_ => Self::Owned(JsValue::from_str(name)),
}
}
}
impl EventName {
fn with<R>(&self, call: impl FnOnce(&JsValue) -> R) -> R {
match self {
Self::Cached(name) => with_name(*name, call),
Self::Owned(value) => call(value),
}
}
}
pub(super) fn listen(
target: &EventTarget,
name: &EventName,
dispatch: &JsValue,
HandlerId { slot, generation }: HandlerId,
) -> Result<(), JsValue> {
name.with(|name| status(listen_ok(target, name, dispatch, slot, generation)))
}
pub(super) fn listen_bundle(
nodes: &JsValue,
index: u32,
name: &EventName,
dispatch: &JsValue,
HandlerId { slot, generation }: HandlerId,
) -> Result<(), JsValue> {
name.with(|name| {
status(listen_bundle_ok(
nodes, index, name, dispatch, slot, generation,
))
})
}
pub(super) fn unlisten_bundle(
nodes: &JsValue,
index: u32,
name: &EventName,
slot: u32,
) -> Result<(), JsValue> {
name.with(|name| status(unlisten_bundle_ok(nodes, index, name, slot)))
}
fn status(ok: bool) -> Result<(), JsValue> {
if ok {
Ok(())
} else {
Err(take_removal_failure())
}
}
pub(super) fn insert_before(
parent: &Element,
node: &web_sys::Node,
anchor: Option<&web_sys::Node>,
) -> Result<(), JsValue> {
status(insert_before_ok(parent, node, anchor))
}
pub(super) fn remove(target: &EventTarget, name: &EventName, slot: u32) -> Result<(), JsValue> {
name.with(|name| status(unlisten_ok(target, name, slot)))
}
pub(super) struct DescriptorStrings {
component: ComponentId,
version: u32,
selector: JsValue,
schema: JsValue,
identity: JsValue,
}
thread_local! {
static DESCRIPTORS: RefCell<VecDeque<Rc<DescriptorStrings>>> = const { RefCell::new(VecDeque::new()) };
}
pub(super) fn descriptor(component: ComponentId, version: u32) -> Rc<DescriptorStrings> {
super::cached(
&DESCRIPTORS,
32,
|entry| entry.component == component && entry.version == version,
|| {
Rc::new(DescriptorStrings {
component,
version,
selector: JsValue::from_str(&format!(
"[{}=\"{component}\"]",
template::COMPONENT_ATTRIBUTE
)),
schema: JsValue::from_str(&version.to_string()),
identity: JsValue::from_str(&component.to_string()),
})
},
)
}
impl DescriptorStrings {
pub(super) fn native(&self) -> (&JsValue, &JsValue, &JsValue) {
(&self.selector, &self.schema, &self.identity)
}
pub(super) fn roots(&self, document: &Document) -> Result<NodeList, JsValue> {
document
.unchecked_ref::<StringDocument>()
.query(&self.selector)
}
pub(super) fn version_matches(&self, element: &Element) -> bool {
with_name(Name::Version, |name| {
string_element(element).attribute_value(name) == self.schema
})
}
#[cfg(feature = "islands")]
pub(super) fn component_matches(&self, element: &Element) -> bool {
with_name(Name::Component, |name| {
string_element(element).attribute_value(name) == self.identity
})
}
pub(super) fn mark_instance(&self, element: &Element) -> Result<(), JsValue> {
with_name(Name::Instance, |name| {
string_element(element).set_attribute_value(name, &self.identity)
})
}
}
thread_local! {
static STATIC_ATTRIBUTES: RefCell<VecDeque<(&'static str, Rc<JsValue>)>> = const { RefCell::new(VecDeque::new()) };
}
pub(super) fn static_attribute(name: &'static str) -> Rc<JsValue> {
super::cached(
&STATIC_ATTRIBUTES,
64,
|(key, _)| *key == name,
|| (name, Rc::new(JsValue::from_str(name))),
)
.1
}