diff --git a/source/web/Div.h b/source/web/Div.h index c4fc10e301..633a0b6fee 100644 --- a/source/web/Div.h +++ b/source/web/Div.h @@ -191,6 +191,45 @@ namespace web { } } + /// Remove the old widget, putting the new widget in its place (i.e., same index) + /// @param the Widget to remove + /// @param the Widget to add + void ReplaceChild(Widget & old_child, Widget new_child) override { + // ensure child is present + emp_assert(1 == std::count( + std::begin(m_children), + std::end(m_children), + old_child + )); + // unregister and remove child + Unregister(*std::find( + std::begin(m_children), + std::end(m_children), + old_child + )); + m_children.emplace(std::find( + std::begin(m_children), + std::end(m_children), + old_child + ), new_child); + m_children.erase( + std::remove( + std::begin(m_children), + std::end(m_children), + old_child + ), + std::end(m_children) + ); + + old_child->parent = nullptr; + // update info for new child + new_child->parent = this; + Register(new_child); + new_child->DoActivate(false); + // render changes + if (state == Widget::ACTIVE) ReplaceHTML(); + } + void DoActivate(bool top_level=true) override { for (auto & child : m_children) child->DoActivate(false); internal::WidgetInfo::DoActivate(top_level); diff --git a/source/web/Document.h b/source/web/Document.h index 233088047f..e3c6a19f75 100644 --- a/source/web/Document.h +++ b/source/web/Document.h @@ -94,6 +94,11 @@ namespace web { info->Append(new_widget); return new_widget; } + template web::Input AddInput(T &&... args) { + web::Input new_widget(std::forward(args)...); + info->Append(new_widget); + return new_widget; + } template web::Table AddTable(T &&... args) { web::Table new_widget(std::forward(args)...); info->Append(new_widget); diff --git a/source/web/Input.h b/source/web/Input.h index 993d918386..e06677a9a1 100644 --- a/source/web/Input.h +++ b/source/web/Input.h @@ -81,7 +81,7 @@ namespace web { if (value != "") HTML << " value=\"" << value << "\""; // Add a current value if there is one. if (step != "") HTML << " step=\"" << step << "\""; // Add a step if there is one. HTML << " id=\"" << id << "\""; // Indicate ID. - HTML << " onchange=\"" << onchange_info << "\""; // Indicate action on change. + HTML << " oninput=\"" << onchange_info << "\""; // Indicate action on change. HTML << ">" << label << ""; // Close and label the Input. } diff --git a/source/web/Listeners.h b/source/web/Listeners.h index 029c682e70..d37c5c5b05 100644 --- a/source/web/Listeners.h +++ b/source/web/Listeners.h @@ -8,6 +8,8 @@ */ +// NEW CODE + #ifndef EMP_WEB_LISTENERS_H #define EMP_WEB_LISTENERS_H @@ -27,43 +29,60 @@ namespace web { /// Track a set of JavaScript Listeners with their callback IDs. class Listeners { private: - std::map listeners; ///< Map triggers to callback IDs + std::map> listeners; ///< Map triggers to list of callback IDs. The outer map associates event names with an inner map, which maps handler IDs to their callbacks. public: Listeners() { ; } Listeners(const Listeners &) = default; Listeners & operator=(const Listeners &) = default; - /// How many listeners are we tracking? - size_t GetSize() const { return listeners.size(); } + /// How many listeners are we tracking? + // Should we just store this? + size_t GetSize() const { + size_t count = 0; + for (auto event_pair : listeners) + for (auto handler_pair : event_pair.second) { + count++; + } + return count; + } /// Use a pre-calculated function ID with a new listener. - Listeners & Set(const std::string & name, size_t fun_id) { - emp_assert(!Has(name)); - listeners[name] = fun_id; + Listeners & Set(const std::string & event_name, size_t fun_id, std::string handler_id="default") { + emp_assert(!HasHandler(event_name, handler_id)); + listeners[event_name][handler_id] = fun_id; return *this; } /// Calculate its own function ID with JSWrap. template - Listeners & Set(const std::string & name, const std::function & in_fun) { - emp_assert(!Has(name)); - listeners[name] = JSWrap(in_fun); + Listeners & Set(const std::string & event_name, const std::function & in_fun, std::string handler_id="default") { + emp_assert(!HasHandler(event_name, handler_id)); + listeners[event_name][handler_id] = JSWrap(in_fun); return *this; } - /// Determine if a specified listener exists. + /// Determine if any listener exists with the specified event name. bool Has(const std::string & event_name) const { return listeners.find(event_name) != listeners.end(); } - /// Get the ID associated with a specific listener. - size_t GetID(const std::string & event_name) { - emp_assert(Has(event_name)); - return listeners[event_name]; + /// Determine if any listener exists with the specified event name and handler ID. + bool HasHandler(const std::string & event_name, std::string handler_id = "default") const { + if (listeners.find(event_name) != listeners.end()) { + const std::map& m = listeners.at(event_name); + return m.find(handler_id) != m.end(); + } + return false; + } + + /// Get the ID associated with a specific listener. + size_t GetID(const std::string & event_name, std::string handler_id = "default") { + emp_assert(HasHandler(event_name, handler_id)); + return listeners[event_name][handler_id]; } - const std::map & GetMap() const { + const std::map> & GetMap() const { return listeners; } @@ -73,32 +92,56 @@ namespace web { listeners.clear(); } - /// Remove a specific listener. + /// Remove all listeners with the given event name. void Remove(const std::string & event_name) { // @CAO: Delete function to be called. listeners.erase(event_name); } + /// Remove the listener with the given event name and handler ID. + void Remove(const std::string & event_name, std::string handler_id = "default") { + // @CAO: Delete function to be called. + if (Has(event_name)) + { + listeners[event_name].erase(handler_id); + std::cout << "removed listener " << handler_id << std::endl; + } + } + /// Apply all of the listeners being tracked. - void Apply(const std::string & widget_id) { + void Apply(const std::string & widget_id, bool add_before_onclick = false) { // Find the current object only once. #ifdef __EMSCRIPTEN__ EM_ASM_ARGS({ var id = UTF8ToString($0); emp_i.cur_obj = $( '#' + id ); }, widget_id.c_str()); + if(add_before_onclick){ + EM_ASM({ + var onclick_handler = emp_i.onclick; + emp_i.removeProp('onclick'); + }); + } #endif for (auto event_pair : listeners) { -#ifdef __EMSCRIPTEN__ - EM_ASM_ARGS({ - var name = UTF8ToString($0); - emp_i.cur_obj.on( name, function(evt) { emp.Callback($1, evt); } ); - }, event_pair.first.c_str(), event_pair.second); -#else - std::cout << "Setting '" << widget_id << "' listener '" << event_pair.first - << "' to '" << event_pair.second << "'."; + for (auto handler_pair : event_pair.second) { + size_t & fun_id = handler_pair.second; + #ifdef __EMSCRIPTEN__ + EM_ASM_ARGS({ + var name = UTF8ToString($0); + emp_i.cur_obj.on( name, function(evt) { emp.Callback($1, evt); } ); + }, event_pair.first.c_str(), fun_id); + if(add_before_onclick){ + EM_ASM({ + emp_i.click(onclick_handler); + }); + } + #else + std::cout << "Setting '" << widget_id << "' listener '" << event_pair.first + << "' to '" << fun_id << "'."; #endif + } } } @@ -106,13 +149,27 @@ namespace web { /// Apply a SPECIFIC listener. static void Apply(const std::string & widget_id, const std::string event_name, - size_t fun_id) { + size_t fun_id, bool add_before_onclick = false) { #ifdef __EMSCRIPTEN__ EM_ASM_ARGS({ var id = UTF8ToString($0); var name = UTF8ToString($1); + if($3){ + var onclick_handler = $('#' + id).prop('onclick'); + if(onclick_handler){ + $('#' + id).prop('onclick', null); + alert('bumping onclick back ' + '$(#' + id + ').prop(onclick, null)'); + } + } $( '#' + id ).on( name, function(evt) { emp.Callback($2, evt); } ); - }, widget_id.c_str(), event_name.c_str(), fun_id); + if($3){ + if(onclick_handler){ + //$('#' + id).click(onclick_handler); + //$('#' + id).on('click', onclick_handler); + //$('#' + id).attr('onclick', 'onclick_handler();'); + } + } + }, widget_id.c_str(), event_name.c_str(), fun_id, add_before_onclick); #else std::cout << "Setting '" << widget_id << "' listener '" << event_name << "' to function id '" << fun_id << "'."; diff --git a/source/web/Table.h b/source/web/Table.h index 4aa8015bb9..582f98f638 100644 --- a/source/web/Table.h +++ b/source/web/Table.h @@ -537,13 +537,15 @@ namespace web { } /// Apply CSS to appropriate component based on current state. - void DoListen(const std::string & event_name, size_t fun_id) override { - parent_t::DoListen(event_name, fun_id); + void DoListen(const std::string & event_name, size_t fun_id, + const std::string handler_id="default", + bool add_before_onclick = false) override { + parent_t::DoListen(event_name, fun_id, handler_id, add_before_onclick); } public: - TableWidget(size_t r, size_t c, const std::string & in_id="") - : WidgetFacet(in_id), cur_row(0), cur_col(0) + TableWidget(size_t r, size_t c, const std::string & in_id=""): + WidgetFacet(in_id), cur_row(0), cur_col(0) { emp_assert(r > 0 && c > 0); // Ensure that we have rows and columns! info = new internal::TableInfo(in_id); diff --git a/source/web/Tutorial.h b/source/web/Tutorial.h new file mode 100644 index 0000000000..3eb2a6328f --- /dev/null +++ b/source/web/Tutorial.h @@ -0,0 +1,1008 @@ +#ifndef EMP_TUTORIAL_H +#define EMP_TUTORIAL_H + +#ifdef __EMSCRIPTEN__ + #include "web/web.h" +#endif + +#include "base/vector.h" +#include "base/Ptr.h" +#include +#include + +#include +#include +#include +#include +#include + +#ifdef __EMSCRIPTEN__ + namespace UI = emp::web; +#endif +class Trigger; +class State; +class Tutorial; + + + + + +class Trigger { + + friend class Tutorial; + friend class State; + +protected: + + virtual ~Trigger(){;} + + emp::Ptr tutorial_ptr; // pointer to the Tutorial so we can notify it when this trigger fires. + + bool active = false; + + // The same trigger can be used to move between multiple pairs of states, so store all the pairs here. + std::unordered_map next_state_map; + + std::function callback; + + + bool IsActive() {return active;} + + void Notify( ); + + void SetTutorial(emp::Ptr tut) {tutorial_ptr = tut;} + + // Given a state, what state are we set to move to next? + std::string GetNextState(std::string current_state) { + emp_assert(HasState(current_state)); + return next_state_map[current_state]; + } + + // not used, either delete or make Tutorial method for it + std::vector GetStates() { + std::vector states; + for (auto state_pair : next_state_map) + states.push_back(state_pair.first); + return states; + } + + // Does the given state contain this trigger? + bool HasState(std::string state_name) { + return (next_state_map.find(state_name) != next_state_map.end()); + } + + // How many states contain this trigger? + int GetStateCount() { + return next_state_map.size(); + } + + void ManualFire(std::string current_state) { + emp_assert(HasState(current_state)); + Notify(); + } + + void SetCallback(std::function cb) { callback = cb; } + + + virtual void Activate() = 0; + virtual void Deactivate() = 0; + + + // Helper functions to keep bookkeeping stuff out of Activate/Deactivate. + // Makes it simpler to override those functions in custom classes. + void PerformActivation() { + if (active) return; + std::cout << "in Trigger Perform activation" << std::endl; + Activate(); + active = true; + } + void PerformDeactivation() { + if (!active) return; + Deactivate(); + active = false; + } + + // Add a pair of states that this trigger is associated with (it can move the tutorial from state to next_state). + void AddStatePair(std::string state, std::string next_state) { + emp_assert(!HasState(state)); + emp_assert(state != next_state); + next_state_map[state] = next_state; + } + + void RemoveState(std::string state_name) { + emp_assert(HasState(state_name)); + next_state_map.erase(state_name); + + } + +}; + + + +#ifdef __EMSCRIPTEN__ +template +class EventListenerTrigger : public Trigger { + + friend class Tutorial; + friend class State; + +protected: + + UI::internal::WidgetFacet& widget; + std::string event_name; + std::string handler_id; + +public: + + EventListenerTrigger(UI::internal::WidgetFacet& _widget, const std::string& _event_name): + widget(_widget), event_name(_event_name) { + } + + void Activate() override{ + std::cout << "In Activate! Event name: " << event_name.c_str() << std::endl; + widget.On(event_name.c_str(), [this]() { this->Notify(); }, event_name + "_tutorial_handler"); + + } + + void Deactivate() override { + std::cout << "In Deactivate! Event name: " << event_name.c_str() << std::endl; + widget.RemoveListener(event_name, event_name + "_tutorial_handler"); + } + +}; +#endif + + + + +class ManualTrigger : public Trigger { + +friend class Tutorial; +friend class State; + +private: + + ManualTrigger() {} + + void Activate() override {} + void Deactivate() override {} + +}; + + +class VisualEffect { + + friend class Tutorial; + friend class State; + + +private: + + bool active = false; + + // Set of all states using this visual + std::unordered_set states_set; + + virtual void Activate() = 0; + virtual void Deactivate() = 0; + + // Helper functions to keep bookkeeping stuff out of Activate/Deactivate. + // Makes it simpler to override those functions in custom classes. + void PerformActivation() { + if (active) return; + Activate(); + active = true; + } + void PerformDeactivation() { + if (!active) return; + Deactivate(); + active = false; + } + + + void AddState(std::string state_name) { + states_set.insert(state_name); + } + + void RemoveState(std::string state_name) { + states_set.erase(state_name); + } + + // How many states contain this visual? + int GetStateCount() { + return states_set.size(); + } + + bool IsActive() {return active;} +}; + + +#ifdef __EMSCRIPTEN__ +template +class CSSEffect : public VisualEffect { + + friend class Tutorial; + friend class State; + +protected: + + UI::internal::WidgetFacet& widget; + + std::unordered_map new_attributes_map; + std::unordered_map saved_attributes_map; + + CSSEffect(UI::internal::WidgetFacet& _widget, std::string attr, std::string val) : widget(_widget) { + new_attributes_map[attr] = val; + } + + void Activate() override { + for (auto attr_pair : new_attributes_map) { + saved_attributes_map[attr_pair.first] = widget.GetCSS(attr_pair.first); // store the starting value to reset it after + widget.SetCSS(attr_pair.first, attr_pair.second); + } + } + + void Deactivate() override { + for (auto attr_pair : new_attributes_map) { + widget.SetCSS(attr_pair.first, saved_attributes_map[attr_pair.first]); + //std::cout << "resetting attribute " << attr_pair.first << " to " << saved_attributes_map[attr_pair.first] << std::endl; + } + } + + +}; +#endif + + +#ifdef __EMSCRIPTEN__ +template +class PopoverEffect : public VisualEffect { + + friend class Tutorial; + friend class State; + +UI::Div parent_widget; +UI::internal::WidgetFacet& widget; +UI::Div popover_container; +UI::Div popover_text; +UI::Div popover_arrow; +//UI::Widget& original_parent_widget; +std::string message; +std::string popover_id; + public: + + //PopoverEffect(UI::internal::WidgetFacet& _widget, UI::Widget & _orig_parent, std::string _message) : + PopoverEffect(UI::internal::WidgetFacet& _widget, std::string _message) : + parent_widget(_widget.GetID() + "_popover_parent"), + widget(_widget), + popover_container(_widget.GetID() + "_popover_container"), + popover_text(_widget.GetID() + "_popover_text"), + popover_arrow(_widget.GetID() + "_popover_arrow"), + //original_parent_widget(_orig_parent), + message(_message){ + } + + void Activate() { + + emp_assert(parent_widget != nullptr); + + std::cout << "Adding popover" << std::endl; + widget.WrapWithInPlace(parent_widget); + std::cout << "1" << std::endl; + parent_widget.SetCSS("position", "relative"); + std::cout << "2" << std::endl; + popover_text << message; + popover_text.SetAttr("class", "popup_text"); + popover_arrow.SetAttr("class", "popup_arrow"); + std::cout << "3" << std::endl; + popover_container << popover_text; + popover_container << popover_arrow; + popover_container.SetAttr("class", "popup_container popup_show"); + std::cout << "4" << std::endl; + parent_widget << popover_container; + if(widget.GetCSS("float") != ""){ + parent_widget.SetCSS("float", widget.GetCSS("float")); + } + std::cout << "finished adding" << std::endl; + } + + + void Deactivate() { + std::cout << "Removing popover" << std::endl; + popover_container.SetAttr("class", "popup_container"); + std::cout << "1" << std::endl; + parent_widget->RemoveChild(widget); + std::cout << "2" << std::endl; + parent_widget->parent->ReplaceChild(parent_widget, widget); + std::cout << "Removed popover" << std::endl; +} + +}; +#endif + + +#ifdef __EMSCRIPTEN__ +class OverlayEffect : public VisualEffect { + + friend class Tutorial; + friend class State; + +private: + + UI::Div& parent; + UI::Div overlay; + std::string color; + float opacity; + int z_index; + bool intercept_mouse; + + OverlayEffect(UI::Div& _parent, std::string _color, float _opacity, int _z_index, bool _intercept_mouse) : + parent(_parent), color(_color), opacity(_opacity), z_index(_z_index), intercept_mouse(_intercept_mouse) {std::cout << "Overlay Constructor" << std::endl;} + + void Activate() { + + UI::Div over("overlay"); + overlay = over; + + overlay.SetAttr("class", "Tutorial-Overlay-Effect"); + overlay.SetCSS("background-color", color); + overlay.SetCSS("opacity", opacity); + overlay.SetCSS("z_index", z_index); + overlay.SetCSS("position", "absolute"); + overlay.SetCSS("width", "100%"); + overlay.SetCSS("height", "100%"); + overlay.SetCSS("top", "0px"); + overlay.SetCSS("left", "0px"); + if (!intercept_mouse) + overlay.SetCSS("pointer-events", "none"); + + parent << overlay; + + } + + void Deactivate() { + + overlay -> parent -> RemoveChild(overlay); + std::cout << "removed overlay" << std::endl; +} + +}; +#endif + + +class State { + + friend class Tutorial; + +private: + + std::unordered_set trigger_id_set; + std::unordered_set visual_id_set; + + + std::string name; + std::function callback; + + State(){;} + State(std::string _name) : name(_name) {} + + void SetCallback(std::function cb) { callback = cb; } + + bool HasTrigger(std::string trigger_id) { + return (trigger_id_set.find(trigger_id) != trigger_id_set.end()); + } + + bool HasVisualEffect(std::string visual_id) { + return (visual_id_set.find(visual_id) != visual_id_set.end()); + } + + // add the trigger id to set of id's + void AddTrigger(std::string trigger_id) { + emp_assert(!HasTrigger(trigger_id)); + trigger_id_set.insert(trigger_id); + } + + // remove the trigger id from set of id's + void RemoveTrigger(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + trigger_id_set.erase(trigger_id); + } + + // add the visual id to set of id's + void AddVisualEffect(std::string visual_id) { + emp_assert(!HasVisualEffect(visual_id)); + visual_id_set.insert(visual_id); + } + + // remove the visual id from set of id's + void RemoveVisualEffect(std::string visual_id) { + visual_id_set.erase(visual_id); + } + + // Activate all triggers and visuals for this state. Called when the state is entered. + void Activate(const std::unordered_map>& trigger_ptr_map, + const std::unordered_map>& visual_ptr_map) { + std::cout << "Activate state: " << name << std::endl; + std::cout << "Activateing " << trigger_id_set.size() << " triggers!" << std::endl; + std::cout << "Activateing " << visual_id_set.size() << " visuals!" << std::endl; + + // Activate all triggers + for(auto trigger_id : trigger_id_set) { + trigger_ptr_map.at(trigger_id) -> PerformActivation(); + //trigger_ptr_map.at(trigger_id) -> SetActive(); + + } + + // Activate all visuals + for(auto visual_id : visual_id_set) { + visual_ptr_map.at(visual_id) -> PerformActivation(); + //visual_ptr_map.at(visual_id) -> SetActive(); + } + + } + + // Deactivate all triggers and visuals for this state. Called when the state is exited. + void Deactivate(const std::unordered_map>& trigger_ptr_map, + const std::unordered_map>& visual_ptr_map) { + + // Deactivate all triggers + for(auto trigger_id : trigger_id_set) { + trigger_ptr_map.at(trigger_id) -> PerformDeactivation(); + //trigger_ptr_map.at(trigger_id) -> SetInactive(); + } + + // Deactivate all visuals + for(auto visual_id : visual_id_set) { + visual_ptr_map.at(visual_id) -> PerformDeactivation(); + //visual_ptr_map.at(visual_id) -> SetInactive(); + } + + std::cout << "Deactivate state: " << name << std::endl; + std::cout << "Removing " << trigger_id_set.size() << " triggers!" << std::endl; + std::cout << "Removing " << visual_id_set.size() << " visuals!" << std::endl; + } + + // how many Triggers does this state have? + size_t GetTriggerCount() { + return trigger_id_set.size(); + } + + // how many VisualEffects does this state have? + size_t GetVisualEffectCount() { + return visual_id_set.size(); + } + + +}; + + + +class Tutorial { + + friend void Trigger::Notify(); // Trigger's Notify() can access our private members. Needed so it can call OnTrigger(). + + +private: + + bool active = false; + + std::unordered_map states; // Store all the states for this Tutorial + std::unordered_map> trigger_ptr_map; // Store all the triggers for this Tutorial + std::unordered_map> visual_ptr_map; // Store all the visualeffects for this Tutorial + + std::string current_state; + + size_t num_triggers_added = 0; + size_t num_visuals_added = 0; + + // Retrieve a State object given its ID. + State & GetState(std::string & state_name) { + //add assert + return states.at(state_name); + } + + void DeleteTrigger(std::string trigger_id) { + delete trigger_ptr_map[trigger_id]; + trigger_ptr_map.erase(trigger_id); + } + + void DeleteVisualEffect(std::string visual_id) { + delete visual_ptr_map[visual_id]; + visual_ptr_map.erase(visual_id); + } + + // Retrieve a pointer to the Trigger with the given ID + emp::Ptr GetTrigger(std::string trigger_id) { + return trigger_ptr_map[trigger_id]; + } + + + // Retrieve a pointer to the Trigger with the given ID + emp::Ptr GetVisualEffect(std::string visual_id) { + return visual_ptr_map[visual_id]; + } + + // A Trigger calls this when it's fired, passing a pointer to itself. + void OnTrigger(emp::Ptr trigger) { + + std::cout << "Leaving state " << current_state << std::endl; + + // Deactivate current state + GetState(current_state).Deactivate(trigger_ptr_map, visual_ptr_map); + + //move to the next state + current_state = trigger -> GetNextState(current_state) ; + GetState(current_state).Activate(trigger_ptr_map, visual_ptr_map); + + std::cout << "Entering state " << current_state << std::endl; + + // Stop here if this state has no triggers + if (GetState(current_state).GetTriggerCount() == 0) + { + Stop(); + } + + // execute callbacks for the trigger and state + if (trigger -> callback) trigger -> callback(); + if (GetState(current_state).callback) GetState(current_state).callback(); + + + } + + + + +public: + + // -------------------------------- INTERFACE ---------------------------------------- + + // These are the only functions to be called outside of this file :P + + + bool IsActive() { + return active; + } + + std::string GetCurrentState() { + if (active) return current_state; + return ""; + } + + // Is the given trigger id an existing trigger? + bool HasTrigger(std::string trigger_id) { + return trigger_ptr_map.find(trigger_id) != trigger_ptr_map.end(); + } + + // Is the given visual id an existing trigger? + bool HasVisualEffect(std::string visual_id) { + std::cout << "In HasVisualEffect" << std::endl; + std::cout << (visual_ptr_map.find(visual_id) != visual_ptr_map.end()) << std::endl; + return visual_ptr_map.find(visual_id) != visual_ptr_map.end(); + } + + // Is the given state name an existing state? + bool HasState(std::string state_name) { + return states.find(state_name) != states.end(); // any other checks? + } + + // Launch into the tutorial at a particular state + void StartAtState(std::string state_name){ + + // Deactivate current state + if (active) + GetState(current_state).Deactivate(trigger_ptr_map, visual_ptr_map); + + current_state = state_name; + + // Stop here if new state is an end state + if (GetState(current_state).GetTriggerCount() == 0) + { + Stop(); + return; + } + + std::cout << "visual size in Start: " << GetState(current_state).GetVisualEffectCount() << std::endl; + GetState(current_state).Activate(trigger_ptr_map, visual_ptr_map); + active = true; + + // state callback, if any + if (GetState(current_state).callback) + GetState(current_state).callback(); + } + + // End the tutorial + void Stop() { + + if (!active) + return; + + // Deactivate current state + if (HasState(current_state)) + GetState(current_state).Deactivate(trigger_ptr_map, visual_ptr_map); + + active = false; + + std::cout << "Tutorial Finished!" << std::endl; + } + + // Create and store a new state with given name + Tutorial& AddState(std::string state_name, std::function callback=nullptr) { + emp_assert(!HasState(state_name)); + states.emplace(std::make_pair(state_name, State(state_name))); + GetState(state_name).SetCallback(callback); + + return *this; + } + + + + Tutorial& AddManualTrigger(std::string cur_state, std::string next_state, std::string trigger_id="", + std::function callback=nullptr) { + + if (trigger_id.empty()) + trigger_id = std::string("unnamed_trigger_") + std::to_string(num_triggers_added); + + emp_assert(!HasState(trigger_id)); + + emp::Ptr trigger_ptr = new ManualTrigger(); + trigger_ptr -> SetTutorial(this); + trigger_ptr -> AddStatePair(cur_state, next_state); + + trigger_ptr_map[trigger_id] = trigger_ptr; + GetState(cur_state).AddTrigger(trigger_id); + + if (cur_state == current_state) { + trigger_ptr -> Activate(); + } + + trigger_ptr -> SetCallback(callback); + + num_triggers_added++; + + return *this; + } + + +#ifdef __EMSCRIPTEN__ + template + Tutorial& AddEventListenerTrigger(std::string cur_state, std::string next_state, + UI::internal::WidgetFacet& w, std::string event_name, + std::string trigger_id="", std::function callback=nullptr) + { + if (trigger_id.empty()) + trigger_id = std::string("unnamed_trigger_") + std::to_string(num_triggers_added); + + emp_assert(!HasState(trigger_id)); + + emp::Ptr trigger_ptr = new EventListenerTrigger(w, event_name); + trigger_ptr -> SetTutorial(this); + trigger_ptr -> AddStatePair(cur_state, next_state); + + trigger_ptr_map[trigger_id] = trigger_ptr; + GetState(cur_state).AddTrigger(trigger_id); + + if (cur_state == current_state) { + trigger_ptr -> Activate(); + } + + trigger_ptr -> SetCallback(callback); + + num_triggers_added++; + + return *this; + } +#endif + + + Tutorial& AddExistingTrigger(std::string cur_state, std::string next_state, std::string trigger_id) { + + emp::Ptr trigger_ptr = trigger_ptr_map[trigger_id]; + trigger_ptr -> AddStatePair(cur_state, next_state); + GetState(cur_state).AddTrigger(trigger_id); + + return *this; + } + + + template + Tutorial& AddCustomTrigger(std::string cur_state, std::string next_state, Args&&... args, + std::string trigger_id="", std::function callback=nullptr) { + + std::cout << "The trigger id is: " << trigger_id << std::endl; + + + static_assert(std::is_base_of::value, "T must derive from Trigger"); + emp::Ptr trigger_ptr = new T(std::forward(args)...); + //std::cout << "Created trigger of type: " << static_cast(trigger_ptr)->GetType() << std::endl; + + trigger_ptr -> SetTutorial(this); + trigger_ptr -> AddStatePair(cur_state, next_state); + + trigger_ptr_map[trigger_id] = trigger_ptr; + GetState(cur_state).AddTrigger(trigger_id); + + if (cur_state == current_state) { + trigger_ptr -> Activate(); + } + + num_triggers_added++; + + return *this; + + } + + + Tutorial& RemoveTrigger(std::string trigger_id, std::string state_name) { + emp_assert(HasTrigger(trigger_id)); + + emp::Ptr trigger_ptr = GetTrigger(trigger_id); + + // deactivate the trigger if active + if (trigger_ptr -> IsActive()) + trigger_ptr -> Deactivate(); + + // remove state from trigger + trigger_ptr -> RemoveState(state_name); + + // remove the trigger from state + GetState(state_name).RemoveTrigger(trigger_id); + + // remove from tutorial if necessary + if (trigger_ptr -> GetStateCount() == 0) + DeleteTrigger(trigger_id); + + return *this; + } + + + Tutorial& FireTrigger(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + GetTrigger(trigger_id) -> ManualFire(current_state); + + return *this; + } + + + Tutorial& ActivateTrigger(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + std::cout << "Trying to activate trigger" << std::endl; + + emp::Ptr trigger_ptr = GetTrigger(trigger_id); + trigger_ptr -> PerformActivation(); + + return *this; + } + + Tutorial& DeactivateTrigger(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + + std::cout << "Try to deactivate trigger" << std::endl; + + emp::Ptr trigger_ptr = GetTrigger(trigger_id); + trigger_ptr -> PerformDeactivation(); + + return *this; + } + + +#ifdef __EMSCRIPTEN__ + template + Tutorial& AddCSSEffect(std::string state_name, UI::internal::WidgetFacet& w, + std::string attr, std::string val, std::string visual_id="") + { + emp::Ptr visual_ptr = new CSSEffect(w, attr, val); + visual_ptr -> AddState(state_name); + + if (visual_id.empty()) + visual_id = std::string("unnamed_visual_") + std::to_string(num_visuals_added); + + + visual_ptr_map[visual_id] = visual_ptr; + GetState(state_name).AddVisualEffect(visual_id); + + if (state_name == current_state){ + visual_ptr -> Activate(); + } + + num_visuals_added++; + + return *this; + } +#endif + +#ifdef __EMSCRIPTEN__ + template + Tutorial& AddPopoverEffect(std::string state_name, UI::internal::WidgetFacet& w, + std::string message, std::string visual_id="") + { + emp::Ptr visual_ptr = new PopoverEffect(w, message); + visual_ptr -> AddState(state_name); + + if (visual_id.empty()) + visual_id = std::string("unnamed_visual_") + std::to_string(num_visuals_added); + + + + visual_ptr_map[visual_id] = visual_ptr; + GetState(state_name).AddVisualEffect(visual_id); + + if (state_name == current_state){ + visual_ptr -> Activate(); + } + + num_visuals_added++; + + return *this; + } +#endif + + +#ifdef __EMSCRIPTEN__ + Tutorial& AddOverlayEffect(std::string state_name, UI::Div& parent, std::string color="black", float opacity=0.4, + int z_index=1000, bool intercept_mouse=false, std::string visual_id="") { + + std::cout << "Add Overlay Effect" << std::endl; + emp_assert(HasState(state_name)); + + + emp::Ptr visual_ptr = new OverlayEffect(parent, color, opacity, z_index, intercept_mouse); + visual_ptr -> AddState(state_name); + + if (visual_id.empty()) + visual_id = std::string("unnamed_visual_") + std::to_string(num_visuals_added); + + std::cout << visual_id << std::endl; + emp_assert(!HasVisualEffect(visual_id)); + + visual_ptr_map[visual_id] = visual_ptr; + GetState(state_name).AddVisualEffect(visual_id); + + if (state_name == current_state){ + visual_ptr -> Activate(); + } + + num_visuals_added++; + + return *this; + } +#endif + + template + Tutorial& AddCustomVisualEffect(std::string state_name, Args&&... args, + std::string visual_id) { + + emp_assert(!HasVisualEffect(visual_id)); + + static_assert(std::is_base_of::value, "T must derive from VisualEffect"); + emp::Ptr visual_ptr = new T(std::forward(args)...); + + visual_ptr -> AddState(state_name); + + if (visual_id.empty()) + visual_id = std::string("unnamed_visual_") + std::to_string(num_visuals_added); + + visual_ptr_map[visual_id] = visual_ptr; + GetState(state_name).AddVisualEffect(visual_id); + + if (state_name == current_state) { + visual_ptr -> Activate(); + } + + num_visuals_added++; + + return *this; + + } + + Tutorial& RemoveVisualEffect(std::string visual_id, std::string state_name) { + emp_assert(HasVisualEffect(visual_id)); + + emp::Ptr visual_ptr = GetVisualEffect(visual_id); + + // deactivate the trigger if active + if (visual_ptr -> IsActive()) + visual_ptr -> Deactivate(); + + // remove state from visual + visual_ptr -> RemoveState(state_name); + + // remove visual from state + GetState(state_name).RemoveVisualEffect(visual_id); + + // remove from tutorial if necessary + if (visual_ptr -> GetStateCount() == 0) + DeleteVisualEffect(visual_id); + + return *this; + } + + Tutorial& ActivateVisualEffect(std::string visual_id) { + emp_assert(HasVisualEffect(visual_id)); + + emp::Ptr visual_ptr = GetVisualEffect(visual_id); + visual_ptr -> PerformActivation(); + + return *this; + } + + Tutorial& DeactivateVisualEffect(std::string visual_id) { + std::cout << "In DeactivateVisualEffect" << std::endl; + std::cout << visual_id << std::endl; + emp_assert(HasVisualEffect(visual_id)); + + emp::Ptr visual_ptr = GetVisualEffect(visual_id); + visual_ptr -> PerformDeactivation(); + + return *this; + } + + + Tutorial& SetStateCallback(std::string state_name, std::function fun) { + emp_assert(HasState(state_name)); + GetState(state_name).callback = fun; + + return *this; + } + + Tutorial& SetTriggerCallback(std::string trigger_id, std::function fun) { + emp_assert(HasTrigger(trigger_id)); + GetTrigger(trigger_id) -> callback = fun; + + return *this; + } + + + + + bool IsTriggerActive(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + + return GetTrigger(trigger_id) -> IsActive(); + } + + int GetTriggerCount(std::string trigger_id) { + emp_assert(HasTrigger(trigger_id)); + + return GetTrigger(trigger_id) -> GetStateCount(); + } + + + bool IsVisualEffectActive(std::string visual_id) { + emp_assert(HasVisualEffect(visual_id)); + + return GetVisualEffect(visual_id) -> IsActive(); + } + + int GetStateVisualEffectCount(std::string state_name, std::string visual_id) { + emp_assert(HasState(state_name)); + emp_assert(HasVisualEffect(visual_id)); + + return GetState(state_name).GetVisualEffectCount(); + } + + + bool StateHasTrigger(std::string state_name, std::string trigger_id) { + emp_assert(HasState(state_name)); + emp_assert(HasTrigger(trigger_id)); + + return GetState(state_name).HasTrigger(trigger_id); + } + + bool StateHasVisual(std::string state_name, std::string visual_id) { + emp_assert(HasState(state_name)); + emp_assert(HasVisualEffect(visual_id)); + + return GetState(state_name).HasVisualEffect(visual_id); + } + + + +}; + + + +void Trigger::Notify(){ + tutorial_ptr -> OnTrigger(this); +} + +#endif diff --git a/source/web/Widget.h b/source/web/Widget.h index 8c31103356..df9101e0a8 100644 --- a/source/web/Widget.h +++ b/source/web/Widget.h @@ -138,6 +138,7 @@ namespace web { bool IsD3Visualiation() const { return GetInfoTypeName() == "D3VisualizationInfo"; } const std::string & GetID() const; ///< What is the HTML string ID for this Widget? + /// Retrieve a specific CSS trait associated with this Widget. /// Note: CSS-related options may be overridden in derived classes that have multiple styles. @@ -253,6 +254,7 @@ namespace web { virtual void AddChild(Widget in) { ; } virtual void RemoveChild(Widget & child) { ; } + virtual void ReplaceChild(Widget & old_child, Widget new_child) { ; } // Record dependants. Dependants are only acted upon when this widget's action is // triggered (e.g. a button is pressed) @@ -581,9 +583,9 @@ namespace web { } /// Listener options may be overridden in derived classes that have multiple listen targets. /// By default DoListen will track new listens and set them up immediately, if active. - virtual void DoListen(const std::string & event_name, size_t fun_id) { - info->extras.listen.Set(event_name, fun_id); - if (IsActive()) Listeners::Apply(info->id, event_name, fun_id); + virtual void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", bool add_before_onclick = false) { + info->extras.listen.Set(event_name, fun_id, handler_id); + if (IsActive()) Listeners::Apply(info->id, event_name, fun_id, add_before_onclick); } public: @@ -645,37 +647,45 @@ namespace web { /// Provide an event and a function that will be called when that event is triggered. /// In this case, the function as no arguments. - return_t & On(const std::string & event_name, const std::function & fun) { + return_t & On(const std::string & event_name, const std::function & fun, + const std::string handler_id="default", + bool add_before_onclick = false) { emp_assert(info != nullptr); size_t fun_id = JSWrap(fun); - DoListen(event_name, fun_id); + DoListen(event_name, fun_id, handler_id, add_before_onclick); return (return_t &) *this; } /// Provide an event and a function that will be called when that event is triggered. /// In this case, the function takes a mouse event as an argument, with full info about mouse. return_t & On(const std::string & event_name, - const std::function & fun) { + const std::function & fun, + const std::string handler_id="default", + bool add_before_onclick = false) { emp_assert(info != nullptr); size_t fun_id = JSWrap(fun); - DoListen(event_name, fun_id); + DoListen(event_name, fun_id, handler_id, add_before_onclick); return (return_t &) *this; } - + /// Provide an event and a function that will be called when that event is triggered. /// In this case, the function takes a keyboard event as an argument, with full info about keyboard. return_t & On(const std::string & event_name, - const std::function & fun) { + const std::function & fun, + const std::string handler_id="default", + bool add_before_onclick = false) { emp_assert(info != nullptr); size_t fun_id = JSWrap(fun); - DoListen(event_name, fun_id); + DoListen(event_name, fun_id, handler_id, add_before_onclick); return (return_t &) *this; } /// Provide an event and a function that will be called when that event is triggered. /// In this case, the function takes two doubles which will be filled in with mouse coordinates. return_t & On(const std::string & event_name, - const std::function & fun) { + const std::function & fun, + const std::string handler_id="default", + bool add_before_onclick = false) { emp_assert(info != nullptr); auto fun_cb = [this, fun](MouseEvent evt){ double x = evt.clientX - GetXPos(); @@ -683,54 +693,60 @@ namespace web { fun(x,y); }; size_t fun_id = JSWrap(fun_cb); - DoListen(event_name, fun_id); + DoListen(event_name, fun_id, handler_id, add_before_onclick); + return (return_t &) *this; + } + + return_t & RemoveListener(const std::string & event_name, const std::string handler_id="default") { + info->extras.listen.Remove(event_name, handler_id); + info -> ReplaceHTML(); return (return_t &) *this; } /// Provide a function to be called when the window is resized. - template return_t & OnResize(T && arg) { return On("resize", arg); } + template return_t & OnResize(T && arg, const std::string handler_id="default") { return On("resize", arg, handler_id); } /// Provide a function to be called when the mouse button is clicked in this Widget. - template return_t & OnClick(T && arg) { return On("click", arg); } + template return_t & OnClick(T && arg, const std::string handler_id="default") { return On("click", arg, handler_id); } /// Provide a function to be called when the mouse button is double clicked in this Widget. - template return_t & OnDoubleClick(T && arg) { return On("dblclick", arg); } + template return_t & OnDoubleClick(T && arg, const std::string handler_id="default") { return On("dblclick", arg, handler_id); } /// Provide a function to be called when the mouse button is pushed down in this Widget. - template return_t & OnMouseDown(T && arg) { return On("mousedown", arg); } + template return_t & OnMouseDown(T && arg, const std::string handler_id="default") { return On("mousedown", arg, handler_id); } /// Provide a function to be called when the mouse button is released in this Widget. - template return_t & OnMouseUp(T && arg) { return On("mouseup", arg); } + template return_t & OnMouseUp(T && arg, const std::string handler_id="default") { return On("mouseup", arg, handler_id); } /// Provide a function to be called whenever the mouse moves in this Widget. - template return_t & OnMouseMove(T && arg) { return On("mousemove", arg); } + template return_t & OnMouseMove(T && arg, const std::string handler_id="default") { return On("mousemove", arg, handler_id); } /// Provide a function to be called whenever the mouse leaves the Widget. - template return_t & OnMouseOut(T && arg) { return On("mouseout", arg); } + template return_t & OnMouseOut(T && arg, const std::string handler_id="default") { return On("mouseout", arg, handler_id); } /// Provide a function to be called whenever the mouse moves over the Widget. - template return_t & OnMouseOver(T && arg) { return On("mouseover", arg); } + template return_t & OnMouseOver(T && arg, const std::string handler_id="default") { return On("mouseover", arg, handler_id); } /// Provide a function to be called whenever the mouse wheel moves in this Widget. - template return_t & OnMouseWheel(T && arg) { return On("mousewheel", arg); } + template return_t & OnMouseWheel(T && arg, const std::string handler_id="default") { return On("mousewheel", arg, handler_id); } /// Provide a function to be called whenever a key is pressed down in this Widget. - template return_t & OnKeydown(T && arg) { return On("keydown", arg); } + template return_t & OnKeydown(T && arg, const std::string handler_id="default") { return On("keydown", arg, handler_id); } /// Provide a function to be called whenever a key is pressed down and released in this Widget. - template return_t & OnKeypress(T && arg) { return On("keypress", arg); } + template return_t & OnKeypress(T && arg, const std::string handler_id="default") { return On("keypress", arg, handler_id); } /// Provide a function to be called whenever a key is pressed released in this Widget. - template return_t & OnKeyup(T && arg) { return On("keyup", arg); } + template return_t & OnKeyup(T && arg, const std::string handler_id="default") { return On("keyup", arg, handler_id); } /// Provide a function to be called whenever text is copied in this Widget. - template return_t & OnCopy(T && arg) { return On("copy", arg); } + template return_t & OnCopy(T && arg, const std::string handler_id="default") { return On("copy", arg, handler_id); } /// Provide a function to be called whenever text is cut in this Widget. - template return_t & OnCut(T && arg) { return On("cut", arg); } + template return_t & OnCut(T && arg, const std::string handler_id="default") { return On("cut", arg, handler_id); } /// Provide a function to be called whenever text is pasted in this Widget. - template return_t & OnPaste(T && arg) { return On("paste", arg); } + template return_t & OnPaste(T && arg, const std::string handler_id="default") { return On("paste", arg, handler_id); } /// Create a tooltip for this Widget. return_t & SetTitle(const std::string & _in) { return SetAttr("title", _in); } @@ -858,8 +874,9 @@ namespace web { /// Wrap a wrapper around this Widget. /// @param wrapper the wrapper that will be placed around this Widget + /// @param if in_place in true, wrapper widget maintains same position in parent div /// @return this Widget - return_t & WrapWith(Widget wrapper) { + return_t & WrapWith(Widget wrapper, bool in_place) { // if this Widget is already nested within a parent // we'll need to wedge the wrapper between this Widget and the parent @@ -872,10 +889,17 @@ namespace web { )); const auto parent_info = Info((return_t &) *this)->parent; - - // switch out parent's existing child for wrapper - parent_info->RemoveChild((return_t &) *this); - parent_info->AddChild(wrapper); + + if(in_place){ + this->Deactivate(false); + // switch out parent's existing child for wrapper + parent_info->ReplaceChild((return_t &) *this, wrapper); + } + else{ + // remove existing element and add wrapper + parent_info->RemoveChild((return_t &) *this); + parent_info->AddChild(wrapper); + } } else if (Info(wrapper)->ptr_count == 1) { emp::LibraryWarning( "Only one reference held to wrapper. ", @@ -885,6 +909,7 @@ namespace web { // put this Widget inside of the wrapper wrapper << (return_t &) *this; + if(in_place) wrapper.Redraw(); return (return_t &) *this; } diff --git a/source/web/_TableCell.h b/source/web/_TableCell.h index 4025305090..414443475b 100644 --- a/source/web/_TableCell.h +++ b/source/web/_TableCell.h @@ -37,7 +37,9 @@ namespace web { } /// Update a listener for this cell (override default Table) - void DoListen(const std::string & event_name, size_t fun_id) override { + void DoListen(const std::string & event_name, size_t fun_id, + const std::string handler_id="default", + bool add_before_onclick = false) override { Info()->rows[cur_row].data[cur_col].extras.listen.Set(event_name, fun_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } diff --git a/source/web/_TableCol.h b/source/web/_TableCol.h index dfcd342840..8ea35b8293 100644 --- a/source/web/_TableCol.h +++ b/source/web/_TableCol.h @@ -36,9 +36,10 @@ namespace web { if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } - void DoListen(const std::string & event_name, size_t fun_id) override { + void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", + bool add_before_onclick = false) override { if (Info()->cols.size() == 0) Info()->cols.resize(GetNumCols()); - Info()->cols[cur_col].extras.listen.Set(event_name, fun_id); + Info()->cols[cur_col].extras.listen.Set(event_name, fun_id, handler_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } diff --git a/source/web/_TableColGroup.h b/source/web/_TableColGroup.h index 86ecc703fb..9f09616141 100644 --- a/source/web/_TableColGroup.h +++ b/source/web/_TableColGroup.h @@ -36,9 +36,10 @@ namespace web { if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } - void DoListen(const std::string & event_name, size_t fun_id) override { + void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", + bool add_before_onclick = false) override { if (Info()->col_groups.size() == 0) Info()->col_groups.resize(GetNumCols()); - Info()->col_groups[cur_col].extras.listen.Set(event_name, fun_id); + Info()->col_groups[cur_col].extras.listen.Set(event_name, fun_id, handler_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } diff --git a/source/web/_TableRow.h b/source/web/_TableRow.h index 90e783076b..9d8d46d4d6 100644 --- a/source/web/_TableRow.h +++ b/source/web/_TableRow.h @@ -34,8 +34,9 @@ namespace web { if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } - void DoListen(const std::string & event_name, size_t fun_id) override { - Info()->rows[cur_row].extras.listen.Set(event_name, fun_id); + void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", + bool add_before_onclick = false) override { + Info()->rows[cur_row].extras.listen.Set(event_name, fun_id, handler_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } diff --git a/source/web/_TableRowGroup.h b/source/web/_TableRowGroup.h index 2c59a40fef..5f6453ec7d 100644 --- a/source/web/_TableRowGroup.h +++ b/source/web/_TableRowGroup.h @@ -36,9 +36,10 @@ namespace web { if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } - void DoListen(const std::string & event_name, size_t fun_id) override { + void DoListen(const std::string & event_name, size_t fun_id, const std::string handler_id="default", + bool add_before_onclick = false) override { if (Info()->row_groups.size() == 0) Info()->row_groups.resize(GetNumRows()); - Info()->row_groups[cur_row].extras.listen.Set(event_name, fun_id); + Info()->row_groups[cur_row].extras.listen.Set(event_name, fun_id, handler_id); if (IsActive()) Info()->ReplaceHTML(); // @CAO only should replace cell's CSS } diff --git a/tests/web/Tutorial.cc b/tests/web/Tutorial.cc new file mode 100644 index 0000000000..68640df6ad --- /dev/null +++ b/tests/web/Tutorial.cc @@ -0,0 +1,692 @@ +// This file is part of Empirical, https://github.com/devosoft/Empirical +// Copyright (C) Michigan State University, 2020. +// Released under the MIT Software license; see doc/LICENSE + +#include +#include + +#include "base/assert.h" +#include "web/_MochaTestRunner.h" +#include "web/Document.h" +#include "web/Element.h" +#include "web/Tutorial.h" +#include "web/web.h" + + + // event listener trigger - events added on enter state, added on manual activation, removed on manual deactivation, removed on removal + +//Test events added on state enter +struct Test_EventListenerTrigger_0 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_0(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.AddEventListenerTrigger("state1", "state2", div, "hover", "hovertrigger"); + tut.StartAtState("state1"); + } + + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events added on state enter", function() + { + + describe("#testdiv", function() { + + it('should have an event listener on click', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.notEqual(jQuery._data(testdiv, "events" )['click'], null); + }); + + it('should have an event listener on hover', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.notEqual(jQuery._data(testdiv, "events" )['hover'], null); + }); + }); + }); + }); + } +}; + + +// Test listeners removed on exit state +struct Test_EventListenerTrigger_1 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_1(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.AddEventListenerTrigger("state1", "state2", div, "hover", "hovertrigger"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); + tut.StartAtState("state1"); + tut.FireTrigger("manualtrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events removed on state exit", function() + { + + describe("#testdiv", function() { + + it('should NOT have any event listeners', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(jQuery._data(testdiv, "events"), null); + }); + }); + }); + }); + } +}; + + + +// Test listeners removed on manual deactivation +struct Test_EventListenerTrigger_2 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_2(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.StartAtState("state1"); + tut.DeactivateTrigger("clicktrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events removed on manual deactivation", function() + { + + describe("#testdiv", function() { + + it('should NOT have any event listeners', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(jQuery._data(testdiv, "events"), null); + }); + }); + }); + }); + } + +}; + + +// Test listeners re-added on manual activation +struct Test_EventListenerTrigger_3 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_3(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.StartAtState("state1"); + tut.DeactivateTrigger("clicktrigger"); + tut.ActivateTrigger("clicktrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events added on manual activation", function() + { + + describe("#testdiv", function() { + + it('should have an event listener on click after manual activation', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.notEqual(jQuery._data(testdiv, "events") ["click"], null); + }); + }); + }); + }); + } + +}; + + +// Test event listeners removed after trigger removal +struct Test_EventListenerTrigger_4 : emp::web::BaseTest { + + Tutorial tut; + + Test_EventListenerTrigger_4(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddEventListenerTrigger("state1", "state2", div, "click", "clicktrigger"); + tut.StartAtState("state1"); + tut.RemoveTrigger("clicktrigger", "state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddEventListenerTrigger events removed on trigger removal", function() + { + + describe("#testdiv", function() { + + it('should NOT have an event listener after trigger removal', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(jQuery._data(testdiv, "events"), null); + }); + }); + }); + }); + } + +}; + + +// overlayeffect - added to parent div on enter state, removed on exit state, added on manual activation, removed on manual deactivation, removed on removal + +// Test overlay added to parent div on enter state +struct Test_OverlayEffect_0 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_0(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc); + tut.StartAtState("state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay added on state enter", function() + { + + describe("#testdiv", function() { + + it('doc should have one overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 1); + chai.assert(overlays[0].parentNode = document); + }); + }); + }); + }); + } + +}; + + + +// Test overlay removed on exit state +struct Test_OverlayEffect_1 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_1(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc); + tut.StartAtState("state1"); + tut.FireTrigger("manualtrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay removed on state exit", function() + { + + describe("#testdiv", function() { + + it('doc should NOT have an overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 0); + }); + }); + }); + }); + } + +}; + + +// Test overlay removed on manual deactivation +struct Test_OverlayEffect_2 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_2(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc, "blue", 0.4, 1000, false, "overlay"); + tut.StartAtState("state1"); + tut.DeactivateVisualEffect("overlay"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay removed on manual deactivation", function() + { + + describe("#testdiv", function() { + + it('doc should NOT have an overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 0); + }); + }); + }); + }); + } + +}; + + +// Test overlay added on manual activation after manual deactivation +struct Test_OverlayEffect_3 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_3(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc, "blue", 0.4, 1000, false, "overlay"); + tut.StartAtState("state1"); + tut.DeactivateVisualEffect("overlay"); + tut.ActivateVisualEffect("overlay"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay added on manual activation after manual deactivation", function() + { + + describe("#testdiv", function() { + + it('doc should have one overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 1); + chai.assert(overlays[0].parentNode = document); + }); + }); + }); + }); + } + +}; + + + +// Test overlay removed on removal +struct Test_OverlayEffect_4 : emp::web::BaseTest { + + Tutorial tut; + + Test_OverlayEffect_4(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddOverlayEffect("state1", doc, "blue", 0.4, 1000, false, "overlay"); + tut.StartAtState("state1"); + tut.RemoveVisualEffect("overlay", "state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddOverlayEffect overlay removed on manual deactivation", function() + { + + describe("#testdiv", function() { + + it('doc should NOT have an overlay as a child', function() { + var overlays = document.getElementsByClassName("Tutorial-Overlay-Effect"); + chai.assert.equal(overlays.length, 0); + }); + }); + }); + }); + } + +}; + + + + +// css effect - attr changed on enter state, manual activation. reverted on exit state, manual deactivation, removal. + +// Test css attribute changed on enter state +struct Test_CSSEffect_0 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_0(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute changed on enter state", function() + { + + describe("#testdiv", function() { + + it('div background color should be seagreen', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(testdiv.style.backgroundColor, "seagreen"); + }); + }); + }); + }); + } + +}; + + +// Test css attribute reverted on exit state +struct Test_CSSEffect_1 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_1(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + tut.FireTrigger("manualtrigger"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute reverted on exit state", function() + { + + describe("#testdiv", function() { + + it('div background color should be transparent', function() { + var testdiv = document.getElementById('testdiv'); + console.log(testdiv.style.backgroundColor); + chai.assert.equal(testdiv.style.backgroundColor, ''); + }); + }); + }); + }); + } + +}; + + + + +// Test css attribute changed on manual activation after manual deactivation +struct Test_CSSEffect_2 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_2(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + tut.DeactivateVisualEffect("css_effect"); + tut.ActivateVisualEffect("css_effect"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute changed on enter state", function() + { + + describe("#testdiv", function() { + + it('div background color should be seagreen', function() { + var testdiv = document.getElementById('testdiv'); + chai.assert.equal(testdiv.style.backgroundColor, "seagreen"); + }); + }); + }); + }); + } + +}; + + + +// Test css attribute reverted on manual deactivation +struct Test_CSSEffect_3 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_3(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + tut.DeactivateVisualEffect("css_effect"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute reverted on manual deactivation", function() + { + + describe("#testdiv", function() { + + it('div background color should be transparent', function() { + var testdiv = document.getElementById('testdiv'); + console.log(testdiv.style.backgroundColor); + chai.assert.equal(testdiv.style.backgroundColor, ''); + }); + }); + }); + }); + } + +}; + + +// Test css attribute reverted on effect removal +struct Test_CSSEffect_4 : emp::web::BaseTest { + + Tutorial tut; + + Test_CSSEffect_4(): BaseTest({"emp_test_container"}) + { + emp::web::Div& doc = Doc("emp_test_container"); + + emp::web::Div div("testdiv"); + div << "this is a Div"; + doc << div; + + tut.AddState("state1"); + tut.AddState("state2"); + tut.AddManualTrigger("state1", "state2", "manualtrigger"); // needed so the tutorial doesn't start and immediately stop + tut.AddCSSEffect("state1", div, "background-color", "seagreen", "css_effect"); + tut.StartAtState("state1"); + tut.RemoveVisualEffect("css_effect", "state1"); + } + + void Describe() override + { + EM_ASM + ({ + describe("Tutorial::AddCSSEffect css attribute reverted on removal", function() + { + + describe("#testdiv", function() { + + it('div background color should be transparent', function() { + var testdiv = document.getElementById('testdiv'); + console.log(testdiv.style.backgroundColor); + chai.assert.equal(testdiv.style.backgroundColor, ''); + }); + }); + }); + }); + } + +}; + + + + +emp::web::MochaTestRunner test_runner; +int main() { + + test_runner.Initialize({"emp_test_container"}); + + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddEventListenerTrigger"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddOverlayEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + test_runner.AddTest("Test Tutorial::AddCSSEffect"); + + + test_runner.Run(); +} diff --git a/tests/web/Tutorial_cpp.cc b/tests/web/Tutorial_cpp.cc new file mode 100644 index 0000000000..c55fd8e010 --- /dev/null +++ b/tests/web/Tutorial_cpp.cc @@ -0,0 +1,57 @@ +// This file is part of Empirical, https://github.com/devosoft/Empirical +// Copyright (C) Michigan State University, 2016-2017. +// Released under the MIT Software license; see doc/LICENSE + +#define CATCH_CONFIG_MAIN +#include "third-party/Catch/single_include/catch2/catch.hpp" + +#include +#include + +#include "web/Tutorial.h" + +TEST_CASE("Test AddState/HasState", "[web][Tutorial]") { + // Create a tutorial, it should not have state "test_state" at start + // It should have the state "test_state" after we add it! + Tutorial tut; + REQUIRE(!tut.HasState("test_state")); + tut.AddState("test_state"); + REQUIRE(tut.HasState("test_state")); + REQUIRE(!tut.HasState("test_state_2")); + tut.AddState("test_state_2"); + REQUIRE(tut.HasState("test_state_2")); + REQUIRE(tut.HasState("test_state")); +} + +TEST_CASE("Test StartAtState/Stop/IsActive/GetCurrentState", "[web][Tutorial]") { + // Create a tutorial with two states + // Tutorial should start as inactive with current state "" + // StartAtState should change active status and the current state + + // Setup + Tutorial tut; + REQUIRE(!tut.HasState("state_1")); + REQUIRE(!tut.HasState("state_2")); + tut.AddState("state_1"); + tut.AddState("state_2"); + REQUIRE(tut.HasState("state_1")); + REQUIRE(tut.HasState("state_2")); + REQUIRE(!tut.IsActive()); + REQUIRE(tut.GetCurrentState() == ""); + // Activate state_1 + tut.StartAtState("state_1"); + REQUIRE(tut.IsActive()); + REQUIRE(tut.GetCurrentState() == "state_1"); + // End tutorial + tut.Stop(); + REQUIRE(!tut.IsActive()); + REQUIRE(tut.GetCurrentState() == ""); + // Re-activate, this time with state_2 + tut.StartAtState("state_2"); + REQUIRE(tut.IsActive()); + REQUIRE(tut.GetCurrentState() == "state_2"); + // Stop again + tut.Stop(); + REQUIRE(!tut.IsActive()); + REQUIRE(tut.GetCurrentState() == ""); +}