mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
[ADD] tools: create owl-devtools extension
This commit adds the code of the owl devtools extension. This extension can be added on chrome or on firefox for developpers to be able to inspect the contents of all owl applications that are present on any web page, see all the components, interract with them and their content to some extend and perform a bit of profiling with their renders.
This commit is contained in:
committed by
Géry Debongnie
parent
a9323c3fcd
commit
ef5e4a0637
+1
-1
@@ -28,4 +28,4 @@ release-notes.md
|
|||||||
.rpt2_cache
|
.rpt2_cache
|
||||||
|
|
||||||
# useful in some cases
|
# useful in some cases
|
||||||
/temp
|
/temp
|
||||||
|
|||||||
@@ -113,6 +113,7 @@ Are you new to Owl? This is the place to start!
|
|||||||
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
- [Why did Odoo build Owl?](doc/miscellaneous/why_owl.md)
|
||||||
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
- [Changelog (from owl 1.x to 2.x)](CHANGELOG.md)
|
||||||
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
|
- [Notes on compiled templates](doc/miscellaneous/compiled_template.md)
|
||||||
|
- [Owl devtools extension](doc/tools/devtools.md)
|
||||||
|
|
||||||
## Installing Owl
|
## Installing Owl
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
# Owl Devtools Browser extension
|
||||||
|
|
||||||
|
The owl devtools browser extension is an extension available on chrome or firefox which adds an owl tab
|
||||||
|
to the browser devtools in order to inspect all owl apps that are present on any web page, their components
|
||||||
|
and allows to interract with their data to a certain extend. There is also a profiler available to visualize
|
||||||
|
the components' lifecycle and be able to trace their origin.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
In the owl root folder:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
For chrome:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:devtools-chrome
|
||||||
|
```
|
||||||
|
|
||||||
|
For firefox:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build:devtools-firefox
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also run:
|
||||||
|
```bash
|
||||||
|
npm run dev:devtools-chrome
|
||||||
|
```
|
||||||
|
or
|
||||||
|
```bash
|
||||||
|
npm run dev:devtools-firefox
|
||||||
|
```
|
||||||
|
to avoid recompiling owl and gain time if it has already been done.
|
||||||
|
|
||||||
|
To run the extension:
|
||||||
|
|
||||||
|
In google chrome: go to your chrome extensions admin panel, activate developer mode and click on `Load unpacked`.
|
||||||
|
Select the output folder (dist/devtools) and that's it, your extension is active!
|
||||||
|
There is a convenient refresh button on the extension card (still on the same admin page) to update your code.
|
||||||
|
Do note that if you got some problems, you may need to completly remove and reload the extension to completly refresh the extension.
|
||||||
|
|
||||||
|
In firefox: go to the address about:debugging#/runtime/this-firefox and click on `Load temporary Add-on...`.
|
||||||
|
Select any file of the output folder (dist/devtools) and that's it, your extension is active!
|
||||||
|
Here, you can use the reload button to refresh the extension.
|
||||||
|
|
||||||
|
Note you may have to open another window or reload your tab to see the extension working.
|
||||||
|
Also note that the extension will run properly only on pages that have this branch's version of owl.
|
||||||
|
|
||||||
Generated
+4201
-2082
File diff suppressed because it is too large
Load Diff
+11
-2
@@ -16,6 +16,11 @@
|
|||||||
"build:runtime": "rollup -c --failAfterWarnings runtime",
|
"build:runtime": "rollup -c --failAfterWarnings runtime",
|
||||||
"build:compiler": "rollup -c --failAfterWarnings compiler",
|
"build:compiler": "rollup -c --failAfterWarnings compiler",
|
||||||
"build": "npm run build:bundle",
|
"build": "npm run build:bundle",
|
||||||
|
"build:devtools": "rollup -c ./tools/devtools/rollup.config.js",
|
||||||
|
"dev:devtools-chrome": "npm run build:devtools -- --config-browser=chrome",
|
||||||
|
"dev:devtools-firefox": "npm run build:devtools -- --config-browser=firefox",
|
||||||
|
"build:devtools-chrome": "NODE_ENV=production npm run dev:devtools-chrome",
|
||||||
|
"build:devtools-firefox": "NODE_ENV=production npm run dev:devtools-firefox",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
@@ -23,8 +28,8 @@
|
|||||||
"playground": "npm run build && npm run playground:serve",
|
"playground": "npm run build && npm run playground:serve",
|
||||||
"preplayground:watch": "npm run build",
|
"preplayground:watch": "npm run build",
|
||||||
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
|
"playground:watch": "npm-run-all --parallel playground:serve \"build:* -- --watch\"",
|
||||||
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --write",
|
"prettier": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --write",
|
||||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md} --check",
|
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
||||||
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
||||||
"publish": "npm run build && npm publish",
|
"publish": "npm run build && npm publish",
|
||||||
"release": "node tools/release.js",
|
"release": "node tools/release.js",
|
||||||
@@ -56,7 +61,11 @@
|
|||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "2.4.1",
|
"prettier": "2.4.1",
|
||||||
"rollup": "^2.56.3",
|
"rollup": "^2.56.3",
|
||||||
|
"rollup-plugin-copy": "^3.3.0",
|
||||||
|
"rollup-plugin-delete": "^2.0.0",
|
||||||
"rollup-plugin-dts": "^4.2.2",
|
"rollup-plugin-dts": "^4.2.2",
|
||||||
|
"rollup-plugin-execute": "^1.1.1",
|
||||||
|
"rollup-plugin-string": "^3.0.0",
|
||||||
"rollup-plugin-terser": "^7.0.2",
|
"rollup-plugin-terser": "^7.0.2",
|
||||||
"rollup-plugin-typescript2": "^0.31.1",
|
"rollup-plugin-typescript2": "^0.31.1",
|
||||||
"source-map-support": "^0.5.10",
|
"source-map-support": "^0.5.10",
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ async function compileTemplates(files) {
|
|||||||
const fnName = slugify(name);
|
const fnName = slugify(name);
|
||||||
try {
|
try {
|
||||||
const fn = compile(template).toString().replace('anonymous', fnName);
|
const fn = compile(template).toString().replace('anonymous', fnName);
|
||||||
templates.push(`owl.App.registerTemplate("${name}", ${fn});\n`);
|
templates.push(`"${name}": ${fn},\n`);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errors.push({ name, fileName, e });
|
errors.push({ name, fileName, e });
|
||||||
}
|
}
|
||||||
@@ -99,7 +99,7 @@ async function compileTemplates(files) {
|
|||||||
}
|
}
|
||||||
console.log(`${templates.length} templates compiled`);
|
console.log(`${templates.length} templates compiled`);
|
||||||
|
|
||||||
return templates.join("\n");
|
return `export const templates = {\n ${templates.join("\n")} \n}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const templatesPath = process.argv[2];
|
const templatesPath = process.argv[2];
|
||||||
|
|||||||
Binary file not shown.
+7
File diff suppressed because one or more lines are too long
+4
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 21 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 5.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "Owl devtools",
|
||||||
|
"version": "1.0",
|
||||||
|
"manifest_version": 3,
|
||||||
|
"description": "Chrome devtools extension for Odoo Owl framework",
|
||||||
|
"icons": {
|
||||||
|
"128": "assets/icon128.png"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default_icon": {
|
||||||
|
"128": "assets/icon_disabled128.png"
|
||||||
|
},
|
||||||
|
"default_title": "Owl devtools",
|
||||||
|
"default_popup": "popup_app/popup.html"
|
||||||
|
},
|
||||||
|
"permissions": ["scripting", "storage"],
|
||||||
|
"host_permissions": ["http://*/*", "https://*/*"],
|
||||||
|
"content_security_policy": {
|
||||||
|
"script-src": "self",
|
||||||
|
"object-src": "self"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"devtools_page": "devtools_app/devtools.html",
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["content.js"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "Owl devtools",
|
||||||
|
"version": "1.0",
|
||||||
|
"description": "Firefox devtools extension for Odoo Owl framework",
|
||||||
|
"manifest_version": 2,
|
||||||
|
"browser_specific_settings": {
|
||||||
|
"gecko": {
|
||||||
|
"id": "@owl-devtools",
|
||||||
|
"strict_min_version": "74.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"icons": {
|
||||||
|
"128": "assets/icon128.png"
|
||||||
|
},
|
||||||
|
"browser_action": {
|
||||||
|
"default_icon": {
|
||||||
|
"128": "assets/icon_disabled128.png"
|
||||||
|
},
|
||||||
|
"default_title": "Owl devtools",
|
||||||
|
"default_popup": "popup_app/popup.html"
|
||||||
|
},
|
||||||
|
"web_accessible_resources": [
|
||||||
|
"popup_app/popup.html",
|
||||||
|
"devtools_app/devtools.html",
|
||||||
|
"devtools_app/components_panel.html"
|
||||||
|
],
|
||||||
|
"permissions": ["storage", "scripting", "<all_urls>"],
|
||||||
|
"background": {
|
||||||
|
"scripts": ["background.js"]
|
||||||
|
},
|
||||||
|
"devtools_page": "devtools_app/devtools.html",
|
||||||
|
"content_security_policy": "script-src 'self' 'unsafe-eval' blob:; object-src 'self'",
|
||||||
|
"content_scripts": [
|
||||||
|
{
|
||||||
|
"matches": ["<all_urls>"],
|
||||||
|
"js": ["content.js"]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import terser from "rollup-plugin-terser";
|
||||||
|
import copy from "rollup-plugin-copy";
|
||||||
|
import execute from "rollup-plugin-execute";
|
||||||
|
import del from "rollup-plugin-delete";
|
||||||
|
import { string } from "rollup-plugin-string";
|
||||||
|
|
||||||
|
export default ({ "config-browser": browser }) => {
|
||||||
|
const isProduction = process.env.NODE_ENV === "production";
|
||||||
|
const isChrome = browser === "chrome";
|
||||||
|
const filesToMove = [
|
||||||
|
{ src: "tools/devtools/assets/**/*", dest: "dist/devtools/assets/" },
|
||||||
|
{
|
||||||
|
src: "tools/devtools/src/devtools_app/devtools.html",
|
||||||
|
dest: "dist/devtools/devtools_app",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "tools/devtools/src/devtools_app/devtools_panel.html",
|
||||||
|
dest: "dist/devtools/devtools_app",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
src: "tools/devtools/src/page_scripts/owl_devtools_global_hook.js",
|
||||||
|
dest: "dist/devtools/page_scripts",
|
||||||
|
},
|
||||||
|
{ src: "tools/devtools/src/fonts/*", dest: "dist/devtools/fonts/" },
|
||||||
|
{ src: "tools/devtools/src/popup_app/popup.html", dest: "dist/devtools/popup_app" },
|
||||||
|
{ src: "tools/devtools/src/background.html", dest: "dist/devtools" },
|
||||||
|
{ src: "tools/devtools/src/main.css", dest: "dist/devtools/popup_app" },
|
||||||
|
{ src: "tools/devtools/src/main.css", dest: "dist/devtools/devtools_app" },
|
||||||
|
{
|
||||||
|
src: isChrome
|
||||||
|
? "tools/devtools/manifest-chrome.json"
|
||||||
|
: "tools/devtools/manifest-firefox.json",
|
||||||
|
dest: "dist/devtools",
|
||||||
|
rename: "manifest.json",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function generateRule(input, format = "esm") {
|
||||||
|
return {
|
||||||
|
input: input,
|
||||||
|
output: [
|
||||||
|
{
|
||||||
|
format: format,
|
||||||
|
file: input.replace("tools/devtools/src", "dist/devtools"),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
plugins: [
|
||||||
|
string({
|
||||||
|
include: "**/page_scripts/owl_devtools_global_hook.js",
|
||||||
|
}),
|
||||||
|
isProduction && terser.terser(),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const commands = new Array(2);
|
||||||
|
commands[1] =
|
||||||
|
"npm run compile_templates -- tools/devtools/src && mv templates.js tools/devtools/assets/templates.js";
|
||||||
|
const firstRule = generateRule("tools/devtools/src/page_scripts/owl_devtools_global_hook.js");
|
||||||
|
if (isProduction)
|
||||||
|
commands[0] =
|
||||||
|
"npm run build && cp dist/owl.iife.js tools/devtools/assets/owl.js && npm run build:compiler";
|
||||||
|
else commands[0] = "cp dist/owl.iife.js tools/devtools/assets/owl.js";
|
||||||
|
firstRule.plugins.push(execute(commands, true));
|
||||||
|
const secondRule = generateRule("tools/devtools/src/content.js");
|
||||||
|
secondRule.plugins.push(copy({ targets: filesToMove }));
|
||||||
|
const lastRule = generateRule("tools/devtools/src/background.js");
|
||||||
|
lastRule.plugins.push(del({ targets: "tools/devtools/assets/*.js" }));
|
||||||
|
|
||||||
|
return [
|
||||||
|
firstRule,
|
||||||
|
secondRule,
|
||||||
|
generateRule("tools/devtools/src/devtools_app/devtools.js"),
|
||||||
|
generateRule("tools/devtools/src/utils.js"),
|
||||||
|
generateRule("tools/devtools/src/devtools_app/devtools_panel.js"),
|
||||||
|
generateRule("tools/devtools/src/popup_app/popup.js"),
|
||||||
|
lastRule,
|
||||||
|
];
|
||||||
|
};
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<script type="module" src="./background.js"></script>
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
import { IS_FIREFOX, getActiveTabURL } from "./utils";
|
||||||
|
|
||||||
|
let owlStatus = 0;
|
||||||
|
|
||||||
|
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||||
|
|
||||||
|
// Used to keep track of the tabs where the owl devtools have been opened
|
||||||
|
const activePanels = new Set();
|
||||||
|
|
||||||
|
// Load the devtools global hook this way when running on manifest v3 chrome
|
||||||
|
if (!IS_FIREFOX) {
|
||||||
|
chrome.scripting.registerContentScripts([
|
||||||
|
{
|
||||||
|
id: "owlDevtoolsGLobalHook",
|
||||||
|
matches: ["<all_urls>"],
|
||||||
|
js: ["page_scripts/owl_devtools_global_hook.js"],
|
||||||
|
world: chrome.scripting.ExecutionWorld.MAIN,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the owlStatus variable and the extension icon accordingly
|
||||||
|
function setOwlStatus(status) {
|
||||||
|
owlStatus = status;
|
||||||
|
if (IS_FIREFOX) {
|
||||||
|
browser.browserAction.setIcon({
|
||||||
|
path: owlStatus === 2 ? "assets/icon128.png" : "assets/icon_disabled128.png",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
browserInstance.action.setIcon({
|
||||||
|
path: owlStatus === 2 ? "assets/icon128.png" : "assets/icon_disabled128.png",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete from the set when the tab is closed
|
||||||
|
browserInstance.tabs.onRemoved.addListener((tabId) => {
|
||||||
|
activePanels.delete(tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check owl status on tab update
|
||||||
|
browserInstance.tabs.onUpdated.addListener((tab) => {
|
||||||
|
browserInstance.tabs.get(tab, (tabData) => {
|
||||||
|
if (tabData.status === "complete") {
|
||||||
|
setOwlStatus(0);
|
||||||
|
checkOwlStatus(tabData.id);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check owl status on tab activation
|
||||||
|
browserInstance.tabs.onActivated.addListener((activeInfo) => {
|
||||||
|
setOwlStatus(0);
|
||||||
|
checkOwlStatus(activeInfo.tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// send a message to the window which will be intercepted by the page script and will result in a response of type owlStatus
|
||||||
|
function checkOwlStatus(tabId) {
|
||||||
|
browserInstance.scripting.executeScript({
|
||||||
|
target: { tabId: tabId },
|
||||||
|
func: () => {
|
||||||
|
window.postMessage({
|
||||||
|
source: "owl-devtools-background",
|
||||||
|
type: "checkOwlStatus",
|
||||||
|
});
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Messages handler for the background script
|
||||||
|
browserInstance.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
|
||||||
|
// Send back the owl status to the sender
|
||||||
|
if (message.type === "getOwlStatus") {
|
||||||
|
sendResponse({ result: owlStatus });
|
||||||
|
return true;
|
||||||
|
} else if (message.type === "owlStatus") {
|
||||||
|
setOwlStatus(message.data);
|
||||||
|
// Dummy message to test if the extension context is still valid
|
||||||
|
} else if (message.type === "keepAlive") {
|
||||||
|
return;
|
||||||
|
// Relay the received message to the devtools app
|
||||||
|
} else if (message.type === "newDevtoolsPanel") {
|
||||||
|
const tab = await getActiveTabURL();
|
||||||
|
activePanels.add(tab);
|
||||||
|
// This is solely for firefox which doesnt allow access to the chrome.tabs api inside devtools
|
||||||
|
} else if (message.type === "getActiveTabURL") {
|
||||||
|
getActiveTabURL().then((tab) => {
|
||||||
|
sendResponse({ result: tab });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
const tab = await getActiveTabURL();
|
||||||
|
if (!activePanels.has(tab)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const port = browserInstance.runtime.connect({ name: "OwlDevtoolsPort" });
|
||||||
|
port.postMessage(
|
||||||
|
message.data
|
||||||
|
? { type: message.type, data: message.data, origin: message.origin }
|
||||||
|
: { type: message.type, origin: message.origin }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
import globalHook from "./page_scripts/owl_devtools_global_hook";
|
||||||
|
import { IS_FIREFOX } from "./utils";
|
||||||
|
|
||||||
|
// Relays the owlDevtools__... type top window messages to the background script so that it can relay it to the devtools app
|
||||||
|
window.addEventListener(
|
||||||
|
"message",
|
||||||
|
function (event) {
|
||||||
|
if (event.data.type && event.data.source === "owl-devtools") {
|
||||||
|
try {
|
||||||
|
chrome.runtime.sendMessage(
|
||||||
|
event.data.data
|
||||||
|
? { type: event.data.type, data: event.data.data, origin: event.data.origin }
|
||||||
|
: { type: event.data.type, origin: event.data.origin }
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
// Extension context invalidated, cannot be handled here since the whole communication system
|
||||||
|
// inside the extension is dead in this case.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
// Load the devtools global hook this way when running on firefox
|
||||||
|
if (IS_FIREFOX) {
|
||||||
|
const script = document.createElement("script");
|
||||||
|
script.textContent = globalHook;
|
||||||
|
document.documentElement.appendChild(script);
|
||||||
|
script.parentNode.removeChild(script);
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
<script src="devtools.js"></script>
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import { IS_FIREFOX } from "../utils";
|
||||||
|
|
||||||
|
let created = false;
|
||||||
|
let browserInstance = IS_FIREFOX ? browser : chrome;
|
||||||
|
|
||||||
|
// Try to load the owl panel each 1000 ms in case it (re)appears on the page later on
|
||||||
|
const checkInterval = setInterval(createPanelsIfOwl, 1000);
|
||||||
|
|
||||||
|
createPanelsIfOwl();
|
||||||
|
|
||||||
|
// Create the owl devtools panel if owl on the page is available at a sufficient version
|
||||||
|
function createPanelsIfOwl() {
|
||||||
|
if (created) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
browserInstance.devtools.inspectedWindow.eval(
|
||||||
|
"window.__OWL_DEVTOOLS__?.Fiber !== undefined;",
|
||||||
|
async (hasOwl) => {
|
||||||
|
if (!hasOwl || created) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
created = true;
|
||||||
|
browserInstance.devtools.panels.create(
|
||||||
|
"Owl",
|
||||||
|
"../../assets/icon128.png",
|
||||||
|
IS_FIREFOX ? "devtools_panel.html" : "devtools_app/devtools_panel.html"
|
||||||
|
);
|
||||||
|
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel" });
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="stylesheet" href="../assets/font-awesome.min.css">
|
||||||
|
<link rel="stylesheet" href="../assets/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="main.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="../assets/owl.js"></script>
|
||||||
|
<script src="devtools_panel.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { DevtoolsWindow } from "./devtools_window/devtools_window";
|
||||||
|
const { mount } = owl;
|
||||||
|
import { templates } from "../../assets/templates.js";
|
||||||
|
|
||||||
|
for (const template in templates) {
|
||||||
|
owl.App.registerTemplate(template, templates[template]);
|
||||||
|
}
|
||||||
|
mount(DevtoolsWindow, document.body, { dev: true });
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
import { useStore } from "../../../store/store";
|
||||||
|
|
||||||
|
const { Component } = owl;
|
||||||
|
|
||||||
|
export class ComponentSearchBar extends Component {
|
||||||
|
static template = "devtools.ComponentSearchBar";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSearch(event) {
|
||||||
|
if (event.key !== "Enter") {
|
||||||
|
this.store.updateSearch(event.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Go to the next search result repeatedly while enter is pressed
|
||||||
|
onSearchKeyDown(event) {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
this.store.componentSearch.getNextSearch();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.ComponentSearchBar" owl="1">
|
||||||
|
<div class="pointer-icon ms-1 px-2 py-1" t-on-click.stop='() => this.store.toggleSelector()'>
|
||||||
|
<i title="Select an element in the page to inspect the corresponding component" class="fa fa-mouse-pointer" t-attf-style="color: {{store.componentSearch.activeSelector ? 'var(--active-icon)' : 'var(--text-color)'}};"></i>
|
||||||
|
</div>
|
||||||
|
<div class="icons-separator"/>
|
||||||
|
<div class="d-flex align-items-center ms-2 flex-grow-1">
|
||||||
|
<i class="fa fa-search search-icon" aria-hidden="true"></i>
|
||||||
|
<input type="text" class="search-input ms-1 w-100 border-0 h-100" placeholder="Search" t-on-keyup.stop="updateSearch" t-on-keydown.stop="onSearchKeyDown" t-att-value="store.componentSearch.search"/>
|
||||||
|
<t t-if="store.componentSearch.search.length > 0">
|
||||||
|
<t t-esc="store.componentSearch.searchResults.length ? store.componentSearch.searchIndex + 1 : 0"/>|<t t-esc="store.componentSearch.searchResults.length"/>
|
||||||
|
<i class="fa fa-angle-up lg-icon utility-icon ms-1 p-1" t-on-click.stop="() => this.store.componentSearch.getPrevSearch()"></i>
|
||||||
|
<i class="fa fa-angle-down lg-icon utility-icon p-1" t-on-click.stop="() => this.store.componentSearch.getNextSearch()"></i>
|
||||||
|
<i class="fa fa-times lg-icon utility-icon p-1 me-2" t-on-click.stop='() => this.store.updateSearch("")'></i>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
const { Component, onWillUnmount, useExternalListener } = owl;
|
||||||
|
import { TreeElement } from "./tree_element/tree_element";
|
||||||
|
import { DetailsWindow } from "./details_window/details_window";
|
||||||
|
import { ComponentSearchBar } from "./component_search_bar/component_search_bar";
|
||||||
|
import { useStore } from "../../store/store";
|
||||||
|
|
||||||
|
export class ComponentsTab extends Component {
|
||||||
|
static template = "devtools.ComponentsTab";
|
||||||
|
|
||||||
|
static components = { TreeElement, DetailsWindow, ComponentSearchBar };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
this.flushRendersTimeout = false;
|
||||||
|
useExternalListener(document, "keydown", this.onKeyboardEvent);
|
||||||
|
|
||||||
|
onWillUnmount(() => {
|
||||||
|
window.removeEventListener("mousemove", this.onMouseMove);
|
||||||
|
window.removeEventListener("mouseup", this.onMouseUp);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply the right action depending on which arrow key is pressed (on keydown)
|
||||||
|
onKeyboardEvent(event) {
|
||||||
|
switch (event.key) {
|
||||||
|
case "ArrowLeft":
|
||||||
|
this.store.toggleOrSelectPrevElement(true);
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
case "ArrowUp":
|
||||||
|
this.store.toggleOrSelectPrevElement(false);
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
case "ArrowRight":
|
||||||
|
this.store.toggleOrSelectNextElement(true);
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
case "ArrowDown":
|
||||||
|
this.store.toggleOrSelectNextElement(false);
|
||||||
|
event.preventDefault();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMouseDown = () => {
|
||||||
|
// Add event listeners for mouse move and mouse up events
|
||||||
|
// to allow the user to drag the split screen border
|
||||||
|
window.addEventListener("mousemove", this.onMouseMove);
|
||||||
|
window.addEventListener("mouseup", this.onMouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Adjust the position of the split between the left and right right window of the components tab
|
||||||
|
onMouseMove = (event) => {
|
||||||
|
this.store.splitPosition = Math.max(
|
||||||
|
Math.min((event.clientX / window.innerWidth) * 100, 85),
|
||||||
|
15
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMouseUp = () => {
|
||||||
|
// Remove the event listeners when the user releases the mouse button
|
||||||
|
window.removeEventListener("mousemove", this.onMouseMove);
|
||||||
|
window.removeEventListener("mouseup", this.onMouseUp);
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.ComponentsTab" owl="1">
|
||||||
|
<t t-if="store.apps.length === 0">
|
||||||
|
<div class="status-message d-flex justify-content-center align-items-center">
|
||||||
|
There are no apps currently running.
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="position-relative overflow-hidden d-flex flex-row h-100">
|
||||||
|
<div class="split-screen-left d-flex flex-column" t-attf-style="width:{{store.splitPosition}}%;">
|
||||||
|
<div class="panel-top d-flex align-items-center">
|
||||||
|
<ComponentSearchBar/>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-auto h-100 font-monospace">
|
||||||
|
<div id="tree-wrapper">
|
||||||
|
<t t-foreach="store.apps" t-as="app" t-key="app_index">
|
||||||
|
<TreeElement component="app"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="split-screen-border user-select-none"
|
||||||
|
t-on-mousedown="onMouseDown"
|
||||||
|
/>
|
||||||
|
<div class="split-screen-right d-flex flex-column font-monospace" t-attf-style="width:{{100 - store.splitPosition}}%;">
|
||||||
|
<DetailsWindow t-if="store.activeComponent"/>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="status-message d-flex justify-content-center align-items-center">
|
||||||
|
There was an error while processing this component.
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
+33
@@ -0,0 +1,33 @@
|
|||||||
|
const { Component, useRef, useEffect } = owl;
|
||||||
|
import { useStore } from "../../../store/store";
|
||||||
|
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
||||||
|
import { Subscriptions } from "./subscriptions/subscriptions";
|
||||||
|
|
||||||
|
export class DetailsWindow extends Component {
|
||||||
|
static template = "devtools.DetailsWindow";
|
||||||
|
static components = { ObjectTreeElement, Subscriptions };
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
this.contextMenu = useRef("contextmenu");
|
||||||
|
this.contextMenuId = this.store.contextMenu.id++;
|
||||||
|
this.contextMenuEvent;
|
||||||
|
// Open the context menu when the ids match
|
||||||
|
useEffect(
|
||||||
|
(menuId) => {
|
||||||
|
if (menuId === this.contextMenuId) {
|
||||||
|
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => [this.store.contextMenu.activeMenu]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
openMenu(ev) {
|
||||||
|
this.contextMenuEvent = ev;
|
||||||
|
this.store.contextMenu.activeMenu = this.contextMenuId;
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleCategory(ev, category) {
|
||||||
|
this.store.activeComponent[category].toggled = !this.store.activeComponent[category].toggled;
|
||||||
|
}
|
||||||
|
}
|
||||||
+92
@@ -0,0 +1,92 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.DetailsWindow" owl="1">
|
||||||
|
<div class="panel-top d-flex align-items-center">
|
||||||
|
<div class="ms-1 p-1 text-truncate" style="width: 100%;">
|
||||||
|
<span t-if="store.activeComponent.path.length > 1"><</span>
|
||||||
|
<b style="color: var(--component-color); cursor: pointer;" t-on-mouseover.stop="() => this.store.highlightComponent(this.store.activeComponent.path)" t-on-click.stop="() => this.store.focusSelectedComponent()" t-on-contextmenu.prevent="openMenu" t-esc="store.activeComponent.name"/>
|
||||||
|
<span t-if="store.activeComponent.path.length > 1">></span>
|
||||||
|
<span class="version" t-else="">owl=<t t-esc="store.activeComponent.version"/></span>
|
||||||
|
</div>
|
||||||
|
<t t-if="store.activeComponent.path.length > 1">
|
||||||
|
<i title="Inspect component in the Elements tab" class="fa fa-eye utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('DOM')"></i>
|
||||||
|
<i title="Store component as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole()"></i>
|
||||||
|
<i title="Inspect source code of component" class="fa fa-file-code-o utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('source')"></i>
|
||||||
|
<i title="Trigger rerender of the component" class="fa fa-refresh utility-icon p-1" t-on-click.stop="() => this.store.refreshComponent()"></i>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<i title="Store app as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole()"></i>
|
||||||
|
<i title="Inspect source code of the app" class="fa fa-file-code-o utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('source')"></i>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div class="details-container">
|
||||||
|
<div t-if="store.activeComponent.env.children.length > 0" id="env" class="details-panel ps-2 py-1">
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'env')">
|
||||||
|
<i class="fa mx-1 pointer-icon"
|
||||||
|
t-att-class="{'fa-caret-right': !store.activeComponent.env.toggled, 'fa-caret-down': store.activeComponent.env.toggled}"
|
||||||
|
/><b>env</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<t t-if="store.activeComponent.env.toggled" t-foreach="store.activeComponent.env.children" t-as="env" t-key="env_index">
|
||||||
|
<ObjectTreeElement object="env"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div t-if="store.activeComponent.props.children.length > 0" class="details-panel ps-2 py-1">
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'props')">
|
||||||
|
<i class="fa mx-1 pointer-icon"
|
||||||
|
t-att-class="{'fa-caret-right': !store.activeComponent.props.toggled, 'fa-caret-down': store.activeComponent.props.toggled}"
|
||||||
|
/><b>props</b>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<t t-if="store.activeComponent.props.toggled" t-foreach="store.activeComponent.props.children" t-as="prop" t-key="prop_index">
|
||||||
|
<ObjectTreeElement object="prop"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<div t-if="store.activeComponent.subscriptions.children.length > 0" id="subscriptions" class="details-panel ps-2 py-1">
|
||||||
|
<div class="d-flex">
|
||||||
|
<div title="Shows all the targets that will trigger a re-render of the component when one of its associated keys is modified" class="w-100 text-truncate" t-on-click.stop="(ev) => this.toggleCategory(ev, 'subscriptions')">
|
||||||
|
<i class="fa mx-1 pointer-icon"
|
||||||
|
t-att-class="{'fa-caret-right': !store.activeComponent.subscriptions.toggled, 'fa-caret-down': store.activeComponent.subscriptions.toggled}"
|
||||||
|
/><b>observed state</b>
|
||||||
|
</div>
|
||||||
|
<i title="Store observed states as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole([...this.store.activeComponent.path, {type: 'item', value: 'subscriptions'}])"></i>
|
||||||
|
</div>
|
||||||
|
<Subscriptions t-if="store.activeComponent.subscriptions.toggled"/>
|
||||||
|
</div>
|
||||||
|
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
|
||||||
|
<div class="d-flex mb-2">
|
||||||
|
<div class="w-100 text-truncate" t-on-click.stop="(ev) => this.toggleCategory(ev, 'instance')">
|
||||||
|
<i class="fa mx-1 pointer-icon"
|
||||||
|
t-att-class="{'fa-caret-right': !store.activeComponent.instance.toggled, 'fa-caret-down': store.activeComponent.instance.toggled}"
|
||||||
|
/><b>instance</b>
|
||||||
|
</div>
|
||||||
|
<t t-if="store.activeComponent.path.length > 1">
|
||||||
|
<i title="Inspect compiled template" class="fa fa-hashtag utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('compiled template')"></i>
|
||||||
|
<i title="Send raw template to console" class="fa fa-file-word-o utility-icon p-1" t-on-click.stop="() => this.store.inspectComponent('raw template')"></i>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
<t t-if="store.activeComponent.instance.toggled" t-foreach="store.activeComponent.instance.children" t-as="instance" t-key="instance_index">
|
||||||
|
<ObjectTreeElement object="instance"/>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="contextmenu">
|
||||||
|
<ul class="my-1">
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('source', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||||
|
<t t-if="store.activeComponent.path.length !== 1">
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('DOM', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||||
|
<li t-on-click.stop="() => this.store.refreshComponent(store.activeComponent.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('raw template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
import { useStore } from "../../../../store/store";
|
||||||
|
|
||||||
|
const { Component, useState, useEffect, useRef } = owl;
|
||||||
|
|
||||||
|
export class ObjectTreeElement extends Component {
|
||||||
|
static template = "devtools.ObjectTreeElement";
|
||||||
|
|
||||||
|
static components = { ObjectTreeElement };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({
|
||||||
|
editMode: false,
|
||||||
|
menuTop: 0,
|
||||||
|
menuLeft: 0,
|
||||||
|
});
|
||||||
|
this.contextMenu = useRef("contextmenu");
|
||||||
|
const inputRef = useRef("input");
|
||||||
|
this.store = useStore();
|
||||||
|
this.contextMenuId = this.store.contextMenu.id++;
|
||||||
|
this.contextMenuEvent,
|
||||||
|
useEffect(
|
||||||
|
(menuId) => {
|
||||||
|
if (menuId === this.contextMenuId) {
|
||||||
|
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => [this.store.contextMenu.activeMenu]
|
||||||
|
);
|
||||||
|
useEffect(
|
||||||
|
(editMode) => {
|
||||||
|
// Focus on the input when it is created
|
||||||
|
if (editMode) {
|
||||||
|
inputRef.el.select();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => [this.state.editMode]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get pathAsString() {
|
||||||
|
return JSON.stringify(this.props.object.path);
|
||||||
|
}
|
||||||
|
|
||||||
|
get objectName() {
|
||||||
|
return this.props.object.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
get objectLineClass() {
|
||||||
|
// Prototype items will be dyed down to appear less important
|
||||||
|
if (this.pathAsString.includes('{"type":"prototype",')) {
|
||||||
|
return { attenuate: true };
|
||||||
|
}
|
||||||
|
// Same for subscription items which are not present in the keys while the keys will be bold
|
||||||
|
if (this.props.object.objectType === "subscription" && this.props.object.depth > 0) {
|
||||||
|
if (this.props.keys.includes(this.props.object.name.toString())) {
|
||||||
|
return { "fw-bolder": true };
|
||||||
|
}
|
||||||
|
return { attenuate: true };
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
get objectPadding() {
|
||||||
|
return this.props.object.depth * 0.8 + 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
openMenu(ev) {
|
||||||
|
this.contextMenuEvent = ev;
|
||||||
|
this.store.contextMenu.activeMenu = this.contextMenuId;
|
||||||
|
}
|
||||||
|
|
||||||
|
setupEditMode() {
|
||||||
|
if (!this.state.editMode) {
|
||||||
|
if (!this.props.object.hasChildren) {
|
||||||
|
this.state.editMode = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
editObject(ev) {
|
||||||
|
let value = ev.target.value;
|
||||||
|
if (ev.keyCode === 13 && value !== "") {
|
||||||
|
this.store.editObjectTreeElement(this.props.object.path, value, this.props.object.objectType);
|
||||||
|
this.state.editMode = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.ObjectTreeElement" owl="1">
|
||||||
|
<div class="m-0 p-0 text-nowrap w-100 object-line"
|
||||||
|
t-att-class="objectLineClass"
|
||||||
|
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
||||||
|
t-on-contextmenu.prevent="openMenu"
|
||||||
|
>
|
||||||
|
<div t-attf-style="padding-left: {{objectPadding}}rem">
|
||||||
|
<i class="fa px-1 pointer-icon caret"
|
||||||
|
t-att-class="{'fa-caret-right': !props.object.toggled, 'fa-caret-down': props.object.toggled}"
|
||||||
|
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
|
||||||
|
/>
|
||||||
|
<t t-esc="objectName"/>
|
||||||
|
<t t-if="props.object.content.length > 0">: </t>
|
||||||
|
<t t-if="props.object.contentType == 'getter'">
|
||||||
|
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
|
||||||
|
<t t-esc="props.object.content"/>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<span class="object-content" t-att-class="objectLineClass" t-on-dblclick.stop="setupEditMode">
|
||||||
|
<t t-if="state.editMode">
|
||||||
|
<input t-attf-id="objectEditionInput/{{pathAsString}}" t-ref="input" type="text" placeholder="" t-att-value="props.object.content" t-on-keydown.stop="editObject"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<t t-esc="props.object.content"/>
|
||||||
|
</t>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="contextmenu">
|
||||||
|
<ul class="my-1">
|
||||||
|
<li t-on-click="() => this.store.logObjectInConsole(this.props.object.path)" class="custom-menu-item py-1 px-4 text-nowrap">Store as global variable</li>
|
||||||
|
<t t-if='props.object.contentType == "function"'>
|
||||||
|
<li t-on-click="() => this.store.inspectFunctionSource(this.props.object.path)" class="custom-menu-item py-1 px-4 text-nowrap">Inspect function source code</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<t t-if="props.object.toggled">
|
||||||
|
<t t-foreach="props.object.children" t-as="child" t-key="child.name">
|
||||||
|
<ObjectTreeElement t-if="props.object.objectType === 'subscription'" object="child" keys="props.keys"/>
|
||||||
|
<ObjectTreeElement t-else="" object="child"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
+30
@@ -0,0 +1,30 @@
|
|||||||
|
const { Component } = owl;
|
||||||
|
import { useStore } from "../../../../store/store";
|
||||||
|
import { ObjectTreeElement } from "../object_tree_element/object_tree_element";
|
||||||
|
|
||||||
|
export class Subscriptions extends Component {
|
||||||
|
static template = "devtools.Subscriptions";
|
||||||
|
|
||||||
|
static components = { ObjectTreeElement };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to display the keys in a compact way
|
||||||
|
keysContent(index) {
|
||||||
|
const keys = this.store.activeComponent.subscriptions.children[index].keys;
|
||||||
|
let content = JSON.stringify(keys);
|
||||||
|
const maxLength = 50;
|
||||||
|
content = content.replace(/,/g, ", ");
|
||||||
|
if (content.length > maxLength) {
|
||||||
|
content = content.slice(0, content.lastIndexOf(",", maxLength - 5)) + ", ...]";
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
expandKeys(event, index) {
|
||||||
|
this.store.activeComponent.subscriptions.children[index].keysExpanded =
|
||||||
|
!this.store.activeComponent.subscriptions.children[index].keysExpanded;
|
||||||
|
}
|
||||||
|
}
|
||||||
+24
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.Subscriptions" owl="1">
|
||||||
|
<div id="subscriptionsPanel">
|
||||||
|
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
|
||||||
|
<div class="my-2">
|
||||||
|
<div class="my-0 p-0 object-line" t-on-click.stop="(ev) => this.expandKeys(ev, subscription_index)">
|
||||||
|
<span class="ps-1 text-nowrap">
|
||||||
|
<i class="fa fa-caret-right ms-1" t-attf-style="cursor: pointer;{{subscription.keysExpanded ? 'transform: rotate(90deg);' : ''}}"></i>
|
||||||
|
keys: <span class="key-name"><t t-esc="this.keysContent(subscription_index)"/></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div t-foreach="subscription.keys" t-as="key" t-key="key_index" class="my-0 p-0 object-line" t-attf-style="display: {{subscription.keysExpanded ? 'flex' : 'none'}}">
|
||||||
|
<div style="transform: translateX(calc(1.1rem))" class="key-content">
|
||||||
|
<i class="fa fa-caret-right mx-1" t-attf-style="cursor: pointer; visibility: hidden;"></i>
|
||||||
|
<t t-esc="key"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<ObjectTreeElement object="subscription.target" keys="subscription.keys"/>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
+50
@@ -0,0 +1,50 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
const { Component, onWillRender } = owl;
|
||||||
|
|
||||||
|
export class HighlightText extends Component {
|
||||||
|
setup() {
|
||||||
|
onWillRender(() => {
|
||||||
|
const splitText = this.splitFuzzySearch(this.props.originalText, this.props.searchValue);
|
||||||
|
this.splitText =
|
||||||
|
this.props.searchValue.length && splitText.length > 1
|
||||||
|
? splitText
|
||||||
|
: [this.props.originalText];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic to split the text to highlight it according to a fuzzy search pattern
|
||||||
|
splitFuzzySearch(text, search) {
|
||||||
|
if (!search || search.length === 0) {
|
||||||
|
return [text];
|
||||||
|
}
|
||||||
|
let splits = [""];
|
||||||
|
let searchIndex = 0;
|
||||||
|
for (const letter of text) {
|
||||||
|
if (
|
||||||
|
!(searchIndex >= search.length) &&
|
||||||
|
(letter === search[searchIndex] || letter === search[searchIndex].toUpperCase())
|
||||||
|
) {
|
||||||
|
if (splits.length % 2) {
|
||||||
|
splits.push(letter);
|
||||||
|
} else {
|
||||||
|
splits[splits.length - 1] += letter;
|
||||||
|
}
|
||||||
|
searchIndex++;
|
||||||
|
} else {
|
||||||
|
if (splits.length % 2) {
|
||||||
|
splits[splits.length - 1] += letter;
|
||||||
|
} else {
|
||||||
|
splits.push(letter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return splits;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HighlightText.template = "utils.HighlightText";
|
||||||
|
HighlightText.props = {
|
||||||
|
originalText: String,
|
||||||
|
searchValue: String,
|
||||||
|
};
|
||||||
|
HighlightText.highlightClass = "highlight-search";
|
||||||
+9
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="utils.HighlightText" owl="1">
|
||||||
|
<t t-foreach="splitText" t-as="name" t-key="name_index">
|
||||||
|
<b t-if="name_index % 2" t-esc="name" t-att-class="constructor.highlightClass"/>
|
||||||
|
<t t-else="" t-esc="name"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
+108
@@ -0,0 +1,108 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { isElementInCenterViewport, minimizeKey } from "../../../../utils";
|
||||||
|
import { useStore } from "../../../store/store";
|
||||||
|
import { HighlightText } from "./highlight_text/highlight_text";
|
||||||
|
|
||||||
|
const { Component, useRef, useState, useEffect, onMounted } = owl;
|
||||||
|
|
||||||
|
export class TreeElement extends Component {
|
||||||
|
static template = "devtools.TreeElement";
|
||||||
|
|
||||||
|
static components = { TreeElement, HighlightText };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({
|
||||||
|
searched: false,
|
||||||
|
});
|
||||||
|
this.store = useStore();
|
||||||
|
this.contextMenu = useRef("contextmenu");
|
||||||
|
this.element = useRef("element");
|
||||||
|
this.contextMenuId = this.store.contextMenu.id++;
|
||||||
|
this.contextMenuEvent;
|
||||||
|
this.stringifiedPath = JSON.stringify(this.props.component.path);
|
||||||
|
// Scroll to the selected element when it changes
|
||||||
|
onMounted(() => {
|
||||||
|
if (this.props.component.selected) {
|
||||||
|
this.element.el.scrollIntoView({ block: "center", behavior: "auto" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
useEffect(
|
||||||
|
(selected) => {
|
||||||
|
if (selected) {
|
||||||
|
if (!isElementInCenterViewport(this.element.el)) {
|
||||||
|
this.element.el.scrollIntoView({ block: "center", behavior: "smooth" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.store.selectedElement = this.element.el;
|
||||||
|
},
|
||||||
|
() => [this.props.component.selected]
|
||||||
|
);
|
||||||
|
// Open the context menu when the ids match
|
||||||
|
useEffect(
|
||||||
|
(menuId) => {
|
||||||
|
if (menuId === this.contextMenuId) {
|
||||||
|
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => [this.store.contextMenu.activeMenu]
|
||||||
|
);
|
||||||
|
// Effect to apply a short highlight effect to the component when it is rendered
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
if (this.store.renderPaths.has(this.stringifiedPath)) {
|
||||||
|
const treeElement = this.element.el;
|
||||||
|
treeElement.classList.add("render-highlight");
|
||||||
|
setTimeout(() => {
|
||||||
|
treeElement.classList.add("highlight-fade");
|
||||||
|
treeElement.classList.remove("render-highlight");
|
||||||
|
setTimeout(() => {
|
||||||
|
treeElement.classList.remove("highlight-fade");
|
||||||
|
this.blockHighlight = false;
|
||||||
|
}, 500);
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => [this.store.renderPaths.size]
|
||||||
|
);
|
||||||
|
// Used to know when the component is in the search bar results
|
||||||
|
useEffect(
|
||||||
|
(searchResults) => {
|
||||||
|
if (searchResults.includes(this.props.component.path)) {
|
||||||
|
this.state.searched = true;
|
||||||
|
} else {
|
||||||
|
this.state.searched = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => [this.store.componentSearch.searchResults]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get componentPadding() {
|
||||||
|
return this.props.component.depth * 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
get minimizedKey() {
|
||||||
|
return minimizeKey(this.props.component.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
openMenu(ev) {
|
||||||
|
this.contextMenuEvent = ev;
|
||||||
|
this.store.contextMenu.activeMenu = this.contextMenuId;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand/fold the component node
|
||||||
|
toggleDisplay() {
|
||||||
|
this.props.component.toggled = !this.props.component.toggled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to select the component node
|
||||||
|
toggleComponent() {
|
||||||
|
if (this.store.settings.toggleOnSelected) {
|
||||||
|
this.toggleDisplay();
|
||||||
|
}
|
||||||
|
if (!this.props.component.selected) {
|
||||||
|
this.store.selectComponent(this.props.component.path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+55
@@ -0,0 +1,55 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.TreeElement" owl="1">
|
||||||
|
<div t-ref="element"
|
||||||
|
t-att-class="{'component-selected': props.component.selected,'component-highlighted': props.component.highlighted}"
|
||||||
|
class="tree-component m-0 p-0 w-100 text-nowrap user-select-none"
|
||||||
|
t-on-contextmenu.prevent="openMenu"
|
||||||
|
t-on-mouseover.stop="() => this.store.highlightComponent(props.component.path)"
|
||||||
|
t-on-click.stop="toggleComponent"
|
||||||
|
>
|
||||||
|
<div class="component-wrapper" t-attf-style="padding-left: {{componentPadding}}rem">
|
||||||
|
<i class="fa px-1 pointer-icon caret"
|
||||||
|
t-att-class="{'fa-caret-right': !props.component.toggled, 'fa-caret-down': props.component.toggled}"
|
||||||
|
t-on-click.stop="toggleDisplay"
|
||||||
|
t-attf-style="{{props.component.children.length > 0 ? '' : 'visibility: hidden;'}}"
|
||||||
|
/>
|
||||||
|
<span t-if="props.component.depth"><</span>
|
||||||
|
<span style="color: var(--component-color);">
|
||||||
|
<HighlightText originalText="props.component.name" searchValue="state.searched ? store.componentSearch.search : ''"/>
|
||||||
|
<t t-if="minimizedKey.length > 0">
|
||||||
|
<span t-if="minimizedKey.length > 0" style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||||
|
<t t-esc="minimizedKey"/>
|
||||||
|
</span>
|
||||||
|
</t>
|
||||||
|
</span>
|
||||||
|
<span t-if="props.component.depth">></span>
|
||||||
|
<span class="version" t-else="">owl=<t t-esc="props.component.version"/></span>
|
||||||
|
</div>
|
||||||
|
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="contextmenu">
|
||||||
|
<ul class="my-1">
|
||||||
|
<li t-on-click.stop="() => this.store.toggleComponentAndChildren(props.component, true)" class="custom-menu-item py-1 px-4">Expand children</li>
|
||||||
|
<li t-on-click.stop="() => this.store.toggleComponentAndChildren(props.component, false)" class="custom-menu-item py-1 px-4">Fold all children</li>
|
||||||
|
<li t-on-click.stop="() => this.store.foldDirectChildren(props.component)" class="custom-menu-item py-1 px-4">Fold direct children</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('source', props.component.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||||
|
<t t-if="props.component.path.length !== 1">
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.component.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||||
|
<li t-on-click.stop="() => this.store.refreshComponent(props.component.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.component.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.component.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<t t-if="props.component.toggled">
|
||||||
|
<t t-foreach="props.component.children" t-as="child" t-key="child.key">
|
||||||
|
<TreeElement component="child_value"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
const { Component } = owl;
|
||||||
|
import { ComponentsTab } from "./components_tab/components_tab";
|
||||||
|
import { Tab } from "./tab/tab";
|
||||||
|
import { ProfilerTab } from "./profiler_tab/profiler_tab";
|
||||||
|
import { useStore } from "../store/store";
|
||||||
|
|
||||||
|
export class DevtoolsWindow extends Component {
|
||||||
|
static props = [];
|
||||||
|
static template = "devtools.DevtoolsWindow";
|
||||||
|
static components = { ComponentsTab, Tab, ProfilerTab };
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
selectFrame(ev) {
|
||||||
|
const val = ev.target.value;
|
||||||
|
this.store.selectFrame(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.DevtoolsWindow" owl="1">
|
||||||
|
<div id="container" class="d-flex w-100 h-100 flex-column position-absolute overflow-hidden" t-on-mouseover.stop="() => this.store.removeHighlights()" t-on-mouseout.stop="() => this.store.removeHighlights()">
|
||||||
|
<t t-if="!store.extensionContextStatus">
|
||||||
|
<div class="status-message d-flex justify-content-center align-items-center">
|
||||||
|
Extension context is invalid. Please close the devtools and reload the page.
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-elif="store.owlStatus">
|
||||||
|
<div class="panel-top d-flex align-items-center custom-navbar">
|
||||||
|
<Tab tabName="'ComponentsTab'"/>
|
||||||
|
<Tab tabName="'ProfilerTab'"/>
|
||||||
|
<select t-if="store.frameUrls.length > 1" class="form-select form-select-sm custom-select navbar-select border-0" t-on-change="selectFrame">
|
||||||
|
<t t-foreach="store.frameUrls" t-as="frame" t-key="frame_index">
|
||||||
|
<option t-att-value="frame"><t t-esc="frame"/></option>
|
||||||
|
</t>
|
||||||
|
</select>
|
||||||
|
<i class="ms-auto p-1 me-1 lg-icon fa pointer-icon" title="Toggle dark mode" t-att-class="{ 'fa-sun-o': store.settings.darkMode, 'fa-moon-o' : !store.settings.darkMode}" t-on-click.stop="() => this.store.toggleDarkMode()"></i>
|
||||||
|
<i class="p-1 me-1 lg-icon fa fa-repeat pointer-icon" title="Refresh extension" t-on-click.stop="() => this.store.refreshExtension()"></i>
|
||||||
|
</div>
|
||||||
|
<ComponentsTab t-if="store.page === 'ComponentsTab'"/>
|
||||||
|
<ProfilerTab t-if="store.page === 'ProfilerTab'"/>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<div class="status-message d-flex justify-content-center align-items-center">
|
||||||
|
Owl is not loaded on this page.
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import { minimizeKey } from "../../../../utils";
|
||||||
|
import { useStore } from "../../../store/store";
|
||||||
|
|
||||||
|
const { Component, useEffect, useRef } = owl;
|
||||||
|
|
||||||
|
export class Event extends Component {
|
||||||
|
static template = "devtools.Event";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
this.componentContextMenu = useRef("componentContextmenu");
|
||||||
|
this.componentContextMenuId = this.store.contextMenu.id++;
|
||||||
|
this.contextMenuEvent,
|
||||||
|
useEffect(
|
||||||
|
(menuId) => {
|
||||||
|
if (menuId === this.componentContextMenuId) {
|
||||||
|
this.store.contextMenu.open(this.contextMenuEvent, this.componentContextMenu.el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => [this.store.contextMenu.activeMenu]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatting for displaying the key of the component
|
||||||
|
get minimizedKey() {
|
||||||
|
return minimizeKey(this.props.event.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same for the origin component
|
||||||
|
get originMinimizedKey() {
|
||||||
|
return minimizeKey(this.props.event.origin.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
get renderTime() {
|
||||||
|
if (Number.isInteger(this.props.event.time)) {
|
||||||
|
if (this.props.event.time === 0) {
|
||||||
|
return "<1";
|
||||||
|
} else {
|
||||||
|
return this.props.event.time;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.props.event.time < 0.1) {
|
||||||
|
return "<0.1";
|
||||||
|
} else {
|
||||||
|
return this.props.event.time.toFixed(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand/fold the event
|
||||||
|
toggleDisplay() {
|
||||||
|
if (this.props.event.origin) {
|
||||||
|
this.props.event.toggled = !this.props.event.toggled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openComponentMenu(ev) {
|
||||||
|
if (this.props.event.type === "destroy") {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.contextMenuEvent = ev;
|
||||||
|
this.store.contextMenu.activeMenu = this.componentContextMenuId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.Event" owl="1">
|
||||||
|
<div class="event-container">
|
||||||
|
<div class="my-0 p-0 object-line" t-on-click.stop="toggleDisplay">
|
||||||
|
<div class="ps-2 text-nowrap">
|
||||||
|
<i class="fa px-1 pointer-icon caret"
|
||||||
|
t-att-class="{'fa-caret-right': !props.event.toggled, 'fa-caret-down': props.event.toggled}"
|
||||||
|
t-attf-style="visibility: {{props.event.origin ? '' : 'hidden'}};"
|
||||||
|
/>
|
||||||
|
<t t-esc="props.event.type"/>:
|
||||||
|
<<span style="cursor:pointer; color: var(--component-color);"
|
||||||
|
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||||
|
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||||
|
t-on-contextmenu="openComponentMenu"
|
||||||
|
t-esc="props.event.component"
|
||||||
|
/>
|
||||||
|
<t t-if="minimizedKey.length > 0">
|
||||||
|
<span t-if="minimizedKey.length > 0" style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||||
|
<t t-esc="minimizedKey"/>
|
||||||
|
</span>
|
||||||
|
</t>>
|
||||||
|
<span>
|
||||||
|
(<t t-esc="renderTime"/>ms)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<t t-if="props.event.toggled">
|
||||||
|
<div class="my-0 pt-1 object-line">
|
||||||
|
<i class="fa fa-caret-right mx-1 pe-2" style="visibility: hidden;"></i>
|
||||||
|
<span>
|
||||||
|
origin:
|
||||||
|
<<span style="cursor:pointer; color: var(--component-color);"
|
||||||
|
t-on-click.stop="() => this.store.selectComponent(props.event.origin.path)"
|
||||||
|
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.origin.path)"
|
||||||
|
t-esc="props.event.origin.component"
|
||||||
|
/>
|
||||||
|
<t t-if="originMinimizedKey.length > 0">
|
||||||
|
<span style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||||
|
<t t-esc="originMinimizedKey"/>
|
||||||
|
</span>
|
||||||
|
</t>>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="componentContextmenu">
|
||||||
|
<ul class="my-1">
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('source', props.event.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||||
|
<t t-if="props.event.path.length !== 1">
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.event.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||||
|
<li t-on-click.stop="() => this.store.refreshComponent(props.event.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.event.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.event.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
import { minimizeKey } from "../../../../utils";
|
||||||
|
import { useStore } from "../../../store/store";
|
||||||
|
|
||||||
|
const { Component, useRef, useEffect } = owl;
|
||||||
|
|
||||||
|
export class EventNode extends Component {
|
||||||
|
static template = "devtools.EventNode";
|
||||||
|
|
||||||
|
static components = { EventNode };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
this.nodeContextMenu = useRef("nodeContextMenu");
|
||||||
|
this.nodeContextMenuId = this.store.contextMenu.id++;
|
||||||
|
this.componentContextMenu = useRef("componentContextmenu");
|
||||||
|
this.componentContextMenuId = this.store.contextMenu.id++;
|
||||||
|
this.contextMenuEvent,
|
||||||
|
useEffect(
|
||||||
|
(menuId) => {
|
||||||
|
if (menuId === this.nodeContextMenuId) {
|
||||||
|
this.store.contextMenu.open(this.contextMenuEvent, this.nodeContextMenu.el);
|
||||||
|
}
|
||||||
|
if (menuId === this.componentContextMenuId) {
|
||||||
|
this.store.contextMenu.open(this.contextMenuEvent, this.componentContextMenu.el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => [this.store.contextMenu.activeMenu]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
get eventPadding() {
|
||||||
|
return this.props.event.depth * 0.8 + 0.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
openNodeMenu(ev) {
|
||||||
|
if (this.props.event.children.length) {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.contextMenuEvent = ev;
|
||||||
|
this.store.contextMenu.activeMenu = this.nodeContextMenuId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
openComponentMenu(ev) {
|
||||||
|
if (this.props.event.type === "destroy") {
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
ev.preventDefault();
|
||||||
|
this.contextMenuEvent = ev;
|
||||||
|
this.store.contextMenu.activeMenu = this.componentContextMenuId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand/fold the event node
|
||||||
|
toggleDisplay() {
|
||||||
|
this.props.event.toggled = !this.props.event.toggled;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatting for displaying the key of the component
|
||||||
|
get minimizedKey() {
|
||||||
|
return minimizeKey(this.props.event.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
get renderTime() {
|
||||||
|
if (Number.isInteger(this.props.event.time)) {
|
||||||
|
if (this.props.event.time === 0) {
|
||||||
|
return "<1";
|
||||||
|
} else {
|
||||||
|
return this.props.event.time;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.props.event.time < 1.0) {
|
||||||
|
return "<1";
|
||||||
|
} else {
|
||||||
|
return this.props.event.time.toFixed(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+60
@@ -0,0 +1,60 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.EventNode" owl="1">
|
||||||
|
<div class="my-0 p-0 object-line"
|
||||||
|
t-on-click.stop="toggleDisplay"
|
||||||
|
t-on-contextmenu="openNodeMenu"
|
||||||
|
>
|
||||||
|
<div class="text-nowrap" t-attf-style="padding-left: {{eventPadding}}rem">
|
||||||
|
<i class="fa px-1 pointer-icon caret"
|
||||||
|
t-att-class="{'fa-caret-right': !props.event.toggled, 'fa-caret-down': props.event.toggled}"
|
||||||
|
t-attf-style="visibility: {{props.event.children.length > 0 ? '' : 'hidden'}};"
|
||||||
|
/>
|
||||||
|
<span>
|
||||||
|
<t t-esc="props.event.type"/>:
|
||||||
|
<<span style="cursor:pointer; color: var(--component-color);"
|
||||||
|
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||||
|
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||||
|
t-on-contextmenu.stop="openComponentMenu"
|
||||||
|
t-esc="props.event.component"/>
|
||||||
|
<t t-if="minimizedKey.length > 0">
|
||||||
|
<span t-if="minimizedKey.length > 0" style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||||
|
<t t-esc="minimizedKey"/>
|
||||||
|
</span>
|
||||||
|
</t>>
|
||||||
|
<span>
|
||||||
|
(<t t-esc="renderTime"/>ms)
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div t-if="store.contextMenu.activeMenu === nodeContextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="nodeContextMenu">
|
||||||
|
<ul class="my-1">
|
||||||
|
<li t-on-click.stop="() => this.store.toggleEventAndChildren(props.event, true)" class="custom-menu-item py-1 px-4">Expand children</li>
|
||||||
|
<li t-on-click.stop="() => this.store.toggleEventAndChildren(props.event, false)" class="custom-menu-item py-1 px-4">Fold all children</li>
|
||||||
|
<li t-on-click.stop="() => this.store.foldDirectChildren(props.event)" class="custom-menu-item py-1 px-4">Fold direct children</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-attf-style="top: {{store.contextMenu.top}}; left: {{store.contextMenu.left}}" t-ref="componentContextmenu">
|
||||||
|
<ul class="my-1">
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('source', props.event.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||||
|
<t t-if="props.event.path.length !== 1">
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.event.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||||
|
<li t-on-click.stop="() => this.store.refreshComponent(props.event.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.event.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||||
|
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.event.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<t t-if="props.event.toggled">
|
||||||
|
<t t-foreach="props.event.children" t-as="child" t-key="child.id">
|
||||||
|
<EventNode event="child"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
+23
@@ -0,0 +1,23 @@
|
|||||||
|
import { useStore } from "../../../store/store";
|
||||||
|
|
||||||
|
const { Component } = owl;
|
||||||
|
|
||||||
|
export class EventSearchBar extends Component {
|
||||||
|
static template = "devtools.EventSearchBar";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
// On keyup
|
||||||
|
updateSearch(event) {
|
||||||
|
if (!(event.keyCode === 13)) {
|
||||||
|
const search = event.target.value;
|
||||||
|
this.store.updateSearch(search);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clearSearch() {
|
||||||
|
this.store.updateSearch("");
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
@@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.EventSearchBar" owl="1">
|
||||||
|
<div class="search-bar-wrapper">
|
||||||
|
<i class="fa fa-search search-icon" aria-hidden="true"></i>
|
||||||
|
<input type="text" class="search-input" placeholder="Search" t-on-keyup.stop="updateSearch" t-att-value="store.eventSearch.search"/>
|
||||||
|
<t t-if="store.eventSearch.search.length > 0">
|
||||||
|
<i class="fa fa-times lg-icon utility-icon pe-2" t-on-click.stop="clearSearch"></i>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
const { Component } = owl;
|
||||||
|
import { useStore } from "../../store/store";
|
||||||
|
import { Event } from "./event/event";
|
||||||
|
import { EventNode } from "./event_node/event_node";
|
||||||
|
import { EventSearchBar } from "./event_search_bar/event_search_bar";
|
||||||
|
|
||||||
|
export class ProfilerTab extends Component {
|
||||||
|
static template = "devtools.ProfilerTab";
|
||||||
|
|
||||||
|
static components = { Event, EventNode, EventSearchBar };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
showHelp() {
|
||||||
|
return this.store.events.length < 1 && !this.store.activeRecorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectDisplayMode(ev) {
|
||||||
|
const val = ev.target.value;
|
||||||
|
if (val === "Tree") {
|
||||||
|
this.store.buildEventsTree();
|
||||||
|
this.store.eventsTreeView = true;
|
||||||
|
} else {
|
||||||
|
this.store.eventsTreeView = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.ProfilerTab" owl="1">
|
||||||
|
<div class="position-relative overflow-hidden d-flex flex-column h-100">
|
||||||
|
<div class="panel-top d-flex align-items-center">
|
||||||
|
<i title="Start/Stop Recording" class="fa fa-circle pointer-icon ms-1 p-1" t-attf-style="color: {{store.activeRecorder ? 'var(--active-recorder)' : 'var(--text-color)'}};" t-on-click.stop="() => this.store.toggleRecording()" aria-hidden="true"></i>
|
||||||
|
<i title="Clear events" class="fa fa-ban pointer-icon p-1 px-2" t-on-click.stop="() => this.store.clearEventsConsole()" aria-hidden="true"></i>
|
||||||
|
<div class="icons-separator mx-1"/>
|
||||||
|
<select class="form-select form-select-sm custom-select border-0" t-on-change="selectDisplayMode">
|
||||||
|
<option t-att-selected="store.eventsTreeView" value="Tree">Tree view</option>
|
||||||
|
<option t-att-selected="!store.eventsTreeView" value="List">Events log</option>
|
||||||
|
</select>
|
||||||
|
<i title="Collapse All" class="fa fa-list me-1" t-on-click="() => this.store.collapseAll()"></i>
|
||||||
|
<div class="icons-separator"/>
|
||||||
|
<label class="mx-2 form-check-label pointer-icon" title="Trace renderings in console">
|
||||||
|
<input type="checkbox" class="form-check-input me-1" t-att-checked="store.traceRenderings" t-on-input="() => this.store.toggleTracing()"/> Trace Renderings
|
||||||
|
</label>
|
||||||
|
<label class="mx-2 form-check-label pointer-icon" title="Trace subscriptions in console (warning: it is VERY verbose)">
|
||||||
|
<input type="checkbox" class="form-check-input me-1" t-att-checked="store.traceSubscriptions" t-on-input="() => this.store.toggleSubscriptionTracing()"/> Trace Subscriptions
|
||||||
|
</label>
|
||||||
|
<!-- <EventSearchBar/> -->
|
||||||
|
</div>
|
||||||
|
<div class="events-container h-100 font-monospace">
|
||||||
|
<t t-if="showHelp()">
|
||||||
|
<div class="status-message d-flex justify-content-center align-items-center">
|
||||||
|
<div>
|
||||||
|
Click on the <i class="fa fa-circle" style="font-size: 0.8em;"></i> button to start recording events.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<t t-if="store.eventsTreeView">
|
||||||
|
<t t-foreach="store.eventsTree" t-as="event" t-key="event.id">
|
||||||
|
<EventNode event="event"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
<t t-else="">
|
||||||
|
<t t-foreach="store.events" t-as="event" t-key="event_index">
|
||||||
|
<Event event="event"/>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { useStore } from "../../store/store";
|
||||||
|
|
||||||
|
const { Component } = owl;
|
||||||
|
|
||||||
|
export class Tab extends Component {
|
||||||
|
static props = ["tabName"];
|
||||||
|
|
||||||
|
static template = "devtools.Tab";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.store = useStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
get active() {
|
||||||
|
return this.props.tabName === this.store.page;
|
||||||
|
}
|
||||||
|
|
||||||
|
get name() {
|
||||||
|
switch (this.props.tabName) {
|
||||||
|
case "ComponentsTab":
|
||||||
|
return "Components";
|
||||||
|
case "ProfilerTab":
|
||||||
|
return "Profiler";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectTab(ev) {
|
||||||
|
this.store.switchTab(this.props.tabName);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="devtools.Tab" owl="1">
|
||||||
|
<div t-on-click="selectTab" class="navbar-btn d-block" t-att-class="active
|
||||||
|
? 'btn-selected'
|
||||||
|
: ''">
|
||||||
|
<t t-esc="name" />
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,983 @@
|
|||||||
|
const { reactive, useState, toRaw } = owl;
|
||||||
|
import { fuzzySearch, IS_FIREFOX, getActiveTabURL } from "../../utils";
|
||||||
|
import globalHook from "../../page_scripts/owl_devtools_global_hook";
|
||||||
|
|
||||||
|
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||||
|
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
contextMenu: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
id: 0,
|
||||||
|
activeMenu: -1,
|
||||||
|
// Opens the context menu corresponding with the given menu html element
|
||||||
|
open(event, menu) {
|
||||||
|
const menuWidth = menu.offsetWidth;
|
||||||
|
const menuHeight = menu.offsetHeight;
|
||||||
|
let x = event.clientX;
|
||||||
|
let y = event.clientY;
|
||||||
|
if (x + menuWidth > window.innerWidth) {
|
||||||
|
x = window.innerWidth - menuWidth;
|
||||||
|
}
|
||||||
|
if (y + menuHeight > window.innerHeight) {
|
||||||
|
y = window.innerHeight - menuHeight;
|
||||||
|
}
|
||||||
|
this.left = x + "px";
|
||||||
|
// Need 25px offset because of the main navbar from the browser devtools
|
||||||
|
this.top = y - 25 + "px";
|
||||||
|
},
|
||||||
|
// Close the currently displayed context menu
|
||||||
|
close() {
|
||||||
|
this.activeMenu = -1;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
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: [] },
|
||||||
|
version: "1.0",
|
||||||
|
},
|
||||||
|
selectedElement: null,
|
||||||
|
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);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
const apps = await evalFunctionInWindow(
|
||||||
|
"getComponentsTree",
|
||||||
|
fromOld && this.activeComponent ? [this.activeComponent.path, this.apps] : [],
|
||||||
|
this.activeFrame
|
||||||
|
);
|
||||||
|
this.apps = apps ? apps : [];
|
||||||
|
if (!fromOld && this.settings.expandByDefault) {
|
||||||
|
this.apps.forEach((tree) => expandNodes(tree));
|
||||||
|
}
|
||||||
|
const component = await evalFunctionInWindow(
|
||||||
|
"getComponentDetails",
|
||||||
|
fromOld && this.activeComponent ? [this.activeComponent.path, this.activeComponent] : [],
|
||||||
|
this.activeFrame
|
||||||
|
);
|
||||||
|
this.activeComponent = component;
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
const details = await evalFunctionInWindow(
|
||||||
|
"getComponentDetails",
|
||||||
|
[component.path],
|
||||||
|
this.activeFrame
|
||||||
|
);
|
||||||
|
this.activeComponent = details;
|
||||||
|
if (!this.activeComponent) {
|
||||||
|
await this.loadComponentsTree(false);
|
||||||
|
}
|
||||||
|
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) {
|
||||||
|
if (!obj.hasChildren) {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
evalInWindow(
|
||||||
|
`__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = ${
|
||||||
|
store.devtoolsId
|
||||||
|
}; __OWL__DEVTOOLS_GLOBAL_HOOK__.frame = ${JSON.stringify(frame)};`,
|
||||||
|
frame
|
||||||
|
);
|
||||||
|
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
|
||||||
|
resetData() {
|
||||||
|
this.loadComponentsTree(false);
|
||||||
|
this.events = [];
|
||||||
|
this.eventsTree = [];
|
||||||
|
this.activeRecorder = false;
|
||||||
|
evalFunctionInWindow("toggleEventsRecording", [false, 0]);
|
||||||
|
evalFunctionInWindow("toggleTracing", [false]);
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
this.resetData();
|
||||||
|
},
|
||||||
|
|
||||||
|
// 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");
|
||||||
|
}
|
||||||
|
browserInstance.storage.local.set({ owl_devtools_dark_mode: this.settings.darkMode });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Instantiate the store
|
||||||
|
export function useStore() {
|
||||||
|
return useState(store);
|
||||||
|
}
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
store.devtoolsId = await getTabURL();
|
||||||
|
|
||||||
|
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||||
|
|
||||||
|
// 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
|
||||||
|
document.addEventListener("click", () => store.contextMenu.close(), { capture: true });
|
||||||
|
document.addEventListener("contextmenu", () => store.contextMenu.close(), { capture: true });
|
||||||
|
|
||||||
|
// Make sure the events recorder is at its initial state in every frame
|
||||||
|
for (const frame of store.frameUrls) {
|
||||||
|
evalFunctionInWindow("toggleEventsRecording", [false, 0], frame);
|
||||||
|
}
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Heartbeat message to test whether the extension context is still valid or not
|
||||||
|
setInterval(() => {
|
||||||
|
if (store.extensionContextStatus) {
|
||||||
|
try {
|
||||||
|
browserInstance.runtime.sendMessage({ type: "keepAlive" });
|
||||||
|
} catch (e) {
|
||||||
|
store.extensionContextStatus = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
let flushRendersTimeout = false;
|
||||||
|
// Connect to the port to communicate to the background script
|
||||||
|
browserInstance.runtime.onConnect.addListener((port) => {
|
||||||
|
if (!port.name === "OwlDevtoolsPort") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
port.onMessage.addListener(async (msg) => {
|
||||||
|
// Reload the tree after checking if the scripts are loaded when this message is received
|
||||||
|
if (msg.type === "Reload") {
|
||||||
|
const tab = await getTabURL();
|
||||||
|
// Since this message is sent to all devtools windows, only take it into account when this is the active tab
|
||||||
|
if (tab !== store.devtoolsId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
|
||||||
|
if (store.owlStatus) {
|
||||||
|
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||||
|
store.resetData();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
|
||||||
|
if (msg.type === "FrameReady") {
|
||||||
|
const tab = await getTabURL();
|
||||||
|
// Same as for the reload message
|
||||||
|
if (tab !== store.devtoolsId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
store.updateIFrameList();
|
||||||
|
store.owlStatus = true;
|
||||||
|
store.resetData();
|
||||||
|
}
|
||||||
|
// We need to reload the components tree when the set of apps in the page is modified
|
||||||
|
if (msg.type === "RefreshApps") {
|
||||||
|
store.loadComponentsTree(true);
|
||||||
|
}
|
||||||
|
// Filter out the messages that are not destined to this devtools tab. The messages above may be sent before
|
||||||
|
// the devtoolsId is set
|
||||||
|
if (msg.origin.id !== store.devtoolsId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// When message of type Flush is received, overwrite the component tree with the new one from page
|
||||||
|
// A flush message is sent everytime a component is rendered on the page
|
||||||
|
if (msg.type === "Flush") {
|
||||||
|
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));
|
||||||
|
clearTimeout(flushRendersTimeout);
|
||||||
|
flushRendersTimeout = setTimeout(() => {
|
||||||
|
store.renderPaths.clear();
|
||||||
|
}, 100);
|
||||||
|
store.loadComponentsTree(true);
|
||||||
|
}
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
// Stop the DOM element selector tool upon receiving the StopSelector message
|
||||||
|
if (msg.type === "StopSelector") {
|
||||||
|
store.componentSearch.activeSelector = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for recording an event when the event message is received
|
||||||
|
if (msg.type === "Event") {
|
||||||
|
let events = msg.data;
|
||||||
|
loadEvents(events);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Load all settings from the chrome sync storage
|
||||||
|
async function loadSettings() {
|
||||||
|
let storage = await browserInstance.storage.local.get();
|
||||||
|
if (storage.owl_devtools_dark_mode === undefined) {
|
||||||
|
// Load dark mode based on the global settings of the chrome devtools
|
||||||
|
darkMode = browserInstance.devtools.panels.themeName === "dark";
|
||||||
|
} else {
|
||||||
|
darkMode = storage.owl_devtools_dark_mode;
|
||||||
|
}
|
||||||
|
store.settings.darkMode = darkMode;
|
||||||
|
if (darkMode) {
|
||||||
|
document.querySelector("html").classList.add("dark-mode");
|
||||||
|
} else {
|
||||||
|
document.querySelector("html").classList.remove("dark-mode");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
function expandNodes(node) {
|
||||||
|
node.toggled = true;
|
||||||
|
for (const child of node.children) {
|
||||||
|
expandNodes(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
// This happens in firefox when the method is called inside devtools so we ask the background to execute it instead
|
||||||
|
browserInstance.runtime.sendMessage({ type: "getActiveTabURL" }).then((response) => {
|
||||||
|
return response.result;
|
||||||
|
});
|
||||||
|
} 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,291 @@
|
|||||||
|
@font-face {
|
||||||
|
font-family: 'Fira Mono';
|
||||||
|
src: url('../assets/FiraMono-Medium.ttf') format('truetype');
|
||||||
|
}
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--border-color: lightgray;
|
||||||
|
--navbar-selected: #5f9cf0;
|
||||||
|
--component-hover: #d4e3f2;
|
||||||
|
--bs-font-monospace: "Fira Mono";
|
||||||
|
--text-color: #494949;
|
||||||
|
--object-color: rgb(119, 34, 255);
|
||||||
|
--prototype-color: rgb(179 133 255);
|
||||||
|
--text-proto-color: #818181;
|
||||||
|
--highlight-color: yellow;
|
||||||
|
--component-color: rgb(48, 74, 219);
|
||||||
|
--key-name: rgb(192 166 118);
|
||||||
|
--key-content: rgb(141 112 188 / 59%);
|
||||||
|
--subscription-key: rgb(34, 49, 255);
|
||||||
|
--component-highlighted: #f0f8fb;
|
||||||
|
--component-selected: #d4e3f2;
|
||||||
|
--active-icon: rgb(41, 134, 255);
|
||||||
|
--active-recorder: rgb(255, 31, 31);
|
||||||
|
--navbar-bg: #f1f3f4;
|
||||||
|
--background-color: white;
|
||||||
|
--text-selected: white;
|
||||||
|
--menu-highlight-bg: rgb(201, 201, 201);
|
||||||
|
--version-bg: teal;
|
||||||
|
/* to change the color here, put it in stroke='%23[color in hexadecimal]' */
|
||||||
|
--select-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23444444' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-mode {
|
||||||
|
--border-color: #838383;
|
||||||
|
--navbar-selected: #33a3ff;
|
||||||
|
--component-hover: #597179;
|
||||||
|
--text-color: #c9c9c9;
|
||||||
|
--object-color: rgb(188 147 255);
|
||||||
|
--prototype-color: rgb(134 105 180);
|
||||||
|
--text-proto-color: #818181;
|
||||||
|
--highlight-color: yellow;
|
||||||
|
--component-color: rgb(135 153 255);
|
||||||
|
--key-name: rgb(171 212 253 / 51%);
|
||||||
|
--key-content: rgb(227 213 252 / 60%);
|
||||||
|
--subscription-key: rgb(154 161 255);
|
||||||
|
--component-highlighted: #525d61;
|
||||||
|
--component-selected: #287e99;
|
||||||
|
--active-icon: rgb(93 163 255);
|
||||||
|
--active-recorder: rgb(255, 31, 31);
|
||||||
|
--navbar-bg: #484a4b;
|
||||||
|
--background-color: rgb(45 45 45);
|
||||||
|
--text-selected: #c9c9c9;
|
||||||
|
--menu-highlight-bg: rgb(65, 65, 65);
|
||||||
|
--version-bg: #805900ad;
|
||||||
|
color-scheme: dark;
|
||||||
|
/* to change the color here, put it in stroke='%23[color in hexadecimal]' */
|
||||||
|
--select-icon: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23c9c9c9' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M2 5l6 6 6-6'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
|
||||||
|
.caret {
|
||||||
|
width: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#container {
|
||||||
|
font-size: 0.75em;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-message {
|
||||||
|
min-height: 70vh;
|
||||||
|
font-size: 1.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-navbar {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 0 !important;
|
||||||
|
background: var(--navbar-bg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
color: var(--text-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-btn {
|
||||||
|
height: 23px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
padding-top: 0.2em;
|
||||||
|
color: var(--text-color);
|
||||||
|
border-right: solid 1px lightgrey;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-btn:hover {
|
||||||
|
color: var(--navbar-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-selected {
|
||||||
|
background-color: var(--navbar-selected);
|
||||||
|
color: var(--text-selected);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-selected:hover{
|
||||||
|
color: var(--text-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
#tree-wrapper {
|
||||||
|
min-width: max-content;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-component:hover {
|
||||||
|
background-color: var(--component-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-line.attenuate {
|
||||||
|
color: var(--text-proto-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-content {
|
||||||
|
color: var(--object-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.object-content.attenuate {
|
||||||
|
color: var(--prototype-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.event-container {
|
||||||
|
border-bottom: 1px solid rgb(240, 238, 238);
|
||||||
|
padding-top: 2px!important;
|
||||||
|
padding-bottom: 2px!important;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.getter-content:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.render-highlight {
|
||||||
|
background-color: var(--highlight-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-fade {
|
||||||
|
transition: background-color 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.highlight-search {
|
||||||
|
background-color: var(--highlight-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-name {
|
||||||
|
color: var(--key-content);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-select {
|
||||||
|
max-width: max-content;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
color: var(--text-color) !important;
|
||||||
|
background-color: var(--background-color);
|
||||||
|
background-image: var(--select-icon);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar-select {
|
||||||
|
font-size: .7rem !important;
|
||||||
|
padding-top: 0.35rem !important;
|
||||||
|
border-right: solid 1px var(--border-color) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:focus {
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-panel {
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-panel:first-child {
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-highlighted {
|
||||||
|
background-color: var(--component-highlighted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-selected {
|
||||||
|
background-color: var(--component-selected);
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-screen-border {
|
||||||
|
width: 10px;
|
||||||
|
cursor: col-resize;
|
||||||
|
margin: 0px -5px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.blank-space {
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.key-content {
|
||||||
|
color: var(--subscription-key);
|
||||||
|
}
|
||||||
|
|
||||||
|
.version {
|
||||||
|
margin-left: 5px;
|
||||||
|
background-color: var(--version-bg);
|
||||||
|
color: white;
|
||||||
|
padding: 2px 3px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-top {
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.events-container {
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.split-screen-right {
|
||||||
|
margin: 0;
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
background-color: var(--background-color);
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon {
|
||||||
|
position: relative;
|
||||||
|
bottom: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.utility-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.lg-icon {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-caret-right {
|
||||||
|
font-size: 1.15em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icons-separator {
|
||||||
|
width: 1px;
|
||||||
|
height: 17px;
|
||||||
|
background-color: var(--border-color);
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pointer-icon {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu {
|
||||||
|
position: absolute;
|
||||||
|
color: var(--text-color);
|
||||||
|
background-color: var(--background-color);
|
||||||
|
border: 1px solid gray;
|
||||||
|
z-index: 1;
|
||||||
|
box-shadow: 1px 2px 5px #888;
|
||||||
|
font-family: var(--bs-font-sans-serif);
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-menu-item:hover {
|
||||||
|
background-color: var(--menu-highlight-bg);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,11 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="stylesheet" href="../assets/bootstrap.min.css">
|
||||||
|
<link rel="stylesheet" href="main.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script src="../assets/owl.js"></script>
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import { templates } from "../../assets/templates.js";
|
||||||
|
import { getOwlStatus } from "../utils";
|
||||||
|
const { Component, useState, onWillStart, mount, App } = owl;
|
||||||
|
|
||||||
|
class PopUpApp extends Component {
|
||||||
|
static template = "popup.PopUpApp";
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.state = useState({ status: 0 });
|
||||||
|
onWillStart(async () => {
|
||||||
|
try {
|
||||||
|
this.state.status = await getOwlStatus();
|
||||||
|
} catch (e) {
|
||||||
|
this.state.status = -1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const template in templates) {
|
||||||
|
App.registerTemplate(template, templates[template]);
|
||||||
|
}
|
||||||
|
mount(PopUpApp, document.body, { dev: true });
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="popup.PopUpApp">
|
||||||
|
<div class="container m-0 p-4 text-white bg-dark" style="width: 370px">
|
||||||
|
<p t-if="state.status === 1">
|
||||||
|
Owl is detected on this page but the version seems to be outdated.
|
||||||
|
Please upgrade to a newer version in order to use the devtools.
|
||||||
|
</p>
|
||||||
|
<p t-elif="state.status === 2">
|
||||||
|
Owl is detected on this page.
|
||||||
|
Open DevTools and look for the Owl panel.
|
||||||
|
</p>
|
||||||
|
<p t-else="">
|
||||||
|
Owl is not detected on this page.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
export const IS_FIREFOX = navigator.userAgent.indexOf("Firefox") !== -1;
|
||||||
|
|
||||||
|
const browserInstance = IS_FIREFOX ? browser : chrome;
|
||||||
|
|
||||||
|
export async function getOwlStatus() {
|
||||||
|
const response = await browserInstance.runtime.sendMessage({ type: "getOwlStatus" });
|
||||||
|
return response.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getActiveTabURL() {
|
||||||
|
const [tab] = await browserInstance.tabs.query({ active: true, lastFocusedWindow: true });
|
||||||
|
return tab.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// inspired from https://www.tutorialspoint.com/fuzzy-search-algorithm-in-javascript
|
||||||
|
// Check if the query matches with the base in a fuzzy search way
|
||||||
|
export function fuzzySearch(baseString, queryString) {
|
||||||
|
const base = baseString.toLowerCase();
|
||||||
|
const query = queryString.toLowerCase();
|
||||||
|
let queryIndex = 0;
|
||||||
|
let baseIndex = -1;
|
||||||
|
let character;
|
||||||
|
// Loop through each character in the query string
|
||||||
|
while ((character = query[queryIndex++])) {
|
||||||
|
// Find the index of the character in the base string, starting from the previous index plus 1
|
||||||
|
baseIndex = base.indexOf(character, baseIndex + 1);
|
||||||
|
// If the character is not found, return false
|
||||||
|
if (baseIndex === -1) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// All characters in the query string were found in the base string, so return true
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the given element is vertically centered in the user view (between 25 and 75% of the height)
|
||||||
|
export function isElementInCenterViewport(el) {
|
||||||
|
const rect = el.getBoundingClientRect();
|
||||||
|
return (
|
||||||
|
rect.top >= 0 &&
|
||||||
|
rect.bottom >= 0.25 * (window.innerHeight || document.documentElement.clientHeight) &&
|
||||||
|
rect.bottom <= 0.75 * (window.innerHeight || document.documentElement.clientHeight)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatting for displaying the key of the component
|
||||||
|
export function minimizeKey(key) {
|
||||||
|
if (key.startsWith("__")) {
|
||||||
|
const split = key.split("__");
|
||||||
|
if (split.length > 2) {
|
||||||
|
key = key.substring(4 + split[1].length, key.length);
|
||||||
|
} else {
|
||||||
|
key = "";
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
|
return key;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user