From 0ddc2ea8c475f4b4dcf5cf46a8c385317468abfa Mon Sep 17 00:00:00 2001 From: Timothy Flynn Date: Tue, 5 Dec 2023 16:16:12 -0500 Subject: [PATCH] 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. --- Base/res/ladybird/inspector.js | 130 ++++++++++++++---- .../Libraries/LibWebView/InspectorClient.cpp | 56 ++++++++ .../Libraries/LibWebView/InspectorClient.h | 5 + 3 files changed, 161 insertions(+), 30 deletions(-) diff --git a/Base/res/ladybird/inspector.js b/Base/res/ladybird/inspector.js index cae5854c26..10dcc7a915 100644 --- a/Base/res/ladybird/inspector.js +++ b/Base/res/ladybird/inspector.js @@ -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 = `
`; - - 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 = `
`; + + 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 = " "; + + let container = document.createElement("span"); + container.appendChild(nbsp); + container.appendChild(editor); + + domNode.parentNode.insertBefore(container, domNode.parentNode.lastChild); }; const requestContextMenu = (clientX, clientY, domNode) => { diff --git a/Userland/Libraries/LibWebView/InspectorClient.cpp b/Userland/Libraries/LibWebView/InspectorClient.cpp index 197fef9630..8a221adf5d 100644 --- a/Userland/Libraries/LibWebView/InspectorClient.cpp +++ b/Userland/Libraries/LibWebView/InspectorClient.cpp @@ -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; diff --git a/Userland/Libraries/LibWebView/InspectorClient.h b/Userland/Libraries/LibWebView/InspectorClient.h index a3a84a338a..16fbb6ab27 100644 --- a/Userland/Libraries/LibWebView/InspectorClient.h +++ b/Userland/Libraries/LibWebView/InspectorClient.h @@ -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 on_requested_dom_node_text_context_menu; Function on_requested_dom_node_tag_context_menu; Function on_requested_dom_node_attribute_context_menu;