LibWebView: Add Inspector actions to be used as context menu callbacks

These allow for triggering an edit of a DOM node (as an alternative to
double-clicking), removing a DOM node, and adding/removing DOM node
attributes.
This commit is contained in:
Timothy Flynn 2023-12-05 16:16:12 -05:00 committed by Andreas Kling
parent 9a5fe740c6
commit 0ddc2ea8c4
3 changed files with 161 additions and 30 deletions

View file

@ -136,6 +136,28 @@ inspector.clearInspectedDOMNode = () => {
}
};
inspector.editDOMNodeID = nodeID => {
if (pendingEditDOMNode === null) {
return;
}
inspector.inspectDOMNodeID(nodeID);
editDOMNode(pendingEditDOMNode);
pendingEditDOMNode = null;
};
inspector.addAttributeToDOMNodeID = nodeID => {
if (pendingEditDOMNode === null) {
return;
}
inspector.inspectDOMNodeID(nodeID);
addAttributeToDOMNode(pendingEditDOMNode);
pendingEditDOMNode = null;
};
inspector.createPropertyTables = (computedStyle, resolvedStyle, customProperties) => {
const createPropertyTable = (tableID, properties) => {
let oldTable = document.getElementById(tableID);
@ -176,62 +198,110 @@ 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;
const createDOMEditor = (onHandleChange, onCancelChange) => {
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
);
try {
onHandleChange(input.value);
} catch {
cancelChange();
}
};
const cancelChange = () => {
input.removeEventListener("change", handleChange);
input.removeEventListener("blur", cancelChange);
selectedDOMNode.classList.add("selected");
input.parentNode.replaceChild(domNode, input);
onCancelChange(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();
});
return input;
};
const parseDOMAttributes = value => {
let element = document.createElement("div");
element.innerHTML = `<div ${value}></div>`;
return element.children[0].attributes;
};
const editDOMNode = domNode => {
if (selectedDOMNode === null) {
return;
}
const domNodeID = selectedDOMNode.dataset.id;
const handleChange = value => {
const type = domNode.dataset.nodeType;
if (type === "text" || type === "comment") {
inspector.setDOMNodeText(domNodeID, value);
} else if (type === "tag") {
const element = document.createElement(value);
inspector.setDOMNodeTag(domNodeID, value);
} else if (type === "attribute") {
const attributes = parseDOMAttributes(value);
inspector.replaceDOMNodeAttribute(domNodeID, domNode.dataset.attributeName, attributes);
}
};
const cancelChange = editor => {
editor.parentNode.replaceChild(domNode, editor);
};
let editor = createDOMEditor(handleChange, cancelChange);
editor.value = domNode.innerText;
domNode.parentNode.replaceChild(editor, domNode);
};
const addAttributeToDOMNode = domNode => {
if (selectedDOMNode === null) {
return;
}
const domNodeID = selectedDOMNode.dataset.id;
const handleChange = value => {
const attributes = parseDOMAttributes(value);
inspector.addDOMNodeAttributes(domNodeID, attributes);
};
const cancelChange = () => {
container.remove();
};
let editor = createDOMEditor(handleChange, cancelChange);
editor.placeholder = 'name="value"';
let nbsp = document.createElement("span");
nbsp.innerHTML = "&nbsp;";
let container = document.createElement("span");
container.appendChild(nbsp);
container.appendChild(editor);
domNode.parentNode.insertBefore(container, domNode.parentNode.lastChild);
};
const requestContextMenu = (clientX, clientY, domNode) => {

View file

@ -141,6 +141,13 @@ InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImple
inspect();
};
m_inspector_web_view.on_inspector_added_dom_node_attributes = [this](auto node_id, auto const& attributes) {
m_content_web_view.add_dom_node_attributes(node_id, attributes);
m_pending_selection = node_id;
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);
@ -216,6 +223,55 @@ void InspectorClient::select_node(i32 node_id)
m_inspector_web_view.run_javascript(script);
}
void InspectorClient::context_menu_edit_dom_node()
{
VERIFY(m_context_menu_dom_node_id.has_value());
auto script = MUST(String::formatted("inspector.editDOMNodeID({});", *m_context_menu_dom_node_id));
m_inspector_web_view.run_javascript(script);
m_context_menu_dom_node_id.clear();
m_context_menu_tag_or_attribute_name.clear();
}
void InspectorClient::context_menu_remove_dom_node()
{
VERIFY(m_context_menu_dom_node_id.has_value());
m_content_web_view.remove_dom_node(*m_context_menu_dom_node_id);
m_pending_selection = m_body_node_id;
inspect();
m_context_menu_dom_node_id.clear();
m_context_menu_tag_or_attribute_name.clear();
}
void InspectorClient::context_menu_add_dom_node_attribute()
{
VERIFY(m_context_menu_dom_node_id.has_value());
auto script = MUST(String::formatted("inspector.addAttributeToDOMNodeID({});", *m_context_menu_dom_node_id));
m_inspector_web_view.run_javascript(script);
m_context_menu_dom_node_id.clear();
m_context_menu_tag_or_attribute_name.clear();
}
void InspectorClient::context_menu_remove_dom_node_attribute()
{
VERIFY(m_context_menu_dom_node_id.has_value());
VERIFY(m_context_menu_tag_or_attribute_name.has_value());
m_content_web_view.replace_dom_node_attribute(*m_context_menu_dom_node_id, *m_context_menu_tag_or_attribute_name, {});
m_pending_selection = m_context_menu_dom_node_id;
inspect();
m_context_menu_dom_node_id.clear();
m_context_menu_tag_or_attribute_name.clear();
}
void InspectorClient::load_inspector()
{
StringBuilder builder;

View file

@ -26,6 +26,11 @@ public:
void select_default_node();
void clear_selection();
void context_menu_edit_dom_node();
void context_menu_remove_dom_node();
void context_menu_add_dom_node_attribute();
void context_menu_remove_dom_node_attribute();
Function<void(Gfx::IntPoint)> on_requested_dom_node_text_context_menu;
Function<void(Gfx::IntPoint, String const&)> on_requested_dom_node_tag_context_menu;
Function<void(Gfx::IntPoint, String const&)> on_requested_dom_node_attribute_context_menu;