//SPDX-License-Identifier: LGPL-3.0-only //SPDX-FileCopyrightText: Copyright (C) 2026 Catcrafts® // Browser DOM bindings — absorbed from Crafter.CppDOM. Only meaningful // when the build defined CRAFTER_GRAPHICS_WINDOW_DOM (i.e. a wasm32-* // target paired with additional/dom-env.js); on native platforms the // partition still parses but every method links to nothing because the // build excludes the impl. project.cpp's per-target module set is what // makes the partition disappear from the native libs. // // Two-class hierarchy (simplified from CppDOM's three-class design): // HtmlElementPtr — non-owning element reference. Tracks every handler // it registered and clears them on destruction, so // the silent-leak bug from CppDOM's bare-pointer // flavour can't happen. Releases the JS handle on // destruction. // HtmlElement — `HtmlElementPtr` plus element ownership: removes // the element from the DOM on destruction. The new // `Create` factory builds a fresh element under a // parent and returns one of these. // // Move-only throughout. The JS handle (`ptr`) of a moved-from instance // is zeroed so the destructor of the moved-from carcass is a no-op. export module Crafter.Graphics:Dom; #ifdef CRAFTER_GRAPHICS_WINDOW_DOM import std; import :DomEvents; export namespace Crafter::Dom { class HtmlElementPtr { public: // Opaque JS-side element cookie. 0 = "no element"; the destructor // treats 0 as "moved-from, nothing to release". std::int32_t ptr; // Look up an existing element by its DOM id. HtmlElementPtr(const std::string_view id); // Look up + replace innerHTML in one step (matches CppDOM). HtmlElementPtr(const std::string_view id, const std::string_view html); protected: // Adopt a JS handle directly (used by HtmlElement::Create — the // JS side has already minted the handle, no second lookup needed). // Tagged-type ctor so the public id-based overloads stay // unambiguous from the call site. struct FromHandle { std::int32_t handle; }; explicit HtmlElementPtr(FromHandle h) noexcept : ptr(h.handle) {} public: // Move-only — copying would silently double-free the JS handle. HtmlElementPtr(HtmlElementPtr&&) noexcept; HtmlElementPtr& operator=(HtmlElementPtr&&) noexcept; HtmlElementPtr(const HtmlElementPtr&) = delete; HtmlElementPtr& operator=(const HtmlElementPtr&) = delete; ~HtmlElementPtr(); // DOM ops ───────────────────────────────────────────────────── void SetInnerHTML(const std::string_view html); void SetStyle(const std::string_view style); // WARNING: SetProperty sets a *CSS* property (el.style.setProperty). // It is NOT setAttribute — the name is historical. For href / src / // disabled / aria-* / data-*, use SetAttribute below. void SetProperty(const std::string_view property, const std::string_view value); void AddClass(const std::string_view className); void RemoveClass(const std::string_view className); void ToggleClass(const std::string_view className); bool HasClass(const std::string_view className); std::string GetValue(); void SetValue(const std::string_view value); // Attributes ────────────────────────────────────────────────── // // Real setAttribute/getAttribute access. Before these existed the // only way to change an href or toggle `disabled` was to re-render // the parent's innerHTML, which destroys every descendant and every // listener attached to them just to change one string. void SetAttribute(const std::string_view name, const std::string_view value); void RemoveAttribute(const std::string_view name); // nullopt = attribute absent. Distinct from an empty string, which // means present-with-no-value (`alt=""`, `disabled=""`) — for // boolean attributes presence alone is the signal. std::optional GetAttribute(const std::string_view name); bool HasAttribute(const std::string_view name); // Checkbox / radio state. GetValue cannot express this: `el.value` // on a checkbox is "on" whether or not it is ticked. bool GetChecked(); void SetChecked(bool checked); // Move keyboard focus to / away from this element. AddFocusListener // could already observe focus, but nothing could set it, which makes // an accessible "jump to first invalid field" impossible. void Focus(); void Blur(); // Listener API — each Add* returns an opaque id that can be // passed to the matching Remove*. The destructor automatically // removes every handler still registered, so manual removal is // optional. Returns 0 only if registration failed at the JS // boundary (the element was already collected). The 23 event // types are 1:1 with CppDOM's surface. // // `preventDefault` (mouse / key / wheel kinds only — the others are // not cancelable events, so the flag would be a silent no-op and is // deliberately absent): // Cancels the browser's default action. The JS bridge applies it // BEFORE dispatching into wasm, so a trap in the callback cannot let // the default action through anyway. // // This is what makes client-side routing over real, crawlable // `` links possible: take the click, cancel the // navigation, then Router::PushState. Without it the handler runs // *and* the browser performs a full page load. It is also mandatory // on dragover/drop — a drop target does not function at all unless // the default is cancelled. // // Wheel additionally registers with `{ passive: false }` when set, // or the browser drops the cancellation with a console warning. // // AddSubmitListener has no flag: its handler always preventDefaults, // because a native form POST would navigate away from the wasm app. std::int32_t AddClickListener(std::function callback, bool preventDefault = false); void RemoveClickListener(std::int32_t id); std::int32_t AddMouseOverListener(std::function callback, bool preventDefault = false); void RemoveMouseOverListener(std::int32_t id); std::int32_t AddMouseOutListener(std::function callback, bool preventDefault = false); void RemoveMouseOutListener(std::int32_t id); std::int32_t AddMouseMoveListener(std::function callback, bool preventDefault = false); void RemoveMouseMoveListener(std::int32_t id); std::int32_t AddMouseDownListener(std::function callback, bool preventDefault = false); void RemoveMouseDownListener(std::int32_t id); std::int32_t AddMouseUpListener(std::function callback, bool preventDefault = false); void RemoveMouseUpListener(std::int32_t id); std::int32_t AddFocusListener(std::function callback); void RemoveFocusListener(std::int32_t id); std::int32_t AddBlurListener(std::function callback); void RemoveBlurListener(std::int32_t id); std::int32_t AddKeyDownListener(std::function callback, bool preventDefault = false); void RemoveKeyDownListener(std::int32_t id); std::int32_t AddKeyUpListener(std::function callback, bool preventDefault = false); void RemoveKeyUpListener(std::int32_t id); std::int32_t AddKeyPressListener(std::function callback, bool preventDefault = false); void RemoveKeyPressListener(std::int32_t id); std::int32_t AddChangeListener(std::function callback); void RemoveChangeListener(std::int32_t id); std::int32_t AddSubmitListener(std::function callback); void RemoveSubmitListener(std::int32_t id); std::int32_t AddInputListener(std::function callback); void RemoveInputListener(std::int32_t id); std::int32_t AddResizeListener(std::function callback); void RemoveResizeListener(std::int32_t id); std::int32_t AddScrollListener(std::function callback); void RemoveScrollListener(std::int32_t id); std::int32_t AddContextMenuListener(std::function callback, bool preventDefault = false); void RemoveContextMenuListener(std::int32_t id); std::int32_t AddDragStartListener(std::function callback, bool preventDefault = false); void RemoveDragStartListener(std::int32_t id); std::int32_t AddDragEndListener(std::function callback, bool preventDefault = false); void RemoveDragEndListener(std::int32_t id); std::int32_t AddDropListener(std::function callback, bool preventDefault = false); void RemoveDropListener(std::int32_t id); std::int32_t AddDragOverListener(std::function callback, bool preventDefault = false); void RemoveDragOverListener(std::int32_t id); std::int32_t AddDragEnterListener(std::function callback, bool preventDefault = false); void RemoveDragEnterListener(std::int32_t id); std::int32_t AddDragLeaveListener(std::function callback, bool preventDefault = false); void RemoveDragLeaveListener(std::int32_t id); std::int32_t AddWheelListener(std::function callback, bool preventDefault = false); void RemoveWheelListener(std::int32_t id); protected: // Per-event-kind handler-id lists. Each `Add*Listener` push_backs // the id it returns; `RemoveAllHandlers` walks every list and // calls the matching JS Remove on each id. The lists are indexed // by event kind so a single handler-id collision across kinds // (allowed by the JS bridge — counters are per-event-kind) // doesn't cause a wrong-kind remove. std::vector handlerIds_[24]; // Shared cleanup used by the destructor AND by move-assignment. // Removes every registered handler from both the C++ map and // the JS side, then frees the JS handle. Leaves `ptr == 0` on // return so a second call is a no-op. void RemoveAllHandlersAndFree(); }; class HtmlElement : public HtmlElementPtr { public: // Adopt an existing element by id; the dtor will remove it from // the DOM. Useful when the caller created the element in markup // and wants C++ to own its lifetime from now on. HtmlElement(const std::string_view id); HtmlElement(const std::string_view id, const std::string_view html); HtmlElement(HtmlElement&&) noexcept; HtmlElement& operator=(HtmlElement&&) noexcept; ~HtmlElement(); // Create a new element and append it to `parent`. Optional `id` // sets the new element's DOM id (use it when you want to look the // element up from non-C++ code later); empty leaves the element // unnamed. Returns an owning handle — the element is removed from // the DOM when this `HtmlElement` is destroyed. static HtmlElement Create(const HtmlElementPtr& parent, std::string_view tagName, std::string_view id = {}); // Convenience: create a top-level element under document.body. static HtmlElement CreateInBody(std::string_view tagName, std::string_view id = {}); private: // Hidden ctor used by Create/CreateInBody — takes an already // allocated JS handle via the base class FromHandle tag. Kept // private so user code doesn't construct an HtmlElement from a // raw handle by accident. explicit HtmlElement(HtmlElementPtr::FromHandle h) noexcept : HtmlElementPtr(h) {} }; } #endif // CRAFTER_GRAPHICS_WINDOW_DOM