diff --git a/nui/include/nui/frontend/utility/delocalized.hpp b/nui/include/nui/frontend/utility/delocalized.hpp new file mode 100644 index 00000000..2be3635a --- /dev/null +++ b/nui/include/nui/frontend/utility/delocalized.hpp @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace Nui +{ + /** + * @brief A delocalized element can switch positions in several slots. + */ + template + class Delocalized + { + public: + Delocalized(Nui::ElementRenderer renderer = {}) + : delocalizedElement_{[&renderer]() -> decltype(delocalizedElement_) { + if (renderer) + { + auto element = Dom::Element::makeElement(HtmlElement{"div", &RegularHtmlElementBridge}); + element->replaceElement(renderer); + return element; + } + else + { + return {}; + } + }()} + , slotId_{std::make_unique>()} + {} + Delocalized(Delocalized const&) = delete; + Delocalized(Delocalized&&) = default; + Delocalized& operator=(Delocalized const&) = delete; + Delocalized& operator=(Delocalized&&) = default; + + std::shared_ptr element() + { + return delocalizedElement_; + } + void element(Nui::ElementRenderer renderer) + { + delocalizedElement_ = Dom::Element::makeElement(HtmlElement{"div", &RegularHtmlElementBridge}); + delocalizedElement_->replaceElement(renderer); + } + bool hasElement() const + { + return delocalizedElement_ != nullptr; + } + void initializeIfEmpty(Nui::ElementRenderer renderer) + { + if (!hasElement()) + element(std::move(renderer)); + } + + void slot(SlotId slot) + { + *slotId_ = slot; + } + SlotId slot() const + { + return slotId_->value(); + } + + template + friend ElementRenderer delocalizedSlot( + SlotIdB slot, + Delocalized& delocalizedElement, + std::vector wrapperAttributes, + Nui::ElementRenderer alternative); + + private: + std::shared_ptr delocalizedElement_; + // Wrapped in a unique_ptr for pointer stability. + std::unique_ptr> slotId_; + }; + + template + ElementRenderer delocalizedSlot( + SlotId slot, + Delocalized& delocalizedElement, + std::vector wrapperAttributes = {}, + ElementRenderer alternative = Elements::div{Attributes::style = "display: none"}()) + { + using namespace Elements; + using namespace Attributes; + + auto element = delocalizedElement.delocalizedElement_; + + return Elements::div{std::move(wrapperAttributes)}( + observe(*delocalizedElement.slotId_), + [element, slot, &observedId = *delocalizedElement.slotId_, alternative]() mutable { + return [element, slot, &observedId, alternative]( + Dom::Element& wrapper, Renderer const& gen) -> std::shared_ptr { + if (element && slot == observedId.value()) + { + wrapper.val().call("appendChild", element->val()); + return nil()(wrapper, gen); + } + else + { + if (alternative) + return alternative(wrapper, gen); + else + return nil()(wrapper, gen); + } + }; + }); + } + + inline ElementRenderer delocalizedSlot( + char const* slot, + Delocalized& delocalizedElement, + std::vector wrapperAttributes = {}, + ElementRenderer alternative = Elements::div{Attributes::style = "display: none"}()) + { + return delocalizedSlot( + std::string{slot}, delocalizedElement, std::move(wrapperAttributes), std::move(alternative)); + } +} \ No newline at end of file diff --git a/nui/test/nui/engine/array.cpp b/nui/test/nui/engine/array.cpp index 5eed705e..09ed7530 100644 --- a/nui/test/nui/engine/array.cpp +++ b/nui/test/nui/engine/array.cpp @@ -107,8 +107,9 @@ namespace Nui::Tests::Engine printIndent(indent + 1); allValues[(*it)->uid()].print(indent + 1, referenceStack); } + std::cout << "\n"; printIndent(indent); - std::cout << "\n]"; + std::cout << "]"; } void Array::updateArrayObject() diff --git a/nui/test/nui/engine/document.cpp b/nui/test/nui/engine/document.cpp index 32ee78b2..710085eb 100644 --- a/nui/test/nui/engine/document.cpp +++ b/nui/test/nui/engine/document.cpp @@ -55,6 +55,8 @@ namespace Nui::Tests::Engine auto elem = createBasicElement(tag); elem.set("nodeType", int{1}); elem.set("appendChild", Function{[self = elem](Nui::val value) -> Nui::val { + if (value.hasOwnProperty("parentNode")) + value["parentNode"].call("removeChild", value); value.set("parentNode", self); self["childNodes"].template as().push_back(value.handle()); return self["children"].template as().push_back(value.handle()); diff --git a/nui/test/nui/test_delocalized.hpp b/nui/test/nui/test_delocalized.hpp new file mode 100644 index 00000000..5e99369e --- /dev/null +++ b/nui/test/nui/test_delocalized.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include + +#include "common_test_fixture.hpp" +#include "engine/global_object.hpp" +#include "engine/document.hpp" + +#include +#include +#include + +#include +#include + +namespace Nui::Tests +{ + using namespace Engine; + using namespace std::string_literals; + + class TestDelocalized : public CommonTestFixture + { + protected: + auto bodyChildren() + { + return Nui::val::global("document")["body"]["children"]; + } + + public: + Delocalized delocalizedElement_; + }; + + TEST_F(TestDelocalized, SingleSlotDelocalizedRender) + { + using namespace Nui::Elements; + using namespace Nui::Attributes; + using Nui::Elements::div; + using Nui::Elements::span; + + delocalizedElement_.initializeIfEmpty(span{}("Hello")); + delocalizedElement_.slot("slot1"); + + render(body{}(delocalizedSlot("slot1", delocalizedElement_))); + + ASSERT_EQ(bodyChildren()["length"].as(), 1); + EXPECT_EQ(bodyChildren()[0]["tagName"].as(), "div"); + ASSERT_EQ(bodyChildren()[0]["children"]["length"].as(), 1); + EXPECT_EQ(bodyChildren()[0]["children"][0]["tagName"].as(), "span"); + EXPECT_EQ(bodyChildren()[0]["children"][0]["textContent"].as(), "Hello"); + } + + TEST_F(TestDelocalized, SlotShowsAlternativeWhenNotActive) + { + using namespace Nui::Elements; + using namespace Nui::Attributes; + using Nui::Elements::div; + using Nui::Elements::span; + + delocalizedElement_.initializeIfEmpty(span{}("Hello")); + delocalizedElement_.slot("slot2"); + + render(body{}(delocalizedSlot("slot1", delocalizedElement_))); + + ASSERT_EQ(bodyChildren()["length"].as(), 1); + EXPECT_EQ(bodyChildren()[0]["tagName"].as(), "div"); + ASSERT_EQ(bodyChildren()[0]["children"]["length"].as(), 1); + EXPECT_EQ(bodyChildren()[0]["children"][0]["tagName"].as(), "div"); + EXPECT_EQ(bodyChildren()[0]["children"][0]["attributes"]["style"].as(), "display: none"); + } + + TEST_F(TestDelocalized, WrapperGetsAttributesAssignedWhenSet) + { + using namespace Nui::Elements; + using namespace Nui::Attributes; + using Nui::Elements::div; + using Nui::Elements::span; + + delocalizedElement_.initializeIfEmpty(span{}("Hello")); + delocalizedElement_.slot("slot1"); + + render(body{}(delocalizedSlot("slot1", delocalizedElement_, std::vector{class_ = "wrapper"}))); + + ASSERT_EQ(bodyChildren()["length"].as(), 1); + EXPECT_EQ(bodyChildren()[0]["tagName"].as(), "div"); + EXPECT_EQ(bodyChildren()[0]["attributes"]["class"].as(), "wrapper"); + ASSERT_EQ(bodyChildren()[0]["children"]["length"].as(), 1); + EXPECT_EQ(bodyChildren()[0]["children"][0]["tagName"].as(), "span"); + } + + TEST_F(TestDelocalized, CanUseDifferentReplacementElement) + { + using namespace Nui::Elements; + using namespace Nui::Attributes; + using Nui::Elements::div; + using Nui::Elements::span; + + delocalizedElement_.initializeIfEmpty(span{}("Hello")); + delocalizedElement_.slot("slotX"); + + render(body{}(delocalizedSlot("slot1", delocalizedElement_, {}, div{}("Hello")))); + + ASSERT_EQ(bodyChildren()["length"].as(), 1); + EXPECT_EQ(bodyChildren()[0]["tagName"].as(), "div"); + ASSERT_EQ(bodyChildren()[0]["children"]["length"].as(), 1); + EXPECT_EQ(bodyChildren()[0]["children"][0]["tagName"].as(), "div"); + EXPECT_EQ(bodyChildren()[0]["children"][0]["textContent"].as(), "Hello"); + } +} diff --git a/nui/test/nui/tests.cpp b/nui/test/nui/tests.cpp index 2dd23568..b4acb30d 100644 --- a/nui/test/nui/tests.cpp +++ b/nui/test/nui/tests.cpp @@ -7,6 +7,7 @@ #include "components/test_table.hpp" #include "components/test_dialog.hpp" #include "components/test_select.hpp" +#include "test_delocalized.hpp" #include