defmodule Dala.Ui.Embedded.Webview do @compile {:nowarn_undefined, [:Nx]} @moduledoc """ Bidirectional JS bridge for the native WebView component. ## Overview: Use `Dala.Ui.Widgets.webview/1` to embed the component, then call these functions from `handle_info` to communicate with the page. ## JS Side (injected automatically): // Send a message to Elixir window.dala.send({ event: "clicked", id: 42 }) // Receive a message from Elixir window.dala.onMessage(function(data) { console.log(data) }) ## Elixir Side: def handle_info({:webview, :message, %{"event" => "clicked", "id" => id}}, socket) do {:noreply, socket} end def handle_info({:webview, :blocked, url}, socket) do # A navigation attempt was blocked by the allow: whitelist {:noreply, socket} end ## Navigation Functions: - `navigate/2` - Navigate to URL - `reload/1` - Reload current page - `stop_loading/1` - Stop loading - `go_forward/1` - Go forward in history ## Interact Actions: - `{:tap, selector}` - Tap an element - `{:type, selector, text}` - Type text - `{:clear, selector}` - Clear input - `{:eval, js_code}` - Evaluate JavaScript - `{:scroll, selector, dx, dy}` - Scroll element - `{:wait, selector, timeout_ms}` - Wait for element Results arrive via: - `{:webview, :eval_result, json}` - JS eval result - `{:webview, :interact_result, %{"action" => ..., "success" => ...}}` """ @doc """ Evaluate arbitrary JavaScript in the current WebView and return the result asynchronously via `handle_info({:webview, :eval_result, result}, socket)`. The result is JSON-decoded before delivery. """ @spec eval_js(Dala.Ui.Socket.t(), String.t()) :: Dala.Ui.Socket.t() def eval_js(socket, code) when is_binary(code) do Dala.Platform.Native.webview_eval_js(code) socket end @doc """ Navigate to a new URL in the WebView. """ @spec navigate(Dala.Ui.Socket.t(), String.t()) :: Dala.Ui.Socket.t() def navigate(socket, url) when is_binary(url) do js = "window.location.href = #{Jason.encode!(url)}" Dala.Platform.Native.webview_eval_js(js) socket end @doc """ Reload the current page. """ @spec reload(Dala.Ui.Socket.t()) :: Dala.Ui.Socket.t() def reload(socket) do Dala.Platform.Native.webview_eval_js("window.location.reload()") socket end @doc """ Stop loading the current page. """ @spec stop_loading(Dala.Ui.Socket.t()) :: Dala.Ui.Socket.t() def stop_loading(socket) do Dala.Platform.Native.webview_eval_js("window.stop()") socket end @doc """ Go forward in the WebView history (if possible). """ @spec go_forward(Dala.Ui.Socket.t()) :: Dala.Ui.Socket.t() def go_forward(socket) do Dala.Platform.Native.webview_eval_js( "if (window.history.length > 1) window.history.forward()" ) socket end @doc """ High-level interact API for driving WebView content programmatically. Actions: * `{:tap, selector}` - Tap an element matching CSS selector * `{:type, selector, text}` - Type text into an input element * `{:clear, selector}` - Clear an input element * `{:eval, js_code}` - Evaluate JS and return result via `:eval_result` * `{:scroll, selector, dx, dy}` - Scroll an element by delta * `{:wait, selector, timeout_ms}` - Wait for element to appear (via polling) Results arrive as `handle_info({:webview, :interact_result, %{"action" => ..., "success" => ...}}, socket)`. """ @spec interact(Dala.Ui.Socket.t(), tuple()) :: Dala.Ui.Socket.t() def interact(socket, action) do js = interact_js(action) Dala.Platform.Native.webview_eval_js(js) socket end defp interact_js({:tap, selector}) when is_binary(selector) do """ (function() { var el = document.querySelector(#{Jason.encode!(selector)}); if (el) { el.click(); return {action: "tap", success: true, selector: #{Jason.encode!(selector)}}; } return {action: "tap", success: false, error: "Element not found"}; })() """ end defp interact_js({:type, selector, text}) when is_binary(selector) and is_binary(text) do """ (function() { var el = document.querySelector(#{Jason.encode!(selector)}); if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) { el.value = #{Jason.encode!(text)}; el.dispatchEvent(new Event('input', {bubbles: true})); return {action: "type", success: true, selector: #{Jason.encode!(selector)}}; } return {action: "type", success: false, error: "Input element not found"}; })() """ end defp interact_js({:clear, selector}) when is_binary(selector) do """ (function() { var el = document.querySelector(#{Jason.encode!(selector)}); if (el && (el.tagName === 'INPUT' || el.tagName === 'TEXTAREA')) { el.value = ''; el.dispatchEvent(new Event('input', {bubbles: true})); return {action: "clear", success: true}; } return {action: "clear", success: false}; })() """ end defp interact_js({:eval, code}) when is_binary(code) do code end defp interact_js({:scroll, selector, dx, dy}) when is_binary(selector) and is_number(dx) and is_number(dy) do """ (function() { var el = document.querySelector(#{Jason.encode!(selector)}); if (el) { el.scrollLeft += #{dx}; el.scrollTop += #{dy}; return {action: "scroll", success: true}; } return {action: "scroll", success: false}; })() """ end defp interact_js({:wait, selector, timeout_ms}) when is_binary(selector) and is_integer(timeout_ms) do """ (function() { var timeout = #{timeout_ms}; var interval = 100; var elapsed = 0; var check = function() { if (document.querySelector(#{Jason.encode!(selector)})) { return {action: "wait", success: true, selector: #{Jason.encode!(selector)}}; } elapsed += interval; if (elapsed >= timeout) { return {action: "wait", success: false, error: "Timeout waiting for element"}; } setTimeout(check, interval); }; check(); })() """ end defp interact_js(_), do: "throw new Error('Unknown interact action')" @doc """ Take a screenshot of the WebView content. Returns the PNG data as a binary via: handle_info({:webview, :screenshot, png_data}, socket) Note: Currently not implemented on all platforms. """ @spec screenshot(Dala.Ui.Socket.t()) :: Dala.Ui.Socket.t() def screenshot(socket) do Dala.Platform.Native.webview_screenshot() socket end @doc """ Push a message from Elixir into the WebView page. Calls `window.dala._dispatch(json)` in JS, which delivers the data to all `window.dala.onMessage` handlers. """ @spec post_message(Dala.Ui.Socket.t(), term()) :: Dala.Ui.Socket.t() def post_message(socket, data) do json = :json.encode(data) Dala.Platform.Native.webview_post_message(json) socket end end