mirror of
https://github.com/LadybirdBrowser/ladybird.git
synced 2025-06-10 01:51:03 +09:00
LibWebView: Implement a JavaScript console tab in the Inspector
This adds a JS console to the bottom section of the Inspector WebView. Much of this code is based on the existing WebView::ConsoleClient, but ported to fit the inspector model. That is, much of the code from that class is now handled in the Inspector's JS.
This commit is contained in:
parent
9f374a5d2a
commit
d8a700d9be
Notes:
sideshowbarker
2024-07-17 11:30:54 +09:00
Author: https://github.com/trflynn89
Commit: d8a700d9be
Pull-request: https://github.com/SerenityOS/serenity/pull/22111
4 changed files with 341 additions and 3 deletions
|
@ -138,6 +138,58 @@ details > :not(:first-child) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.console {
|
||||||
|
font-family: Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-output {
|
||||||
|
height: calc(100% - 75px);
|
||||||
|
overflow: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input {
|
||||||
|
width: 100%;
|
||||||
|
height: 24px;
|
||||||
|
padding: 4px;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input input {
|
||||||
|
width: calc(100% - 60px);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.console-prompt {
|
||||||
|
color: cyan;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input {
|
||||||
|
background-color: rgb(57, 57, 57);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input input:focus {
|
||||||
|
outline: 1px dashed cyan;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: light) {
|
||||||
|
.console-prompt {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input {
|
||||||
|
background-color: rgb(229, 229, 229);
|
||||||
|
}
|
||||||
|
|
||||||
|
.console-input input:focus {
|
||||||
|
outline: 1px dashed blue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.property-table {
|
.property-table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,12 @@ let selectedBottomTabButton = null;
|
||||||
|
|
||||||
let selectedDOMNode = null;
|
let selectedDOMNode = null;
|
||||||
|
|
||||||
|
let consoleGroupStack = [];
|
||||||
|
let consoleGroupNextID = 0;
|
||||||
|
|
||||||
|
let consoleHistory = [];
|
||||||
|
let consoleHistoryIndex = 0;
|
||||||
|
|
||||||
const selectTab = (tabButton, tabID, selectedTab, selectedTabButton) => {
|
const selectTab = (tabButton, tabID, selectedTab, selectedTabButton) => {
|
||||||
let tab = document.getElementById(tabID);
|
let tab = document.getElementById(tabID);
|
||||||
|
|
||||||
|
@ -36,8 +42,8 @@ const selectBottomTab = (tabButton, tabID) => {
|
||||||
let initialTopTabButton = document.getElementById("dom-tree-button");
|
let initialTopTabButton = document.getElementById("dom-tree-button");
|
||||||
selectTopTab(initialTopTabButton, "dom-tree");
|
selectTopTab(initialTopTabButton, "dom-tree");
|
||||||
|
|
||||||
let initialBottomTabButton = document.getElementById("computed-style-button");
|
let initialBottomTabButton = document.getElementById("console-button");
|
||||||
selectBottomTab(initialBottomTabButton, "computed-style");
|
selectBottomTab(initialBottomTabButton, "console");
|
||||||
|
|
||||||
const scrollToElement = element => {
|
const scrollToElement = element => {
|
||||||
// Include an offset to prevent the element being placed behind the fixed `tab-controls` header.
|
// Include an offset to prevent the element being placed behind the fixed `tab-controls` header.
|
||||||
|
@ -131,6 +137,140 @@ const inspectDOMNode = domNode => {
|
||||||
inspector.inspectDOMNode(domNode.dataset.id, domNode.dataset.pseudoElement);
|
inspector.inspectDOMNode(domNode.dataset.id, domNode.dataset.pseudoElement);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const executeConsoleScript = consoleInput => {
|
||||||
|
const script = consoleInput.value;
|
||||||
|
|
||||||
|
if (!/\S/.test(script)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consoleHistory.length === 0 || consoleHistory[consoleHistory.length - 1] !== script) {
|
||||||
|
consoleHistory.push(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
consoleHistoryIndex = consoleHistory.length;
|
||||||
|
|
||||||
|
inspector.executeConsoleScript(script);
|
||||||
|
consoleInput.value = "";
|
||||||
|
};
|
||||||
|
|
||||||
|
const setConsoleInputToPreviousHistoryItem = consoleInput => {
|
||||||
|
if (consoleHistoryIndex === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
--consoleHistoryIndex;
|
||||||
|
|
||||||
|
const script = consoleHistory[consoleHistoryIndex];
|
||||||
|
consoleInput.value = script;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setConsoleInputToNextHistoryItem = consoleInput => {
|
||||||
|
if (consoleHistory.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastIndex = consoleHistory.length - 1;
|
||||||
|
|
||||||
|
if (consoleHistoryIndex < lastIndex) {
|
||||||
|
++consoleHistoryIndex;
|
||||||
|
|
||||||
|
consoleInput.value = consoleHistory[consoleHistoryIndex];
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (consoleHistoryIndex === lastIndex) {
|
||||||
|
++consoleHistoryIndex;
|
||||||
|
|
||||||
|
consoleInput.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const consoleParentGroup = () => {
|
||||||
|
if (consoleGroupStack.length === 0) {
|
||||||
|
return document.getElementById("console-output");
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastConsoleGroup = consoleGroupStack[consoleGroupStack.length - 1];
|
||||||
|
return document.getElementById(`console-group-${lastConsoleGroup.id}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const scrollConsoleToBottom = () => {
|
||||||
|
let consoleOutput = document.getElementById("console-output");
|
||||||
|
|
||||||
|
// FIXME: It should be sufficient to scrollTo a y value of document.documentElement.offsetHeight,
|
||||||
|
// but due to an unknown bug offsetHeight seems to not be properly updated after spamming
|
||||||
|
// a lot of document changes.
|
||||||
|
//
|
||||||
|
// The setTimeout makes the scrollTo async and allows the DOM to be updated.
|
||||||
|
setTimeout(function () {
|
||||||
|
consoleOutput.scrollTo(0, 1_000_000_000);
|
||||||
|
}, 0);
|
||||||
|
};
|
||||||
|
|
||||||
|
inspector.appendConsoleOutput = output => {
|
||||||
|
let parent = consoleParentGroup();
|
||||||
|
|
||||||
|
let element = document.createElement("p");
|
||||||
|
element.innerHTML = atob(output);
|
||||||
|
|
||||||
|
parent.appendChild(element);
|
||||||
|
scrollConsoleToBottom();
|
||||||
|
};
|
||||||
|
|
||||||
|
inspector.clearConsoleOutput = () => {
|
||||||
|
let consoleOutput = document.getElementById("console-output");
|
||||||
|
consoleOutput.innerHTML = "";
|
||||||
|
|
||||||
|
consoleGroupStack = [];
|
||||||
|
};
|
||||||
|
|
||||||
|
inspector.beginConsoleGroup = (label, startExpanded) => {
|
||||||
|
let parent = consoleParentGroup();
|
||||||
|
|
||||||
|
const group = {
|
||||||
|
id: ++consoleGroupNextID,
|
||||||
|
label: label,
|
||||||
|
};
|
||||||
|
consoleGroupStack.push(group);
|
||||||
|
|
||||||
|
let details = document.createElement("details");
|
||||||
|
details.id = `console-group-${group.id}`;
|
||||||
|
details.open = startExpanded;
|
||||||
|
|
||||||
|
let summary = document.createElement("summary");
|
||||||
|
summary.innerHTML = atob(label);
|
||||||
|
|
||||||
|
details.appendChild(summary);
|
||||||
|
parent.appendChild(details);
|
||||||
|
scrollConsoleToBottom();
|
||||||
|
};
|
||||||
|
|
||||||
|
inspector.endConsoleGroup = () => {
|
||||||
|
consoleGroupStack.pop();
|
||||||
|
};
|
||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
let consoleInput = document.getElementById("console-input");
|
||||||
|
consoleInput.focus();
|
||||||
|
|
||||||
|
consoleInput.addEventListener("keydown", event => {
|
||||||
|
const UP_ARROW_KEYCODE = 38;
|
||||||
|
const DOWN_ARROW_KEYCODE = 40;
|
||||||
|
const RETURN_KEYCODE = 13;
|
||||||
|
|
||||||
|
if (event.keyCode === UP_ARROW_KEYCODE) {
|
||||||
|
setConsoleInputToPreviousHistoryItem(consoleInput);
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (event.keyCode === DOWN_ARROW_KEYCODE) {
|
||||||
|
setConsoleInputToNextHistoryItem(consoleInput);
|
||||||
|
event.preventDefault();
|
||||||
|
} else if (event.keyCode === RETURN_KEYCODE) {
|
||||||
|
executeConsoleScript(consoleInput);
|
||||||
|
event.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
inspector.inspectorLoaded();
|
inspector.inspectorLoaded();
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
#include <AK/JsonObject.h>
|
#include <AK/JsonObject.h>
|
||||||
#include <AK/StringBuilder.h>
|
#include <AK/StringBuilder.h>
|
||||||
#include <LibCore/Resource.h>
|
#include <LibCore/Resource.h>
|
||||||
|
#include <LibJS/MarkupGenerator.h>
|
||||||
#include <LibWeb/Infra/Strings.h>
|
#include <LibWeb/Infra/Strings.h>
|
||||||
#include <LibWebView/InspectorClient.h>
|
#include <LibWebView/InspectorClient.h>
|
||||||
#include <LibWebView/SourceHighlighter.h>
|
#include <LibWebView/SourceHighlighter.h>
|
||||||
|
@ -63,11 +64,21 @@ InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImple
|
||||||
m_inspector_web_view.run_javascript(script);
|
m_inspector_web_view.run_javascript(script);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
m_content_web_view.on_received_console_message = [this](auto message_index) {
|
||||||
|
handle_console_message(message_index);
|
||||||
|
};
|
||||||
|
|
||||||
|
m_content_web_view.on_received_console_messages = [this](auto start_index, auto const& message_types, auto const& messages) {
|
||||||
|
handle_console_messages(start_index, message_types, messages);
|
||||||
|
};
|
||||||
|
|
||||||
m_inspector_web_view.enable_inspector_prototype();
|
m_inspector_web_view.enable_inspector_prototype();
|
||||||
m_inspector_web_view.use_native_user_style_sheet();
|
m_inspector_web_view.use_native_user_style_sheet();
|
||||||
|
|
||||||
m_inspector_web_view.on_inspector_loaded = [this]() {
|
m_inspector_web_view.on_inspector_loaded = [this]() {
|
||||||
inspect();
|
inspect();
|
||||||
|
|
||||||
|
m_content_web_view.js_console_request_messages(0);
|
||||||
};
|
};
|
||||||
|
|
||||||
m_inspector_web_view.on_inspector_selected_dom_node = [this](auto node_id, auto const& pseudo_element) {
|
m_inspector_web_view.on_inspector_selected_dom_node = [this](auto node_id, auto const& pseudo_element) {
|
||||||
|
@ -98,6 +109,12 @@ InspectorClient::InspectorClient(ViewImplementation& content_web_view, ViewImple
|
||||||
m_inspector_web_view.run_javascript(builder.string_view());
|
m_inspector_web_view.run_javascript(builder.string_view());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
m_inspector_web_view.on_inspector_executed_console_script = [this](auto const& script) {
|
||||||
|
append_console_source(script);
|
||||||
|
|
||||||
|
m_content_web_view.js_console_input(script.to_deprecated_string());
|
||||||
|
};
|
||||||
|
|
||||||
load_inspector();
|
load_inspector();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -105,6 +122,8 @@ InspectorClient::~InspectorClient()
|
||||||
{
|
{
|
||||||
m_content_web_view.on_received_dom_tree = nullptr;
|
m_content_web_view.on_received_dom_tree = nullptr;
|
||||||
m_content_web_view.on_received_accessibility_tree = nullptr;
|
m_content_web_view.on_received_accessibility_tree = nullptr;
|
||||||
|
m_content_web_view.on_received_console_message = nullptr;
|
||||||
|
m_content_web_view.on_received_console_messages = nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InspectorClient::inspect()
|
void InspectorClient::inspect()
|
||||||
|
@ -119,6 +138,11 @@ void InspectorClient::reset()
|
||||||
m_pending_selection.clear();
|
m_pending_selection.clear();
|
||||||
|
|
||||||
m_dom_tree_loaded = false;
|
m_dom_tree_loaded = false;
|
||||||
|
|
||||||
|
clear_console_output();
|
||||||
|
m_highest_notified_message_index = -1;
|
||||||
|
m_highest_received_message_index = -1;
|
||||||
|
m_waiting_for_messages = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
void InspectorClient::select_hovered_node()
|
void InspectorClient::select_hovered_node()
|
||||||
|
@ -189,11 +213,22 @@ void InspectorClient::load_inspector()
|
||||||
<div id="inspector-bottom" class="split-view-container" style="height: 40%">
|
<div id="inspector-bottom" class="split-view-container" style="height: 40%">
|
||||||
<div class="tab-controls-container">
|
<div class="tab-controls-container">
|
||||||
<div class="tab-controls">
|
<div class="tab-controls">
|
||||||
|
<button id="console-button" onclick="selectBottomTab(this, 'console')">Console</button>
|
||||||
<button id="computed-style-button" onclick="selectBottomTab(this, 'computed-style')">Computed Style</button>
|
<button id="computed-style-button" onclick="selectBottomTab(this, 'computed-style')">Computed Style</button>
|
||||||
<button id="resolved-style-button" onclick="selectBottomTab(this, 'resolved-style')">Resolved Style</button>
|
<button id="resolved-style-button" onclick="selectBottomTab(this, 'resolved-style')">Resolved Style</button>
|
||||||
<button id="custom-properties-button" onclick="selectBottomTab(this, 'custom-properties')">Custom Properties</button>
|
<button id="custom-properties-button" onclick="selectBottomTab(this, 'custom-properties')">Custom Properties</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="console" class="tab-content">
|
||||||
|
<div class="console">
|
||||||
|
<div id="console-output" class="console-output"></div>
|
||||||
|
<div class="console-input">
|
||||||
|
<label for="console-input" class="console-prompt">>></label>
|
||||||
|
<input id="console-input" type="text" placeholder="Enter statement to execute">
|
||||||
|
<button id="console-clear" title="Clear the console output" onclick="inspector.clearConsoleOutput()">X</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)~~~"sv);
|
)~~~"sv);
|
||||||
|
|
||||||
auto generate_property_table = [&](auto name) {
|
auto generate_property_table = [&](auto name) {
|
||||||
|
@ -384,4 +419,100 @@ String InspectorClient::generate_accessibility_tree(JsonObject const& accessibil
|
||||||
return MUST(builder.to_string());
|
return MUST(builder.to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void InspectorClient::request_console_messages()
|
||||||
|
{
|
||||||
|
VERIFY(!m_waiting_for_messages);
|
||||||
|
|
||||||
|
m_content_web_view.js_console_request_messages(m_highest_received_message_index + 1);
|
||||||
|
m_waiting_for_messages = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void InspectorClient::handle_console_message(i32 message_index)
|
||||||
|
{
|
||||||
|
if (message_index <= m_highest_received_message_index) {
|
||||||
|
dbgln("Notified about console message we already have");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (message_index <= m_highest_notified_message_index) {
|
||||||
|
dbgln("Notified about console message we're already aware of");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_highest_notified_message_index = message_index;
|
||||||
|
|
||||||
|
if (!m_waiting_for_messages)
|
||||||
|
request_console_messages();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InspectorClient::handle_console_messages(i32 start_index, ReadonlySpan<DeprecatedString> message_types, ReadonlySpan<DeprecatedString> messages)
|
||||||
|
{
|
||||||
|
auto end_index = start_index + static_cast<i32>(message_types.size()) - 1;
|
||||||
|
if (end_index <= m_highest_received_message_index) {
|
||||||
|
dbgln("Received old console messages");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (size_t i = 0; i < message_types.size(); ++i) {
|
||||||
|
auto const& type = message_types[i];
|
||||||
|
auto const& message = messages[i];
|
||||||
|
|
||||||
|
if (type == "html"sv)
|
||||||
|
append_console_output(message);
|
||||||
|
else if (type == "clear"sv)
|
||||||
|
clear_console_output();
|
||||||
|
else if (type == "group"sv)
|
||||||
|
begin_console_group(message, true);
|
||||||
|
else if (type == "groupCollapsed"sv)
|
||||||
|
begin_console_group(message, false);
|
||||||
|
else if (type == "groupEnd"sv)
|
||||||
|
end_console_group();
|
||||||
|
else
|
||||||
|
VERIFY_NOT_REACHED();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_highest_received_message_index = end_index;
|
||||||
|
m_waiting_for_messages = false;
|
||||||
|
|
||||||
|
if (m_highest_received_message_index < m_highest_notified_message_index)
|
||||||
|
request_console_messages();
|
||||||
|
}
|
||||||
|
|
||||||
|
void InspectorClient::append_console_source(StringView source)
|
||||||
|
{
|
||||||
|
StringBuilder builder;
|
||||||
|
|
||||||
|
builder.append("<span class=\"console-prompt\">> </span>"sv);
|
||||||
|
builder.append(MUST(JS::MarkupGenerator::html_from_source(source)));
|
||||||
|
|
||||||
|
append_console_output(builder.string_view());
|
||||||
|
}
|
||||||
|
|
||||||
|
void InspectorClient::append_console_output(StringView html)
|
||||||
|
{
|
||||||
|
auto html_base64 = MUST(encode_base64(html.bytes()));
|
||||||
|
|
||||||
|
auto script = MUST(String::formatted("inspector.appendConsoleOutput(\"{}\");", html_base64));
|
||||||
|
m_inspector_web_view.run_javascript(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InspectorClient::clear_console_output()
|
||||||
|
{
|
||||||
|
static constexpr auto script = "inspector.clearConsoleOutput();"sv;
|
||||||
|
m_inspector_web_view.run_javascript(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InspectorClient::begin_console_group(StringView label, bool start_expanded)
|
||||||
|
{
|
||||||
|
auto label_base64 = MUST(encode_base64(label.bytes()));
|
||||||
|
|
||||||
|
auto script = MUST(String::formatted("inspector.beginConsoleGroup(\"{}\", {});", label_base64, start_expanded));
|
||||||
|
m_inspector_web_view.run_javascript(script);
|
||||||
|
}
|
||||||
|
|
||||||
|
void InspectorClient::end_console_group()
|
||||||
|
{
|
||||||
|
static constexpr auto script = "inspector.endConsoleGroup();"sv;
|
||||||
|
m_inspector_web_view.run_javascript(script);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,11 +26,22 @@ public:
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void load_inspector();
|
void load_inspector();
|
||||||
|
|
||||||
String generate_dom_tree(JsonObject const&);
|
String generate_dom_tree(JsonObject const&);
|
||||||
String generate_accessibility_tree(JsonObject const&);
|
String generate_accessibility_tree(JsonObject const&);
|
||||||
|
|
||||||
void select_node(i32 node_id);
|
void select_node(i32 node_id);
|
||||||
|
|
||||||
|
void request_console_messages();
|
||||||
|
void handle_console_message(i32 message_index);
|
||||||
|
void handle_console_messages(i32 start_index, ReadonlySpan<DeprecatedString> message_types, ReadonlySpan<DeprecatedString> messages);
|
||||||
|
|
||||||
|
void append_console_source(StringView);
|
||||||
|
void append_console_output(StringView);
|
||||||
|
void clear_console_output();
|
||||||
|
|
||||||
|
void begin_console_group(StringView label, bool start_expanded);
|
||||||
|
void end_console_group();
|
||||||
|
|
||||||
ViewImplementation& m_content_web_view;
|
ViewImplementation& m_content_web_view;
|
||||||
ViewImplementation& m_inspector_web_view;
|
ViewImplementation& m_inspector_web_view;
|
||||||
|
|
||||||
|
@ -38,6 +49,10 @@ private:
|
||||||
Optional<i32> m_pending_selection;
|
Optional<i32> m_pending_selection;
|
||||||
|
|
||||||
bool m_dom_tree_loaded { false };
|
bool m_dom_tree_loaded { false };
|
||||||
|
|
||||||
|
i32 m_highest_notified_message_index { -1 };
|
||||||
|
i32 m_highest_received_message_index { -1 };
|
||||||
|
bool m_waiting_for_messages { false };
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue