mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a5a1e61dd |
@@ -15,3 +15,11 @@ class SomeComponent extends Component {
|
|||||||
The `t-portal` directive takes a valid css selector as argument. The content of
|
The `t-portal` directive takes a valid css selector as argument. The content of
|
||||||
the portalled template will be mounted at the corresponding location. Note that
|
the portalled template will be mounted at the corresponding location. Note that
|
||||||
Owl need to insert an empty text node at the location of the portalled content.
|
Owl need to insert an empty text node at the location of the portalled content.
|
||||||
|
|
||||||
|
The `t-portal` directive supports a `.closest` modifier. It is useful to select
|
||||||
|
the closest target from the portal location: Owl will look for a target in the
|
||||||
|
current parent element, then in its parent, and so on until it finds it.
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<div t-portal.closest="'.target'">some content</div>
|
||||||
|
```
|
||||||
|
|||||||
+2
-1
@@ -10,7 +10,7 @@
|
|||||||
<link rel="stylesheet" href="assets/milligram.css">
|
<link rel="stylesheet" href="assets/milligram.css">
|
||||||
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
|
<link rel="stylesheet" href="assets/highlight.tomorrow.css">
|
||||||
<link rel="stylesheet" href="assets/main.css">
|
<link rel="stylesheet" href="assets/main.css">
|
||||||
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
|
<script src="./owl.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<header class="container">
|
<header class="container">
|
||||||
@@ -68,6 +68,7 @@
|
|||||||
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
|
<p><a href=".">OWL</a> is licensed under LGPLv3.<br>Logo from <a href="https://github.com/googlefonts/noto-emoji">Google Noto Emoji Font</a>, licensed under Apache License 2.0</p>
|
||||||
</footer>
|
</footer>
|
||||||
<script src="assets/highlight.pack.js"></script>
|
<script src="assets/highlight.pack.js"></script>
|
||||||
|
<script type="importmap">{ "imports": { "@odoo/owl": "./owl.js" } }</script>
|
||||||
<script type="module" src="display_code.js"></script>
|
<script type="module" src="display_code.js"></script>
|
||||||
<script type="module" src="counter.js"></script>
|
<script type="module" src="counter.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+18
-50
@@ -175,21 +175,11 @@ function createAttrUpdater(attr) {
|
|||||||
}
|
}
|
||||||
function attrsSetter(attrs) {
|
function attrsSetter(attrs) {
|
||||||
if (isArray(attrs)) {
|
if (isArray(attrs)) {
|
||||||
if (attrs[0] === "class") {
|
setAttribute.call(this, attrs[0], attrs[1]);
|
||||||
setClass.call(this, attrs[1]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setAttribute.call(this, attrs[0], attrs[1]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
for (let k in attrs) {
|
for (let k in attrs) {
|
||||||
if (k === "class") {
|
setAttribute.call(this, k, attrs[k]);
|
||||||
setClass.call(this, attrs[k]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setAttribute.call(this, k, attrs[k]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,12 +191,7 @@ function attrsUpdater(attrs, oldAttrs) {
|
|||||||
if (val === oldAttrs[1]) {
|
if (val === oldAttrs[1]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === "class") {
|
setAttribute.call(this, name, val);
|
||||||
updateClass.call(this, val, oldAttrs[1]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setAttribute.call(this, name, val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
removeAttribute.call(this, oldAttrs[0]);
|
removeAttribute.call(this, oldAttrs[0]);
|
||||||
@@ -216,23 +201,13 @@ function attrsUpdater(attrs, oldAttrs) {
|
|||||||
else {
|
else {
|
||||||
for (let k in oldAttrs) {
|
for (let k in oldAttrs) {
|
||||||
if (!(k in attrs)) {
|
if (!(k in attrs)) {
|
||||||
if (k === "class") {
|
removeAttribute.call(this, k);
|
||||||
updateClass.call(this, "", oldAttrs[k]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
removeAttribute.call(this, k);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let k in attrs) {
|
for (let k in attrs) {
|
||||||
const val = attrs[k];
|
const val = attrs[k];
|
||||||
if (val !== oldAttrs[k]) {
|
if (val !== oldAttrs[k]) {
|
||||||
if (k === "class") {
|
setAttribute.call(this, k, val);
|
||||||
updateClass.call(this, val, oldAttrs[k]);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setAttribute.call(this, k, val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3900,10 +3875,6 @@ class CodeGenerator {
|
|||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
translate(str) {
|
|
||||||
const match = translationRE.exec(str);
|
|
||||||
return match[1] + this.translateFn(match[2]) + match[3];
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* @returns the newly created block name, if any
|
* @returns the newly created block name, if any
|
||||||
*/
|
*/
|
||||||
@@ -3981,7 +3952,8 @@ class CodeGenerator {
|
|||||||
let { block, forceNewBlock } = ctx;
|
let { block, forceNewBlock } = ctx;
|
||||||
let value = ast.value;
|
let value = ast.value;
|
||||||
if (value && ctx.translate !== false) {
|
if (value && ctx.translate !== false) {
|
||||||
value = this.translate(value);
|
const match = translationRE.exec(value);
|
||||||
|
value = match[1] + this.translateFn(match[2]) + match[3];
|
||||||
}
|
}
|
||||||
if (!ctx.inPreTag) {
|
if (!ctx.inPreTag) {
|
||||||
value = value.replace(whitespaceRE, " ");
|
value = value.replace(whitespaceRE, " ");
|
||||||
@@ -4522,12 +4494,11 @@ class CodeGenerator {
|
|||||||
else {
|
else {
|
||||||
let value;
|
let value;
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
|
||||||
if (ast.value) {
|
if (ast.value) {
|
||||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
value = `\`${defaultValue}\``;
|
value = `\`${ast.defaultValue}\``;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -4908,10 +4879,10 @@ function parseDOMNode(node, ctx) {
|
|||||||
let model = null;
|
let model = null;
|
||||||
for (let attr of nodeAttrsNames) {
|
for (let attr of nodeAttrsNames) {
|
||||||
const value = node.getAttribute(attr);
|
const value = node.getAttribute(attr);
|
||||||
if (attr === "t-on" || attr === "t-on-") {
|
if (attr.startsWith("t-on")) {
|
||||||
throw new OwlError("Missing event name with t-on directive");
|
if (attr === "t-on") {
|
||||||
}
|
throw new OwlError("Missing event name with t-on directive");
|
||||||
if (attr.startsWith("t-on-")) {
|
}
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attr.slice(5)] = value;
|
on[attr.slice(5)] = value;
|
||||||
}
|
}
|
||||||
@@ -5535,7 +5506,7 @@ function compile(template, options = {}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do not modify manually. This file is generated by the release script.
|
// do not modify manually. This file is generated by the release script.
|
||||||
const version = "2.1.3";
|
const version = "2.1.2";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Scheduler
|
// Scheduler
|
||||||
@@ -5614,8 +5585,6 @@ window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = {
|
|||||||
apps: new Set(),
|
apps: new Set(),
|
||||||
Fiber: Fiber,
|
Fiber: Fiber,
|
||||||
RootFiber: RootFiber,
|
RootFiber: RootFiber,
|
||||||
toRaw: toRaw,
|
|
||||||
reactive: reactive,
|
|
||||||
});
|
});
|
||||||
class App extends TemplateSet {
|
class App extends TemplateSet {
|
||||||
constructor(Root, config = {}) {
|
constructor(Root, config = {}) {
|
||||||
@@ -5868,9 +5837,8 @@ function useChildSubEnv(envExtension) {
|
|||||||
* will run a cleanup function before patching and before unmounting the
|
* will run a cleanup function before patching and before unmounting the
|
||||||
* the component.
|
* the component.
|
||||||
*
|
*
|
||||||
* @template T
|
* @param {Effect} effect the effect to run on component mount and/or patch
|
||||||
* @param {Effect<T>} effect the effect to run on component mount and/or patch
|
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||||
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
|
|
||||||
* dependencies that will decide if the effect needs to be cleaned up and
|
* dependencies that will decide if the effect needs to be cleaned up and
|
||||||
* run again. If the dependencies did not change, the effect will not run
|
* run again. If the dependencies did not change, the effect will not run
|
||||||
* again. The default value returns an array containing only NaN because
|
* again. The default value returns an array containing only NaN because
|
||||||
@@ -5952,6 +5920,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
|||||||
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
export { App, Component, EventBus, OwlError, __info__, blockDom, loadFile, markRaw, markup, mount, onError, onMounted, onPatched, onRendered, onWillDestroy, onWillPatch, onWillRender, onWillStart, onWillUnmount, onWillUpdateProps, reactive, status, toRaw, useChildSubEnv, useComponent, useEffect, useEnv, useExternalListener, useRef, useState, useSubEnv, validate, validateType, whenReady, xml };
|
||||||
|
|
||||||
|
|
||||||
__info__.date = '2023-06-28T09:17:13.630Z';
|
__info__.date = '2023-04-29T07:45:54.333Z';
|
||||||
__info__.hash = '432ff44';
|
__info__.hash = 'aabb755';
|
||||||
__info__.url = 'https://github.com/odoo/owl';
|
__info__.url = 'https://github.com/odoo/owl';
|
||||||
|
|||||||
Generated
+4
-4
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.1.4",
|
"version": "2.1.3",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5594,9 +5594,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"tough-cookie": {
|
"tough-cookie": {
|
||||||
"version": "4.1.3",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.2.tgz",
|
||||||
"integrity": "sha512-aX/y5pVRkfRnfmuX+OdbSdXvPe6ieKX/G2s7e98f4poJHnqH3281gDPm/metm6E/WRamfx7WC4HUqkWHfQHprw==",
|
"integrity": "sha512-G9fqXWoYFZgTc2z8Q5zaHy/vJMjm+WV0AkAeHxVCQiEB1b+dGvWzFW6QV07cY5jQ5gRkeid2qIkzkxUnmoQZUQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"requires": {
|
"requires": {
|
||||||
"psl": "^1.1.33",
|
"psl": "^1.1.33",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.1.4",
|
"version": "2.1.3",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"module": "dist/owl.es.js",
|
"module": "dist/owl.es.js",
|
||||||
|
|||||||
@@ -447,11 +447,6 @@ export class CodeGenerator {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(str: string): string {
|
|
||||||
const match = translationRE.exec(str) as any;
|
|
||||||
return match[1] + this.translateFn(match[2]) + match[3];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @returns the newly created block name, if any
|
* @returns the newly created block name, if any
|
||||||
*/
|
*/
|
||||||
@@ -532,7 +527,8 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
let value = ast.value;
|
let value = ast.value;
|
||||||
if (value && ctx.translate !== false) {
|
if (value && ctx.translate !== false) {
|
||||||
value = this.translate(value);
|
const match = translationRE.exec(value) as any;
|
||||||
|
value = match[1] + this.translateFn(match[2]) + match[3];
|
||||||
}
|
}
|
||||||
if (!ctx.inPreTag) {
|
if (!ctx.inPreTag) {
|
||||||
value = value.replace(whitespaceRE, " ");
|
value = value.replace(whitespaceRE, " ");
|
||||||
@@ -1099,11 +1095,10 @@ export class CodeGenerator {
|
|||||||
} else {
|
} else {
|
||||||
let value: string;
|
let value: string;
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
|
||||||
if (ast.value) {
|
if (ast.value) {
|
||||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||||
} else {
|
} else {
|
||||||
value = `\`${defaultValue}\``;
|
value = `\`${ast.defaultValue}\``;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
value = expr;
|
value = expr;
|
||||||
@@ -1376,7 +1371,9 @@ export class CodeGenerator {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const target = compileExpr(ast.target);
|
const target = compileExpr(ast.target);
|
||||||
const blockString = `${id}({target: ${target},slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
|
const blockString = `${id}({target: ${target},${
|
||||||
|
ast.isClosest ? "isClosest: true," : ""
|
||||||
|
}slots: {'default': {__render: ${name}.bind(this), __ctx: ${ctxStr}}}}, key + \`${key}\`, node, ctx, Portal)`;
|
||||||
if (block) {
|
if (block) {
|
||||||
this.insertAnchor(block);
|
this.insertAnchor(block);
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-7
@@ -169,6 +169,7 @@ export interface ASTTranslation {
|
|||||||
export interface ASTTPortal {
|
export interface ASTTPortal {
|
||||||
type: ASTType.TPortal;
|
type: ASTType.TPortal;
|
||||||
target: string;
|
target: string;
|
||||||
|
isClosest: boolean;
|
||||||
content: AST;
|
content: AST;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,10 +337,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
|
|
||||||
for (let attr of nodeAttrsNames) {
|
for (let attr of nodeAttrsNames) {
|
||||||
const value = node.getAttribute(attr)!;
|
const value = node.getAttribute(attr)!;
|
||||||
if (attr === "t-on" || attr === "t-on-") {
|
if (attr.startsWith("t-on")) {
|
||||||
throw new OwlError("Missing event name with t-on directive");
|
if (attr === "t-on") {
|
||||||
}
|
throw new OwlError("Missing event name with t-on directive");
|
||||||
if (attr.startsWith("t-on-")) {
|
}
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attr.slice(5)] = value;
|
on[attr.slice(5)] = value;
|
||||||
} else if (attr.startsWith("t-model")) {
|
} else if (attr.startsWith("t-model")) {
|
||||||
@@ -833,11 +834,18 @@ function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
|
function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
|
||||||
if (!node.hasAttribute("t-portal")) {
|
let target, isClosest;
|
||||||
|
if (node.hasAttribute("t-portal")) {
|
||||||
|
target = node.getAttribute("t-portal")!;
|
||||||
|
node.removeAttribute("t-portal");
|
||||||
|
isClosest = false;
|
||||||
|
} else if (node.hasAttribute("t-portal.closest")) {
|
||||||
|
target = node.getAttribute("t-portal.closest")!;
|
||||||
|
node.removeAttribute("t-portal.closest");
|
||||||
|
isClosest = true;
|
||||||
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const target = node.getAttribute("t-portal")!;
|
|
||||||
node.removeAttribute("t-portal");
|
|
||||||
const content = parseNode(node, ctx);
|
const content = parseNode(node, ctx);
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return {
|
return {
|
||||||
@@ -848,6 +856,7 @@ function parseTPortal(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
return {
|
return {
|
||||||
type: ASTType.TPortal,
|
type: ASTType.TPortal,
|
||||||
target,
|
target,
|
||||||
|
isClosest,
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import { Scheduler } from "./scheduler";
|
|||||||
import { validateProps } from "./template_helpers";
|
import { validateProps } from "./template_helpers";
|
||||||
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
import { TemplateSet, TemplateSetConfig } from "./template_set";
|
||||||
import { validateTarget } from "./utils";
|
import { validateTarget } from "./utils";
|
||||||
import { toRaw, reactive } from "./reactivity";
|
|
||||||
|
|
||||||
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
// reimplement dev mode stuff see last change in 0f7a8289a6fb8387c3c1af41c6664b2a8448758f
|
||||||
|
|
||||||
@@ -40,8 +39,6 @@ declare global {
|
|||||||
apps: Set<App>;
|
apps: Set<App>;
|
||||||
Fiber: typeof Fiber;
|
Fiber: typeof Fiber;
|
||||||
RootFiber: typeof RootFiber;
|
RootFiber: typeof RootFiber;
|
||||||
toRaw: typeof toRaw;
|
|
||||||
reactive: typeof reactive;
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -50,8 +47,6 @@ window.__OWL_DEVTOOLS__ ||= {
|
|||||||
apps: new Set<App>(),
|
apps: new Set<App>(),
|
||||||
Fiber: Fiber,
|
Fiber: Fiber,
|
||||||
RootFiber: RootFiber,
|
RootFiber: RootFiber,
|
||||||
toRaw: toRaw,
|
|
||||||
reactive: reactive,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export class App<
|
export class App<
|
||||||
|
|||||||
@@ -36,18 +36,10 @@ export function createAttrUpdater(attr: string): Setter<HTMLElement> {
|
|||||||
|
|
||||||
export function attrsSetter(this: HTMLElement, attrs: any) {
|
export function attrsSetter(this: HTMLElement, attrs: any) {
|
||||||
if (isArray(attrs)) {
|
if (isArray(attrs)) {
|
||||||
if (attrs[0] === "class") {
|
setAttribute.call(this, attrs[0], attrs[1]);
|
||||||
setClass.call(this, attrs[1]);
|
|
||||||
} else {
|
|
||||||
setAttribute.call(this, attrs[0], attrs[1]);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
for (let k in attrs) {
|
for (let k in attrs) {
|
||||||
if (k === "class") {
|
setAttribute.call(this, k, attrs[k]);
|
||||||
setClass.call(this, attrs[k]);
|
|
||||||
} else {
|
|
||||||
setAttribute.call(this, k, attrs[k]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,11 +52,7 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
|||||||
if (val === oldAttrs[1]) {
|
if (val === oldAttrs[1]) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (name === "class") {
|
setAttribute.call(this, name, val);
|
||||||
updateClass.call(this, val, oldAttrs[1]);
|
|
||||||
} else {
|
|
||||||
setAttribute.call(this, name, val);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
removeAttribute.call(this, oldAttrs[0]);
|
removeAttribute.call(this, oldAttrs[0]);
|
||||||
setAttribute.call(this, name, val);
|
setAttribute.call(this, name, val);
|
||||||
@@ -72,21 +60,13 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
|||||||
} else {
|
} else {
|
||||||
for (let k in oldAttrs) {
|
for (let k in oldAttrs) {
|
||||||
if (!(k in attrs)) {
|
if (!(k in attrs)) {
|
||||||
if (k === "class") {
|
removeAttribute.call(this, k);
|
||||||
updateClass.call(this, "", oldAttrs[k]);
|
|
||||||
} else {
|
|
||||||
removeAttribute.call(this, k);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let k in attrs) {
|
for (let k in attrs) {
|
||||||
const val = attrs[k];
|
const val = attrs[k];
|
||||||
if (val !== oldAttrs[k]) {
|
if (val !== oldAttrs[k]) {
|
||||||
if (k === "class") {
|
setAttribute.call(this, k, val);
|
||||||
updateClass.call(this, val, oldAttrs[k]);
|
|
||||||
} else {
|
|
||||||
setAttribute.call(this, k, val);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-13
@@ -59,35 +59,28 @@ export function useChildSubEnv(envExtension: Env) {
|
|||||||
// useEffect
|
// useEffect
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
type EffectDeps<T extends any[]> = T | (T extends [...infer H, never] ? EffectDeps<H> : never);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @template T
|
* @param {...any} dependencies the dependencies computed by computeDependencies
|
||||||
* @param {...T} dependencies the dependencies computed by computeDependencies
|
|
||||||
* @returns {void|(()=>void)} a cleanup function that reverses the side
|
* @returns {void|(()=>void)} a cleanup function that reverses the side
|
||||||
* effects of the effect callback.
|
* effects of the effect callback.
|
||||||
*/
|
*/
|
||||||
type Effect<T extends [...T]> = (...dependencies: EffectDeps<T>) => void | (() => void);
|
type Effect = (...dependencies: any[]) => void | (() => void);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This hook will run a callback when a component is mounted and patched, and
|
* This hook will run a callback when a component is mounted and patched, and
|
||||||
* will run a cleanup function before patching and before unmounting the
|
* will run a cleanup function before patching and before unmounting the
|
||||||
* the component.
|
* the component.
|
||||||
*
|
*
|
||||||
* @template T
|
* @param {Effect} effect the effect to run on component mount and/or patch
|
||||||
* @param {Effect<T>} effect the effect to run on component mount and/or patch
|
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||||
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
|
|
||||||
* dependencies that will decide if the effect needs to be cleaned up and
|
* dependencies that will decide if the effect needs to be cleaned up and
|
||||||
* run again. If the dependencies did not change, the effect will not run
|
* run again. If the dependencies did not change, the effect will not run
|
||||||
* again. The default value returns an array containing only NaN because
|
* again. The default value returns an array containing only NaN because
|
||||||
* NaN !== NaN, which will cause the effect to rerun on every patch.
|
* NaN !== NaN, which will cause the effect to rerun on every patch.
|
||||||
*/
|
*/
|
||||||
export function useEffect<T extends [...T]>(
|
export function useEffect(effect: Effect, computeDependencies: () => any[] = () => [NaN]) {
|
||||||
effect: Effect<T>,
|
|
||||||
computeDependencies: () => T = () => [NaN] as never
|
|
||||||
) {
|
|
||||||
let cleanup: (() => void) | void;
|
let cleanup: (() => void) | void;
|
||||||
let dependencies: T;
|
let dependencies: any[];
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
dependencies = computeDependencies();
|
dependencies = computeDependencies();
|
||||||
cleanup = effect(...dependencies);
|
cleanup = effect(...dependencies);
|
||||||
|
|||||||
+24
-7
@@ -5,20 +5,34 @@ import { OwlError } from "./error_handling";
|
|||||||
|
|
||||||
const VText: any = text("").constructor;
|
const VText: any = text("").constructor;
|
||||||
|
|
||||||
|
function getTarget(
|
||||||
|
currentParentEl: HTMLElement | Document,
|
||||||
|
selector: string,
|
||||||
|
isClosest: boolean
|
||||||
|
): HTMLElement | null {
|
||||||
|
if (!isClosest || currentParentEl === document) {
|
||||||
|
return document.querySelector(selector);
|
||||||
|
}
|
||||||
|
const attempt = currentParentEl.querySelector(selector) as HTMLElement | null;
|
||||||
|
return attempt || getTarget(currentParentEl.parentElement!, selector, true);
|
||||||
|
}
|
||||||
|
|
||||||
class VPortal extends VText implements Partial<VNode<VPortal>> {
|
class VPortal extends VText implements Partial<VNode<VPortal>> {
|
||||||
content: BDom | null;
|
content: BDom | null;
|
||||||
selector: string;
|
selector: string;
|
||||||
|
isClosest: boolean;
|
||||||
target: HTMLElement | null = null;
|
target: HTMLElement | null = null;
|
||||||
|
|
||||||
constructor(selector: string, content: BDom) {
|
constructor(selector: string, isClosest: boolean, content: BDom) {
|
||||||
super("");
|
super("");
|
||||||
this.selector = selector;
|
this.selector = selector;
|
||||||
|
this.isClosest = isClosest;
|
||||||
this.content = content;
|
this.content = content;
|
||||||
}
|
}
|
||||||
|
|
||||||
mount(parent: HTMLElement, anchor: ChildNode) {
|
mount(parent: HTMLElement, anchor: ChildNode) {
|
||||||
super.mount(parent, anchor);
|
super.mount(parent, anchor);
|
||||||
this.target = document.querySelector(this.selector) as any;
|
this.target = getTarget(parent, this.selector, this.isClosest);
|
||||||
if (this.target) {
|
if (this.target) {
|
||||||
this.content!.mount(this.target!, null);
|
this.content!.mount(this.target!, null);
|
||||||
} else {
|
} else {
|
||||||
@@ -54,16 +68,19 @@ class VPortal extends VText implements Partial<VNode<VPortal>> {
|
|||||||
export function portalTemplate(app: any, bdom: any, helpers: any) {
|
export function portalTemplate(app: any, bdom: any, helpers: any) {
|
||||||
let { callSlot } = helpers;
|
let { callSlot } = helpers;
|
||||||
return function template(ctx: any, node: any, key = ""): any {
|
return function template(ctx: any, node: any, key = ""): any {
|
||||||
return new VPortal(ctx.props.target, callSlot(ctx, node, key, "default", false, null));
|
return new VPortal(
|
||||||
|
ctx.props.target,
|
||||||
|
ctx.props.isClosest,
|
||||||
|
callSlot(ctx, node, key, "default", false, null)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Portal extends Component {
|
export class Portal extends Component {
|
||||||
static template = "__portal__";
|
static template = "__portal__";
|
||||||
static props = {
|
static props = {
|
||||||
target: {
|
target: String,
|
||||||
type: String,
|
isClosest: { type: Boolean, optional: true },
|
||||||
},
|
|
||||||
slots: true,
|
slots: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,7 +90,7 @@ export class Portal extends Component {
|
|||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const portal: VPortal = node.bdom;
|
const portal: VPortal = node.bdom;
|
||||||
if (!portal.target) {
|
if (!portal.target) {
|
||||||
const target: HTMLElement = document.querySelector(this.props.target);
|
const target = getTarget(portal.parentEl, this.props.target, this.props.isClosest);
|
||||||
if (target) {
|
if (target) {
|
||||||
portal.content!.moveBeforeDOMNode(target.firstChild, target);
|
portal.content!.moveBeforeDOMNode(target.firstChild, target);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
// do not modify manually. This file is generated by the release script.
|
// do not modify manually. This file is generated by the release script.
|
||||||
export const version = "2.1.4";
|
export const version = "2.1.3";
|
||||||
|
|||||||
@@ -145,34 +145,3 @@ test("class attribute (with a preexisting value", async () => {
|
|||||||
patch(tree, block([""]));
|
patch(tree, block([""]));
|
||||||
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
|
expect(fixture.innerHTML).toBe(`<div class="tomato"></div>`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("block-class attributes with preexisting class attribute", async () => {
|
|
||||||
const block = createBlock('<div block-attributes="0" class="owl"></div>');
|
|
||||||
const tree = block([{ class: "eagle" }]);
|
|
||||||
|
|
||||||
mount(tree, fixture);
|
|
||||||
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
|
|
||||||
|
|
||||||
patch(tree, block([{ class: "falcon" }]));
|
|
||||||
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
|
|
||||||
|
|
||||||
patch(tree, block([{}]));
|
|
||||||
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("block-class attributes (array syntax) with preexisting class attribute", async () => {
|
|
||||||
const block = createBlock('<div block-attributes="0" class="owl"></div>');
|
|
||||||
const tree = block([["class", "eagle"]]);
|
|
||||||
|
|
||||||
mount(tree, fixture);
|
|
||||||
expect(fixture.innerHTML).toBe(`<div class="owl eagle"></div>`);
|
|
||||||
|
|
||||||
patch(tree, block([["class", "falcon"]]));
|
|
||||||
expect(fixture.innerHTML).toBe(`<div class="owl falcon"></div>`);
|
|
||||||
|
|
||||||
patch(tree, block([["class", ""]]));
|
|
||||||
expect(fixture.innerHTML).toBe(`<div class="owl"></div>`);
|
|
||||||
|
|
||||||
patch(tree, block([["class", "buzzard"]]));
|
|
||||||
expect(fixture.innerHTML).toBe(`<div class="owl buzzard"></div>`);
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -707,123 +707,6 @@ exports[`attributes updating classes (with obj notation) 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`attributes various combinations of class, t-att-class, and t-att 1`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\">content</div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let attr1 = {class:'a'};
|
|
||||||
return block1([attr1]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`attributes various combinations of class, t-att-class, and t-att 2`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\" class=\\"c\\">content</div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let attr1 = {class:'a'};
|
|
||||||
let attr2 = {'b':true};
|
|
||||||
return block1([attr1, attr2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`attributes various combinations of class, t-att-class, and t-att 3`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div block-attributes=\\"0\\" class=\\"c\\" block-attribute-1=\\"class\\">content</div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let attr1 = {class:'a'};
|
|
||||||
let attr2 = {'b':true};
|
|
||||||
return block1([attr1, attr2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`attributes various combinations of class, t-att-class, and t-att 4`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div class=\\"c\\" block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let attr1 = {class:'a'};
|
|
||||||
let attr2 = {'b':true};
|
|
||||||
return block1([attr1, attr2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`attributes various combinations of class, t-att-class, and t-att 5`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\" block-attributes=\\"1\\">content</div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let attr1 = {'b':true};
|
|
||||||
let attr2 = {class:'a'};
|
|
||||||
return block1([attr1, attr2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`attributes various combinations of class, t-att-class, and t-att 6`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let attr1 = {'b':true};
|
|
||||||
return block1([attr1]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`attributes various combinations of class, t-att-class, and t-att 7`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div class=\\"c\\" block-attribute-0=\\"class\\">content</div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let attr1 = ('b');
|
|
||||||
return block1([attr1]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`attributes various combinations of class, t-att-class, and t-att 8`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div block-attributes=\\"0\\" block-attribute-1=\\"class\\">content</div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let attr1 = {class:'a'};
|
|
||||||
let attr2 = {'b':true};
|
|
||||||
return block1([attr1, attr2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`attributes various escapes 1`] = `
|
exports[`attributes various escapes 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,79 +1,5 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`translation support body of t-sets are translated 1`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
ctx = Object.create(ctx);
|
|
||||||
ctx[isBoundary] = 1
|
|
||||||
setContextValue(ctx, \\"label\\", \`translated\`);
|
|
||||||
return text(ctx['label']);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`translation support body of t-sets inside translation=off are not translated 1`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
ctx = Object.create(ctx);
|
|
||||||
ctx[isBoundary] = 1
|
|
||||||
setContextValue(ctx, \\"label\\", \`untranslated\`);
|
|
||||||
return text(ctx['label']);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`translation support body of t-sets with html content are translated 1`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div>translated</div>\`);
|
|
||||||
|
|
||||||
function value1(ctx, node, key = \\"\\") {
|
|
||||||
return block1();
|
|
||||||
}
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
ctx = Object.create(ctx);
|
|
||||||
ctx[isBoundary] = 1
|
|
||||||
ctx[\`label\`] = new LazyValue(value1, ctx, this, node, key);
|
|
||||||
return safeOutput(ctx['label']);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`translation support body of t-sets with text and html content are translated 1`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
let { isBoundary, withDefault, LazyValue, safeOutput } = helpers;
|
|
||||||
|
|
||||||
let block3 = createBlock(\`<div>translated</div>\`);
|
|
||||||
|
|
||||||
function value1(ctx, node, key = \\"\\") {
|
|
||||||
const b2 = text(\` translated \`);
|
|
||||||
const b3 = block3();
|
|
||||||
return multi([b2, b3]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
ctx = Object.create(ctx);
|
|
||||||
ctx[isBoundary] = 1
|
|
||||||
ctx[\`label\`] = new LazyValue(value1, ctx, this, node, key);
|
|
||||||
return safeOutput(ctx['label']);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`translation support can set and remove translatable attributes 1`] = `
|
exports[`translation support can set and remove translatable attributes 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -126,21 +52,6 @@ exports[`translation support some attributes are translated 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`translation support t-set and falsy t-value: t-body are translated 1`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
let { isBoundary, withDefault, setContextValue } = helpers;
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
ctx = Object.create(ctx);
|
|
||||||
ctx[isBoundary] = 1
|
|
||||||
setContextValue(ctx, \\"label\\", withDefault(false, \`translated\`));
|
|
||||||
return text(ctx['label']);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
|
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -371,33 +371,4 @@ describe("attributes", () => {
|
|||||||
// not sure about this. maybe we want to remove the attribute?
|
// not sure about this. maybe we want to remove the attribute?
|
||||||
expect(fixture.innerHTML).toBe('<div class="hoy a b"></div>');
|
expect(fixture.innerHTML).toBe('<div class="hoy a b"></div>');
|
||||||
});
|
});
|
||||||
|
|
||||||
test("various combinations of class, t-att-class, and t-att", () => {
|
|
||||||
const template1 = `<div t-att="{ class: 'a' }" class="c">content</div>`;
|
|
||||||
expect(renderToString(template1)).toBe('<div class="c a">content</div>');
|
|
||||||
|
|
||||||
const template2 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}" class="c">content</div>`;
|
|
||||||
expect(renderToString(template2)).toBe('<div class="c a b">content</div>');
|
|
||||||
|
|
||||||
const template3 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
|
|
||||||
expect(renderToString(template3)).toBe('<div class="c a b">content</div>');
|
|
||||||
|
|
||||||
const template4 = `<div class="c" t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
|
|
||||||
expect(renderToString(template4)).toBe('<div class="c a b">content</div>');
|
|
||||||
|
|
||||||
const template5 = `<div class="c" t-att-class="{'b': true}" t-att="{ class: 'a' }">content</div>`;
|
|
||||||
expect(renderToString(template5)).toBe('<div class="c b a">content</div>');
|
|
||||||
|
|
||||||
const template6 = `<div class="c" t-att-class="{'b': true}">content</div>`;
|
|
||||||
expect(renderToString(template6)).toBe('<div class="c b">content</div>');
|
|
||||||
|
|
||||||
const template7 = `<div class="c" t-attf-class="{{'b'}}">content</div>`;
|
|
||||||
expect(renderToString(template7)).toBe('<div class="c b">content</div>');
|
|
||||||
|
|
||||||
const template8 = `<div t-att="{ class: 'a' }" class="c" t-att-class="{'b': true}">content</div>`;
|
|
||||||
expect(renderToString(template8)).toBe('<div class="c a b">content</div>');
|
|
||||||
|
|
||||||
const template9 = `<div t-att="{ class: 'a' }" t-att-class="{'b': true}">content</div>`;
|
|
||||||
expect(renderToString(template9)).toBe('<div class="a b">content</div>');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1147,24 +1147,6 @@ describe("qweb parser", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("t-onclick without dash", async () => {
|
|
||||||
expect(() => parse(`<button t-onclick="add">Click</button>`)).toThrowError(
|
|
||||||
"Unknown QWeb directive: 't-onclick'"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("t-on without event", async () => {
|
|
||||||
expect(() => parse(`<button t-on="add">Click</button>`)).toThrowError(
|
|
||||||
"Missing event name with t-on directive"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("t-on- without event", async () => {
|
|
||||||
expect(() => parse(`<button t-on-="add">Click</button>`)).toThrowError(
|
|
||||||
"Missing event name with t-on directive"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// t-model
|
// t-model
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1293,12 +1275,6 @@ describe("qweb parser", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("component with event handler", async () => {
|
|
||||||
expect(() => parse(`<MyComponent t-onclick="someMethod"/>`)).toThrowError(
|
|
||||||
"unsupported directive on Component: t-onclick"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("component with t-ref", async () => {
|
test("component with t-ref", async () => {
|
||||||
expect(() => parse(`<MyComponent t-ref="something"/>`)).toThrow(
|
expect(() => parse(`<MyComponent t-ref="something"/>`)).toThrow(
|
||||||
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop."
|
"t-ref is no longer supported on components. Consider exposing only the public part of the component's API through a callback prop."
|
||||||
@@ -2022,6 +1998,7 @@ describe("qweb parser", () => {
|
|||||||
test("t-portal", async () => {
|
test("t-portal", async () => {
|
||||||
expect(parse(`<t t-portal="target">Content</t>`)).toEqual({
|
expect(parse(`<t t-portal="target">Content</t>`)).toEqual({
|
||||||
type: ASTType.TPortal,
|
type: ASTType.TPortal,
|
||||||
|
isClosest: false,
|
||||||
target: "target",
|
target: "target",
|
||||||
content: { type: ASTType.Text, value: "Content" },
|
content: { type: ASTType.Text, value: "Content" },
|
||||||
});
|
});
|
||||||
@@ -2032,6 +2009,7 @@ describe("qweb parser", () => {
|
|||||||
condition: "condition",
|
condition: "condition",
|
||||||
content: {
|
content: {
|
||||||
content: { type: ASTType.Text, value: "Content" },
|
content: { type: ASTType.Text, value: "Content" },
|
||||||
|
isClosest: false,
|
||||||
target: "target",
|
target: "target",
|
||||||
type: ASTType.TPortal,
|
type: ASTType.TPortal,
|
||||||
},
|
},
|
||||||
@@ -2040,4 +2018,13 @@ describe("qweb parser", () => {
|
|||||||
type: ASTType.TIf,
|
type: ASTType.TIf,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-portal with .closest", async () => {
|
||||||
|
expect(parse(`<t t-portal.closest="target">Content</t>`)).toEqual({
|
||||||
|
type: ASTType.TPortal,
|
||||||
|
isClosest: true,
|
||||||
|
target: "target",
|
||||||
|
content: { type: ASTType.Text, value: "Content" },
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -100,74 +100,4 @@ describe("translation support", () => {
|
|||||||
expect(translateFn).toHaveBeenCalledWith("some word");
|
expect(translateFn).toHaveBeenCalledWith("some word");
|
||||||
expect(fixture.innerHTML).toBe("<div>un mot</div>");
|
expect(fixture.innerHTML).toBe("<div>un mot</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("body of t-sets are translated", async () => {
|
|
||||||
class SomeComponent extends Component {
|
|
||||||
static template = xml`
|
|
||||||
<t t-set="label">untranslated</t>
|
|
||||||
<t t-esc="label"/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const translateFn = () => "translated";
|
|
||||||
|
|
||||||
await mount(SomeComponent, fixture, { translateFn });
|
|
||||||
expect(fixture.innerHTML).toBe("translated");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("body of t-sets inside translation=off are not translated", async () => {
|
|
||||||
class SomeComponent extends Component {
|
|
||||||
static template = xml`
|
|
||||||
<t t-translation="off">
|
|
||||||
<t t-set="label">untranslated</t>
|
|
||||||
<t t-esc="label"/>
|
|
||||||
</t>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const translateFn = () => "translated";
|
|
||||||
|
|
||||||
await mount(SomeComponent, fixture, { translateFn });
|
|
||||||
expect(fixture.innerHTML).toBe("untranslated");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("body of t-sets with html content are translated", async () => {
|
|
||||||
class SomeComponent extends Component {
|
|
||||||
static template = xml`
|
|
||||||
<t t-set="label"><div>untranslated</div></t>
|
|
||||||
<t t-out="label"/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const translateFn = () => "translated";
|
|
||||||
|
|
||||||
await mount(SomeComponent, fixture, { translateFn });
|
|
||||||
expect(fixture.innerHTML).toBe("<div>translated</div>");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("body of t-sets with text and html content are translated", async () => {
|
|
||||||
class SomeComponent extends Component {
|
|
||||||
static template = xml`
|
|
||||||
<t t-set="label">
|
|
||||||
some text
|
|
||||||
<div>untranslated</div>
|
|
||||||
</t>
|
|
||||||
<t t-out="label"/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const translateFn = () => "translated";
|
|
||||||
|
|
||||||
await mount(SomeComponent, fixture, { translateFn });
|
|
||||||
expect(fixture.innerHTML).toBe(" translated <div>translated</div>");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("t-set and falsy t-value: t-body are translated", async () => {
|
|
||||||
class SomeComponent extends Component {
|
|
||||||
static template = xml`
|
|
||||||
<t t-set="label" t-value="false">untranslated</t>
|
|
||||||
<t t-esc="label"/>`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const translateFn = () => "translated";
|
|
||||||
|
|
||||||
await mount(SomeComponent, fixture, { translateFn });
|
|
||||||
expect(fixture.innerHTML).toBe("translated");
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -999,3 +999,26 @@ exports[`Portal: UI/UX focus is kept across re-renders 2`] = `
|
|||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`portal .closest suffix basic use of .suffix 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const Portal = app.Portal;
|
||||||
|
const comp1 = app.createComponent(null, false, true, false, false);
|
||||||
|
|
||||||
|
let block2 = createBlock(\`<p class=\\"target\\">far target</p>\`);
|
||||||
|
let block3 = createBlock(\`<div><p class=\\"target\\">close target</p><block-child-0/></div>\`);
|
||||||
|
|
||||||
|
function slot1(ctx, node, key = \\"\\") {
|
||||||
|
return text(\`portal content\`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const b2 = block2();
|
||||||
|
const b5 = comp1({target: '.target',isClosest: true,slots: {'default': {__render: slot1.bind(this), __ctx: ctx}}}, key + \`__1\`, node, ctx, Portal);
|
||||||
|
const b3 = block3([], [b5]);
|
||||||
|
return multi([b2, b3]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|||||||
@@ -1028,3 +1028,21 @@ describe("Portal: Props validation", () => {
|
|||||||
expect(error!.message).toBe(`invalid portal target`);
|
expect(error!.message).toBe(`invalid portal target`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("portal .closest suffix", () => {
|
||||||
|
test("basic use of .suffix", async () => {
|
||||||
|
class Parent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<p class="target">far target</p>
|
||||||
|
<div>
|
||||||
|
<p class="target">close target</p>
|
||||||
|
<t t-portal.closest="'.target'">portal content</t>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
await mount(Parent, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe(
|
||||||
|
'<p class="target">far target</p><div><p class="target">close targetportal content</p></div>'
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Owl devtools",
|
"name": "Owl devtools",
|
||||||
"version": "1.1.1",
|
"version": "1.0",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"description": "Chrome devtools extension for Odoo Owl framework",
|
"description": "Chrome devtools extension for Odoo Owl framework",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
+2
-1
@@ -1,10 +1,11 @@
|
|||||||
const { Component, useRef, useEffect } = owl;
|
const { Component, useRef, useEffect } = owl;
|
||||||
import { useStore } from "../../../store/store";
|
import { useStore } from "../../../store/store";
|
||||||
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
||||||
|
import { Subscriptions } from "./subscriptions/subscriptions";
|
||||||
|
|
||||||
export class DetailsWindow extends Component {
|
export class DetailsWindow extends Component {
|
||||||
static template = "devtools.DetailsWindow";
|
static template = "devtools.DetailsWindow";
|
||||||
static components = { ObjectTreeElement };
|
static components = { ObjectTreeElement, Subscriptions };
|
||||||
setup() {
|
setup() {
|
||||||
this.store = useStore();
|
this.store = useStore();
|
||||||
this.contextMenu = useRef("contextmenu");
|
this.contextMenu = useRef("contextmenu");
|
||||||
|
|||||||
+2
-6
@@ -53,11 +53,7 @@
|
|||||||
</div>
|
</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>
|
<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>
|
</div>
|
||||||
<div t-if="store.activeComponent.subscriptions.toggled" id="subscriptionsPanel">
|
<Subscriptions t-if="store.activeComponent.subscriptions.toggled"/>
|
||||||
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
|
|
||||||
<ObjectTreeElement object="subscription.target"/>
|
|
||||||
</t>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
|
<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="d-flex mb-2">
|
||||||
@@ -76,7 +72,7 @@
|
|||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
<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">
|
<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>
|
<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">
|
<t t-if="store.activeComponent.path.length !== 1">
|
||||||
|
|||||||
+10
-9
@@ -41,22 +41,23 @@ export class ObjectTreeElement extends Component {
|
|||||||
return JSON.stringify(this.props.object.path);
|
return JSON.stringify(this.props.object.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
get keyChanges() {
|
get objectName() {
|
||||||
return this.props.object.keys?.includes("Symbol(Key changes)");
|
return this.props.object.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
classFor(object) {
|
get objectLineClass() {
|
||||||
// Prototype items will be dyed down to appear less important
|
// Prototype items will be dyed down to appear less important
|
||||||
if (object.path.some((item) => item?.type === "prototype") && !object.keepLit) {
|
if (this.pathAsString.includes('{"type":"prototype",')) {
|
||||||
return "attenuate";
|
return { attenuate: true };
|
||||||
}
|
}
|
||||||
// Same for subscription items which are not present in the keys while the keys will be bold
|
// Same for subscription items which are not present in the keys while the keys will be bold
|
||||||
if (object.objectType === "subscription" && object.depth > 0) {
|
if (this.props.object.objectType === "subscription" && this.props.object.depth > 0) {
|
||||||
if (this.props.object.keys?.includes(object.name.toString())) {
|
if (this.props.keys.includes(this.props.object.name.toString())) {
|
||||||
return "fw-bolder";
|
return { "fw-bolder": true };
|
||||||
}
|
}
|
||||||
return "attenuate";
|
return { attenuate: true };
|
||||||
}
|
}
|
||||||
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
get objectPadding() {
|
get objectPadding() {
|
||||||
|
|||||||
+7
-7
@@ -2,7 +2,7 @@
|
|||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
<t t-name="devtools.ObjectTreeElement" owl="1">
|
<t t-name="devtools.ObjectTreeElement" owl="1">
|
||||||
<div class="m-0 p-0 text-nowrap w-100 object-line"
|
<div class="m-0 p-0 text-nowrap w-100 object-line"
|
||||||
t-att-class="props.class"
|
t-att-class="objectLineClass"
|
||||||
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
||||||
t-on-contextmenu.prevent="openMenu"
|
t-on-contextmenu.prevent="openMenu"
|
||||||
>
|
>
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
t-att-class="{'fa-caret-right': !props.object.toggled, 'fa-caret-down': props.object.toggled}"
|
t-att-class="{'fa-caret-right': !props.object.toggled, 'fa-caret-down': props.object.toggled}"
|
||||||
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
|
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
|
||||||
/>
|
/>
|
||||||
<t t-esc="props.object.name"/>
|
<t t-esc="objectName"/>
|
||||||
<t t-if="props.object.content.length > 0">: </t>
|
<t t-if="props.object.content.length > 0">: </t>
|
||||||
<t t-if="props.object.contentType == 'getter'">
|
<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)">
|
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
|
||||||
@@ -28,10 +28,9 @@
|
|||||||
</t>
|
</t>
|
||||||
</span>
|
</span>
|
||||||
</t>
|
</t>
|
||||||
<span t-if="keyChanges" class="key-changes ms-1 badge p-1" title="Key additions/deletions are observed">+/-</span>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
<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">
|
<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>
|
<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"'>
|
<t t-if='props.object.contentType == "function"'>
|
||||||
@@ -39,9 +38,10 @@
|
|||||||
</t>
|
</t>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<t t-if="props.object.toggled" t-key="contextMenuId">
|
<t t-if="props.object.toggled">
|
||||||
<t t-foreach="props.object.children" t-as="child" t-key="child_index">
|
<t t-foreach="props.object.children" t-as="child" t-key="child.name">
|
||||||
<ObjectTreeElement object="child" class="this.classFor(child)"/>
|
<ObjectTreeElement t-if="props.object.objectType === 'subscription'" object="child" keys="props.keys"/>
|
||||||
|
<ObjectTreeElement t-else="" object="child"/>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
</t>
|
</t>
|
||||||
|
|||||||
+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>
|
||||||
+1
-29
@@ -1,11 +1,9 @@
|
|||||||
/** @odoo-module **/
|
/** @odoo-module **/
|
||||||
|
|
||||||
import { isElementInCenterViewport, minimizeKey, IS_FIREFOX } from "../../../../utils";
|
import { isElementInCenterViewport, minimizeKey } from "../../../../utils";
|
||||||
import { useStore } from "../../../store/store";
|
import { useStore } from "../../../store/store";
|
||||||
import { HighlightText } from "./highlight_text/highlight_text";
|
import { HighlightText } from "./highlight_text/highlight_text";
|
||||||
|
|
||||||
const browserInstance = IS_FIREFOX ? browser : chrome;
|
|
||||||
|
|
||||||
const { Component, useRef, useState, useEffect, onMounted } = owl;
|
const { Component, useRef, useState, useEffect, onMounted } = owl;
|
||||||
|
|
||||||
export class TreeElement extends Component {
|
export class TreeElement extends Component {
|
||||||
@@ -107,30 +105,4 @@ export class TreeElement extends Component {
|
|||||||
this.store.selectComponent(this.props.component.path);
|
this.store.selectComponent(this.props.component.path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Adds the component name to the components toggle blacklist if not already present
|
|
||||||
// Else, remove it from the blacklist
|
|
||||||
toggleComponentToBlacklist() {
|
|
||||||
if (this.store.settings.componentsToggleBlacklist.has(this.props.component.name)) {
|
|
||||||
if (!this.props.component.toggled) {
|
|
||||||
this.props.component.toggled = !this.props.component.toggled;
|
|
||||||
}
|
|
||||||
this.store.settings.componentsToggleBlacklist.delete(this.props.component.name);
|
|
||||||
browserInstance.storage.local.set({
|
|
||||||
owlDevtoolsComponentsToggleBlacklist: Array.from(
|
|
||||||
this.store.settings.componentsToggleBlacklist
|
|
||||||
),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (this.props.component.toggled) {
|
|
||||||
this.props.component.toggled = !this.props.component.toggled;
|
|
||||||
}
|
|
||||||
this.store.settings.componentsToggleBlacklist.add(this.props.component.name);
|
|
||||||
browserInstance.storage.local.set({
|
|
||||||
owlDevtoolsComponentsToggleBlacklist: Array.from(
|
|
||||||
this.store.settings.componentsToggleBlacklist
|
|
||||||
),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-5
@@ -26,7 +26,7 @@
|
|||||||
<span t-if="props.component.depth">></span>
|
<span t-if="props.component.depth">></span>
|
||||||
<span class="version" t-else="">owl=<t t-esc="props.component.version"/></span>
|
<span class="version" t-else="">owl=<t t-esc="props.component.version"/></span>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
<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">
|
<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, 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.toggleComponentAndChildren(props.component, false)" class="custom-menu-item py-1 px-4">Fold all children</li>
|
||||||
@@ -43,10 +43,6 @@
|
|||||||
<t t-else="">
|
<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>
|
<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>
|
</t>
|
||||||
<li t-on-click.stop="() => this.toggleComponentToBlacklist()" class="custom-menu-item py-1 px-4">
|
|
||||||
<t t-if="store.settings.componentsToggleBlacklist.has(props.component.name)">Don't fold component by default</t>
|
|
||||||
<t t-else="">Fold component by default</t>
|
|
||||||
</li>
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8" ?>
|
||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
<t t-name="devtools.Event" owl="1">
|
<t t-name="devtools.Event" owl="1">
|
||||||
<div class="event-container" t-att-class="{ 'event-last': props.event.isLast }">
|
<div class="event-container">
|
||||||
<div class="my-0 p-0 object-line" t-on-click.stop="toggleDisplay">
|
<div class="my-0 p-0 object-line" t-on-click.stop="toggleDisplay">
|
||||||
<div class="ps-2 text-nowrap">
|
<div class="ps-2 text-nowrap">
|
||||||
<i class="fa px-1 pointer-icon caret"
|
<i class="fa px-1 pointer-icon caret"
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-ref="componentContextmenu">
|
<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">
|
<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>
|
<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">
|
<t t-if="props.event.path.length !== 1">
|
||||||
|
|||||||
+2
-2
@@ -28,14 +28,14 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.contextMenu.activeMenu === nodeContextMenuId" class="custom-menu" t-ref="nodeContextMenu">
|
<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">
|
<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, 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.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>
|
<li t-on-click.stop="() => this.store.foldDirectChildren(props.event)" class="custom-menu-item py-1 px-4">Fold direct children</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-ref="componentContextmenu">
|
<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">
|
<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>
|
<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">
|
<t t-if="props.event.path.length !== 1">
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ export const store = reactive({
|
|||||||
expandByDefault: true,
|
expandByDefault: true,
|
||||||
toggleOnSelected: false,
|
toggleOnSelected: false,
|
||||||
darkmode: false,
|
darkmode: false,
|
||||||
componentsToggleBlacklist: new Set(),
|
|
||||||
},
|
},
|
||||||
contextMenu: {
|
contextMenu: {
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
id: 0,
|
id: 0,
|
||||||
activeMenu: -1,
|
activeMenu: -1,
|
||||||
// Opens the context menu corresponding with the given menu html element
|
// Opens the context menu corresponding with the given menu html element
|
||||||
@@ -28,9 +29,9 @@ export const store = reactive({
|
|||||||
if (y + menuHeight > window.innerHeight) {
|
if (y + menuHeight > window.innerHeight) {
|
||||||
y = window.innerHeight - menuHeight;
|
y = window.innerHeight - menuHeight;
|
||||||
}
|
}
|
||||||
menu.style.left = x + "px";
|
this.left = x + "px";
|
||||||
// Need 25px offset because of the main navbar from the browser devtools
|
// Need 25px offset because of the main navbar from the browser devtools
|
||||||
menu.style.top = y - 25 + "px";
|
this.top = y - 25 + "px";
|
||||||
},
|
},
|
||||||
// Close the currently displayed context menu
|
// Close the currently displayed context menu
|
||||||
close() {
|
close() {
|
||||||
@@ -102,19 +103,21 @@ export const store = reactive({
|
|||||||
if (IS_FIREFOX) {
|
if (IS_FIREFOX) {
|
||||||
await evalInWindow("window.$0 = $0;", this.activeFrame);
|
await evalInWindow("window.$0 = $0;", this.activeFrame);
|
||||||
}
|
}
|
||||||
const [apps, details] = await evalFunctionInWindow(
|
const apps = await evalFunctionInWindow(
|
||||||
"getComponentsTree",
|
"getComponentsTree",
|
||||||
fromOld && this.activeComponent
|
fromOld && this.activeComponent ? [this.activeComponent.path, this.apps] : [],
|
||||||
? [this.activeComponent.path, this.apps, this.activeComponent]
|
|
||||||
: [],
|
|
||||||
this.activeFrame
|
this.activeFrame
|
||||||
);
|
);
|
||||||
this.apps = apps ? apps : [];
|
this.apps = apps ? apps : [];
|
||||||
if (!fromOld && this.settings.expandByDefault) {
|
if (!fromOld && this.settings.expandByDefault) {
|
||||||
this.apps.forEach((tree) => expandNodes(tree, true));
|
this.apps.forEach((tree) => expandNodes(tree));
|
||||||
}
|
}
|
||||||
keepEnvLit(details);
|
const component = await evalFunctionInWindow(
|
||||||
this.activeComponent = details;
|
"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
|
// Select a component by retrieving its details from the page based on its path
|
||||||
@@ -151,11 +154,9 @@ export const store = reactive({
|
|||||||
[component.path],
|
[component.path],
|
||||||
this.activeFrame
|
this.activeFrame
|
||||||
);
|
);
|
||||||
if (!details) {
|
this.activeComponent = details;
|
||||||
|
if (!this.activeComponent) {
|
||||||
await this.loadComponentsTree(false);
|
await this.loadComponentsTree(false);
|
||||||
} else {
|
|
||||||
keepEnvLit(details);
|
|
||||||
this.activeComponent = details;
|
|
||||||
}
|
}
|
||||||
if (this.page !== "ComponentsTab") {
|
if (this.page !== "ComponentsTab") {
|
||||||
this.switchTab("ComponentsTab");
|
this.switchTab("ComponentsTab");
|
||||||
@@ -413,7 +414,12 @@ export const store = reactive({
|
|||||||
if (!scriptsLoaded) {
|
if (!scriptsLoaded) {
|
||||||
await loadScripts(frame);
|
await loadScripts(frame);
|
||||||
}
|
}
|
||||||
evalFunctionInWindow("initDevtools", [frame], frame);
|
evalInWindow(
|
||||||
|
`__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = ${
|
||||||
|
store.devtoolsId
|
||||||
|
}; __OWL__DEVTOOLS_GLOBAL_HOOK__.frame = ${JSON.stringify(frame)};`,
|
||||||
|
frame
|
||||||
|
);
|
||||||
if (!this.frameUrls.includes(frame)) {
|
if (!this.frameUrls.includes(frame)) {
|
||||||
this.frameUrls = [...this.frameUrls, frame];
|
this.frameUrls = [...this.frameUrls, frame];
|
||||||
}
|
}
|
||||||
@@ -497,18 +503,13 @@ export const store = reactive({
|
|||||||
},
|
},
|
||||||
|
|
||||||
// Reset all the relevant data about the page currently stored
|
// Reset all the relevant data about the page currently stored
|
||||||
async resetData() {
|
resetData() {
|
||||||
await loadSettings();
|
|
||||||
this.loadComponentsTree(false);
|
this.loadComponentsTree(false);
|
||||||
this.events = [];
|
this.events = [];
|
||||||
this.eventsTree = [];
|
this.eventsTree = [];
|
||||||
this.activeRecorder = false;
|
this.activeRecorder = false;
|
||||||
evalFunctionInWindow("toggleEventsRecording", [false, 0]);
|
evalFunctionInWindow("toggleEventsRecording", [false, 0]);
|
||||||
this.traceRenderings = false;
|
|
||||||
evalFunctionInWindow("toggleTracing", [false]);
|
evalFunctionInWindow("toggleTracing", [false]);
|
||||||
this.traceSubscriptions = false;
|
|
||||||
evalFunctionInWindow("toggleSubscriptionTracing", [false]);
|
|
||||||
this.updateIFrameList();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
// Triggers manually the rendering of the selected component
|
// Triggers manually the rendering of the selected component
|
||||||
@@ -604,7 +605,7 @@ export const store = reactive({
|
|||||||
// Refresh the whole extension
|
// Refresh the whole extension
|
||||||
async refreshExtension() {
|
async refreshExtension() {
|
||||||
await loadScripts();
|
await loadScripts();
|
||||||
await this.resetData();
|
this.resetData();
|
||||||
},
|
},
|
||||||
|
|
||||||
// Toggle dark mode in the extension and store result in the storage
|
// Toggle dark mode in the extension and store result in the storage
|
||||||
@@ -615,7 +616,7 @@ export const store = reactive({
|
|||||||
} else {
|
} else {
|
||||||
document.querySelector("html").classList.remove("dark-mode");
|
document.querySelector("html").classList.remove("dark-mode");
|
||||||
}
|
}
|
||||||
browserInstance.storage.local.set({ owlDevtoolsDarkMode: this.settings.darkMode });
|
browserInstance.storage.local.set({ owl_devtools_dark_mode: this.settings.darkMode });
|
||||||
},
|
},
|
||||||
|
|
||||||
openDocumentation() {
|
openDocumentation() {
|
||||||
@@ -633,9 +634,7 @@ init();
|
|||||||
async function init() {
|
async function init() {
|
||||||
store.devtoolsId = await getTabURL();
|
store.devtoolsId = await getTabURL();
|
||||||
|
|
||||||
evalFunctionInWindow("initDevtools", []);
|
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||||
|
|
||||||
await loadSettings();
|
|
||||||
|
|
||||||
// We want to load the base components tree when the devtools tab is first opened
|
// We want to load the base components tree when the devtools tab is first opened
|
||||||
store.loadComponentsTree(false);
|
store.loadComponentsTree(false);
|
||||||
@@ -652,6 +651,8 @@ async function init() {
|
|||||||
evalFunctionInWindow("toggleEventsRecording", [false, 0], frame);
|
evalFunctionInWindow("toggleEventsRecording", [false, 0], frame);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
loadSettings();
|
||||||
|
|
||||||
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel", id: store.devtoolsId });
|
browserInstance.runtime.sendMessage({ type: "newDevtoolsPanel", id: store.devtoolsId });
|
||||||
|
|
||||||
// Heartbeat message to test whether the extension context is still valid or not
|
// Heartbeat message to test whether the extension context is still valid or not
|
||||||
@@ -666,7 +667,7 @@ async function init() {
|
|||||||
}, 500);
|
}, 500);
|
||||||
}
|
}
|
||||||
|
|
||||||
let rootRendersTimeout = false;
|
let flushRendersTimeout = false;
|
||||||
// Connect to the port to communicate to the background script
|
// Connect to the port to communicate to the background script
|
||||||
browserInstance.runtime.onConnect.addListener((port) => {
|
browserInstance.runtime.onConnect.addListener((port) => {
|
||||||
if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) {
|
if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) {
|
||||||
@@ -675,23 +676,23 @@ browserInstance.runtime.onConnect.addListener((port) => {
|
|||||||
if (msg.type === "Reload") {
|
if (msg.type === "Reload") {
|
||||||
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
|
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
|
||||||
if (store.owlStatus) {
|
if (store.owlStatus) {
|
||||||
evalFunctionInWindow("initDevtools", []);
|
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||||
await store.resetData();
|
store.resetData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
|
// Received when a frame has been delayed when loading the scripts due to owl being lazy loaded
|
||||||
if (msg.type === "FrameReady") {
|
if (msg.type === "FrameReady") {
|
||||||
store.updateIFrameList();
|
store.updateIFrameList();
|
||||||
store.owlStatus = true;
|
store.owlStatus = true;
|
||||||
await store.resetData();
|
store.resetData();
|
||||||
}
|
}
|
||||||
// We need to reload the components tree when the set of apps in the page is modified
|
// We need to reload the components tree when the set of apps in the page is modified
|
||||||
if (msg.type === "RefreshApps") {
|
if (msg.type === "RefreshApps") {
|
||||||
store.loadComponentsTree(true);
|
store.loadComponentsTree(true);
|
||||||
}
|
}
|
||||||
// When message of type Complete is received, overwrite the component tree with the new one from page
|
// When message of type Flush is received, overwrite the component tree with the new one from page
|
||||||
// A Complete message is sent everytime a root render is triggered on the page
|
// A flush message is sent everytime a component is rendered on the page
|
||||||
if (msg.type === "Complete") {
|
if (msg.type === "Flush") {
|
||||||
if (msg.origin.frame !== store.activeFrame) {
|
if (msg.origin.frame !== store.activeFrame) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -700,8 +701,8 @@ browserInstance.runtime.onConnect.addListener((port) => {
|
|||||||
}
|
}
|
||||||
// This determines which components will have a short highlight effect in the tree to indicate they have been rendered
|
// 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));
|
store.renderPaths.add(JSON.stringify(msg.data));
|
||||||
clearTimeout(rootRendersTimeout);
|
clearTimeout(flushRendersTimeout);
|
||||||
rootRendersTimeout = setTimeout(() => {
|
flushRendersTimeout = setTimeout(() => {
|
||||||
store.renderPaths.clear();
|
store.renderPaths.clear();
|
||||||
}, 100);
|
}, 100);
|
||||||
store.loadComponentsTree(true);
|
store.loadComponentsTree(true);
|
||||||
@@ -739,12 +740,11 @@ browserInstance.runtime.onConnect.addListener((port) => {
|
|||||||
// Load all settings from the chrome sync storage
|
// Load all settings from the chrome sync storage
|
||||||
async function loadSettings() {
|
async function loadSettings() {
|
||||||
let storage = await browserInstance.storage.local.get();
|
let storage = await browserInstance.storage.local.get();
|
||||||
// Darkmode
|
if (storage.owl_devtools_dark_mode === undefined) {
|
||||||
if (storage.owlDevtoolsDarkMode === undefined) {
|
|
||||||
// Load dark mode based on the global settings of the chrome devtools
|
// Load dark mode based on the global settings of the chrome devtools
|
||||||
darkMode = browserInstance.devtools.panels.themeName === "dark";
|
darkMode = browserInstance.devtools.panels.themeName === "dark";
|
||||||
} else {
|
} else {
|
||||||
darkMode = storage.owlDevtoolsDarkMode;
|
darkMode = storage.owl_devtools_dark_mode;
|
||||||
}
|
}
|
||||||
store.settings.darkMode = darkMode;
|
store.settings.darkMode = darkMode;
|
||||||
if (darkMode) {
|
if (darkMode) {
|
||||||
@@ -752,12 +752,6 @@ async function loadSettings() {
|
|||||||
} else {
|
} else {
|
||||||
document.querySelector("html").classList.remove("dark-mode");
|
document.querySelector("html").classList.remove("dark-mode");
|
||||||
}
|
}
|
||||||
// Components toggle blacklist
|
|
||||||
if (storage.owlDevtoolsComponentsToggleBlacklist !== undefined) {
|
|
||||||
store.settings.componentsToggleBlacklist = new Set(
|
|
||||||
storage.owlDevtoolsComponentsToggleBlacklist
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to handle and store a batch of events coming from the page
|
// Function to handle and store a batch of events coming from the page
|
||||||
@@ -782,7 +776,6 @@ function loadEvents(events) {
|
|||||||
}
|
}
|
||||||
event.origin = null;
|
event.origin = null;
|
||||||
event.toggled = false;
|
event.toggled = false;
|
||||||
event.isLast = false;
|
|
||||||
// Logic to retrace the origin of the event if it is not a root render event
|
// Logic to retrace the origin of the event if it is not a root render event
|
||||||
if (!event.type.includes("render")) {
|
if (!event.type.includes("render")) {
|
||||||
for (let i = store.events.length - 1; i >= 0; i--) {
|
for (let i = store.events.length - 1; i >= 0; i--) {
|
||||||
@@ -832,7 +825,6 @@ function loadEvents(events) {
|
|||||||
// Make sure we add the event while keeping the whole list ordered by id
|
// Make sure we add the event while keeping the whole list ordered by id
|
||||||
addEventSorted(event);
|
addEventSorted(event);
|
||||||
}
|
}
|
||||||
store.events[store.events.length - 1].isLast = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Deselect component and remove highlight on all children
|
// Deselect component and remove highlight on all children
|
||||||
@@ -879,39 +871,10 @@ function highlightChildren(component) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Expand the node given in entry and all of its children
|
// Expand the node given in entry and all of its children
|
||||||
function expandNodes(node, blacklist = false) {
|
function expandNodes(node) {
|
||||||
if (blacklist && store.settings.componentsToggleBlacklist.has(node.name)) {
|
node.toggled = true;
|
||||||
node.toggled = false;
|
|
||||||
} else {
|
|
||||||
node.toggled = true;
|
|
||||||
}
|
|
||||||
for (const child of node.children) {
|
for (const child of node.children) {
|
||||||
expandNodes(child, blacklist);
|
expandNodes(child);
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// This function transforms the env part of the details such that all env keys are not
|
|
||||||
// greyed out in the UI at their first occurence
|
|
||||||
function keepEnvLit(details) {
|
|
||||||
let alreadyMet = new Set();
|
|
||||||
for (let i = 0; i < details.env.children.length; i++) {
|
|
||||||
if (i < details.env.children.length - 1) {
|
|
||||||
alreadyMet.add(details.env.children[i].name);
|
|
||||||
} else {
|
|
||||||
let lastElement = details.env.children[i];
|
|
||||||
while (lastElement.children.at(-1).name === "[[Prototype]]") {
|
|
||||||
for (const [index, child] of lastElement.children.entries()) {
|
|
||||||
if (index < lastElement.children.length - 1) {
|
|
||||||
if (!alreadyMet.has(child.name)) {
|
|
||||||
child.keepLit = true;
|
|
||||||
alreadyMet.add(child.name);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
lastElement = child;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,10 +126,6 @@
|
|||||||
color: var(--prototype-color);
|
color: var(--prototype-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.key-changes {
|
|
||||||
background-color: var(--version-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
.event-container {
|
.event-container {
|
||||||
border-bottom: 1px solid rgb(240, 238, 238);
|
border-bottom: 1px solid rgb(240, 238, 238);
|
||||||
padding-top: 2px!important;
|
padding-top: 2px!important;
|
||||||
@@ -137,10 +133,6 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.event-last {
|
|
||||||
border-bottom: 3px solid rgb(225, 154, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.getter-content:hover {
|
.getter-content:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,10 +11,8 @@
|
|||||||
this.Fiber = window.__OWL_DEVTOOLS__.Fiber;
|
this.Fiber = window.__OWL_DEVTOOLS__.Fiber;
|
||||||
// Same but for RootFiber
|
// Same but for RootFiber
|
||||||
this.RootFiber = window.__OWL_DEVTOOLS__.RootFiber;
|
this.RootFiber = window.__OWL_DEVTOOLS__.RootFiber;
|
||||||
// This is for retrocompatibility purposes since new versions of owl should always expose toRaw and reactive
|
// Set to keep track of the fibers that are in the flush queue
|
||||||
// in __OWL_DEVTOOLS__
|
this.queuedFibers = new WeakSet();
|
||||||
this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw;
|
|
||||||
this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive;
|
|
||||||
// Set to keep track of the HTML elements we added to the page
|
// Set to keep track of the HTML elements we added to the page
|
||||||
this.addedElements = [];
|
this.addedElements = [];
|
||||||
// To keep track of the succession order of the render events
|
// To keep track of the succession order of the render events
|
||||||
@@ -22,6 +20,7 @@
|
|||||||
// Set to keep track of the frame on which this script is loaded
|
// Set to keep track of the frame on which this script is loaded
|
||||||
this.frame = "top";
|
this.frame = "top";
|
||||||
// Allows to launch a message each time an iframe html element is added to the page
|
// Allows to launch a message each time an iframe html element is added to the page
|
||||||
|
const self = this;
|
||||||
const iFrameObserver = new MutationObserver(function (mutationsList) {
|
const iFrameObserver = new MutationObserver(function (mutationsList) {
|
||||||
mutationsList.forEach(function (mutation) {
|
mutationsList.forEach(function (mutation) {
|
||||||
mutation.addedNodes.forEach(function (addedNode) {
|
mutation.addedNodes.forEach(function (addedNode) {
|
||||||
@@ -44,6 +43,8 @@
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
iFrameObserver.observe(document.body, { subtree: true, childList: true });
|
iFrameObserver.observe(document.body, { subtree: true, childList: true });
|
||||||
|
this.appsPatched = false;
|
||||||
|
this.destroyPatched = false;
|
||||||
this.patchAppsSetMethods();
|
this.patchAppsSetMethods();
|
||||||
this.recordEvents = false;
|
this.recordEvents = false;
|
||||||
this.traceRenderings = false;
|
this.traceRenderings = false;
|
||||||
@@ -117,15 +118,6 @@
|
|||||||
length += element.length;
|
length += element.length;
|
||||||
result.push(element);
|
result.push(element);
|
||||||
}
|
}
|
||||||
for (const key of Object.getOwnPropertySymbols(obj)) {
|
|
||||||
if (length > 25) {
|
|
||||||
result.push("...");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const element = key.toString() + ": " + this.serializeItem(obj[key]);
|
|
||||||
length += element.length;
|
|
||||||
result.push(element);
|
|
||||||
}
|
|
||||||
return "{" + result.join(", ") + "}";
|
return "{" + result.join(", ") + "}";
|
||||||
},
|
},
|
||||||
map(obj) {
|
map(obj) {
|
||||||
@@ -173,40 +165,34 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
initDevtools(frame = "top") {
|
|
||||||
if (!this.devtoolsInit) {
|
|
||||||
this.frame = frame;
|
|
||||||
const self = this;
|
|
||||||
// Flush the events batcher when a root render is completed
|
|
||||||
const original_Complete = self.RootFiber.prototype.complete;
|
|
||||||
self.RootFiber.prototype.complete = function () {
|
|
||||||
original_Complete.call(this, ...arguments);
|
|
||||||
const path = self.getComponentPath(this.node);
|
|
||||||
//Add a functionnality to the complete function which sends a message to the window every time it is triggered.
|
|
||||||
window.top.postMessage({
|
|
||||||
source: "owl-devtools",
|
|
||||||
type: "Complete",
|
|
||||||
data: path,
|
|
||||||
origin: { frame: self.frame },
|
|
||||||
});
|
|
||||||
if (self.recordEvents) {
|
|
||||||
window.top.postMessage({
|
|
||||||
source: "owl-devtools",
|
|
||||||
type: "Event",
|
|
||||||
data: self.eventsBatch,
|
|
||||||
});
|
|
||||||
self.eventsBatch = [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
this.devtoolsInit = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Modify the methods of the apps set in order to send a message each time it is modified.
|
// Modify the methods of the apps set in order to send a message each time it is modified.
|
||||||
patchAppsSetMethods() {
|
patchAppsSetMethods() {
|
||||||
const originalAdd = this.apps.add;
|
const originalAdd = this.apps.add;
|
||||||
const originalDelete = this.apps.delete;
|
const originalDelete = this.apps.delete;
|
||||||
|
const self = this;
|
||||||
this.apps.add = function () {
|
this.apps.add = function () {
|
||||||
originalAdd.call(this, ...arguments);
|
originalAdd.call(this, ...arguments);
|
||||||
|
if (!self.destroyPatched) {
|
||||||
|
const newApp = arguments[0];
|
||||||
|
// It is not a given that apps have a root node at creation so we need to wait
|
||||||
|
if (newApp.root) {
|
||||||
|
self.patchDestroyMethod(newApp.root);
|
||||||
|
} else {
|
||||||
|
let root = null;
|
||||||
|
Object.defineProperty(newApp, "root", {
|
||||||
|
get() {
|
||||||
|
return root;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
root = value;
|
||||||
|
if (!self.destroyPatched) {
|
||||||
|
self.patchDestroyMethod(root);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.patchAppMethods();
|
||||||
window.top.postMessage({
|
window.top.postMessage({
|
||||||
source: "owl-devtools",
|
source: "owl-devtools",
|
||||||
type: "RefreshApps",
|
type: "RefreshApps",
|
||||||
@@ -221,24 +207,63 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
patchDestroyMethod(root) {
|
||||||
|
if (!this.destroyPatched) {
|
||||||
|
// Signals when a component is destroyed
|
||||||
|
const originalDestroy = root.constructor.prototype._destroy;
|
||||||
|
const self = this;
|
||||||
|
root.constructor.prototype._destroy = function () {
|
||||||
|
if (self.recordEvents) {
|
||||||
|
const path = self.getComponentPath(this);
|
||||||
|
const event = {
|
||||||
|
type: "destroy",
|
||||||
|
component: this.name,
|
||||||
|
key: this.parentKey,
|
||||||
|
path: path,
|
||||||
|
time: 0,
|
||||||
|
id: self.eventId++,
|
||||||
|
};
|
||||||
|
self.eventsBatch.push(event);
|
||||||
|
const before = performance.now();
|
||||||
|
originalDestroy.call(this, ...arguments);
|
||||||
|
event.time = performance.now() - before;
|
||||||
|
} else {
|
||||||
|
originalDestroy.call(this, ...arguments);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
this.destroyPatched = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Modify methods of each app so that it triggers messages on each flush and component render
|
// Modify methods of each app so that it triggers messages on each flush and component render
|
||||||
patchAppMethods() {
|
patchAppMethods() {
|
||||||
let app;
|
if (this.appsPatched) {
|
||||||
for (const appItem of this.apps) {
|
return;
|
||||||
if (appItem.root) {
|
}
|
||||||
app = appItem;
|
let app = this.apps.values().next().value;
|
||||||
}
|
if (!app) {
|
||||||
}
|
|
||||||
if (!app.root) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const self = this;
|
|
||||||
const originalFlush = app.scheduler.constructor.prototype.flush;
|
const originalFlush = app.scheduler.constructor.prototype.flush;
|
||||||
let inFlush = false;
|
let inFlush = false;
|
||||||
let _render = false;
|
let _render = false;
|
||||||
|
const self = this;
|
||||||
app.scheduler.constructor.prototype.flush = function () {
|
app.scheduler.constructor.prototype.flush = function () {
|
||||||
// Used to know when a render is triggered inside the flush method or not
|
// Used to know when a render is triggered inside the flush method or not
|
||||||
inFlush = true;
|
inFlush = true;
|
||||||
|
[...this.tasks].map((fiber) => {
|
||||||
|
if (fiber.counter === 0 && !self.queuedFibers.has(fiber)) {
|
||||||
|
self.queuedFibers.add(fiber);
|
||||||
|
const path = self.getComponentPath(fiber.node);
|
||||||
|
//Add a functionnality to the flush function which sends a message to the window every time it is triggered.
|
||||||
|
window.top.postMessage({
|
||||||
|
source: "owl-devtools",
|
||||||
|
type: "Flush",
|
||||||
|
data: path,
|
||||||
|
origin: { frame: self.frame },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
originalFlush.call(this, ...arguments);
|
originalFlush.call(this, ...arguments);
|
||||||
inFlush = false;
|
inFlush = false;
|
||||||
};
|
};
|
||||||
@@ -335,27 +360,20 @@
|
|||||||
_render = true;
|
_render = true;
|
||||||
original_Render.call(this, ...arguments);
|
original_Render.call(this, ...arguments);
|
||||||
};
|
};
|
||||||
// Signals when a component is destroyed
|
// Flush the events batcher when a root render is completed
|
||||||
const originalDestroy = app.root.constructor.prototype._destroy;
|
const original_Complete = self.RootFiber.prototype.complete;
|
||||||
app.root.constructor.prototype._destroy = function () {
|
self.RootFiber.prototype.complete = function () {
|
||||||
|
original_Complete.call(this, ...arguments);
|
||||||
if (self.recordEvents) {
|
if (self.recordEvents) {
|
||||||
const path = self.getComponentPath(this);
|
window.top.postMessage({
|
||||||
const event = {
|
source: "owl-devtools",
|
||||||
type: "destroy",
|
type: "Event",
|
||||||
component: this.name,
|
data: self.eventsBatch,
|
||||||
key: this.parentKey,
|
});
|
||||||
path: path,
|
self.eventsBatch = [];
|
||||||
time: 0,
|
|
||||||
id: self.eventId++,
|
|
||||||
};
|
|
||||||
self.eventsBatch.push(event);
|
|
||||||
const before = performance.now();
|
|
||||||
originalDestroy.call(this, ...arguments);
|
|
||||||
event.time = performance.now() - before;
|
|
||||||
} else {
|
|
||||||
originalDestroy.call(this, ...arguments);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
this.appsPatched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// patch reactivity system to activate subscription tracing
|
// patch reactivity system to activate subscription tracing
|
||||||
@@ -370,7 +388,7 @@
|
|||||||
let targetToKeysToCallbacks;
|
let targetToKeysToCallbacks;
|
||||||
|
|
||||||
// Step 1: extract internal values from owl
|
// Step 1: extract internal values from owl
|
||||||
const obj = self.reactive({}, () => {});
|
const obj = owl.reactive({}, () => {});
|
||||||
let count = 0;
|
let count = 0;
|
||||||
WeakMap.prototype.get = function () {
|
WeakMap.prototype.get = function () {
|
||||||
count++;
|
count++;
|
||||||
@@ -419,14 +437,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
toggleTracing(value) {
|
toggleTracing(value) {
|
||||||
if (value) {
|
|
||||||
this.patchAppMethods();
|
|
||||||
this.patchAppMethods = () => {}; // to only patch once
|
|
||||||
}
|
|
||||||
this.traceRenderings = value;
|
this.traceRenderings = value;
|
||||||
return this.traceRenderings;
|
return this.traceRenderings;
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSubscriptionTracing(value) {
|
toggleSubscriptionTracing(value) {
|
||||||
if (value) {
|
if (value) {
|
||||||
this.patchReactivity();
|
this.patchReactivity();
|
||||||
@@ -437,10 +450,6 @@
|
|||||||
}
|
}
|
||||||
// Enables/disables the recording of the render/destroy events based on value
|
// Enables/disables the recording of the render/destroy events based on value
|
||||||
toggleEventsRecording(value, index) {
|
toggleEventsRecording(value, index) {
|
||||||
if (value) {
|
|
||||||
this.patchAppMethods();
|
|
||||||
this.patchAppMethods = () => {}; // to only patch once
|
|
||||||
}
|
|
||||||
this.recordEvents = value;
|
this.recordEvents = value;
|
||||||
this.eventId = index;
|
this.eventId = index;
|
||||||
return this.recordEvents;
|
return this.recordEvents;
|
||||||
@@ -707,7 +716,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (obj) {
|
if (obj) {
|
||||||
obj = this.toRaw(obj);
|
obj = owl.toRaw(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return obj;
|
return obj;
|
||||||
@@ -748,9 +757,6 @@
|
|||||||
child.contentType = "object";
|
child.contentType = "object";
|
||||||
child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true);
|
child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true);
|
||||||
child.hasChildren = true;
|
child.hasChildren = true;
|
||||||
if (!oldTree && type === "env") {
|
|
||||||
child.toggled = true;
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
case "set entries":
|
case "set entries":
|
||||||
case "map entries":
|
case "map entries":
|
||||||
@@ -797,48 +803,57 @@
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (!child.contentType) {
|
if (child.contentType) {
|
||||||
if (obj === null) {
|
if (child.toggled) {
|
||||||
child.content = "null";
|
child.children = this.loadObjectChildren(
|
||||||
child.contentType = "object";
|
child.path,
|
||||||
child.hasChildren = false;
|
child.depth,
|
||||||
} else if (obj === undefined) {
|
child.contentType,
|
||||||
child.content = "undefined";
|
child.objectType,
|
||||||
child.contentType = "undefined";
|
oldTree
|
||||||
child.hasChildren = false;
|
);
|
||||||
|
}
|
||||||
|
return child;
|
||||||
|
}
|
||||||
|
if (obj === null) {
|
||||||
|
child.content = "null";
|
||||||
|
child.contentType = "object";
|
||||||
|
child.hasChildren = false;
|
||||||
|
} else if (obj === undefined) {
|
||||||
|
child.content = "undefined";
|
||||||
|
child.contentType = "undefined";
|
||||||
|
child.hasChildren = false;
|
||||||
|
} else {
|
||||||
|
obj = owl.toRaw(obj);
|
||||||
|
switch (true) {
|
||||||
|
case obj instanceof Map:
|
||||||
|
child.contentType = "map";
|
||||||
|
child.hasChildren = true;
|
||||||
|
break;
|
||||||
|
case obj instanceof Set:
|
||||||
|
child.contentType = "set";
|
||||||
|
child.hasChildren = true;
|
||||||
|
break;
|
||||||
|
case obj instanceof Array:
|
||||||
|
child.contentType = "array";
|
||||||
|
child.hasChildren = obj.length > 0;
|
||||||
|
break;
|
||||||
|
case typeof obj === "function":
|
||||||
|
child.contentType = "function";
|
||||||
|
child.hasChildren = true;
|
||||||
|
break;
|
||||||
|
case obj instanceof Object:
|
||||||
|
child.contentType = "object";
|
||||||
|
child.hasChildren = Object.keys(obj).length > 0;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
child.contentType = typeof obj;
|
||||||
|
child.hasChildren = false;
|
||||||
|
}
|
||||||
|
if (key.type === "set entry") {
|
||||||
|
child.content = this.serializer.serializeItem(obj, true);
|
||||||
} else {
|
} else {
|
||||||
obj = this.toRaw(obj);
|
child.content = this.serializer.serializeContent(obj, child.contentType);
|
||||||
switch (true) {
|
|
||||||
case obj instanceof Map:
|
|
||||||
child.contentType = "map";
|
|
||||||
child.hasChildren = true;
|
|
||||||
break;
|
|
||||||
case obj instanceof Set:
|
|
||||||
child.contentType = "set";
|
|
||||||
child.hasChildren = true;
|
|
||||||
break;
|
|
||||||
case obj instanceof Array:
|
|
||||||
child.contentType = "array";
|
|
||||||
child.hasChildren = obj.length > 0;
|
|
||||||
break;
|
|
||||||
case typeof obj === "function":
|
|
||||||
child.contentType = "function";
|
|
||||||
child.hasChildren = true;
|
|
||||||
break;
|
|
||||||
case obj instanceof Object:
|
|
||||||
child.contentType = "object";
|
|
||||||
child.hasChildren =
|
|
||||||
Object.keys(obj).length || Object.getOwnPropertySymbols(obj).length;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
child.contentType = typeof obj;
|
|
||||||
child.hasChildren = false;
|
|
||||||
}
|
|
||||||
if (key.type === "set entry") {
|
|
||||||
child.content = this.serializer.serializeItem(obj, true);
|
|
||||||
} else {
|
|
||||||
child.content = this.serializer.serializeContent(obj, child.contentType);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (child.toggled) {
|
if (child.toggled) {
|
||||||
@@ -850,7 +865,6 @@
|
|||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.addHighlightedKeys(child);
|
|
||||||
return child;
|
return child;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -860,10 +874,7 @@
|
|||||||
let path = completePath.slice(objPathIndex);
|
let path = completePath.slice(objPathIndex);
|
||||||
let obj;
|
let obj;
|
||||||
if (objType === "subscription") {
|
if (objType === "subscription") {
|
||||||
const subscriptionPath = completePath.slice(0, objPathIndex + 3);
|
obj = oldTree.subscriptions.children[path[1].value].target;
|
||||||
obj = oldTree.subscriptions.children.find(
|
|
||||||
(child) => JSON.stringify(child.target.path) === JSON.stringify(subscriptionPath)
|
|
||||||
).target;
|
|
||||||
path = path.slice(3);
|
path = path.slice(3);
|
||||||
} else {
|
} else {
|
||||||
// Everything here is in component if it is not an app so remove this key of the path in the former case
|
// Everything here is in component if it is not an app so remove this key of the path in the former case
|
||||||
@@ -893,7 +904,7 @@
|
|||||||
const children = [];
|
const children = [];
|
||||||
depth = depth + 1;
|
depth = depth + 1;
|
||||||
let obj = this.getObjectProperty(path);
|
let obj = this.getObjectProperty(path);
|
||||||
let oldBranch = oldTree && this.getObjectInOldTree(oldTree, path, objType);
|
let oldBranch = this.getObjectInOldTree(oldTree, path, objType);
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
@@ -908,7 +919,7 @@
|
|||||||
depth,
|
depth,
|
||||||
objType,
|
objType,
|
||||||
path,
|
path,
|
||||||
oldBranch?.children[0],
|
oldBranch.children[0],
|
||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
children.push(mapKey);
|
children.push(mapKey);
|
||||||
@@ -918,7 +929,7 @@
|
|||||||
depth,
|
depth,
|
||||||
objType,
|
objType,
|
||||||
path,
|
path,
|
||||||
oldBranch?.children[1],
|
oldBranch.children[1],
|
||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
children.push(mapValue);
|
children.push(mapValue);
|
||||||
@@ -929,7 +940,7 @@
|
|||||||
depth,
|
depth,
|
||||||
objType,
|
objType,
|
||||||
path,
|
path,
|
||||||
oldBranch?.children[0],
|
oldBranch.children[0],
|
||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
children.push(setValue);
|
children.push(setValue);
|
||||||
@@ -950,7 +961,7 @@
|
|||||||
depth,
|
depth,
|
||||||
objType,
|
objType,
|
||||||
path,
|
path,
|
||||||
oldBranch?.children[index],
|
oldBranch.children[index],
|
||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
if (child) {
|
if (child) {
|
||||||
@@ -965,7 +976,7 @@
|
|||||||
depth,
|
depth,
|
||||||
objType,
|
objType,
|
||||||
path,
|
path,
|
||||||
oldBranch?.children[index],
|
oldBranch.children[index],
|
||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
if (child) {
|
if (child) {
|
||||||
@@ -982,13 +993,26 @@
|
|||||||
depth,
|
depth,
|
||||||
objType,
|
objType,
|
||||||
path,
|
path,
|
||||||
oldBranch?.children[index],
|
oldBranch.children[index],
|
||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
if (entries) {
|
if (entries) {
|
||||||
children.push(entries);
|
children.push(entries);
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
|
const size = this.serializeObjectChild(
|
||||||
|
obj,
|
||||||
|
{ type: "item", value: "size", childIndex: children.length },
|
||||||
|
depth,
|
||||||
|
objType,
|
||||||
|
path,
|
||||||
|
oldBranch.children[index],
|
||||||
|
oldTree
|
||||||
|
);
|
||||||
|
if (size) {
|
||||||
|
children.push(size);
|
||||||
|
index++;
|
||||||
|
}
|
||||||
Reflect.ownKeys(obj).forEach((key) => {
|
Reflect.ownKeys(obj).forEach((key) => {
|
||||||
const child = this.serializeObjectChild(
|
const child = this.serializeObjectChild(
|
||||||
obj,
|
obj,
|
||||||
@@ -996,7 +1020,7 @@
|
|||||||
depth,
|
depth,
|
||||||
objType,
|
objType,
|
||||||
path,
|
path,
|
||||||
oldBranch?.children[index],
|
oldBranch.children[index],
|
||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
if (child) {
|
if (child) {
|
||||||
@@ -1031,7 +1055,7 @@
|
|||||||
depth,
|
depth,
|
||||||
objType,
|
objType,
|
||||||
path,
|
path,
|
||||||
oldBranch?.children[index],
|
oldBranch.children[index],
|
||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
if (child) children.push(child);
|
if (child) children.push(child);
|
||||||
@@ -1064,14 +1088,14 @@
|
|||||||
});
|
});
|
||||||
proto = Object.getPrototypeOf(proto);
|
proto = Object.getPrototypeOf(proto);
|
||||||
}
|
}
|
||||||
if (obj.__proto__) {
|
if (!(obj.constructor.name === "Object")) {
|
||||||
prototype = this.serializeObjectChild(
|
prototype = this.serializeObjectChild(
|
||||||
obj,
|
obj,
|
||||||
{ type: "prototype", childIndex: children.length },
|
{ type: "prototype", childIndex: children.length },
|
||||||
depth,
|
depth,
|
||||||
objType,
|
objType,
|
||||||
path,
|
path,
|
||||||
oldBranch?.children.at(-1),
|
oldBranch.children.at(-1),
|
||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
children.push(prototype);
|
children.push(prototype);
|
||||||
@@ -1276,15 +1300,16 @@
|
|||||||
children: [],
|
children: [],
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const rawSubscriptions = this.topLevelSubscriptions(node);
|
const rawSubscriptions = node.subscriptions;
|
||||||
component.subscriptions = {
|
component.subscriptions = {
|
||||||
toggled: oldTree ? oldTree.subscriptions.toggled : true,
|
toggled: oldTree ? oldTree.subscriptions.toggled : true,
|
||||||
children: [],
|
children: [],
|
||||||
};
|
};
|
||||||
rawSubscriptions.forEach((rawSubscription) => {
|
rawSubscriptions.forEach((rawSubscription, index) => {
|
||||||
let subscription = {
|
let subscription = {
|
||||||
|
keys: [],
|
||||||
target: {
|
target: {
|
||||||
name: this.targetName(rawSubscription.target, node),
|
name: "target",
|
||||||
contentType:
|
contentType:
|
||||||
typeof rawSubscription.target === "object"
|
typeof rawSubscription.target === "object"
|
||||||
? Array.isArray(rawSubscription.target)
|
? Array.isArray(rawSubscription.target)
|
||||||
@@ -1295,20 +1320,28 @@
|
|||||||
path: [
|
path: [
|
||||||
...path,
|
...path,
|
||||||
{ type: "item", value: "subscriptions" },
|
{ type: "item", value: "subscriptions" },
|
||||||
{ type: "item", value: rawSubscription.index },
|
{ type: "item", value: index },
|
||||||
{ type: "item", value: "target" },
|
{ type: "item", value: "target" },
|
||||||
],
|
],
|
||||||
toggled: false,
|
toggled: false,
|
||||||
objectType: "subscription",
|
objectType: "subscription",
|
||||||
},
|
},
|
||||||
|
keysExpanded: false,
|
||||||
};
|
};
|
||||||
if (
|
if (
|
||||||
oldTree &&
|
oldTree &&
|
||||||
oldTree.subscriptions.children[rawSubscription.index] &&
|
oldTree.subscriptions.children[index] &&
|
||||||
oldTree.subscriptions.children[rawSubscription.index].target.toggled
|
oldTree.subscriptions.children[index].target.toggled
|
||||||
) {
|
) {
|
||||||
subscription.target.toggled = true;
|
subscription.target.toggled = true;
|
||||||
}
|
}
|
||||||
|
rawSubscription.keys.forEach((key) => {
|
||||||
|
if (typeof key === "symbol") {
|
||||||
|
subscription.keys.push(key.toString());
|
||||||
|
} else {
|
||||||
|
subscription.keys.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
if (rawSubscription.target == null) {
|
if (rawSubscription.target == null) {
|
||||||
if (subscription.target.contentType === "undefined") {
|
if (subscription.target.contentType === "undefined") {
|
||||||
subscription.target.content = "undefined";
|
subscription.target.content = "undefined";
|
||||||
@@ -1338,7 +1371,6 @@
|
|||||||
oldTree
|
oldTree
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.addHighlightedKeys(subscription.target);
|
|
||||||
component.subscriptions.children.push(subscription);
|
component.subscriptions.children.push(subscription);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1357,7 +1389,7 @@
|
|||||||
}
|
}
|
||||||
getter.hasChildren = false;
|
getter.hasChildren = false;
|
||||||
} else {
|
} else {
|
||||||
obj = this.toRaw(obj);
|
obj = owl.toRaw(obj);
|
||||||
switch (true) {
|
switch (true) {
|
||||||
case obj instanceof Map:
|
case obj instanceof Map:
|
||||||
getter.contentType = "map";
|
getter.contentType = "map";
|
||||||
@@ -1446,16 +1478,13 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const item = path.pop();
|
const key = path.pop().value;
|
||||||
const obj = this.getObjectProperty(path);
|
const obj = this.getObjectProperty(path);
|
||||||
const key = item.hasOwnProperty("symbolIndex")
|
|
||||||
? Object.getOwnPropertySymbols(obj)[item.symbolIndex]
|
|
||||||
: item.value;
|
|
||||||
if (!obj) {
|
if (!obj) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (objectType === "subscription") {
|
if (objectType === "subscription") {
|
||||||
this.reactive(obj)[key] = value;
|
owl.reactive(obj)[key] = value;
|
||||||
} else {
|
} else {
|
||||||
obj[key] = value;
|
obj[key] = value;
|
||||||
if (objectType === "props" || objectType === "instance") {
|
if (objectType === "props" || objectType === "instance") {
|
||||||
@@ -1524,7 +1553,7 @@
|
|||||||
}
|
}
|
||||||
// Returns the tree of components of the inspected page in a parsed format
|
// Returns the tree of components of the inspected page in a parsed format
|
||||||
// Use inspectedPath to specify the path of the selected component
|
// Use inspectedPath to specify the path of the selected component
|
||||||
getComponentsTree(inspectedPath = null, oldTrees = null, oldDetails = null) {
|
getComponentsTree(inspectedPath = null, oldTrees = null) {
|
||||||
const appsArray = [...this.apps];
|
const appsArray = [...this.apps];
|
||||||
const trees = appsArray.map((app, index) => {
|
const trees = appsArray.map((app, index) => {
|
||||||
let oldTree;
|
let oldTree;
|
||||||
@@ -1577,8 +1606,7 @@
|
|||||||
}
|
}
|
||||||
return appNode;
|
return appNode;
|
||||||
});
|
});
|
||||||
const component = this.getComponentDetails(inspectedPath, oldDetails);
|
return trees ? trees : [];
|
||||||
return trees ? [trees, component] : [];
|
|
||||||
}
|
}
|
||||||
// Recursively fills the components tree as a parsed version
|
// Recursively fills the components tree as a parsed version
|
||||||
fillTree(appNode, treeNode, inspectedPathString, oldBranch) {
|
fillTree(appNode, treeNode, inspectedPathString, oldBranch) {
|
||||||
@@ -1674,54 +1702,6 @@
|
|||||||
inspect(obj);
|
inspect(obj);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
targetName(target, node) {
|
|
||||||
// check on component
|
|
||||||
const { component } = node;
|
|
||||||
for (const [key, value] of Object.entries(component)) {
|
|
||||||
if (target === this.toRaw(value)) {
|
|
||||||
return key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// check on props
|
|
||||||
for (const [key, value] of Object.entries(component.props)) {
|
|
||||||
if (target === this.toRaw(value)) {
|
|
||||||
return `props.${key}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "[unknown]";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Removes subscriptions that are a direct child of another subscription:
|
|
||||||
* they will be reachable from the top level by expanding observed keys.
|
|
||||||
*
|
|
||||||
* @param {ComponentNode} node
|
|
||||||
* @returns {{ keys: PropertyKey[], target: unknown}[]} the top level
|
|
||||||
* subscriptions of the node
|
|
||||||
*/
|
|
||||||
topLevelSubscriptions(node) {
|
|
||||||
const subscriptions = node.subscriptions.map((s, index) => ({ ...s, index }));
|
|
||||||
const topLevelValues = new Set(Object.values(node.component).map((o) => this.toRaw(o)));
|
|
||||||
const toOmit = new Set(
|
|
||||||
subscriptions
|
|
||||||
.flatMap(({ keys, target }) => keys.map((k) => this.toRaw(target[k])))
|
|
||||||
.filter((obj) => !topLevelValues.has(obj))
|
|
||||||
);
|
|
||||||
return subscriptions.filter(({ target }) => !toOmit.has(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
addHighlightedKeys(child) {
|
|
||||||
const { path } = child;
|
|
||||||
const subscriptionIndex = path.findIndex((item) => typeof item !== "string");
|
|
||||||
if (path[subscriptionIndex]?.value === "subscriptions") {
|
|
||||||
const node = this.getComponentNode(path.slice(0, subscriptionIndex));
|
|
||||||
// Add observed keys
|
|
||||||
const targetToKeys = new Map(node.subscriptions.map(({ keys, target }) => [target, keys]));
|
|
||||||
const target = this.getObjectProperty(child.path);
|
|
||||||
child.keys = targetToKeys.get(target)?.map((k) => String(k));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkOwlStatus() {
|
function checkOwlStatus() {
|
||||||
|
|||||||
Reference in New Issue
Block a user