diff --git a/ci/run_tests.sh b/ci/run_tests.sh index 8afb0386..a25c48c1 100755 --- a/ci/run_tests.sh +++ b/ci/run_tests.sh @@ -3,6 +3,8 @@ set -euo pipefail IFS=$'\n\t' +export RUST_BACKTRACE=1 + CARGO_WEB=${CARGO_WEB:-cargo-web} set +e diff --git a/src/lib.rs b/src/lib.rs index 41273beb..2022b82e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -248,6 +248,7 @@ pub mod web { pub use webapi::html_element::{IHtmlElement, HtmlElement, Rect}; pub use webapi::window_or_worker::IWindowOrWorker; pub use webapi::parent_node::IParentNode; + pub use webapi::slotable::ISlotable; pub use webapi::non_element_parent_node::INonElementParentNode; pub use webapi::token_list::TokenList; pub use webapi::node_list::NodeList; @@ -268,6 +269,8 @@ pub mod web { pub use webapi::child_node::IChildNode; pub use webapi::gamepad::{Gamepad, GamepadButton, GamepadMappingType}; pub use webapi::selection::Selection; + pub use webapi::shadow_root::{ShadowRootMode, ShadowRoot}; + pub use webapi::html_elements::SlotContentKind; /// A module containing error types. pub mod error { @@ -304,6 +307,8 @@ pub mod web { pub use webapi::html_elements::CanvasElement; pub use webapi::html_elements::SelectElement; pub use webapi::html_elements::OptionElement; + pub use webapi::html_elements::TemplateElement; + pub use webapi::html_elements::SlotElement; } /// A module containing JavaScript DOM events. @@ -427,6 +432,8 @@ pub mod web { DataTransferItem, DataTransferItemKind, }; + + pub use webapi::events::slot::SlotChangeEvent; } /// APIs related to MIDI. @@ -470,7 +477,8 @@ pub mod traits { IWindowOrWorker, IParentNode, INonElementParentNode, - IChildNode + IChildNode, + ISlotable, }; #[doc(hidden)] diff --git a/src/webapi/document.rs b/src/webapi/document.rs index 4f91bb04..06229c52 100644 --- a/src/webapi/document.rs +++ b/src/webapi/document.rs @@ -1,7 +1,7 @@ use webcore::value::{Reference, Value}; use webcore::try_from::{TryInto, TryFrom}; use webapi::event_target::{IEventTarget, EventTarget}; -use webapi::node::{INode, Node}; +use webapi::node::{INode, Node, CloneKind}; use webapi::element::Element; use webapi::html_element::HtmlElement; use webapi::document_fragment::DocumentFragment; @@ -9,7 +9,7 @@ use webapi::text_node::TextNode; use webapi::location::Location; use webapi::parent_node::IParentNode; use webapi::non_element_parent_node::INonElementParentNode; -use webapi::dom_exception::{InvalidCharacterError, NamespaceError}; +use webapi::dom_exception::{InvalidCharacterError, NamespaceError, NotSupportedError}; /// The `Document` interface represents any web page loaded in the browser and /// serves as an entry point into the web page's content, which is the DOM tree. @@ -181,12 +181,30 @@ impl Document { @{self}.exitPointerLock(); ); } + + /// Import node from another document + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Document/importNode) + // https://dom.spec.whatwg.org/#ref-for-dom-document-importnode + pub fn import_node( &self, n: &N, kind: CloneKind ) -> Result { + let deep = match kind { + CloneKind::Deep => true, + CloneKind::Shallow => false, + }; + + js_try!( + return @{self}.importNode( @{n.as_ref()}, @{deep} ); + ).unwrap() + } } #[cfg(all(test, feature = "web_test"))] mod web_tests { use super::*; + use webapi::node::{Node, INode, CloneKind}; + use webapi::html_elements::TemplateElement; + use webapi::html_element::HtmlElement; #[test] fn test_create_element_invalid_character() { @@ -211,4 +229,22 @@ mod web_tests { v => panic!("expected NamespaceError, got {:?}", v), } } -} \ No newline at end of file + + #[test] + fn test_import_node() { + let document = document(); + let tpl: TemplateElement = Node::from_html("") + .unwrap() + .try_into() + .unwrap(); + + let n = document.import_node(&tpl.content(), CloneKind::Deep).unwrap(); + let child_nodes = n.child_nodes(); + assert_eq!(child_nodes.len(), 1); + + let span_element: HtmlElement = child_nodes.iter().next().unwrap().try_into().unwrap(); + + assert_eq!(span_element.node_name(), "SPAN"); + assert_eq!(js!( return @{span_element}.innerHTML; ), "aaabbbcccddd"); + } +} diff --git a/src/webapi/document_fragment.rs b/src/webapi/document_fragment.rs index 419e4c09..0508fcd7 100644 --- a/src/webapi/document_fragment.rs +++ b/src/webapi/document_fragment.rs @@ -1,6 +1,7 @@ use webcore::value::Reference; use webapi::event_target::{IEventTarget, EventTarget}; use webapi::node::{INode, Node}; +use webapi::parent_node::IParentNode; /// A reference to a JavaScript object DocumentFragment. /// @@ -12,4 +13,5 @@ use webapi::node::{INode, Node}; pub struct DocumentFragment( Reference ); impl IEventTarget for DocumentFragment {} -impl INode for DocumentFragment {} \ No newline at end of file +impl INode for DocumentFragment {} +impl IParentNode for DocumentFragment {} diff --git a/src/webapi/element.rs b/src/webapi/element.rs index d14952e1..61bb215e 100644 --- a/src/webapi/element.rs +++ b/src/webapi/element.rs @@ -1,12 +1,19 @@ use webcore::value::Reference; -use webcore::try_from::TryInto; +use webcore::try_from::{TryFrom, TryInto}; use webapi::dom_exception::{InvalidCharacterError, InvalidPointerId, NoModificationAllowedError, SyntaxError}; use webapi::event_target::{IEventTarget, EventTarget}; use webapi::node::{INode, Node}; use webapi::token_list::TokenList; use webapi::parent_node::IParentNode; use webapi::child_node::IChildNode; -use webcore::try_from::TryFrom; +use webapi::slotable::ISlotable; +use webapi::shadow_root::{ShadowRootMode, ShadowRoot}; +use webapi::dom_exception::{NotSupportedError, InvalidStateError}; + +error_enum_boilerplate! { + AttachShadowError, + NotSupportedError, InvalidStateError +} /// The `IElement` interface represents an object of a [Document](struct.Document.html). /// This interface describes methods and properties common to all @@ -14,7 +21,7 @@ use webcore::try_from::TryFrom; /// /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element) // https://dom.spec.whatwg.org/#element -pub trait IElement: INode + IParentNode + IChildNode { +pub trait IElement: INode + IParentNode + IChildNode + ISlotable { /// The Element.namespaceURI read-only property returns the namespace URI /// of the element, or null if the element is not in a namespace. /// @@ -225,6 +232,40 @@ pub trait IElement: INode + IParentNode + IChildNode { fn insert_html_after( &self, html: &str ) -> Result<(), InsertAdjacentError> { self.insert_adjacent_html(InsertPosition::AfterEnd, html) } + + /// The slot property of the Element interface returns the name of the shadow DOM + /// slot the element is inserted in. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element/slot) + // https://dom.spec.whatwg.org/#ref-for-dom-element-slot + fn slot( &self ) -> String { + js!( + return @{self.as_ref()}.slot; + ).try_into().unwrap() + } + + /// Attach a shadow DOM tree to the specified element and returns a reference to its `ShadowRoot`. + /// It returns a shadow root if successfully attached or `None` if the element cannot be attached. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element/attachShadow) + // https://dom.spec.whatwg.org/#ref-for-dom-element-attachshadow + fn attach_shadow( &self, mode: ShadowRootMode ) -> Result { + js_try!( + return @{self.as_ref()}.attachShadow( { mode: @{mode.as_str()}} ) + ).unwrap() + } + + /// Returns the shadow root of the current element or `None`. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Element/shadowRoot) + // https://dom.spec.whatwg.org/#ref-for-dom-element-shadowroot + fn shadow_root( &self ) -> Option { + unsafe { + js!( + return @{self.as_ref()}.shadowRoot; + ).into_reference_unchecked() + } + } } @@ -244,6 +285,7 @@ impl IElement for Element {} impl< T: IElement > IParentNode for T {} impl< T: IElement > IChildNode for T {} +impl< T: IElement > ISlotable for T {} #[derive(Copy, Clone, PartialEq, Eq, Debug)] pub enum InsertPosition { @@ -278,6 +320,7 @@ impl InsertPosition { mod tests { use super::*; use webapi::document::document; + use webapi::shadow_root::ShadowRootMode; fn div() -> Element { js!( @@ -351,4 +394,20 @@ mod tests { _ => false, }); } + + #[test] + fn test_attach_shadow_mode_open() { + let element = document().create_element("div").unwrap(); + let shadow_root = element.attach_shadow(ShadowRootMode::Open).unwrap(); + assert_eq!(shadow_root.mode(), ShadowRootMode::Open); + assert_eq!(element.shadow_root(), Some(shadow_root)); + } + + #[test] + fn test_attach_shadow_mode_closed() { + let element = document().create_element("div").unwrap(); + let shadow_root = element.attach_shadow(ShadowRootMode::Closed).unwrap(); + assert_eq!(shadow_root.mode(), ShadowRootMode::Closed); + assert!(element.shadow_root().is_none()); + } } diff --git a/src/webapi/events/mod.rs b/src/webapi/events/mod.rs index 6075ef72..adf44e95 100644 --- a/src/webapi/events/mod.rs +++ b/src/webapi/events/mod.rs @@ -8,3 +8,4 @@ pub mod mouse; pub mod pointer; pub mod progress; pub mod socket; +pub mod slot; diff --git a/src/webapi/events/slot.rs b/src/webapi/events/slot.rs new file mode 100644 index 00000000..0ff387a7 --- /dev/null +++ b/src/webapi/events/slot.rs @@ -0,0 +1,15 @@ +use webcore::value::Reference; +use webapi::event::{IEvent, Event}; + +/// The `slotchange` event is fired on an HTMLSlotElement instance +/// (`` element) when the node(s) contained in that slot change. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/Events/slotchange) +// https://dom.spec.whatwg.org/#mutation-observers +#[derive(Clone, Debug, PartialEq, Eq, ReferenceType)] +#[reference(instance_of = "Event")] +#[reference(event = "slotchange")] +#[reference(subclass_of(Event))] +pub struct SlotChangeEvent( Reference ); + +impl IEvent for SlotChangeEvent {} diff --git a/src/webapi/html_elements/mod.rs b/src/webapi/html_elements/mod.rs index a9baf7ad..99bf3965 100644 --- a/src/webapi/html_elements/mod.rs +++ b/src/webapi/html_elements/mod.rs @@ -4,6 +4,8 @@ mod input; mod textarea; mod select; mod option; +mod template; +mod slot; pub use self::canvas::CanvasElement; pub use self::image::ImageElement; @@ -11,5 +13,7 @@ pub use self::input::InputElement; pub use self::textarea::TextAreaElement; pub use self::select::SelectElement; pub use self::option::OptionElement; +pub use self::template::TemplateElement; +pub use self::slot::{SlotElement, SlotContentKind}; -pub use self::select::UnknownValueError; \ No newline at end of file +pub use self::select::UnknownValueError; diff --git a/src/webapi/html_elements/option.rs b/src/webapi/html_elements/option.rs index b160d44b..6e98b767 100644 --- a/src/webapi/html_elements/option.rs +++ b/src/webapi/html_elements/option.rs @@ -39,4 +39,4 @@ impl OptionElement { return @{self}.value; ).try_into().unwrap() } -} \ No newline at end of file +} diff --git a/src/webapi/html_elements/select.rs b/src/webapi/html_elements/select.rs index 8f9e1ee2..39e1cd3a 100644 --- a/src/webapi/html_elements/select.rs +++ b/src/webapi/html_elements/select.rs @@ -228,4 +228,4 @@ mod tests{ assert_eq!(se.selected_indices(), vec![0,2,4]); assert_eq!(se.selected_values(), vec!["first".to_string(), "third".to_string(), "".to_string()]); } -} \ No newline at end of file +} diff --git a/src/webapi/html_elements/slot.rs b/src/webapi/html_elements/slot.rs new file mode 100644 index 00000000..a269189f --- /dev/null +++ b/src/webapi/html_elements/slot.rs @@ -0,0 +1,182 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::element::{IElement, Element}; +use webapi::html_element::{IHtmlElement, HtmlElement}; + +/// An enum which determines whether +/// [SlotElement::assigned_nodes](struct.SlotElement.html#method.assigned_nodes) / +/// [SlotElement::assigned_elements](struct.SlotElement.html#method.assigned_elements) will +/// return the fallback content when nothing has been assigned to the slot. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub enum SlotContentKind { + /// Will only return content assigned. + AssignedOnly, + /// Will return the fallback content if nothing has been assigned. + WithFallback, +} + +impl SlotContentKind { + fn to_bool(&self) -> bool { + match *self { + SlotContentKind::AssignedOnly => false, + SlotContentKind::WithFallback => true, + } + } +} + +/// The HTML `` element represents a placeholder inside a web component that +/// you can fill with your own markup, which lets you create separate DOM trees and +/// present them together. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/slot) +// https://html.spec.whatwg.org/multipage/scripting.html#htmlslotelement +#[derive(Clone, Debug, PartialEq, Eq, ReferenceType)] +#[reference(instance_of = "HTMLSlotElement")] +#[reference(subclass_of(EventTarget, Node, Element, HtmlElement))] +pub struct SlotElement( Reference ); + +impl IEventTarget for SlotElement {} +impl INode for SlotElement {} +impl IElement for SlotElement {} +impl IHtmlElement for SlotElement {} + +impl SlotElement { + /// The slot's name + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/name) + // https://html.spec.whatwg.org/multipage/scripting.html#attr-slot-name + #[inline] + pub fn name ( &self ) -> String { + js! ( + return @{self}.name; + ).try_into().unwrap() + } + + /// Setter of name. + #[inline] + pub fn set_name( &self, new_name: &str ) { + js! ( @(no_return) + @{self}.name = @{new_name}; + ); + } + + /// Returns slot's assigned nodes. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/HTMLSlotElement/assignedNodes) + // https://html.spec.whatwg.org/multipage/scripting.html#the-slot-element:dom-slot-assignednodes + pub fn assigned_nodes( &self, kind: SlotContentKind ) -> Vec { + js! ( + return @{self}.assignedNodes( { flatten: @{kind.to_bool()} } ); + ).try_into().unwrap() + } + + /// Similar to [assigned_nodes()](#method.assigned_nodes) but limited result to only elements. + /// + /// [(Spec)](https://html.spec.whatwg.org/multipage/scripting.html#dom-slot-assignedelements) + // https://html.spec.whatwg.org/multipage/scripting.html#the-slot-element:dom-slot-assignedelements + pub fn assigned_elements( &self, kind: SlotContentKind ) -> Vec { + js! ( + return @{self}.assignedElements( { flatten: @{kind.to_bool()} } ); + ).try_into().unwrap() + } +} + +// Remove unused imports when `#[ignore]` below is removed. +#[allow(unused_imports)] +#[cfg(all(test, feature = "web_test"))] +mod tests { + use super::*; + use webapi::element::{Element, IElement}; + use webapi::html_elements::TemplateElement; + use webapi::node::{CloneKind, INode, Node}; + use webapi::parent_node::IParentNode; + use webapi::shadow_root::ShadowRootMode; + + // `#[ignore]`ed because travis keeps complaining. + #[test] + #[ignore] + fn test_assigned_elements() { + let div: Element = Node::from_html(r#"
+ +
"#) + .unwrap() + .try_into() + .unwrap(); + let tpl: TemplateElement = Node::from_html(r#""#) + .unwrap() + .try_into() + .unwrap(); + + let span1 = div.query_selector("#span1").unwrap().unwrap(); + + let shadow_root = div.attach_shadow(ShadowRootMode::Open).unwrap(); + let n = tpl.content().clone_node(CloneKind::Deep).unwrap(); + + shadow_root.append_child(&n); + + let slot1: SlotElement = shadow_root + .query_selector("#slot1") + .unwrap() + .unwrap() + .try_into() + .unwrap(); + let slot2: SlotElement = shadow_root + .query_selector("#slot2") + .unwrap() + .unwrap() + .try_into() + .unwrap(); + + assert_eq!( + slot1 + .assigned_nodes(SlotContentKind::AssignedOnly) + .iter() + .map(|m| m.clone().try_into().unwrap()) + .collect::>(), + &[span1.clone()] + ); + assert_eq!(slot2.assigned_nodes(SlotContentKind::AssignedOnly).len(), 0); + + assert_eq!( + slot1.assigned_elements(SlotContentKind::AssignedOnly), + &[span1.clone()] + ); + assert_eq!( + slot2.assigned_elements(SlotContentKind::AssignedOnly).len(), + 0 + ); + + assert_eq!( + slot1 + .assigned_nodes(SlotContentKind::WithFallback) + .iter() + .map(|m| m.clone().try_into().unwrap()) + .collect::>(), + &[span1.clone()] + ); + assert_eq!( + slot1.assigned_elements(SlotContentKind::WithFallback), + &[span1.clone()] + ); + + let slot2_nodes = slot2.assigned_nodes(SlotContentKind::WithFallback); + let slot2_elements = slot2.assigned_elements(SlotContentKind::WithFallback); + + assert_eq!( + slot2_nodes + .iter() + .map(|m| m.clone().try_into().unwrap()) + .collect::>(), + slot2_elements + ); + assert_eq!(slot2_nodes.len(), 1); + let fallback_span = slot2_nodes[0].clone(); + + assert_eq!(js!( return @{fallback_span}.id; ), "span3"); + } +} diff --git a/src/webapi/html_elements/template.rs b/src/webapi/html_elements/template.rs new file mode 100644 index 00000000..e6d363d2 --- /dev/null +++ b/src/webapi/html_elements/template.rs @@ -0,0 +1,60 @@ +use webcore::value::Reference; +use webcore::try_from::TryInto; +use webapi::event_target::{IEventTarget, EventTarget}; +use webapi::node::{INode, Node}; +use webapi::element::{IElement, Element}; +use webapi::html_element::{IHtmlElement, HtmlElement}; +use webapi::document_fragment::DocumentFragment; + +/// The HTML `