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.
This commit is contained in:
Timothy Flynn 2023-12-03 16:01:19 -05:00 committed by Andreas Kling
parent 1236cbd41a
commit 6d743ce9e8
3 changed files with 97 additions and 3 deletions

View file

@ -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;

View file

@ -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 = `<div ${input.value}></div>`;
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;

View file

@ -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("</span>"sv);
} else {
builder.appendff("<span class=\"hoverable\" {}>", data_attributes.string_view());
builder.appendff("<span data-node-type=\"text\" class=\"hoverable editable\" {}>", data_attributes.string_view());
builder.append(text);
builder.append("</span>"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("<span class=\"hoverable comment\" {}>", data_attributes.string_view());
builder.appendff("<span data-node-type=\"comment\" class=\"hoverable editable comment\" {}>", data_attributes.string_view());
builder.appendff("&lt;!--{}--&gt;", comment);
builder.append("</span>"sv);
return;
@ -365,14 +385,16 @@ String InspectorClient::generate_dom_tree(JsonObject const& dom_tree)
builder.appendff("<span class=\"hoverable\" {}>", data_attributes.string_view());
builder.append("<span>&lt;</span>"sv);
builder.appendff("<span class=\"tag\">{}</span>", name.to_lowercase());
builder.appendff("<span data-node-type=\"tag\" class=\"editable tag\">{}</span>", 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("&nbsp;"sv);
builder.appendff("<span data-node-type=\"attribute\" data-attribute-name=\"{}\" class=\"editable\">", name);
builder.appendff("<span class=\"attribute-name\">{}</span>", name);
builder.append('=');
builder.appendff("<span class=\"attribute-value\">\"{}\"</span>", value);
builder.append("</span>"sv);
});
}