From 6d743ce9e8481b5a3767d6cef0aa10afe342613c Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Sun, 3 Dec 2023 16:01:19 -0500 Subject: [PATCH] LibWebView: Allow editing the DOM through the Inspector WebView This allows a limited amount of DOM manipulation through the Inspector. Users may edit node tag names, text content, and attributes. To initiate an edit, double-click the tag/text/attribute of interest. To remove an attribute, begin editing the attribute and remove all of its text. To add an attribute, begin editing an existing attribute and add the new attribute's text before or after the existing attribute's text. This isn't going to be the final UX, but works for now just as a consequence of how attribute changes are implemented. A future patch will add more explicit add/delete actions. --- Base/res/html/inspector/inspector.css | 5 ++ Base/res/html/inspector/inspector.js | 67 +++++++++++++++++++ .../Libraries/LibWebView/InspectorClient.cpp | 28 +++++++- 3 files changed, 97 insertions(+), 3 deletions(-) diff --git a/Base/res/html/inspector/inspector.css b/Base/res/html/inspector/inspector.css index b753072ea9..65757f9805 100644 --- a/Base/res/html/inspector/inspector.css +++ b/Base/res/html/inspector/inspector.css @@ -137,6 +137,11 @@ details > :not(:first-child) { padding: 1px; } +.dom-editor { + width: fit-content; + outline: none; +} + @media (prefers-color-scheme: dark) { .hoverable:hover { background-color: #31383e; diff --git a/Base/res/html/inspector/inspector.js b/Base/res/html/inspector/inspector.js index 3c00040c53..44f4bcd15a 100644 --- a/Base/res/html/inspector/inspector.js +++ b/Base/res/html/inspector/inspector.js @@ -96,6 +96,15 @@ inspector.loadDOMTree = tree => { event.preventDefault(); }); } + + domNodes = domTree.querySelectorAll(".editable"); + + for (let domNode of domNodes) { + domNode.addEventListener("dblclick", event => { + editDOMNode(domNode); + event.preventDefault(); + }); + } }; inspector.loadAccessibilityTree = tree => { @@ -166,6 +175,64 @@ const inspectDOMNode = domNode => { inspector.inspectDOMNode(domNode.dataset.id, domNode.dataset.pseudoElement); }; +const editDOMNode = domNode => { + if (selectedDOMNode === null) { + return; + } + + const domNodeID = selectedDOMNode.dataset.id; + const type = domNode.dataset.nodeType; + + selectedDOMNode.classList.remove("selected"); + + let input = document.createElement("input"); + input.classList.add("dom-editor"); + input.classList.add("selected"); + input.value = domNode.innerText; + + const handleChange = () => { + input.removeEventListener("change", handleChange); + input.removeEventListener("blur", cancelChange); + + if (type === "text" || type === "comment") { + inspector.setDOMNodeText(domNodeID, input.value); + } else if (type === "tag") { + try { + const element = document.createElement(input.value); + inspector.setDOMNodeTag(domNodeID, input.value); + } catch { + cancelChange(); + } + } else if (type === "attribute") { + let element = document.createElement("div"); + element.innerHTML = `
`; + + inspector.replaceDOMNodeAttribute( + domNodeID, + domNode.dataset.attributeName, + element.children[0].attributes + ); + } + }; + + const cancelChange = () => { + selectedDOMNode.classList.add("selected"); + input.parentNode.replaceChild(domNode, input); + }; + + input.addEventListener("change", handleChange); + input.addEventListener("blur", cancelChange); + + domNode.parentNode.replaceChild(input, domNode); + + setTimeout(() => { + input.focus(); + + // FIXME: Invoke `select` when it isn't just stubbed out. + // input.select(); + }); +}; + const executeConsoleScript = consoleInput => { const script = consoleInput.value; diff --git a/Userland/Libraries/LibWebView/InspectorClient.cpp b/Userland/Libraries/LibWebView/InspectorClient.cpp index e2b0ed82e1..6cd5c38265 100644 --- a/Userland/Libraries/LibWebView/InspectorClient.cpp +++ b/Userland/Libraries/LibWebView/InspectorClient.cpp @@ -109,6 +109,25 @@ InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImple m_inspector_web_view.run_javascript(builder.string_view()); }; + m_inspector_web_view.on_inspector_set_dom_node_text = [this](auto node_id, auto const& text) { + m_content_web_view.set_dom_node_text(node_id, text); + + m_pending_selection = node_id; + inspect(); + }; + + m_inspector_web_view.on_inspector_set_dom_node_tag = [this](auto node_id, auto const& tag) { + m_pending_selection = m_content_web_view.set_dom_node_tag(node_id, tag); + inspect(); + }; + + m_inspector_web_view.on_inspector_replaced_dom_node_attribute = [this](auto node_id, auto const& name, auto const& replacement_attributes) { + m_content_web_view.replace_dom_node_attribute(node_id, name, replacement_attributes); + + m_pending_selection = node_id; + inspect(); + }; + m_inspector_web_view.on_inspector_executed_console_script = [this](auto const& script) { append_console_source(script); @@ -128,6 +147,7 @@ InspectorClient::~InspectorClient() void InspectorClient::inspect() { + m_dom_tree_loaded = false; m_content_web_view.inspect_dom_tree(); m_content_web_view.inspect_accessibility_tree(); } @@ -326,7 +346,7 @@ String InspectorClient::generate_dom_tree(JsonObject const& dom_tree) builder.append(name); builder.append(""sv); } else { - builder.appendff("", data_attributes.string_view()); + builder.appendff("", data_attributes.string_view()); builder.append(text); builder.append(""sv); } @@ -338,7 +358,7 @@ String InspectorClient::generate_dom_tree(JsonObject const& dom_tree) auto comment = node.get_deprecated_string("data"sv).release_value(); comment = escape_html_entities(comment); - builder.appendff("", data_attributes.string_view()); + builder.appendff("", data_attributes.string_view()); builder.appendff("<!--{}-->", comment); builder.append(""sv); return; @@ -365,14 +385,16 @@ String InspectorClient::generate_dom_tree(JsonObject const& dom_tree) builder.appendff("", data_attributes.string_view()); builder.append("<"sv); - builder.appendff("{}", name.to_lowercase()); + builder.appendff("{}", name.to_lowercase()); if (auto attributes = node.get_object("attributes"sv); attributes.has_value()) { attributes->for_each_member([&builder](auto const& name, auto const& value) { builder.append(" "sv); + builder.appendff("", name); builder.appendff("{}", name); builder.append('='); builder.appendff("\"{}\"", value); + builder.append(""sv); }); }