mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b11a440625 | |||
| 8b1dc4c43d | |||
| c105c6da38 | |||
| 3001420a1d | |||
| 432ff444a1 | |||
| aa3c88a6c4 | |||
| 601a98e649 | |||
| 23c7d19ef0 | |||
| 59c49b5833 | |||
| 2cca0bd819 |
+50
-18
@@ -175,11 +175,21 @@ function createAttrUpdater(attr) {
|
||||
}
|
||||
function attrsSetter(attrs) {
|
||||
if (isArray(attrs)) {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
if (attrs[0] === "class") {
|
||||
setClass.call(this, attrs[1]);
|
||||
}
|
||||
else {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
}
|
||||
}
|
||||
else {
|
||||
for (let k in attrs) {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
if (k === "class") {
|
||||
setClass.call(this, attrs[k]);
|
||||
}
|
||||
else {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -191,7 +201,12 @@ function attrsUpdater(attrs, oldAttrs) {
|
||||
if (val === oldAttrs[1]) {
|
||||
return;
|
||||
}
|
||||
setAttribute.call(this, name, val);
|
||||
if (name === "class") {
|
||||
updateClass.call(this, val, oldAttrs[1]);
|
||||
}
|
||||
else {
|
||||
setAttribute.call(this, name, val);
|
||||
}
|
||||
}
|
||||
else {
|
||||
removeAttribute.call(this, oldAttrs[0]);
|
||||
@@ -201,13 +216,23 @@ function attrsUpdater(attrs, oldAttrs) {
|
||||
else {
|
||||
for (let k in oldAttrs) {
|
||||
if (!(k in attrs)) {
|
||||
removeAttribute.call(this, k);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, "", oldAttrs[k]);
|
||||
}
|
||||
else {
|
||||
removeAttribute.call(this, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let k in attrs) {
|
||||
const val = attrs[k];
|
||||
if (val !== oldAttrs[k]) {
|
||||
setAttribute.call(this, k, val);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, val, oldAttrs[k]);
|
||||
}
|
||||
else {
|
||||
setAttribute.call(this, k, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3875,6 +3900,10 @@ class CodeGenerator {
|
||||
})
|
||||
.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
|
||||
*/
|
||||
@@ -3952,8 +3981,7 @@ class CodeGenerator {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
let value = ast.value;
|
||||
if (value && ctx.translate !== false) {
|
||||
const match = translationRE.exec(value);
|
||||
value = match[1] + this.translateFn(match[2]) + match[3];
|
||||
value = this.translate(value);
|
||||
}
|
||||
if (!ctx.inPreTag) {
|
||||
value = value.replace(whitespaceRE, " ");
|
||||
@@ -4494,11 +4522,12 @@ class CodeGenerator {
|
||||
else {
|
||||
let value;
|
||||
if (ast.defaultValue) {
|
||||
const defaultValue = ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue;
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, \`${ast.defaultValue}\`)`;
|
||||
value = `withDefault(${expr}, \`${defaultValue}\`)`;
|
||||
}
|
||||
else {
|
||||
value = `\`${ast.defaultValue}\``;
|
||||
value = `\`${defaultValue}\``;
|
||||
}
|
||||
}
|
||||
else {
|
||||
@@ -4879,10 +4908,10 @@ function parseDOMNode(node, ctx) {
|
||||
let model = null;
|
||||
for (let attr of nodeAttrsNames) {
|
||||
const value = node.getAttribute(attr);
|
||||
if (attr.startsWith("t-on")) {
|
||||
if (attr === "t-on") {
|
||||
throw new OwlError("Missing event name with t-on directive");
|
||||
}
|
||||
if (attr === "t-on" || attr === "t-on-") {
|
||||
throw new OwlError("Missing event name with t-on directive");
|
||||
}
|
||||
if (attr.startsWith("t-on-")) {
|
||||
on = on || {};
|
||||
on[attr.slice(5)] = value;
|
||||
}
|
||||
@@ -5506,7 +5535,7 @@ function compile(template, options = {}) {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.1.2";
|
||||
const version = "2.1.3";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -5585,6 +5614,8 @@ window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = {
|
||||
apps: new Set(),
|
||||
Fiber: Fiber,
|
||||
RootFiber: RootFiber,
|
||||
toRaw: toRaw,
|
||||
reactive: reactive,
|
||||
});
|
||||
class App extends TemplateSet {
|
||||
constructor(Root, config = {}) {
|
||||
@@ -5837,8 +5868,9 @@ function useChildSubEnv(envExtension) {
|
||||
* will run a cleanup function before patching and before unmounting the
|
||||
* the component.
|
||||
*
|
||||
* @param {Effect} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* @template T
|
||||
* @param {Effect<T>} effect the effect to run on component mount and/or patch
|
||||
* @param {()=>T} [computeDependencies=()=>[NaN]] a callback to compute
|
||||
* 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
|
||||
* again. The default value returns an array containing only NaN because
|
||||
@@ -5920,6 +5952,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 };
|
||||
|
||||
|
||||
__info__.date = '2023-04-29T07:45:54.333Z';
|
||||
__info__.hash = 'aabb755';
|
||||
__info__.date = '2023-06-28T09:17:13.630Z';
|
||||
__info__.hash = '432ff44';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
Generated
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.1.3",
|
||||
"version": "2.1.4",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.1.3",
|
||||
"version": "2.1.4",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
|
||||
@@ -36,10 +36,18 @@ export function createAttrUpdater(attr: string): Setter<HTMLElement> {
|
||||
|
||||
export function attrsSetter(this: HTMLElement, attrs: any) {
|
||||
if (isArray(attrs)) {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
if (attrs[0] === "class") {
|
||||
setClass.call(this, attrs[1]);
|
||||
} else {
|
||||
setAttribute.call(this, attrs[0], attrs[1]);
|
||||
}
|
||||
} else {
|
||||
for (let k in attrs) {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
if (k === "class") {
|
||||
setClass.call(this, attrs[k]);
|
||||
} else {
|
||||
setAttribute.call(this, k, attrs[k]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -52,7 +60,11 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
||||
if (val === oldAttrs[1]) {
|
||||
return;
|
||||
}
|
||||
setAttribute.call(this, name, val);
|
||||
if (name === "class") {
|
||||
updateClass.call(this, val, oldAttrs[1]);
|
||||
} else {
|
||||
setAttribute.call(this, name, val);
|
||||
}
|
||||
} else {
|
||||
removeAttribute.call(this, oldAttrs[0]);
|
||||
setAttribute.call(this, name, val);
|
||||
@@ -60,13 +72,21 @@ export function attrsUpdater(this: HTMLElement, attrs: any, oldAttrs: any) {
|
||||
} else {
|
||||
for (let k in oldAttrs) {
|
||||
if (!(k in attrs)) {
|
||||
removeAttribute.call(this, k);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, "", oldAttrs[k]);
|
||||
} else {
|
||||
removeAttribute.call(this, k);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let k in attrs) {
|
||||
const val = attrs[k];
|
||||
if (val !== oldAttrs[k]) {
|
||||
setAttribute.call(this, k, val);
|
||||
if (k === "class") {
|
||||
updateClass.call(this, val, oldAttrs[k]);
|
||||
} else {
|
||||
setAttribute.call(this, k, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,12 +8,6 @@ import { OwlError } from "./error_handling";
|
||||
import type { ComponentNode } from "./component_node";
|
||||
|
||||
const ObjectCreate = Object.create;
|
||||
const ObjectGetPrototypeOf = Object.getPrototypeOf;
|
||||
const ObjectGetOwnPropertyDescriptors = Object.getOwnPropertyDescriptors;
|
||||
const ObjectDefineProperty = Object.defineProperty;
|
||||
const ObjectEntries = Object.entries;
|
||||
const hasOwnProperty = (obj: Object, prop: PropertyKey) =>
|
||||
Object.prototype.hasOwnProperty.call(obj, prop);
|
||||
/**
|
||||
* This file contains utility functions that will be injected in each template,
|
||||
* to perform various useful tasks in the compiled code.
|
||||
@@ -55,14 +49,8 @@ function callSlot(
|
||||
|
||||
function capture(ctx: any): any {
|
||||
const result = ObjectCreate(ctx);
|
||||
let current = ctx;
|
||||
while (current && current !== Object.prototype) {
|
||||
for (const [key, descriptor] of ObjectEntries(ObjectGetOwnPropertyDescriptors(current))) {
|
||||
if (!hasOwnProperty(result, key) && "value" in descriptor) {
|
||||
ObjectDefineProperty(result, key, descriptor);
|
||||
}
|
||||
}
|
||||
current = ObjectGetPrototypeOf(current);
|
||||
for (let k in ctx) {
|
||||
result[k] = ctx[k];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.1.3";
|
||||
export const version = "2.1.4";
|
||||
|
||||
@@ -145,3 +145,34 @@ test("class attribute (with a preexisting value", async () => {
|
||||
patch(tree, block([""]));
|
||||
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,6 +707,123 @@ 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`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -371,4 +371,33 @@ describe("attributes", () => {
|
||||
// not sure about this. maybe we want to remove the attribute?
|
||||
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>');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,50 +1,5 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`t-call component with an enumerable getter, t-call inside slot 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { capture, markRaw } = helpers;
|
||||
const callTemplate_1 = app.getTemplate(\`sub\`);
|
||||
const comp1 = app.createComponent(\`Child\`, true, true, false, []);
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
return callTemplate_1.call(this, ctx, node, key + \`__1\`);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const ctx1 = capture(ctx);
|
||||
return comp1({slots: markRaw({'default': {__render: slot1.bind(this), __ctx: ctx1}})}, key + \`__2\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call component with an enumerable getter, t-call inside slot 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['foo'];
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call component with an enumerable getter, t-call inside slot 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { callSlot } = helpers;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return callSlot(ctx, node, key, 'default', false, {});
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-call dynamic t-call 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -504,6 +504,48 @@ test("update a sub-component twice in the same frame", async () => {
|
||||
]).toBeLogged();
|
||||
});
|
||||
|
||||
test.only("abcde", async () => {
|
||||
class Parent extends Component {
|
||||
static template = xml`<span t-esc="state.x"/>`;
|
||||
state = useState({ x: 1 });
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
|
||||
async _doIt() {
|
||||
await Promise.resolve();
|
||||
this.state.x++;
|
||||
}
|
||||
|
||||
async doIt() {
|
||||
await this._doIt();
|
||||
this.state.x++;
|
||||
}
|
||||
}
|
||||
|
||||
const parent = await mount(Parent, fixture);
|
||||
expect([
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:mounted",
|
||||
]).toBeLogged();
|
||||
expect(fixture.innerHTML).toBe("<span>1</span>");
|
||||
|
||||
parent.doIt();
|
||||
await nextTick();
|
||||
expect([
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willRender",
|
||||
"Parent:rendered",
|
||||
"Parent:willPatch",
|
||||
"Parent:patched",
|
||||
]).toBeLogged();
|
||||
expect(fixture.innerHTML).toBe("<span>3</span>");
|
||||
});
|
||||
|
||||
test("update a sub-component twice in the same frame, 2", async () => {
|
||||
class ChildA extends Component {
|
||||
static template = xml`<span><t t-esc="val()"/></span>`;
|
||||
|
||||
@@ -425,26 +425,4 @@ describe("t-call", () => {
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("Bchild");
|
||||
});
|
||||
|
||||
test("component with an enumerable getter, t-call inside slot", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`<t t-slot="default"/>`;
|
||||
}
|
||||
class Parent extends Component {
|
||||
static components = { Child };
|
||||
static template = xml`<Child><t t-call="sub"/></Child>`;
|
||||
}
|
||||
// simulate adding a getter with patch in odoo: getter will be enumarable
|
||||
Object.defineProperty(Parent.prototype, "foo", {
|
||||
get() {
|
||||
return 1;
|
||||
},
|
||||
enumerable: true,
|
||||
});
|
||||
const app = new App(Parent);
|
||||
app.addTemplate("sub", `<div t-esc="foo"/>`);
|
||||
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe("<div>1</div>");
|
||||
});
|
||||
});
|
||||
|
||||
+1
-2
@@ -1,11 +1,10 @@
|
||||
const { Component, useRef, useEffect } = owl;
|
||||
import { useStore } from "../../../store/store";
|
||||
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
||||
import { Subscriptions } from "./subscriptions/subscriptions";
|
||||
|
||||
export class DetailsWindow extends Component {
|
||||
static template = "devtools.DetailsWindow";
|
||||
static components = { ObjectTreeElement, Subscriptions };
|
||||
static components = { ObjectTreeElement };
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.contextMenu = useRef("contextmenu");
|
||||
|
||||
+5
-1
@@ -53,7 +53,11 @@
|
||||
</div>
|
||||
<i title="Store observed states as global variable in the console" class="fa fa-bug utility-icon p-1" t-on-click.stop="() => this.store.logObjectInConsole([...this.store.activeComponent.path, {type: 'item', value: 'subscriptions'}])"></i>
|
||||
</div>
|
||||
<Subscriptions t-if="store.activeComponent.subscriptions.toggled"/>
|
||||
<div t-if="store.activeComponent.subscriptions.toggled" id="subscriptionsPanel">
|
||||
<t t-foreach="store.activeComponent.subscriptions.children" t-as="subscription" t-key="subscription_index">
|
||||
<ObjectTreeElement object="subscription.target"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
|
||||
<div class="d-flex mb-2">
|
||||
|
||||
+9
-10
@@ -41,23 +41,22 @@ export class ObjectTreeElement extends Component {
|
||||
return JSON.stringify(this.props.object.path);
|
||||
}
|
||||
|
||||
get objectName() {
|
||||
return this.props.object.name;
|
||||
get keyChanges() {
|
||||
return this.props.object.keys?.includes("Symbol(Key changes)");
|
||||
}
|
||||
|
||||
get objectLineClass() {
|
||||
classFor(object) {
|
||||
// Prototype items will be dyed down to appear less important
|
||||
if (this.pathAsString.includes('{"type":"prototype",')) {
|
||||
return { attenuate: true };
|
||||
if (object.path.some((item) => item?.type === "prototype") && !object.keepLit) {
|
||||
return "attenuate";
|
||||
}
|
||||
// Same for subscription items which are not present in the keys while the keys will be bold
|
||||
if (this.props.object.objectType === "subscription" && this.props.object.depth > 0) {
|
||||
if (this.props.keys.includes(this.props.object.name.toString())) {
|
||||
return { "fw-bolder": true };
|
||||
if (object.objectType === "subscription" && object.depth > 0) {
|
||||
if (this.props.object.keys?.includes(object.name.toString())) {
|
||||
return "fw-bolder";
|
||||
}
|
||||
return { attenuate: true };
|
||||
return "attenuate";
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
get objectPadding() {
|
||||
|
||||
+4
-4
@@ -2,7 +2,7 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.ObjectTreeElement" owl="1">
|
||||
<div class="m-0 p-0 text-nowrap w-100 object-line"
|
||||
t-att-class="objectLineClass"
|
||||
t-att-class="props.class"
|
||||
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
||||
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-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
|
||||
/>
|
||||
<t t-esc="objectName"/>
|
||||
<t t-esc="props.object.name"/>
|
||||
<t t-if="props.object.content.length > 0">: </t>
|
||||
<t t-if="props.object.contentType == 'getter'">
|
||||
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
|
||||
@@ -28,6 +28,7 @@
|
||||
</t>
|
||||
</span>
|
||||
</t>
|
||||
<span t-if="keyChanges" class="key-changes ms-1 badge p-1" title="Key additions/deletions are observed">+/-</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
||||
@@ -40,8 +41,7 @@
|
||||
</div>
|
||||
<t t-if="props.object.toggled" t-key="contextMenuId">
|
||||
<t t-foreach="props.object.children" t-as="child" t-key="child_index">
|
||||
<ObjectTreeElement t-if="props.object.objectType === 'subscription'" object="child" keys="props.keys"/>
|
||||
<ObjectTreeElement t-else="" object="child"/>
|
||||
<ObjectTreeElement object="child" class="this.classFor(child)"/>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
|
||||
-30
@@ -1,30 +0,0 @@
|
||||
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
@@ -1,24 +0,0 @@
|
||||
<?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,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.Event" owl="1">
|
||||
<div class="event-container">
|
||||
<div class="event-container" t-att-class="{ 'event-last': props.event.isLast }">
|
||||
<div class="my-0 p-0 object-line" t-on-click.stop="toggleDisplay">
|
||||
<div class="ps-2 text-nowrap">
|
||||
<i class="fa px-1 pointer-icon caret"
|
||||
|
||||
@@ -102,21 +102,19 @@ export const store = reactive({
|
||||
if (IS_FIREFOX) {
|
||||
await evalInWindow("window.$0 = $0;", this.activeFrame);
|
||||
}
|
||||
const apps = await evalFunctionInWindow(
|
||||
const [apps, details] = await evalFunctionInWindow(
|
||||
"getComponentsTree",
|
||||
fromOld && this.activeComponent ? [this.activeComponent.path, this.apps] : [],
|
||||
fromOld && this.activeComponent
|
||||
? [this.activeComponent.path, this.apps, this.activeComponent]
|
||||
: [],
|
||||
this.activeFrame
|
||||
);
|
||||
this.apps = apps ? apps : [];
|
||||
if (!fromOld && this.settings.expandByDefault) {
|
||||
this.apps.forEach((tree) => expandNodes(tree, true));
|
||||
}
|
||||
const component = await evalFunctionInWindow(
|
||||
"getComponentDetails",
|
||||
fromOld && this.activeComponent ? [this.activeComponent.path, this.activeComponent] : [],
|
||||
this.activeFrame
|
||||
);
|
||||
this.activeComponent = component;
|
||||
keepEnvLit(details);
|
||||
this.activeComponent = details;
|
||||
},
|
||||
|
||||
// Select a component by retrieving its details from the page based on its path
|
||||
@@ -153,9 +151,11 @@ export const store = reactive({
|
||||
[component.path],
|
||||
this.activeFrame
|
||||
);
|
||||
this.activeComponent = details;
|
||||
if (!this.activeComponent) {
|
||||
if (!details) {
|
||||
await this.loadComponentsTree(false);
|
||||
} else {
|
||||
keepEnvLit(details);
|
||||
this.activeComponent = details;
|
||||
}
|
||||
if (this.page !== "ComponentsTab") {
|
||||
this.switchTab("ComponentsTab");
|
||||
@@ -413,12 +413,7 @@ export const store = reactive({
|
||||
if (!scriptsLoaded) {
|
||||
await loadScripts(frame);
|
||||
}
|
||||
evalInWindow(
|
||||
`__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = ${
|
||||
store.devtoolsId
|
||||
}; __OWL__DEVTOOLS_GLOBAL_HOOK__.frame = ${JSON.stringify(frame)};`,
|
||||
frame
|
||||
);
|
||||
evalFunctionInWindow("initDevtools", [frame], frame);
|
||||
if (!this.frameUrls.includes(frame)) {
|
||||
this.frameUrls = [...this.frameUrls, frame];
|
||||
}
|
||||
@@ -638,7 +633,7 @@ init();
|
||||
async function init() {
|
||||
store.devtoolsId = await getTabURL();
|
||||
|
||||
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||
evalFunctionInWindow("initDevtools", []);
|
||||
|
||||
await loadSettings();
|
||||
|
||||
@@ -671,7 +666,7 @@ async function init() {
|
||||
}, 500);
|
||||
}
|
||||
|
||||
let flushRendersTimeout = false;
|
||||
let rootRendersTimeout = false;
|
||||
// Connect to the port to communicate to the background script
|
||||
browserInstance.runtime.onConnect.addListener((port) => {
|
||||
if (port.name === "OwlDevtoolsPort_" + store.devtoolsId) {
|
||||
@@ -680,7 +675,7 @@ browserInstance.runtime.onConnect.addListener((port) => {
|
||||
if (msg.type === "Reload") {
|
||||
store.owlStatus = await evalInWindow("window.__OWL__DEVTOOLS_GLOBAL_HOOK__ !== undefined;");
|
||||
if (store.owlStatus) {
|
||||
evalInWindow("__OWL__DEVTOOLS_GLOBAL_HOOK__.devtoolsId = " + store.devtoolsId + ";");
|
||||
evalFunctionInWindow("initDevtools", []);
|
||||
await store.resetData();
|
||||
}
|
||||
}
|
||||
@@ -694,9 +689,9 @@ browserInstance.runtime.onConnect.addListener((port) => {
|
||||
if (msg.type === "RefreshApps") {
|
||||
store.loadComponentsTree(true);
|
||||
}
|
||||
// When message of type Flush is received, overwrite the component tree with the new one from page
|
||||
// A flush message is sent everytime a component is rendered on the page
|
||||
if (msg.type === "Flush") {
|
||||
// When message of type Complete is received, overwrite the component tree with the new one from page
|
||||
// A Complete message is sent everytime a root render is triggered on the page
|
||||
if (msg.type === "Complete") {
|
||||
if (msg.origin.frame !== store.activeFrame) {
|
||||
return;
|
||||
}
|
||||
@@ -705,8 +700,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
|
||||
store.renderPaths.add(JSON.stringify(msg.data));
|
||||
clearTimeout(flushRendersTimeout);
|
||||
flushRendersTimeout = setTimeout(() => {
|
||||
clearTimeout(rootRendersTimeout);
|
||||
rootRendersTimeout = setTimeout(() => {
|
||||
store.renderPaths.clear();
|
||||
}, 100);
|
||||
store.loadComponentsTree(true);
|
||||
@@ -787,6 +782,7 @@ function loadEvents(events) {
|
||||
}
|
||||
event.origin = null;
|
||||
event.toggled = false;
|
||||
event.isLast = false;
|
||||
// Logic to retrace the origin of the event if it is not a root render event
|
||||
if (!event.type.includes("render")) {
|
||||
for (let i = store.events.length - 1; i >= 0; i--) {
|
||||
@@ -836,6 +832,7 @@ function loadEvents(events) {
|
||||
// Make sure we add the event while keeping the whole list ordered by id
|
||||
addEventSorted(event);
|
||||
}
|
||||
store.events[store.events.length - 1].isLast = true;
|
||||
}
|
||||
|
||||
// Deselect component and remove highlight on all children
|
||||
@@ -893,6 +890,31 @@ function expandNodes(node, blacklist = false) {
|
||||
}
|
||||
}
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fold the node given in entry and all of its children
|
||||
function foldNodes(node) {
|
||||
node.toggled = false;
|
||||
|
||||
@@ -126,6 +126,10 @@
|
||||
color: var(--prototype-color);
|
||||
}
|
||||
|
||||
.key-changes {
|
||||
background-color: var(--version-bg);
|
||||
}
|
||||
|
||||
.event-container {
|
||||
border-bottom: 1px solid rgb(240, 238, 238);
|
||||
padding-top: 2px!important;
|
||||
@@ -133,6 +137,10 @@
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.event-last {
|
||||
border-bottom: 3px solid rgb(225, 154, 0);
|
||||
}
|
||||
|
||||
.getter-content:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
// in __OWL_DEVTOOLS__
|
||||
this.toRaw = window.__OWL_DEVTOOLS__.toRaw ?? window.owl?.toRaw;
|
||||
this.reactive = window.__OWL_DEVTOOLS__.reactive ?? window.owl?.reactive;
|
||||
// Set to keep track of the fibers that are in the flush queue
|
||||
this.queuedFibers = new WeakSet();
|
||||
// Set to keep track of the HTML elements we added to the page
|
||||
this.addedElements = [];
|
||||
// To keep track of the succession order of the render events
|
||||
@@ -24,7 +22,6 @@
|
||||
// Set to keep track of the frame on which this script is loaded
|
||||
this.frame = "top";
|
||||
// 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) {
|
||||
mutationsList.forEach(function (mutation) {
|
||||
mutation.addedNodes.forEach(function (addedNode) {
|
||||
@@ -47,12 +44,7 @@
|
||||
});
|
||||
});
|
||||
iFrameObserver.observe(document.body, { subtree: true, childList: true });
|
||||
this.appsPatched = false;
|
||||
this.destroyPatched = false;
|
||||
this.patchAppsSetMethods();
|
||||
if (this.apps.size > 0) {
|
||||
this.patchAppMethods();
|
||||
}
|
||||
this.recordEvents = false;
|
||||
this.traceRenderings = false;
|
||||
this.traceSubscriptions = false;
|
||||
@@ -125,6 +117,15 @@
|
||||
length += element.length;
|
||||
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(", ") + "}";
|
||||
},
|
||||
map(obj) {
|
||||
@@ -172,34 +173,40 @@
|
||||
};
|
||||
}
|
||||
|
||||
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.
|
||||
patchAppsSetMethods() {
|
||||
const originalAdd = this.apps.add;
|
||||
const originalDelete = this.apps.delete;
|
||||
const self = this;
|
||||
this.apps.add = function () {
|
||||
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({
|
||||
source: "owl-devtools",
|
||||
type: "RefreshApps",
|
||||
@@ -214,72 +221,24 @@
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
patchAppMethods() {
|
||||
if (this.appsPatched) {
|
||||
let app;
|
||||
for (const appItem of this.apps) {
|
||||
if (appItem.root) {
|
||||
app = appItem;
|
||||
}
|
||||
}
|
||||
if (!app.root) {
|
||||
return;
|
||||
}
|
||||
let app = this.apps.values().next().value;
|
||||
const self = this;
|
||||
if (app.root) {
|
||||
this.patchDestroyMethod(app.root);
|
||||
} else {
|
||||
const originalMount = app.constructor.prototype.mount;
|
||||
app.constructor.prototype.mount = async function (...args) {
|
||||
const result = await originalMount.call(this, ...args);
|
||||
const root = this.root;
|
||||
self.patchDestroyMethod(root);
|
||||
app.constructor.prototype.mount = originalMount;
|
||||
return result;
|
||||
};
|
||||
}
|
||||
const originalFlush = app.scheduler.constructor.prototype.flush;
|
||||
let inFlush = false;
|
||||
let _render = false;
|
||||
app.scheduler.constructor.prototype.flush = function () {
|
||||
// Used to know when a render is triggered inside the flush method or not
|
||||
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);
|
||||
inFlush = false;
|
||||
};
|
||||
@@ -376,20 +335,27 @@
|
||||
_render = true;
|
||||
original_Render.call(this, ...arguments);
|
||||
};
|
||||
// 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);
|
||||
// Signals when a component is destroyed
|
||||
const originalDestroy = app.root.constructor.prototype._destroy;
|
||||
app.root.constructor.prototype._destroy = function () {
|
||||
if (self.recordEvents) {
|
||||
window.top.postMessage({
|
||||
source: "owl-devtools",
|
||||
type: "Event",
|
||||
data: self.eventsBatch,
|
||||
});
|
||||
self.eventsBatch = [];
|
||||
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.appsPatched = true;
|
||||
}
|
||||
|
||||
// patch reactivity system to activate subscription tracing
|
||||
@@ -453,9 +419,14 @@
|
||||
}
|
||||
|
||||
toggleTracing(value) {
|
||||
if (value) {
|
||||
this.patchAppMethods();
|
||||
this.patchAppMethods = () => {}; // to only patch once
|
||||
}
|
||||
this.traceRenderings = value;
|
||||
return this.traceRenderings;
|
||||
}
|
||||
|
||||
toggleSubscriptionTracing(value) {
|
||||
if (value) {
|
||||
this.patchReactivity();
|
||||
@@ -466,6 +437,10 @@
|
||||
}
|
||||
// Enables/disables the recording of the render/destroy events based on value
|
||||
toggleEventsRecording(value, index) {
|
||||
if (value) {
|
||||
this.patchAppMethods();
|
||||
this.patchAppMethods = () => {}; // to only patch once
|
||||
}
|
||||
this.recordEvents = value;
|
||||
this.eventId = index;
|
||||
return this.recordEvents;
|
||||
@@ -773,6 +748,9 @@
|
||||
child.contentType = "object";
|
||||
child.content = this.serializer.serializeItem(Object.getPrototypeOf(parentObj), true);
|
||||
child.hasChildren = true;
|
||||
if (!oldTree && type === "env") {
|
||||
child.toggled = true;
|
||||
}
|
||||
break;
|
||||
case "set entries":
|
||||
case "map entries":
|
||||
@@ -819,57 +797,48 @@
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (child.contentType) {
|
||||
if (child.toggled) {
|
||||
child.children = this.loadObjectChildren(
|
||||
child.path,
|
||||
child.depth,
|
||||
child.contentType,
|
||||
child.objectType,
|
||||
oldTree
|
||||
);
|
||||
}
|
||||
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 = this.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);
|
||||
if (!child.contentType) {
|
||||
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 {
|
||||
child.content = this.serializer.serializeContent(obj, child.contentType);
|
||||
obj = this.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 || 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) {
|
||||
@@ -881,6 +850,7 @@
|
||||
oldTree
|
||||
);
|
||||
}
|
||||
this.addHighlightedKeys(child);
|
||||
return child;
|
||||
}
|
||||
|
||||
@@ -890,7 +860,10 @@
|
||||
let path = completePath.slice(objPathIndex);
|
||||
let obj;
|
||||
if (objType === "subscription") {
|
||||
obj = oldTree.subscriptions.children[path[1].value].target;
|
||||
const subscriptionPath = completePath.slice(0, objPathIndex + 3);
|
||||
obj = oldTree.subscriptions.children.find(
|
||||
(child) => JSON.stringify(child.target.path) === JSON.stringify(subscriptionPath)
|
||||
).target;
|
||||
path = path.slice(3);
|
||||
} else {
|
||||
// Everything here is in component if it is not an app so remove this key of the path in the former case
|
||||
@@ -920,7 +893,7 @@
|
||||
const children = [];
|
||||
depth = depth + 1;
|
||||
let obj = this.getObjectProperty(path);
|
||||
let oldBranch = this.getObjectInOldTree(oldTree, path, objType);
|
||||
let oldBranch = oldTree && this.getObjectInOldTree(oldTree, path, objType);
|
||||
if (!obj) {
|
||||
return [];
|
||||
}
|
||||
@@ -935,7 +908,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[0],
|
||||
oldBranch?.children[0],
|
||||
oldTree
|
||||
);
|
||||
children.push(mapKey);
|
||||
@@ -945,7 +918,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[1],
|
||||
oldBranch?.children[1],
|
||||
oldTree
|
||||
);
|
||||
children.push(mapValue);
|
||||
@@ -956,7 +929,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[0],
|
||||
oldBranch?.children[0],
|
||||
oldTree
|
||||
);
|
||||
children.push(setValue);
|
||||
@@ -977,7 +950,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) {
|
||||
@@ -992,7 +965,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) {
|
||||
@@ -1009,7 +982,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (entries) {
|
||||
@@ -1023,7 +996,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) {
|
||||
@@ -1058,7 +1031,7 @@
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children[index],
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) children.push(child);
|
||||
@@ -1091,14 +1064,14 @@
|
||||
});
|
||||
proto = Object.getPrototypeOf(proto);
|
||||
}
|
||||
if (!(obj.constructor.name === "Object")) {
|
||||
if (obj.__proto__) {
|
||||
prototype = this.serializeObjectChild(
|
||||
obj,
|
||||
{ type: "prototype", childIndex: children.length },
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch.children.at(-1),
|
||||
oldBranch?.children.at(-1),
|
||||
oldTree
|
||||
);
|
||||
children.push(prototype);
|
||||
@@ -1303,16 +1276,15 @@
|
||||
children: [],
|
||||
};
|
||||
} else {
|
||||
const rawSubscriptions = node.subscriptions;
|
||||
const rawSubscriptions = this.topLevelSubscriptions(node);
|
||||
component.subscriptions = {
|
||||
toggled: oldTree ? oldTree.subscriptions.toggled : true,
|
||||
children: [],
|
||||
};
|
||||
rawSubscriptions.forEach((rawSubscription, index) => {
|
||||
rawSubscriptions.forEach((rawSubscription) => {
|
||||
let subscription = {
|
||||
keys: [],
|
||||
target: {
|
||||
name: "target",
|
||||
name: this.targetName(rawSubscription.target, node),
|
||||
contentType:
|
||||
typeof rawSubscription.target === "object"
|
||||
? Array.isArray(rawSubscription.target)
|
||||
@@ -1323,28 +1295,20 @@
|
||||
path: [
|
||||
...path,
|
||||
{ type: "item", value: "subscriptions" },
|
||||
{ type: "item", value: index },
|
||||
{ type: "item", value: rawSubscription.index },
|
||||
{ type: "item", value: "target" },
|
||||
],
|
||||
toggled: false,
|
||||
objectType: "subscription",
|
||||
},
|
||||
keysExpanded: false,
|
||||
};
|
||||
if (
|
||||
oldTree &&
|
||||
oldTree.subscriptions.children[index] &&
|
||||
oldTree.subscriptions.children[index].target.toggled
|
||||
oldTree.subscriptions.children[rawSubscription.index] &&
|
||||
oldTree.subscriptions.children[rawSubscription.index].target.toggled
|
||||
) {
|
||||
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 (subscription.target.contentType === "undefined") {
|
||||
subscription.target.content = "undefined";
|
||||
@@ -1374,6 +1338,7 @@
|
||||
oldTree
|
||||
);
|
||||
}
|
||||
this.addHighlightedKeys(subscription.target);
|
||||
component.subscriptions.children.push(subscription);
|
||||
});
|
||||
}
|
||||
@@ -1481,8 +1446,11 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
const key = path.pop().value;
|
||||
const item = path.pop();
|
||||
const obj = this.getObjectProperty(path);
|
||||
const key = item.hasOwnProperty("symbolIndex")
|
||||
? Object.getOwnPropertySymbols(obj)[item.symbolIndex]
|
||||
: item.value;
|
||||
if (!obj) {
|
||||
return;
|
||||
}
|
||||
@@ -1556,7 +1524,7 @@
|
||||
}
|
||||
// Returns the tree of components of the inspected page in a parsed format
|
||||
// Use inspectedPath to specify the path of the selected component
|
||||
getComponentsTree(inspectedPath = null, oldTrees = null) {
|
||||
getComponentsTree(inspectedPath = null, oldTrees = null, oldDetails = null) {
|
||||
const appsArray = [...this.apps];
|
||||
const trees = appsArray.map((app, index) => {
|
||||
let oldTree;
|
||||
@@ -1609,7 +1577,8 @@
|
||||
}
|
||||
return appNode;
|
||||
});
|
||||
return trees ? trees : [];
|
||||
const component = this.getComponentDetails(inspectedPath, oldDetails);
|
||||
return trees ? [trees, component] : [];
|
||||
}
|
||||
// Recursively fills the components tree as a parsed version
|
||||
fillTree(appNode, treeNode, inspectedPathString, oldBranch) {
|
||||
@@ -1705,6 +1674,54 @@
|
||||
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() {
|
||||
|
||||
Reference in New Issue
Block a user