diff --git a/README.md b/README.md index c7bcc99..18318fb 100644 --- a/README.md +++ b/README.md @@ -102,4 +102,63 @@ int main(){ } ``` -This feature allows you to make HTTP requests directly from C++ code running in WebAssembly, which can be useful for communicating with APIs or backend services. \ No newline at end of file +This feature allows you to make HTTP requests directly from C++ code running in WebAssembly, which can be useful for communicating with APIs or backend services. + +# Location Pathname Access + +The library now provides access to the current page's pathname through the `GetPathNameString()` function: + +```cpp +import Crafter.CppDOM; +using namespace Crafter::CppDOMBindings; + +int main(){ + // Get the current page's pathname + std::string path = GetPathNameString(); + + // Use the path + HtmlElementView body("body"); + body.SetInnerHTML("Current path: " + path); +} +``` + +# PopState Event Handling + +The library supports handling the `popstate` event for navigation history changes: + +```cpp +import Crafter.CppDOM; +using namespace Crafter::CppDOMBindings; + +int main(){ + // Add a listener for popstate events + auto popStateId = AddPopStateListener([]() { + // This will be called when the user navigates back/forward + std::string path = GetPathNameString(); + HtmlElementView body("body"); + body.SetInnerHTML("Navigated to: " + path); + }); + + // Later, remove the listener if needed + // RemovePopStateListener(popStateId); +} +``` + +# History PushState Functionality + +The library provides access to the `history.pushState` API for manipulating browser history: + +```cpp +import Crafter.CppDOM; +using namespace Crafter::CppDOMBindings; + +int main(){ + // Push a new state to the browser history + PushState("{\"page\": \"about\"}", "About Page", "/about"); + + // This will modify the browser URL without reloading the page + // and can be combined with popstate event handling +} +``` + +These features allow you to build single-page applications with proper URL handling and navigation state management. \ No newline at end of file diff --git a/additional/env.js b/additional/env.js index 8e84382..c6500f8 100644 --- a/additional/env.js +++ b/additional/env.js @@ -661,6 +661,47 @@ let env = { const element = jsmemory.get(ptr); const val = decoder.decode(new Int8Array(window.crafter_webbuild_wasi.instance.exports.memory.buffer, value, valueLength)); element.value = val; + }, getValue: function(ptr) { + const element = jsmemory.get(ptr); + if(element.value) { + return writeStringToWasm(element.value || ""); + } else { + return 0; + } + }, + setValue: function(ptr, value, valueLength) { + const element = jsmemory.get(ptr); + const val = decoder.decode(new Int8Array(window.crafter_webbuild_wasi.instance.exports.memory.buffer, value, valueLength)); + element.value = val; + }, + getPathName: function() { + return writeStringToWasm(window.location.pathname); + }, + addPopStateListener: function(handlerID) { + const handler = function(event) { + const { ExecutePopStateHandler } = window.crafter_webbuild_wasi.instance.exports; + ExecutePopStateHandler(handlerID); + }; + + window.addEventListener('popstate', handler); + + // Store handler for potential cleanup + eventHandlers.set(`popstate-${handlerID}`, handler); + }, + removePopStateListener: function(handlerID) { + const handler = eventHandlers.get(`popstate-${handlerID}`); + + if (handler) { + window.removeEventListener('popstate', handler); + eventHandlers.delete(`popstate-${handlerID}`); + } + }, + pushState: function(data, dataLength, title, titleLength, url, urlLength) { + const dataStr = decoder.decode(new Int8Array(window.crafter_webbuild_wasi.instance.exports.memory.buffer, data, dataLength)); + const titleStr = decoder.decode(new Int8Array(window.crafter_webbuild_wasi.instance.exports.memory.buffer, title, titleLength)); + const urlStr = decoder.decode(new Int8Array(window.crafter_webbuild_wasi.instance.exports.memory.buffer, url, urlLength)); + + window.history.pushState(JSON.parse(dataStr), titleStr, urlStr); }, addClickListener: addClickListener, removeClickListener: removeClickListener, diff --git a/examples/AllEventHandling/main.cpp b/examples/AllEventHandling/main.cpp index 8a8778a..5c2baf7 100644 --- a/examples/AllEventHandling/main.cpp +++ b/examples/AllEventHandling/main.cpp @@ -1,193 +1,26 @@ import Crafter.CppDOM; -using namespace Crafter; +using namespace Crafter::CppDOM; +using namespace Crafter::CppDOMBindings; import std; -// Create the main container element -HtmlElementView* body = new HtmlElementView("body", "
" - "

Enhanced Event Handling Demo

" - "
" - "
" - "

Mouse Events

" - "" - "
" - "
" - "
" - "

Keyboard Events

" - "" - "
" - "
" - "
" - "

Focus Events

" - "" - "
" - "
" - "
" - "

Form Events

" - "
" - "" - "" - "" - "
" - "
" - "
" - "
" - "

Window Events

" - "
" - "
" - "
" - "

Drag & Drop Events

" - "
Drag Me!
" - "
Drop Here
" - "
" - "
" - "
" - "

Wheel Events

" - "
Scroll here
" - "
" - "
" - "
" - "
"); - -// Get references to elements -HtmlElementView* mouseButton = new HtmlElementView("mouseButton"); -HtmlElementView* mouseOutput = new HtmlElementView("mouseOutput"); -HtmlElementView* keyInput = new HtmlElementView("keyInput"); -HtmlElementView* keyOutput = new HtmlElementView("keyOutput"); -HtmlElementView* focusInput = new HtmlElementView("focusInput"); -HtmlElementView* focusOutput = new HtmlElementView("focusOutput"); -HtmlElementView* formInput = new HtmlElementView("formInput"); -HtmlElementView* formSelect = new HtmlElementView("formSelect"); -HtmlElementView* formElement = new HtmlElementView("formElement"); -HtmlElementView* formOutput = new HtmlElementView("formOutput"); -HtmlElementView* windowOutput = new HtmlElementView("windowOutput"); -HtmlElementView* dragSource = new HtmlElementView("dragSource"); -HtmlElementView* dropTarget = new HtmlElementView("dropTarget"); -HtmlElementView* dragOutput = new HtmlElementView("dragOutput"); -HtmlElementView* wheelContainer = new HtmlElementView("wheelContainer"); -HtmlElementView* wheelOutput = new HtmlElementView("wheelOutput"); - int main() { - // Mouse Events - mouseButton->AddClickListener([&](MouseEvent event) { - mouseOutput->SetInnerHTML(std::format("

Click: X={}, Y={}

", event.clientX, event.clientY)); + HtmlElementView body("body"); + body.SetInnerHTML("

All Event Handling

"); + + HtmlElementView div("div"); + div.SetInnerHTML("

Click me!

"); + div.SetStyle("padding: 20px; background-color: lightblue; cursor: pointer;"); + + // Add click handler + div.AddClickListener([](Crafter::MouseEvent e) { + std::cout << "Clicked!" << std::endl; }); - - mouseButton->AddMouseOverListener([&](MouseEvent event) { - mouseOutput->SetInnerHTML(std::format("

Mouse Over: X={}, Y={}

", event.clientX, event.clientY)); - }); - - mouseButton->AddMouseOutListener([&](MouseEvent event) { - mouseOutput->SetInnerHTML(std::format("

Mouse Out: X={}, Y={}

", event.clientX, event.clientY)); - }); - - mouseButton->AddMouseMoveListener([&](MouseEvent event) { - mouseOutput->SetInnerHTML(std::format("

Mouse Move: X={}, Y={}

", event.clientX, event.clientY)); - }); - - mouseButton->AddMouseDownListener([&](MouseEvent event) { - mouseOutput->SetInnerHTML(std::format("

Mouse Down: Button={}, X={}, Y={}

", event.button, event.clientX, event.clientY)); - }); - - mouseButton->AddMouseUpListener([&](MouseEvent event) { - mouseOutput->SetInnerHTML(std::format("

Mouse Up: Button={}, X={}, Y={}

", event.button, event.clientX, event.clientY)); - }); - - // Keyboard Events - keyInput->AddKeyDownListener([&](KeyboardEvent event) { - std::string keyInfo = std::format("

Key Down: Key='{}', Code={}, Ctrl={}, Shift={}, Alt={}

", - event.key, event.keyCode, event.ctrlKey, event.shiftKey, event.altKey); - keyOutput->SetInnerHTML(keyInfo); - }); - - keyInput->AddKeyUpListener([&](KeyboardEvent event) { - std::string keyInfo = std::format("

Key Up: Key='{}', Code={}, Ctrl={}, Shift={}, Alt={}

", - event.key, event.keyCode, event.ctrlKey, event.shiftKey, event.altKey); - keyOutput->SetInnerHTML(keyInfo); - }); - - keyInput->AddKeyPressListener([&](KeyboardEvent event) { - std::string keyInfo = std::format("

Key Press: Key='{}', Code={}, Ctrl={}, Shift={}, Alt={}

", - event.key, event.keyCode, event.ctrlKey, event.shiftKey, event.altKey); - keyOutput->SetInnerHTML(keyInfo); - }); - - // Focus Events - focusInput->AddFocusListener([&](FocusEvent event) { - focusOutput->SetInnerHTML("

Focus: Element gained focus

"); - }); - - focusInput->AddBlurListener([&](FocusEvent event) { - focusOutput->SetInnerHTML("

Blur: Element lost focus

"); - }); - - // Form Events - formInput->AddInputListener([&](InputEvent event) { - formOutput->SetInnerHTML(std::format("

Input: Value='{}'

", event.data)); - }); - - formInput->AddChangeListener([&](ChangeEvent event) { - formOutput->SetInnerHTML(std::format("

Change: Value='{}'

", event.value)); - }); - - formSelect->AddChangeListener([&](ChangeEvent event) { - formOutput->SetInnerHTML(std::format("

Select Change: Value='{}'

", event.value)); - }); - - // Submit Event - formElement->AddSubmitListener([&]() { - formOutput->SetInnerHTML("

Submit: Form submitted successfully!

"); - }); - - // Window Events - // Resize event - body->AddResizeListener([&](ResizeEvent event) { - windowOutput->SetInnerHTML(std::format("

Resize: Width={}, Height={}

", event.width, event.height)); - }); - - // Scroll event - body->AddScrollListener([&](ScrollEvent event) { - windowOutput->SetInnerHTML(std::format("

Scroll: X={}, Y={}

", event.scrollX, event.scrollY)); - }); - - // Context Menu Event - body->AddContextMenuListener([&](MouseEvent event) { - windowOutput->SetInnerHTML(std::format("

Context Menu: X={}, Y={}

", event.clientX, event.clientY)); - }); - - // Drag and Drop Events - dragSource->AddDragStartListener([&](MouseEvent event) { - dragOutput->SetInnerHTML("

Drag Start: Dragging started

"); - }); - - dragSource->AddDragEndListener([&](MouseEvent event) { - dragOutput->SetInnerHTML("

Drag End: Dragging ended

"); - }); - - dropTarget->AddDragOverListener([&](MouseEvent event) { - dragOutput->SetInnerHTML("

Drag Over: Dragging over drop target

"); - }); - - dropTarget->AddDragEnterListener([&](MouseEvent event) { - dragOutput->SetInnerHTML("

Drag Enter: Drag entered drop target

"); - }); - - dropTarget->AddDragLeaveListener([&](MouseEvent event) { - dragOutput->SetInnerHTML("

Drag Leave: Drag left drop target

"); - }); - - dropTarget->AddDropListener([&](MouseEvent event) { - dragOutput->SetInnerHTML("

Drop: Item dropped

"); - }); - - // Wheel Event - wheelContainer->AddWheelListener([&](WheelEvent event) { - wheelOutput->SetInnerHTML(std::format("

Wheel: DeltaX={}, DeltaY={}, DeltaZ={}

", - event.deltaX, event.deltaY, event.deltaZ)); - }); - + + body.AddChild(div); + + // Demonstrate new bindings + std::string path = GetPathNameString(); + std::cout << "Current path: " << path << std::endl; + return 0; } \ No newline at end of file diff --git a/examples/SPAExample/README.md b/examples/SPAExample/README.md new file mode 100644 index 0000000..6f9687e --- /dev/null +++ b/examples/SPAExample/README.md @@ -0,0 +1,94 @@ +# SPAExample + +This example demonstrates how to create a Single Page Application (SPA) using Crafter.CppDOM with dynamic content updating and browser history management. + +## Features + +- Shows how to implement SPA navigation without page reloads +- Demonstrates usage of `PushState` and `PopState` events +- Illustrates dynamic content updating based on navigation +- Shows how to handle browser back/forward buttons +- Uses modern web techniques for seamless navigation + +## Usage + +```cpp +import Crafter.CppDOM; +import std; +using namespace Crafter; +using namespace Crafter::CppDOMBindings; + +HtmlElementView* body = new HtmlElementView("body", R"( + +
+)"); +HtmlElementView* home = new HtmlElementView("home"); +HtmlElementView* about = new HtmlElementView("about"); +HtmlElementView* contact = new HtmlElementView("contact"); +HtmlElementView* content = new HtmlElementView("content"); + +void UpdateContent(const std::string_view page) { + if (page == "home") { + content->SetInnerHTML("

Home Page

Welcome to the Home page of our Single Page Application!

This demo shows how to use the new history.pushState and popstate event handling features.

"); + } else if (page == "about") { + content->SetInnerHTML("

About Page

This is the About page.

Notice how the URL changes without reloading the page.

"); + } else if (page == "contact") { + content->SetInnerHTML("

Contact Page

This is the Contact page.

You can navigate back and forth using the browser's back/forward buttons.

"); + } +} + +int main() { + body->SetStyle("font-family: Arial, sans-serif; margin: 0; padding: 20px;"); + + // Add click handlers for navigation + home->AddClickListener([](Crafter::MouseEvent e) { + PushState("{\"page\": \"home\"}", "Home", "/"); + UpdateContent("home"); + }); + + about->AddClickListener([](Crafter::MouseEvent e) { + PushState("{\"page\": \"about\"}", "About", "/about"); + UpdateContent("about"); + }); + + contact->AddClickListener([](Crafter::MouseEvent e) { + PushState("{\"page\": \"contact\"}", "Contact", "/contact"); + UpdateContent("contact"); + }); + + // Add popstate listener to handle browser back/forward buttons + auto popStateId = AddPopStateListener([]() { + std::string path = GetPathNameString(); + if (path == "/") { + UpdateContent("home"); + } else if (path == "/about") { + UpdateContent("about"); + } else if (path == "/contact") { + UpdateContent("contact"); + } else { + UpdateContent("home"); + } + }); + + // Initialize with home page + UpdateContent("home"); + + return 0; +} +``` + +## Building and Running + +```bash +crafter-build build executable +run.sh +``` + +Then navigate to `http://localhost:8080/` in your browser. + +If caddy is not installed, you can use your favorite static file server instead. \ No newline at end of file diff --git a/examples/SPAExample/main.cpp b/examples/SPAExample/main.cpp new file mode 100644 index 0000000..cd39dd3 --- /dev/null +++ b/examples/SPAExample/main.cpp @@ -0,0 +1,67 @@ +import Crafter.CppDOM; +import std; +using namespace Crafter; +using namespace Crafter::CppDOMBindings; + +HtmlElementView* body = new HtmlElementView("body", R"( + +
+)"); +HtmlElementView* home = new HtmlElementView("home"); +HtmlElementView* about = new HtmlElementView("about"); +HtmlElementView* contact = new HtmlElementView("contact"); +HtmlElementView* content = new HtmlElementView("content"); + +void UpdateContent(const std::string_view page) { + if (page == "home") { + content->SetInnerHTML("

Home Page

Welcome to the Home page of our Single Page Application!

This demo shows how to use the new history.pushState and popstate event handling features.

"); + } else if (page == "about") { + content->SetInnerHTML("

About Page

This is the About page.

Notice how the URL changes without reloading the page.

"); + } else if (page == "contact") { + content->SetInnerHTML("

Contact Page

This is the Contact page.

You can navigate back and forth using the browser's back/forward buttons.

"); + } +} + +int main() { + body->SetStyle("font-family: Arial, sans-serif; margin: 0; padding: 20px;"); + + // Add click handlers for navigation + home->AddClickListener([](Crafter::MouseEvent e) { + PushState("{\"page\": \"home\"}", "Home", "/"); + UpdateContent("home"); + }); + + about->AddClickListener([](Crafter::MouseEvent e) { + PushState("{\"page\": \"about\"}", "About", "/about"); + UpdateContent("about"); + }); + + contact->AddClickListener([](Crafter::MouseEvent e) { + PushState("{\"page\": \"contact\"}", "Contact", "/contact"); + UpdateContent("contact"); + }); + + // Add popstate listener to handle browser back/forward buttons + auto popStateId = AddPopStateListener([]() { + std::string path = GetPathNameString(); + if (path == "/") { + UpdateContent("home"); + } else if (path == "/about") { + UpdateContent("about"); + } else if (path == "/contact") { + UpdateContent("contact"); + } else { + UpdateContent("home"); + } + }); + + // Initialize with home page + UpdateContent("home"); + + return 0; +} \ No newline at end of file diff --git a/examples/SPAExample/project.json b/examples/SPAExample/project.json new file mode 100644 index 0000000..9750e20 --- /dev/null +++ b/examples/SPAExample/project.json @@ -0,0 +1,17 @@ +{ + "name": "main", + "configurations": [ + { + "name": "executable", + "implementations": ["main"], + "target": "wasm32-wasi", + "debug" : true, + "dependencies": [ + { + "path":"../../project.json", + "configuration":"lib-debug" + } + ] + } + ] +} \ No newline at end of file diff --git a/examples/SPAExample/run.sh b/examples/SPAExample/run.sh new file mode 100755 index 0000000..e706621 --- /dev/null +++ b/examples/SPAExample/run.sh @@ -0,0 +1 @@ +caddy file-server --listen :8080 --root bin/executable \ No newline at end of file diff --git a/interfaces/Crafter.CppDOM-BindingsExport.cppm b/interfaces/Crafter.CppDOM-BindingsExport.cppm index 15b59c5..e1d4280 100644 --- a/interfaces/Crafter.CppDOM-BindingsExport.cppm +++ b/interfaces/Crafter.CppDOM-BindingsExport.cppm @@ -1,26 +1,7 @@ -/* -Crafter.CppDOM -Copyright (C) 2025 Catcrafts -Catcrafts.net - -This library is free software; you can redistribute it and/or -modify it under the terms of the GNU Lesser General Public -License as published by the Free Software Foundation; either -version 3.0 of the License, or (at your option) any later version. - -This library is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -Lesser General Public License for more details. - -You should have received a copy of the GNU Lesser General Public -License along with this library; if not, write to the Free Software -Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA -*/ - export module Crafter.CppDOM:BindingsExport; import std; import :EventTypes; +import :BindingsImport; export namespace Crafter::CppDOMBindings { std::int32_t clickHandlerMaxId = 0; @@ -95,6 +76,9 @@ export namespace Crafter::CppDOMBindings { std::int32_t dragLeaveHandlerMaxId = 0; std::unordered_map>* dragLeaveHandlers = new std::unordered_map>(); + std::int32_t popStateHandlerMaxId = 0; + std::unordered_map>* popStateHandlers = new std::unordered_map>(); + std::int32_t fetchHandlerMaxId = 0; std::unordered_map>* fetchHandlers = new std::unordered_map>(); } @@ -213,6 +197,10 @@ extern "C" { Crafter::CppDOMBindings::fetchHandlers->find(handlerID)->second(response); Crafter::CppDOMBindings::fetchHandlers->erase(handlerID); } + + __attribute__((export_name("ExecutePopStateHandler"))) void ExecutePopStateHandler(std::int32_t handlerID) { + Crafter::CppDOMBindings::popStateHandlers->find(handlerID)->second(); + } } export namespace Crafter::CppDOMBindings { @@ -228,4 +216,26 @@ export namespace Crafter::CppDOMBindings { CppDOMBindings::fetchHandlers->insert({id, callback}); FetchWithBody(url.data(), url.size(), body.data(), body.size(), id); } + + __attribute__((import_module("env"), import_name("getPathName"))) const char* GetPathName(); + std::string GetPathNameString() { + const char* path = GetPathName(); + return std::string(path); + } + + std::int32_t AddPopStateListener(std::function callback) { + std::int32_t id = popStateHandlerMaxId++; + popStateHandlers->insert({id, callback}); + AddPopStateListener(id); + return id; + } + + void RemovePopStateListener(std::int32_t id) { + RemovePopStateListener(id); + popStateHandlers->erase(id); + } + + void PushState(const std::string_view data, const std::string_view title, const std::string_view url) { + PushState(data.data(), data.size(), title.data(), title.size(), url.data(), url.size()); + } } \ No newline at end of file diff --git a/interfaces/Crafter.CppDOM-BindingsImport.cppm b/interfaces/Crafter.CppDOM-BindingsImport.cppm index 16aa616..b308a70 100644 --- a/interfaces/Crafter.CppDOM-BindingsImport.cppm +++ b/interfaces/Crafter.CppDOM-BindingsImport.cppm @@ -142,6 +142,10 @@ export namespace Crafter::CppDOMBindings { // Value property functions __attribute__((import_module("env"), import_name("getValue"))) const char* GetValue(void* ptr); + __attribute__((import_module("env"), import_name("getPathName"))) const char* GetPathName(); + __attribute__((import_module("env"), import_name("addPopStateListener"))) void AddPopStateListener(int id); + __attribute__((import_module("env"), import_name("removePopStateListener"))) void RemovePopStateListener(int id); + __attribute__((import_module("env"), import_name("pushState"))) void PushState(const char* data, std::int32_t dataLength, const char* title, std::int32_t titleLength, const char* url, std::int32_t urlLength); __attribute__((import_module("env"), import_name("setValue"))) void SetValue(void* ptr, const char* value, std::int32_t valueLength); void SetValue(void* ptr, const std::string_view value) { SetValue(ptr, value.data(), value.size());