korvin-core 0.2.1

The core for korvin frontend framework
Documentation
use self::error::{DebugOf, JsError, RawOperationError, RawOperationResult};
use crate::data::{AttributeName, AttributeValue, ElementId, EventListenerWrapper, TagName};
use tracing::instrument;
use wasm_bindgen::JsCast;
use web_sys::{Document, HtmlInputElement, Node};

pub mod attribute;
pub mod error;

#[derive(Debug)]
pub struct Removed<T>(T);

impl<T> Removed<T> {
    pub fn unpack(self) -> T {
        self.0
    }
}

#[instrument(level = "trace", ret)]
pub fn remove_element_in_place(element_id: ElementId) -> Removed<ElementId> {
    element_id.as_ref().remove();
    Removed(element_id)
}

#[instrument(level = "trace", ret, err)]
pub fn pick_up_children(from: ElementId) -> RawOperationResult<Vec<Node>> {
    let parent_nodes = from.as_ref().child_nodes();
    (0..parent_nodes.length())
        .rev()
        .filter_map(|node| parent_nodes.get(node))
        .map(|node| from.as_ref().remove_child(&node).map_err(JsError::from))
        .collect::<Result<Vec<_>, _>>()
        .map(|mut children| {
            children.reverse();
            children
        })
        .map_err(|source| RawOperationError::PickingUpChildren {
            from_parent: DebugOf::new(&from),
            source,
        })
}

#[instrument(level = "trace", ret, err)]
pub fn place_children(to: ElementId, children: Vec<Node>) -> RawOperationResult<()> {
    children
        .into_iter()
        .try_for_each(|node| {
            to.as_ref()
                .append_child(&node)
                .map(|_| ())
                .map_err(JsError::from)
        })
        .map_err(|source| RawOperationError::InsertingChildren {
            to_parent: DebugOf::new(&to),
            source,
        })
}

#[instrument(level = "trace", ret, err)]
pub fn reparent_all(from: ElementId, to: ElementId) -> RawOperationResult<()> {
    pick_up_children(from).and_then(|picked_up| place_children(to, picked_up))
}

#[instrument(level = "trace", ret, err)]
pub fn swap_siblings(node_1: ElementId, node_2: ElementId) -> RawOperationResult<()> {
    node_1
        .as_ref()
        .before_with_node_1(node_2.as_ref())
        .map_err(JsError::from)
        .map_err(|source| RawOperationError::SwappingElements {
            element: DebugOf::new(&node_1),
            with: DebugOf::new(&node_2),
            source,
        })
}

#[instrument(level = "trace", ret, err)]
pub fn replace_element(element: ElementId, with: ElementId) -> RawOperationResult<ElementId> {
    element
        .as_ref()
        .replace_with_with_node_1(with.as_ref())
        .map_err(JsError::from)
        .map_err(|source| RawOperationError::SwappingElements {
            element: DebugOf::new(&element),
            with: DebugOf::new(&with),
            source,
        })
        .map(|_| with)
}

#[instrument(level = "trace", ret, err)]
pub fn create_element(document: &Document, kind: TagName) -> RawOperationResult<ElementId> {
    document
        .create_element(kind.as_ref())
        .map_err(JsError::from)
        .map_err(|source| RawOperationError::CreatingElement { kind, source })
        .map(ElementId::new)
}

pub(crate) fn add_event_listener<EventKind>(
    element: ElementId,
    event_listener: EventListenerWrapper<EventKind>,
) -> RawOperationResult<EventListenerWrapper<EventKind>> {
    element
        .as_ref()
        .add_event_listener_with_callback(
            event_listener.name.as_ref(),
            event_listener.closure.js_function(),
        )
        .map_err(JsError::from)
        .map_err(RawOperationError::AddEventListener)
        .map(|_| event_listener)
}

pub(crate) fn remove_event_listener<EventKind>(
    element: ElementId,
    event_listener: EventListenerWrapper<EventKind>,
) -> RawOperationResult<EventListenerWrapper<EventKind>> {
    element
        .as_ref()
        .remove_event_listener_with_callback(
            event_listener.name.as_ref(),
            event_listener.closure.js_function(),
        )
        .map_err(JsError::from)
        .map_err(RawOperationError::RemoveEventListener)
        .map(|_| event_listener)
}

#[instrument(level = "trace", ret, err)]
pub fn insert_element(element: ElementId, to: ElementId) -> RawOperationResult<ElementId> {
    let inserted = element.clone();
    to.as_ref()
        .append_child(element.as_ref())
        .map_err(JsError::from)
        .map_err(|source| RawOperationError::InsertElement {
            to: DebugOf::new(&to),
            element: DebugOf::new(&element),
            source,
        })
        .map(|_| inserted)
}

#[instrument(level = "trace", ret, err)]
pub fn set_attribute(
    element: ElementId,
    attribute: AttributeName,
    value: Option<AttributeValue>,
) -> RawOperationResult<(AttributeName, Option<AttributeValue>)> {
    let old = element
        .as_ref()
        .get_attribute(attribute.as_ref())
        .map(From::from);
    match value {
        Some(value) => element
            .as_ref()
            .set_attribute(attribute.as_ref(), value.as_ref())
            .map_err(JsError::from)
            .map_err(|source| RawOperationError::SetAttribute {
                element: DebugOf::new(&element),
                attribute: attribute.clone(),
                value: Some(value.clone()),
                source,
            })
            .map(|_| (attribute.clone(), old)),
        None => element
            .as_ref()
            .remove_attribute(attribute.as_ref())
            .map_err(JsError::from)
            .map_err(|source| RawOperationError::SetAttribute {
                element: DebugOf::new(&element),
                attribute: attribute.clone(),
                value: None,
                source,
            })
            .map(|_| (attribute.clone(), old)),
    }
}

#[instrument(level = "trace", ret, err)]
pub fn remove_element(from_parent: ElementId, element: ElementId) -> RawOperationResult<ElementId> {
    from_parent
        .as_ref()
        .remove_child(element.as_ref())
        .map_err(JsError::from)
        .map_err(|source| RawOperationError::RemoveElement {
            from_parent: DebugOf::new(&from_parent),
            element: DebugOf::new(&element),
            source,
        })
        .and_then(
            #[allow(unused_variables)]
            {
                |node| -> RawOperationResult<_> {
                    #[cfg(debug_assertions)]
                    {
                        if (&node) != (element.as_ref().as_ref()) {
                            return Err(RawOperationError::ElementMismatch {
                                expected: DebugOf::new(&element),
                                got: DebugOf::new(&node),
                            });
                        }
                    }

                    Ok(element)
                }
            },
        )
}

#[instrument(level = "trace", ret)]
pub fn set_text(element: ElementId, text: Option<AttributeValue>) -> Option<AttributeValue> {
    let old = element.as_ref().text_content().map(AttributeValue::from);
    element
        .as_ref()
        .set_text_content(text.as_ref().map(|a| a.as_ref()));
    old
}

#[instrument(level = "trace", ret)]
pub fn set_input_value(
    element: ElementId,
    value: AttributeValue,
) -> RawOperationResult<AttributeValue> {
    element
        .as_ref()
        .clone()
        .dyn_into::<HtmlInputElement>()
        .map_err(|actual| RawOperationError::NotAnInputElement {
            element: DebugOf::new(&actual),
        })
        .map(|input| {
            let old = input.value();
            input.set_value(value.as_ref());
            old.into()
        })
}

#[instrument(level = "trace", ret)]
pub fn unset_text(element: ElementId) -> Option<AttributeValue> {
    let old = element.as_ref().text_content().map(AttributeValue::from);
    element.as_ref().set_text_content(None);
    old
}