2023-01-20 13:59:27 +01:00
|
|
|
const { reactive, useState, toRaw } = owl;
|
2023-10-18 13:31:57 +02:00
|
|
|
import { fuzzySearch, IS_FIREFOX, getActiveTabURL, browserInstance } from "../../utils";
|
2023-01-20 13:59:27 +01:00
|
|
|
import globalHook from "../../page_scripts/owl_devtools_global_hook";
|
|
|
|
|
|
|
|
|
|
// Main store which contains all states that needs to be maintained throughout all components in the devtools app
|
|
|
|
|
export const store = reactive({
|
|
|
|
|
devtoolsId: 0,
|
|
|
|
|
settings: {
|
|
|
|
|
expandByDefault: true,
|
|
|
|
|
toggleOnSelected: false,
|
|
|
|
|
darkmode: false,
|
2023-05-24 13:56:41 +02:00
|
|
|
componentsToggleBlacklist: new Set(),
|
2023-01-20 13:59:27 +01:00
|
|
|
},
|
2024-10-23 15:45:29 +02:00
|
|
|
contextMenu: null,
|
2023-01-20 13:59:27 +01:00
|
|
|
isFirefox: IS_FIREFOX,
|
|
|
|
|
frameUrls: ["top"],
|
|
|
|
|
activeFrame: "top",
|
|
|
|
|
page: "ComponentsTab",
|
|
|
|
|
events: [],
|
|
|
|
|
eventsTreeView: true,
|
|
|
|
|
eventsTree: [],
|
|
|
|
|
activeRecorder: false,
|
|
|
|
|
owlStatus: true,
|
|
|
|
|
extensionContextStatus: true,
|
|
|
|
|
splitPosition: window.innerWidth > window.innerHeight ? 45 : 60,
|
|
|
|
|
apps: [],
|
|
|
|
|
traceRenderings: false,
|
|
|
|
|
traceSubscriptions: false,
|
|
|
|
|
activeComponent: {
|
|
|
|
|
path: ["0"],
|
|
|
|
|
name: "App",
|
|
|
|
|
subscriptions: { toggled: true, children: [] },
|
|
|
|
|
props: { toggled: true, children: [] },
|
|
|
|
|
env: { toggled: false, children: [] },
|
|
|
|
|
instance: { toggled: true, children: [] },
|
2024-10-24 16:17:32 +02:00
|
|
|
hooks: { toggled: true, children: [] },
|
2023-01-20 13:59:27 +01:00
|
|
|
version: "1.0",
|
|
|
|
|
},
|
|
|
|
|
selectedElement: null,
|
2024-10-30 14:37:21 +01:00
|
|
|
observedVariables: [],
|
2023-01-20 13:59:27 +01:00
|
|
|
componentSearch: {
|
|
|
|
|
search: "",
|
|
|
|
|
searchResults: [],
|
|
|
|
|
searchIndex: 0,
|
|
|
|
|
activeSelector: false,
|
|
|
|
|
getNextSearch() {
|
|
|
|
|
if (this.searchIndex > -1 && this.searchIndex < this.searchResults.length - 1) {
|
|
|
|
|
store.setSearchIndex(this.searchIndex + 1);
|
|
|
|
|
} else if (this.searchIndex === this.searchResults.length - 1) {
|
|
|
|
|
store.setSearchIndex(0);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
getPrevSearch() {
|
|
|
|
|
if (this.searchIndex > 0) {
|
|
|
|
|
store.setSearchIndex(this.searchIndex - 1);
|
|
|
|
|
} else if (this.searchIndex === 0) {
|
|
|
|
|
store.setSearchIndex(this.searchResults.length - 1);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
// eventSearch: {
|
|
|
|
|
// search: "",
|
|
|
|
|
// searchResults: [],
|
|
|
|
|
// filters: [],
|
|
|
|
|
// },
|
|
|
|
|
renderPaths: new Set(),
|
|
|
|
|
|
|
|
|
|
// Used to navigate between the Components tab and the Events tab
|
|
|
|
|
switchTab(componentName) {
|
|
|
|
|
this.page = componentName;
|
|
|
|
|
this.componentSearch.activeSelector = false;
|
|
|
|
|
evalFunctionInWindow("disableHTMLSelector", [], this.activeFrame);
|
|
|
|
|
},
|
|
|
|
|
|
2024-10-23 15:45:29 +02:00
|
|
|
openContextMenu(event, items) {
|
|
|
|
|
this.contextMenu = {
|
|
|
|
|
position: {
|
|
|
|
|
x: event.clientX,
|
|
|
|
|
y: event.clientY,
|
|
|
|
|
},
|
|
|
|
|
items,
|
|
|
|
|
};
|
|
|
|
|
},
|
|
|
|
|
|
2023-01-20 13:59:27 +01:00
|
|
|
// Load all data related to the components tree using the global hook loaded on the page
|
|
|
|
|
// Use fromOld to specify if we want to keep most of the toggled/selected data of the old tree
|
|
|
|
|
// when generating the new one
|
|
|
|
|
async loadComponentsTree(fromOld) {
|
|
|
|
|
if (IS_FIREFOX) {
|
|
|
|
|
await evalInWindow("window.$0 = $0;", this.activeFrame);
|
|
|
|
|
}
|
2023-07-03 10:57:04 +02:00
|
|
|
const [apps, details] = await evalFunctionInWindow(
|
2023-01-20 13:59:27 +01:00
|
|
|
"getComponentsTree",
|
2023-06-14 15:23:31 +02:00
|
|
|
fromOld && this.activeComponent
|
|
|
|
|
? [this.activeComponent.path, this.apps, this.activeComponent]
|
|
|
|
|
: [],
|
2023-01-20 13:59:27 +01:00
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
this.apps = apps ? apps : [];
|
|
|
|
|
if (!fromOld && this.settings.expandByDefault) {
|
2023-05-24 13:56:41 +02:00
|
|
|
this.apps.forEach((tree) => expandNodes(tree, true));
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
2023-07-03 10:57:04 +02:00
|
|
|
keepEnvLit(details);
|
|
|
|
|
this.activeComponent = details;
|
2023-10-03 16:25:40 +02:00
|
|
|
if (this.componentSearch.search.length) {
|
|
|
|
|
this.updateSearch(this.componentSearch.search);
|
|
|
|
|
}
|
2023-01-20 13:59:27 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Select a component by retrieving its details from the page based on its path
|
|
|
|
|
async selectComponent(path) {
|
|
|
|
|
// Deselect all components
|
|
|
|
|
this.apps.forEach((app) => {
|
|
|
|
|
app.selected = false;
|
|
|
|
|
app.highlighted = false;
|
|
|
|
|
app.children.forEach((child) => {
|
|
|
|
|
deselectComponent(child);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
let component;
|
|
|
|
|
// element is the app here
|
|
|
|
|
if (path.length === 1) {
|
|
|
|
|
component = this.apps[path[0]];
|
|
|
|
|
// the second element in the path is always the root of the app so no need to check
|
|
|
|
|
} else {
|
|
|
|
|
component = this.apps[path[0]].children[0];
|
|
|
|
|
}
|
|
|
|
|
for (let i = 2; i < path.length; i++) {
|
|
|
|
|
component.toggled = true;
|
|
|
|
|
const result = component.children.find((child) => child.key === path[i]);
|
|
|
|
|
if (result) {
|
|
|
|
|
component = result;
|
|
|
|
|
} else {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
component.selected = true;
|
|
|
|
|
highlightChildren(component);
|
2023-10-24 16:10:43 +02:00
|
|
|
this.highlightComponent(path);
|
2023-01-20 13:59:27 +01:00
|
|
|
const details = await evalFunctionInWindow(
|
|
|
|
|
"getComponentDetails",
|
|
|
|
|
[component.path],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
2023-07-03 10:57:04 +02:00
|
|
|
if (!details) {
|
2023-01-20 13:59:27 +01:00
|
|
|
await this.loadComponentsTree(false);
|
2023-07-03 10:57:04 +02:00
|
|
|
} else {
|
|
|
|
|
keepEnvLit(details);
|
|
|
|
|
this.activeComponent = details;
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
|
|
|
|
if (this.page !== "ComponentsTab") {
|
|
|
|
|
this.switchTab("ComponentsTab");
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Update the search state value with the current search string and trigger the search
|
|
|
|
|
updateSearch(search) {
|
|
|
|
|
this.componentSearch.search = search;
|
|
|
|
|
this.componentSearch.searchResults = [];
|
|
|
|
|
this.apps.forEach((app) => this.getComponentSearchResults(search, app));
|
|
|
|
|
if (this.componentSearch.searchResults.length > 0) {
|
|
|
|
|
this.componentSearch.searchIndex = 0;
|
|
|
|
|
this.selectComponent(this.componentSearch.searchResults[0]);
|
|
|
|
|
this.apps.forEach((app) => foldNodes(app));
|
|
|
|
|
for (const result of this.componentSearch.searchResults) {
|
|
|
|
|
this.toggleComponentParents(result);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
this.componentSearch.searchIndex = -1;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Search for results in the components tree given the current search string (in a fuzzy way)
|
|
|
|
|
getComponentSearchResults(search, node) {
|
|
|
|
|
if (search.length < 1) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (fuzzySearch(node.name, search)) {
|
|
|
|
|
this.componentSearch.searchResults.push(node.path);
|
|
|
|
|
}
|
|
|
|
|
if (node.children) {
|
|
|
|
|
node.children.forEach((child) => this.getComponentSearchResults(search, child));
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Same but only record the component names for events
|
|
|
|
|
// getComponentNameSearchResults(search, node) {
|
|
|
|
|
// if (search.length < 1) return;
|
|
|
|
|
// if (fuzzySearch(node.name, search)) {
|
|
|
|
|
// if(!this.eventSearch.searchResults.includes(node.name))
|
|
|
|
|
// this.eventSearch.searchResults.push(node.name);
|
|
|
|
|
// }
|
|
|
|
|
// if (node.children) {
|
|
|
|
|
// node.children.forEach((child) => this.getComponentNameSearchResults(search, child));
|
|
|
|
|
// }
|
|
|
|
|
// },
|
|
|
|
|
|
|
|
|
|
// updateEventSearch(search){
|
|
|
|
|
// this.eventSearch.search = search;
|
|
|
|
|
// this.eventSearch.searchResults = [];
|
|
|
|
|
// this.apps.forEach((app) => this.getComponentNameSearchResults(search, app));
|
|
|
|
|
// [""]
|
|
|
|
|
// },
|
|
|
|
|
|
|
|
|
|
// Toggle all parent components of the specified one to make sure it is visible in the tree
|
|
|
|
|
toggleComponentParents(path) {
|
|
|
|
|
let cp = path.slice(2);
|
|
|
|
|
this.apps[path[0]].toggled = true;
|
|
|
|
|
let component = this.apps[path[0]].children[0];
|
|
|
|
|
for (const key of cp) {
|
|
|
|
|
component.toggled = true;
|
|
|
|
|
component = component.children.find((child) => child.key === key);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Returns access to the specified component in the tree
|
|
|
|
|
getComponentByPath(path) {
|
|
|
|
|
let component;
|
|
|
|
|
if (path.length < 2) {
|
|
|
|
|
component = this.apps[path[0]];
|
|
|
|
|
} else {
|
|
|
|
|
component = this.apps[path[0]].children[0];
|
|
|
|
|
}
|
|
|
|
|
let cp = path.slice(2);
|
|
|
|
|
for (const key of cp) {
|
|
|
|
|
component = component.children.find((child) => child.key === key);
|
|
|
|
|
}
|
|
|
|
|
return component;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// expand/fold the component and its children based on toggle
|
|
|
|
|
toggleComponentAndChildren(component, toggle) {
|
|
|
|
|
if (toggle) {
|
|
|
|
|
expandNodes(component);
|
|
|
|
|
} else {
|
|
|
|
|
foldNodes(component);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
foldDirectChildren(element) {
|
|
|
|
|
for (const child of element.children) {
|
|
|
|
|
child.toggled = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Action related to the left(toggle)/up(not toggle) arrow keys for navigation purpose
|
|
|
|
|
// The resulting behaviour is the same as in the Elements tab of the chrome devtools
|
|
|
|
|
toggleOrSelectPrevElement(toggle) {
|
|
|
|
|
if (toggle) {
|
|
|
|
|
const component = this.getComponentByPath(this.activeComponent.path);
|
|
|
|
|
if (component.children.length > 0 && component.toggled) {
|
|
|
|
|
component.toggled = false;
|
|
|
|
|
} else if (this.activeComponent.path.length > 1) {
|
|
|
|
|
this.selectComponent(this.activeComponent.path.slice(0, -1));
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const parentPath = [...this.activeComponent.path];
|
|
|
|
|
const key = parentPath.pop();
|
|
|
|
|
// If component is an app, find descendant with the highest successive children indexes of the app above
|
|
|
|
|
// or do nothing if there is no app above
|
|
|
|
|
if (parentPath.length === 0) {
|
|
|
|
|
const parent = this.apps;
|
|
|
|
|
const index = Number(key);
|
|
|
|
|
if (index > 0) {
|
|
|
|
|
let sibling = parent[index - 1];
|
|
|
|
|
while (sibling.toggled && sibling.children.length) {
|
|
|
|
|
sibling = sibling.children[sibling.children.length - 1];
|
|
|
|
|
}
|
|
|
|
|
this.selectComponent(sibling.path);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
const parent = this.getComponentByPath(parentPath);
|
|
|
|
|
const index = parent.children.findIndex((child) => child.key === key);
|
|
|
|
|
if (index > 0) {
|
|
|
|
|
let sibling = parent.children[index - 1];
|
|
|
|
|
while (sibling.toggled && sibling.children.length) {
|
|
|
|
|
sibling = sibling.children[sibling.children.length - 1];
|
|
|
|
|
}
|
|
|
|
|
this.selectComponent(sibling.path);
|
|
|
|
|
} else {
|
|
|
|
|
this.selectComponent(parent.path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Action related to the right(toggle)/down(not toggle) arrow keys for navigation purpose
|
|
|
|
|
// The resulting behaviour is the same as in the Elements tab of the chrome devtools
|
|
|
|
|
toggleOrSelectNextElement(toggle) {
|
|
|
|
|
let component = this.getComponentByPath(this.activeComponent.path);
|
|
|
|
|
if (toggle) {
|
|
|
|
|
if (!component.children.length) {
|
|
|
|
|
return;
|
|
|
|
|
} else if (!component.toggled) {
|
|
|
|
|
component.toggled = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// If component has children and is toggled, select its first child
|
|
|
|
|
if (component.toggled && component.children.length) {
|
|
|
|
|
this.selectComponent(component.children[0].path);
|
|
|
|
|
} else {
|
|
|
|
|
const parentPath = [...this.activeComponent.path];
|
|
|
|
|
while (true) {
|
|
|
|
|
const key = parentPath.pop();
|
|
|
|
|
if (parentPath.length === 0) {
|
|
|
|
|
const index = Number(key);
|
|
|
|
|
if (index < this.apps.length - 1) {
|
|
|
|
|
this.selectComponent(this.apps[index + 1].path);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const parent = this.getComponentByPath(parentPath);
|
|
|
|
|
const index = parent.children.findIndex((child) => child.key === key);
|
|
|
|
|
if (index < parent.children.length - 1 && index > -1) {
|
|
|
|
|
this.selectComponent(parent.children[index + 1].path);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Set the search index to the provided one in order to select the current searched component
|
|
|
|
|
setSearchIndex(index) {
|
|
|
|
|
this.componentSearch.searchIndex = index;
|
|
|
|
|
this.selectComponent(this.componentSearch.searchResults[index]);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Replace the (...) content of a getter with the value returned by the corresponding get method
|
|
|
|
|
async loadGetterContent(obj) {
|
|
|
|
|
const result = await evalFunctionInWindow("loadGetterContent", [obj], this.activeFrame);
|
|
|
|
|
Object.keys(obj).forEach((key) => {
|
|
|
|
|
obj[key] = result[key];
|
|
|
|
|
});
|
|
|
|
|
obj.children = [];
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Expand the children of the input object property and load it from page if necessary
|
|
|
|
|
async toggleObjectTreeElementsDisplay(obj) {
|
2023-10-04 14:44:19 +02:00
|
|
|
if (!obj.hasChildren || window.getSelection().toString().length) {
|
2023-01-20 13:59:27 +01:00
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// Since it is sometimes impossible (and always ineffective) to load all descendants of a property
|
|
|
|
|
// when the component details are loaded, we need to populate the children of a property only when
|
|
|
|
|
// it is first expanded. Do note that it has a limit to how deep we can expand (when reaching a circular dependancy)
|
|
|
|
|
if (obj.hasChildren && obj.children.length === 0) {
|
|
|
|
|
const children = await evalFunctionInWindow(
|
|
|
|
|
"loadObjectChildren",
|
|
|
|
|
[obj.path, obj.depth, obj.contentType, obj.objectType, this.activeComponent],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
obj.children = children;
|
|
|
|
|
}
|
|
|
|
|
obj.toggled = !obj.toggled;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Toggle the selector tool which is used to select a component based on the hovered Dom element
|
|
|
|
|
toggleSelector() {
|
|
|
|
|
this.componentSearch.activeSelector = !this.componentSearch.activeSelector;
|
|
|
|
|
evalFunctionInWindow(
|
|
|
|
|
this.componentSearch.activeSelector ? "enableHTMLSelector" : "disableHTMLSelector",
|
|
|
|
|
[],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Update the value of the given object with the new provided one
|
|
|
|
|
editObjectTreeElement(path, value, objectType) {
|
|
|
|
|
evalFunctionInWindow("editObject", [path, value, objectType], this.activeFrame);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// toggle the tracing mode which will record all root render events and send their trace in the console
|
|
|
|
|
async toggleTracing() {
|
|
|
|
|
this.traceRenderings = await evalFunctionInWindow(
|
|
|
|
|
"toggleTracing",
|
|
|
|
|
[!this.traceRenderings],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// toggle subscriptions tracing mode which will record all new subscriptions
|
|
|
|
|
async toggleSubscriptionTracing() {
|
|
|
|
|
this.traceSubscriptions = await evalFunctionInWindow(
|
|
|
|
|
"toggleSubscriptionTracing",
|
|
|
|
|
[!this.traceSubscriptions],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Checks for all iframes in the page, register it and load the scripts inside if not already done
|
|
|
|
|
async updateIFrameList() {
|
|
|
|
|
const frames = await evalFunctionInWindow("getIFrameUrls");
|
|
|
|
|
this.frameUrls = ["top"];
|
|
|
|
|
if (this.activeFrame !== "top") {
|
|
|
|
|
this.selectFrame("top");
|
|
|
|
|
}
|
|
|
|
|
for (const frame of frames) {
|
|
|
|
|
const hasOwl = await evalInWindow("window.__OWL_DEVTOOLS__?.Fiber !== undefined;", frame);
|
|
|
|
|
if (hasOwl) {
|
|
|
|
|
const scriptsLoaded = await evalInWindow(
|
|
|
|
|
"window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;",
|
|
|
|
|
frame
|
|
|
|
|
);
|
|
|
|
|
if (!scriptsLoaded) {
|
|
|
|
|
await loadScripts(frame);
|
|
|
|
|
}
|
2023-06-21 12:58:25 +02:00
|
|
|
evalFunctionInWindow("initDevtools", [frame], frame);
|
2023-01-20 13:59:27 +01:00
|
|
|
if (!this.frameUrls.includes(frame)) {
|
|
|
|
|
this.frameUrls = [...this.frameUrls, frame];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Remove all the highlight boxes created above the html elements in the page to show components
|
|
|
|
|
removeHighlights() {
|
|
|
|
|
if (this.owlStatus && !this.invalidContext) {
|
|
|
|
|
evalFunctionInWindow("removeHighlights", [], this.activeFrame);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Reset the context of all values in the devtools tab and load the one from the given frame
|
|
|
|
|
selectFrame(frame) {
|
|
|
|
|
this.removeHighlights();
|
|
|
|
|
evalFunctionInWindow("toggleEventsRecording", [false, 0], this.activeFrame);
|
|
|
|
|
evalFunctionInWindow("toggleTracing", [false], this.activeFrame);
|
|
|
|
|
evalFunctionInWindow("toggleSubscriptionTracing", [false], this.activeFrame);
|
|
|
|
|
this.events = [];
|
|
|
|
|
this.eventsTree = [];
|
|
|
|
|
this.activeFrame = frame;
|
|
|
|
|
store.loadComponentsTree(false);
|
|
|
|
|
evalFunctionInWindow(
|
|
|
|
|
"toggleEventsRecording",
|
|
|
|
|
[this.activeRecorder, this.events.length],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
evalFunctionInWindow("toggleTracing", [this.traceRenderings], this.activeFrame);
|
|
|
|
|
evalFunctionInWindow("toggleSubscriptionTracing", [this.traceSubscriptions], this.activeFrame);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Constructs the tree that represents the currently recorded events to see them as a tree instead of a temporally accurate list
|
|
|
|
|
buildEventsTree() {
|
|
|
|
|
let tree = [];
|
|
|
|
|
for (const event of this.events) {
|
|
|
|
|
let eventNode = Object.assign({}, event);
|
|
|
|
|
eventNode.children = [];
|
|
|
|
|
eventNode.toggled = true;
|
|
|
|
|
if (!eventNode.origin) {
|
|
|
|
|
eventNode.depth = 0;
|
|
|
|
|
tree.push(eventNode);
|
|
|
|
|
} else {
|
|
|
|
|
// This is litteraly an array.find but which starts searching from the end of the array
|
|
|
|
|
for (let i = tree.length - 1; i >= 0; i--) {
|
|
|
|
|
if (arraysEqual(eventNode.origin.path, tree[i].path)) {
|
|
|
|
|
// path from the origin event (root render) to the direct parent of the current event
|
|
|
|
|
const relativePath = eventNode.path.slice(
|
|
|
|
|
tree[i].path.length,
|
|
|
|
|
eventNode.path.length - 1
|
|
|
|
|
);
|
|
|
|
|
let parent = tree[i];
|
|
|
|
|
// find the direct parent in the tree
|
|
|
|
|
for (const key of relativePath) {
|
|
|
|
|
parent = parent.children.find((child) => child.key === key);
|
|
|
|
|
}
|
|
|
|
|
eventNode.depth = parent.depth + 1;
|
|
|
|
|
parent.children.push(eventNode);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
this.eventsTree = tree;
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// expand/fold the event tree node based on toggle
|
|
|
|
|
toggleEventAndChildren(event, toggle) {
|
|
|
|
|
if (toggle) {
|
|
|
|
|
expandNodes(event);
|
|
|
|
|
} else {
|
|
|
|
|
foldNodes(event);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
collapseAll() {
|
|
|
|
|
for (let event of this.eventsTree) {
|
|
|
|
|
event.toggled = false;
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Reset all the relevant data about the page currently stored
|
2023-05-24 13:56:41 +02:00
|
|
|
async resetData() {
|
|
|
|
|
await loadSettings();
|
2023-01-20 13:59:27 +01:00
|
|
|
this.loadComponentsTree(false);
|
|
|
|
|
this.events = [];
|
|
|
|
|
this.eventsTree = [];
|
|
|
|
|
this.activeRecorder = false;
|
|
|
|
|
evalFunctionInWindow("toggleEventsRecording", [false, 0]);
|
2023-04-27 12:57:57 +02:00
|
|
|
this.traceRenderings = false;
|
2023-01-20 13:59:27 +01:00
|
|
|
evalFunctionInWindow("toggleTracing", [false]);
|
2023-04-27 12:57:57 +02:00
|
|
|
this.traceSubscriptions = false;
|
|
|
|
|
evalFunctionInWindow("toggleSubscriptionTracing", [false]);
|
2023-04-25 09:49:05 +02:00
|
|
|
this.updateIFrameList();
|
2023-01-20 13:59:27 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Triggers manually the rendering of the selected component
|
|
|
|
|
refreshComponent(path = this.activeComponent.path) {
|
|
|
|
|
evalFunctionInWindow("refreshComponent", [path], this.activeFrame);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Allows to log any object in the console, defaults to the active component (or app)
|
|
|
|
|
logObjectInConsole(path) {
|
|
|
|
|
if (!path) {
|
|
|
|
|
if (this.activeComponent.path.length > 1) {
|
|
|
|
|
path = [...this.activeComponent.path, { type: "item", value: "component" }];
|
|
|
|
|
} else {
|
|
|
|
|
path = this.activeComponent.path;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
evalFunctionInWindow("sendObjectToConsole", [path], this.activeFrame);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// inspect the source code of the object given by its path
|
|
|
|
|
async inspectFunctionSource(path) {
|
|
|
|
|
await evalFunctionInWindow("inspectFunctionSource", [path], this.activeFrame);
|
|
|
|
|
if (IS_FIREFOX) {
|
|
|
|
|
await evalInWindow("inspect(window.$temp);", this.activeFrame);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Inspect the given component's data based on the given type
|
|
|
|
|
async inspectComponent(type, path = this.activeComponent.path) {
|
|
|
|
|
switch (type) {
|
|
|
|
|
case "DOM":
|
|
|
|
|
await evalFunctionInWindow("inspectComponentDOM", [path], this.activeFrame);
|
|
|
|
|
break;
|
|
|
|
|
case "source":
|
|
|
|
|
if (path.length > 1) {
|
|
|
|
|
await evalFunctionInWindow(
|
|
|
|
|
"inspectFunctionSource",
|
|
|
|
|
[
|
|
|
|
|
[
|
|
|
|
|
...path,
|
|
|
|
|
{ type: "item", value: "component" },
|
|
|
|
|
{ type: "item", value: "constructor" },
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
await evalFunctionInWindow(
|
|
|
|
|
"inspectFunctionSource",
|
|
|
|
|
[[...path, { type: "item", value: "constructor" }]],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
break;
|
|
|
|
|
case "compiled template":
|
|
|
|
|
await evalFunctionInWindow("inspectComponentCompiledTemplate", [path], this.activeFrame);
|
|
|
|
|
break;
|
|
|
|
|
case "raw template":
|
|
|
|
|
await evalFunctionInWindow("inspectComponentRawTemplate", [path], this.activeFrame);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (IS_FIREFOX && type !== "raw template") {
|
|
|
|
|
await evalInWindow("inspect(window.$temp);", this.activeFrame);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
|
2024-10-24 16:17:32 +02:00
|
|
|
async injectBreakpoint(hook, path, instanceOnly = false, condition = "1") {
|
|
|
|
|
path = [...path];
|
|
|
|
|
await evalFunctionInWindow(
|
|
|
|
|
"injectBreakpoint",
|
|
|
|
|
[hook, path, instanceOnly, condition],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
await this.loadComponentsTree(true);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
async removeBreakpoints() {
|
|
|
|
|
await evalFunctionInWindow("removeBreakpoints", [], this.activeFrame);
|
|
|
|
|
await this.loadComponentsTree(true);
|
|
|
|
|
},
|
|
|
|
|
|
2024-10-30 14:37:21 +01:00
|
|
|
async observeVariable(path) {
|
|
|
|
|
this.observedVariables.push({ path: [...path], visible: false });
|
|
|
|
|
this.observedVariables = await evalFunctionInWindow(
|
|
|
|
|
"getObservedVariables",
|
|
|
|
|
[store.observedVariables],
|
|
|
|
|
store.activeFrame
|
|
|
|
|
);
|
|
|
|
|
await browserInstance.storage.local.set({
|
|
|
|
|
observedVariables: toRaw(this.observedVariables).map((o) => o.path),
|
|
|
|
|
});
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
clearObservedVariable(index) {
|
|
|
|
|
if (index !== undefined) {
|
|
|
|
|
this.observedVariables.splice(index, 1);
|
|
|
|
|
} else {
|
|
|
|
|
this.observedVariables = [];
|
|
|
|
|
}
|
|
|
|
|
browserInstance.storage.local.set({ observedVariables: toRaw(this.observedVariables) });
|
|
|
|
|
},
|
|
|
|
|
|
2023-01-20 13:59:27 +01:00
|
|
|
// Trigger the highlight on the component in the page
|
|
|
|
|
highlightComponent(path) {
|
|
|
|
|
evalFunctionInWindow("highlightComponent", [path], this.activeFrame);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Center the view around the currently selected component
|
|
|
|
|
focusSelectedComponent() {
|
|
|
|
|
this.selectedElement.scrollIntoView({ block: "center", behavior: "smooth" });
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Toggle the recording of events in the page
|
|
|
|
|
async toggleRecording() {
|
|
|
|
|
this.activeRecorder = await evalFunctionInWindow(
|
|
|
|
|
"toggleEventsRecording",
|
|
|
|
|
[!this.activeRecorder, this.events.length],
|
|
|
|
|
this.activeFrame
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Reset all events data
|
|
|
|
|
clearEventsConsole() {
|
|
|
|
|
this.events = [];
|
|
|
|
|
this.eventsTree = [];
|
|
|
|
|
evalFunctionInWindow("resetEvents", [], this.activeFrame);
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Refresh the whole extension
|
|
|
|
|
async refreshExtension() {
|
|
|
|
|
await loadScripts();
|
2023-05-24 13:56:41 +02:00
|
|
|
await this.resetData();
|
2023-01-20 13:59:27 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Toggle dark mode in the extension and store result in the storage
|
|
|
|
|
toggleDarkMode() {
|
|
|
|
|
this.settings.darkMode = !this.settings.darkMode;
|
|
|
|
|
if (this.settings.darkMode) {
|
|
|
|
|
document.querySelector("html").classList.add("dark-mode");
|
|
|
|
|
} else {
|
|
|
|
|
document.querySelector("html").classList.remove("dark-mode");
|
|
|
|
|
}
|
2023-05-24 13:56:41 +02:00
|
|
|
browserInstance.storage.local.set({ owlDevtoolsDarkMode: this.settings.darkMode });
|
2023-01-20 13:59:27 +01:00
|
|
|
},
|
2023-04-12 13:14:56 +02:00
|
|
|
|
|
|
|
|
openDocumentation() {
|
|
|
|
|
browserInstance.runtime.sendMessage({ type: "openDoc" });
|
|
|
|
|
},
|
2023-01-20 13:59:27 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Instantiate the store
|
|
|
|
|
export function useStore() {
|
|
|
|
|
return useState(store);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
init();
|
|
|
|
|
|
|
|
|
|
async function init() {
|
|
|
|
|
store.devtoolsId = await getTabURL();
|
|
|
|
|
|
2023-06-21 12:58:25 +02:00
|
|
|
evalFunctionInWindow("initDevtools", []);
|
2023-01-20 13:59:27 +01:00
|
|
|
|
2023-05-24 13:56:41 +02:00
|
|
|
await loadSettings();
|
|
|
|
|
|
2023-01-20 13:59:27 +01:00
|
|
|
// We want to load the base components tree when the devtools tab is first opened
|
|
|
|
|
store.loadComponentsTree(false);
|
|
|
|
|
|
|
|
|
|
// We also want to detect the different iframes at first loading of the devtools tab
|
|
|
|
|
store.updateIFrameList();
|
|
|
|
|
|
|
|
|
|
// Global listeners to close the currently shown context menu when the user clicks or opens another
|
2024-10-23 15:45:29 +02:00
|
|
|
document.addEventListener("click", () => (store.contextMenu = null), { capture: true });
|
|
|
|
|
document.addEventListener("contextmenu", () => (store.contextMenu = null), { capture: true });
|
|
|
|
|
window.addEventListener("blur", () => (store.contextMenu = null), { capture: true });
|
2023-01-20 13:59:27 +01:00
|
|
|
|
|
|
|
|
// Make sure the events recorder is at its initial state in every frame
|
|
|
|
|
for (const frame of store.frameUrls) {
|
|
|
|
|
evalFunctionInWindow("toggleEventsRecording", [false, 0], frame);
|
|
|
|
|
}
|
|
|
|
|
|
2023-04-14 16:19:24 +02:00
|
|
|
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel", id: store.devtoolsId });
|
|
|
|
|
|
|
|
|
|
// Heartbeat message to test whether the extension context is still valid or not
|
|
|
|
|
setInterval(() => {
|
|
|
|
|
if (store.extensionContextStatus) {
|
|
|
|
|
try {
|
|
|
|
|
browserInstance.runtime.sendMessage({ type: "keepAlive", id: store.devtoolsId });
|
|
|
|
|
} catch (e) {
|
|
|
|
|
store.extensionContextStatus = false;
|
|
|
|
|
}
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
2023-04-14 16:19:24 +02:00
|
|
|
}, 500);
|
2024-10-30 14:37:21 +01:00
|
|
|
// Refresh observed variables values every 200 ms
|
|
|
|
|
setInterval(async () => {
|
2024-12-06 12:40:51 +01:00
|
|
|
if (store.owlStatus) {
|
|
|
|
|
store.observedVariables = await evalFunctionInWindow(
|
|
|
|
|
"getObservedVariables",
|
|
|
|
|
[[...store.observedVariables]],
|
|
|
|
|
store.activeFrame
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-10-30 14:37:21 +01:00
|
|
|
}, 200);
|
2023-04-14 16:19:24 +02:00
|
|
|
}
|
2023-01-20 13:59:27 +01:00
|
|
|
|
2023-06-14 15:23:31 +02:00
|
|
|
let rootRendersTimeout = false;
|
2023-01-20 13:59:27 +01:00
|
|
|
// Connect to the port to communicate to the background script
|
|
|
|
|
browserInstance.runtime.onConnect.addListener((port) => {
|
2023-04-14 16:19:24 +02:00
|
|
|
if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) {
|
|
|
|
|
port.onMessage.addListener(async (msg) => {
|
|
|
|
|
// Reload the tree after checking if the scripts are loaded when this message is received
|
|
|
|
|
if (msg.type === "Reload") {
|
|
|
|
|
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
|
|
|
|
|
if (store.owlStatus) {
|
2023-06-21 12:58:25 +02:00
|
|
|
evalFunctionInWindow("initDevtools", []);
|
2023-05-24 13:56:41 +02:00
|
|
|
await store.resetData();
|
2023-04-14 16:19:24 +02:00
|
|
|
}
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
2023-04-14 16:19:24 +02:00
|
|
|
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
|
|
|
|
|
if (msg.type === "FrameReady") {
|
|
|
|
|
store.updateIFrameList();
|
|
|
|
|
store.owlStatus = true;
|
2023-05-24 13:56:41 +02:00
|
|
|
await store.resetData();
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
2023-04-14 16:19:24 +02:00
|
|
|
// We need to reload the components tree when the set of apps in the page is modified
|
|
|
|
|
if (msg.type === "RefreshApps") {
|
|
|
|
|
store.loadComponentsTree(true);
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
2023-06-14 15:23:31 +02:00
|
|
|
// When message of type Complete is received, overwrite the component tree with the new one from page
|
|
|
|
|
// A Complete message is sent everytime a root render is triggered on the page
|
|
|
|
|
if (msg.type === "Complete") {
|
2023-04-14 16:19:24 +02:00
|
|
|
if (msg.origin.frame !== store.activeFrame) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered
|
|
|
|
|
store.renderPaths.add(JSON.stringify(msg.data));
|
2023-06-14 15:23:31 +02:00
|
|
|
clearTimeout(rootRendersTimeout);
|
|
|
|
|
rootRendersTimeout = setTimeout(() => {
|
2023-04-14 16:19:24 +02:00
|
|
|
store.renderPaths.clear();
|
|
|
|
|
}, 100);
|
|
|
|
|
store.loadComponentsTree(true);
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
2023-04-14 16:19:24 +02:00
|
|
|
// Select the component based on the path received with the SelectElement message
|
|
|
|
|
if (msg.type === "SelectElement") {
|
|
|
|
|
if (!(Array.isArray(msg.data) && msg.data.every((val) => typeof val === "string"))) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
store.selectComponent(msg.data);
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
2023-04-14 16:19:24 +02:00
|
|
|
// Stop the DOM element selector tool upon receiving the StopSelector message
|
|
|
|
|
if (msg.type === "StopSelector") {
|
|
|
|
|
store.componentSearch.activeSelector = false;
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
|
|
|
|
|
2023-04-14 16:19:24 +02:00
|
|
|
// Logic for recording an event when the event message is received
|
|
|
|
|
if (msg.type === "Event") {
|
|
|
|
|
let events = msg.data;
|
|
|
|
|
loadEvents(events);
|
|
|
|
|
}
|
2023-01-20 13:59:27 +01:00
|
|
|
|
2023-04-14 16:19:24 +02:00
|
|
|
// If we know a new iframe has been added to the page, load scripts into it and update the
|
|
|
|
|
// frames list if it has been directly loaded.
|
|
|
|
|
if (msg.type === "NewIFrame") {
|
|
|
|
|
const isLoaded = await loadScripts(msg.data);
|
|
|
|
|
if (isLoaded) {
|
|
|
|
|
store.updateIFrameList();
|
|
|
|
|
}
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
2023-04-14 16:19:24 +02:00
|
|
|
});
|
|
|
|
|
}
|
2023-01-20 13:59:27 +01:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Load all settings from the chrome sync storage
|
|
|
|
|
async function loadSettings() {
|
|
|
|
|
let storage = await browserInstance.storage.local.get();
|
2023-05-24 13:56:41 +02:00
|
|
|
// Darkmode
|
|
|
|
|
if (storage.owlDevtoolsDarkMode === undefined) {
|
2023-01-20 13:59:27 +01:00
|
|
|
// Load dark mode based on the global settings of the chrome devtools
|
|
|
|
|
darkMode = browserInstance.devtools.panels.themeName === "dark";
|
|
|
|
|
} else {
|
2023-05-24 13:56:41 +02:00
|
|
|
darkMode = storage.owlDevtoolsDarkMode;
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
|
|
|
|
store.settings.darkMode = darkMode;
|
|
|
|
|
if (darkMode) {
|
|
|
|
|
document.querySelector("html").classList.add("dark-mode");
|
|
|
|
|
} else {
|
|
|
|
|
document.querySelector("html").classList.remove("dark-mode");
|
|
|
|
|
}
|
2023-05-24 13:56:41 +02:00
|
|
|
// Components toggle blacklist
|
|
|
|
|
if (storage.owlDevtoolsComponentsToggleBlacklist !== undefined) {
|
|
|
|
|
store.settings.componentsToggleBlacklist = new Set(
|
|
|
|
|
storage.owlDevtoolsComponentsToggleBlacklist
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-10-30 14:37:21 +01:00
|
|
|
// Observed variables
|
|
|
|
|
if (storage.observedVariables) {
|
|
|
|
|
store.observedVariables = [];
|
|
|
|
|
for (const path of storage.observedVariables) {
|
|
|
|
|
store.observedVariables.push({
|
|
|
|
|
path: [...path],
|
|
|
|
|
visible: false,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Function to handle and store a batch of events coming from the page
|
|
|
|
|
function loadEvents(events) {
|
|
|
|
|
if (!Array.isArray(events)) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
for (const event of events) {
|
|
|
|
|
// Check if the event data has the right shape for security purpose
|
|
|
|
|
if (
|
|
|
|
|
!isObjectWithShape(event, {
|
|
|
|
|
type: "string",
|
|
|
|
|
component: "string",
|
|
|
|
|
key: "string",
|
|
|
|
|
path: "object",
|
|
|
|
|
time: "number",
|
|
|
|
|
id: "number",
|
|
|
|
|
}) ||
|
|
|
|
|
!(Array.isArray(event.path) && event.path.every((val) => typeof val === "string"))
|
|
|
|
|
) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
event.origin = null;
|
|
|
|
|
event.toggled = false;
|
2023-06-14 16:10:57 +02:00
|
|
|
event.isLast = false;
|
2023-01-20 13:59:27 +01:00
|
|
|
// Logic to retrace the origin of the event if it is not a root render event
|
|
|
|
|
if (!event.type.includes("render")) {
|
|
|
|
|
for (let i = store.events.length - 1; i >= 0; i--) {
|
|
|
|
|
if (
|
|
|
|
|
!store.events[i].origin &&
|
|
|
|
|
event.path.join("/").includes(store.events[i].path.join("/"))
|
|
|
|
|
) {
|
|
|
|
|
event.origin = toRaw(store.events[i]);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
if (
|
|
|
|
|
store.events[i].origin &&
|
|
|
|
|
event.path.join("/").includes(store.events[i].origin.path.join("/"))
|
|
|
|
|
) {
|
|
|
|
|
event.origin = toRaw(store.events[i].origin);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Add the event to the events tree immediatly when the events view is in tree mode
|
|
|
|
|
if (store.eventsTreeView) {
|
|
|
|
|
let eventNode = Object.assign({}, event);
|
|
|
|
|
eventNode.children = [];
|
|
|
|
|
eventNode.toggled = true;
|
|
|
|
|
if (!eventNode.origin) {
|
|
|
|
|
eventNode.depth = 0;
|
|
|
|
|
store.eventsTree.push(eventNode);
|
|
|
|
|
} else {
|
|
|
|
|
// Similar to when we're constructing the whole tree
|
|
|
|
|
for (let i = store.eventsTree.length - 1; i >= 0; i--) {
|
|
|
|
|
if (eventNode.origin.path.join("/") === store.eventsTree[i].path.join("/")) {
|
|
|
|
|
const relativePath = eventNode.path.slice(
|
|
|
|
|
store.eventsTree[i].path.length,
|
|
|
|
|
eventNode.path.length - 1
|
|
|
|
|
);
|
|
|
|
|
let parent = store.eventsTree[i];
|
|
|
|
|
for (const key of relativePath) {
|
|
|
|
|
parent = parent.children.find((child) => child.key === key);
|
|
|
|
|
}
|
|
|
|
|
eventNode.depth = parent.depth + 1;
|
|
|
|
|
parent.children.push(eventNode);
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Make sure we add the event while keeping the whole list ordered by id
|
|
|
|
|
addEventSorted(event);
|
|
|
|
|
}
|
2023-06-14 16:10:57 +02:00
|
|
|
store.events[store.events.length - 1].isLast = true;
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Deselect component and remove highlight on all children
|
|
|
|
|
function deselectComponent(component) {
|
|
|
|
|
component.selected = false;
|
|
|
|
|
component.highlighted = false;
|
|
|
|
|
for (const child of component.children) {
|
|
|
|
|
deselectComponent(child);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Used to check if the given object has the right shape
|
|
|
|
|
function isObjectWithShape(obj, shape) {
|
|
|
|
|
if (typeof obj !== "object" || Array.isArray(obj)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Object.keys(shape).every(
|
|
|
|
|
(key) => obj.hasOwnProperty(key) && typeof obj[key] === shape[key]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// A binary search algorith to efficiently add an event in the events array while keeping it sorted
|
|
|
|
|
function addEventSorted(item) {
|
|
|
|
|
let low = 0;
|
|
|
|
|
let high = store.events.length;
|
|
|
|
|
while (low < high) {
|
|
|
|
|
let mid = Math.floor((low + high) / 2);
|
|
|
|
|
if (store.events[mid].id < item.id) {
|
|
|
|
|
low = mid + 1;
|
|
|
|
|
} else {
|
|
|
|
|
high = mid;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
store.events.splice(low, 0, item);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Apply highlight recursively to all children of a selected component
|
|
|
|
|
function highlightChildren(component) {
|
|
|
|
|
component.children.forEach((child) => {
|
|
|
|
|
child.highlighted = true;
|
|
|
|
|
highlightChildren(child);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Expand the node given in entry and all of its children
|
2023-05-24 13:56:41 +02:00
|
|
|
function expandNodes(node, blacklist = false) {
|
|
|
|
|
if (blacklist && store.settings.componentsToggleBlacklist.has(node.name)) {
|
|
|
|
|
node.toggled = false;
|
|
|
|
|
} else {
|
|
|
|
|
node.toggled = true;
|
|
|
|
|
}
|
2023-01-20 13:59:27 +01:00
|
|
|
for (const child of node.children) {
|
2023-05-24 13:56:41 +02:00
|
|
|
expandNodes(child, blacklist);
|
2023-01-20 13:59:27 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-03 10:57:04 +02:00
|
|
|
// This function transforms the env part of the details such that all env keys are not
|
|
|
|
|
// greyed out in the UI at their first occurence
|
|
|
|
|
function keepEnvLit(details) {
|
|
|
|
|
let alreadyMet = new Set();
|
|
|
|
|
for (let i = 0; i < details.env.children.length; i++) {
|
|
|
|
|
if (i < details.env.children.length - 1) {
|
|
|
|
|
alreadyMet.add(details.env.children[i].name);
|
|
|
|
|
} else {
|
|
|
|
|
let lastElement = details.env.children[i];
|
2023-07-20 11:18:28 +02:00
|
|
|
while (lastElement.children.at(-1)?.name === "[[Prototype]]") {
|
2023-07-03 10:57:04 +02:00
|
|
|
for (const [index, child] of lastElement.children.entries()) {
|
|
|
|
|
if (index < lastElement.children.length - 1) {
|
|
|
|
|
if (!alreadyMet.has(child.name)) {
|
|
|
|
|
child.keepLit = true;
|
|
|
|
|
alreadyMet.add(child.name);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
lastElement = child;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-01-20 13:59:27 +01:00
|
|
|
// Fold the node given in entry and all of its children
|
|
|
|
|
function foldNodes(node) {
|
|
|
|
|
node.toggled = false;
|
|
|
|
|
for (const child of node.children) {
|
|
|
|
|
foldNodes(child);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Load the scripts in the specified frame
|
|
|
|
|
async function loadScripts(frameUrl) {
|
|
|
|
|
return await evalInWindow(globalHook, frameUrl);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Shallow array equality
|
|
|
|
|
function arraysEqual(arr1, arr2) {
|
|
|
|
|
if (arr1.length !== arr2.length) {
|
|
|
|
|
// Check if the arrays are of the same length
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
return arr1.every((val, i) => val === arr2[i]); // Compare each element of the arrays
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function getTabURL() {
|
|
|
|
|
if (IS_FIREFOX) {
|
2023-10-18 13:31:57 +02:00
|
|
|
// It is not possible to run getActiveTabURL inside the devtools when using firefox so we ask the background to execute it instead
|
|
|
|
|
const response = await browserInstance.runtime.sendMessage({ type: "getActiveTabURL" });
|
|
|
|
|
return response.result;
|
2023-01-20 13:59:27 +01:00
|
|
|
} else {
|
|
|
|
|
return await getActiveTabURL();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// General method for executing functions from the loaded scripts in the right frame of the page
|
|
|
|
|
// using the __OWL__DEVTOOLS_GLOBAL_HOOK__. Take the function's args as an array.
|
|
|
|
|
async function evalFunctionInWindow(fn, args = [], frameUrl = "top") {
|
|
|
|
|
const stringifiedArgs = [...args].map((arg) => {
|
|
|
|
|
arg = JSON.stringify(arg);
|
|
|
|
|
return arg;
|
|
|
|
|
});
|
|
|
|
|
const argsString = "(" + stringifiedArgs.join(", ") + ");";
|
|
|
|
|
let script = `__OWL__DEVTOOLS_GLOBAL_HOOK__.${fn}${argsString}`;
|
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
|
|
|
if (frameUrl !== "top") {
|
|
|
|
|
browserInstance.devtools.inspectedWindow.eval(
|
|
|
|
|
script,
|
|
|
|
|
{ frameURL: frameUrl },
|
|
|
|
|
(result, isException) => {
|
|
|
|
|
if (!isException) {
|
|
|
|
|
resolve(result);
|
|
|
|
|
} else {
|
|
|
|
|
reject(script);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
browserInstance.devtools.inspectedWindow.eval(script, (result, isException) => {
|
|
|
|
|
if (!isException) {
|
|
|
|
|
resolve(result);
|
|
|
|
|
} else {
|
|
|
|
|
reject(script);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// General method for executing code in the window using chrome.devtools.inspectedWindow.eval.
|
|
|
|
|
async function evalInWindow(code, frameUrl = "top") {
|
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
|
|
|
if (frameUrl !== "top") {
|
|
|
|
|
browserInstance.devtools.inspectedWindow.eval(
|
|
|
|
|
code,
|
|
|
|
|
{ frameURL: frameUrl },
|
|
|
|
|
(result, isException) => {
|
|
|
|
|
if (!isException) {
|
|
|
|
|
resolve(result);
|
|
|
|
|
} else {
|
|
|
|
|
reject(code);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
);
|
|
|
|
|
} else {
|
|
|
|
|
browserInstance.devtools.inspectedWindow.eval(code, (result, isException) => {
|
|
|
|
|
if (!isException) {
|
|
|
|
|
resolve(result);
|
|
|
|
|
} else {
|
|
|
|
|
reject(code);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|