Compare commits
19 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aec2373e6d | |||
| a9be149e1e | |||
| b365ea5c9c | |||
| b31fa81083 | |||
| 9b656fd9e4 | |||
| 17f4823b13 | |||
| d3bc101177 | |||
| 4287beae19 | |||
| a073c68685 | |||
| 7078d64049 | |||
| ccd31f12d9 | |||
| aff4019cde | |||
| 5ecb4809ff | |||
| c2daecc07b | |||
| fcda17c8e9 | |||
| aeed79c7e5 | |||
| 1c5b6f2573 | |||
| cd9b72158b | |||
| 7fc552e2f8 |
@@ -56,17 +56,19 @@ extensions.
|
||||
|
||||
For reference, here is a list of all standard QWeb directives:
|
||||
|
||||
| Name | Description |
|
||||
| ------------------------------ | --------------------------------------------------------------- |
|
||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-translation` | [Disabling the translation of a node](translations.md) |
|
||||
| Name | Description |
|
||||
| ------------------------------ | ----------------------------------------------------------------------- |
|
||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||
| `t-call` | [Rendering sub templates](#sub-templates) |
|
||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||
| `t-translation` | [Disabling the translation of a node](translations.md) |
|
||||
| `t-translation-context` | [Context of translations within a node](translations.md) |
|
||||
| `t-translation-context-*` | [Context of translation for a specific node attribute](translations.md) |
|
||||
|
||||
The component system in Owl requires additional directives, to express various
|
||||
needs. Here is a list of all Owl specific directives:
|
||||
|
||||
@@ -1,17 +1,28 @@
|
||||
# 🦉 Translations 🦉
|
||||
|
||||
If properly setup, Owl can translate all rendered templates. To do
|
||||
so, it needs a translate function, which takes a string and returns a string.
|
||||
so, it needs a translate function, which takes
|
||||
|
||||
- a string (the term to translate)
|
||||
- a string (the translation context of the term)
|
||||
and returns a string.
|
||||
|
||||
For example:
|
||||
|
||||
```js
|
||||
const translations = {
|
||||
hello: "bonjour",
|
||||
yes: "oui",
|
||||
no: "non",
|
||||
fr: {
|
||||
hello: "bonjour",
|
||||
yes: "oui",
|
||||
no: "non",
|
||||
},
|
||||
pt: {
|
||||
hello: "bom dia",
|
||||
yes: "sim",
|
||||
no: "não",
|
||||
},
|
||||
};
|
||||
const translateFn = (str) => translations[str] || str;
|
||||
const translateFn = (str, ctx) => translations[ctx]?.[str] || str;
|
||||
|
||||
const app = new App(Root, { templates, tranaslateFn });
|
||||
// ...
|
||||
@@ -27,6 +38,11 @@ Once setup, all rendered templates will be translated using `translateFn`:
|
||||
`placeholder`, `label` and `alt`,
|
||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||
if its value is `off`.
|
||||
- the translate function receives as second parameter a context that can be used
|
||||
to contextualized the translation. That context can be set globally on a node
|
||||
and its children by using `t-translation-context`. If a specific node
|
||||
attribute `x` needs another context, that context can be specified with a
|
||||
special directive `t-translation-context-x`.
|
||||
|
||||
So, with the above `translateFn`, the following templates:
|
||||
|
||||
@@ -46,6 +62,22 @@ will be rendered as:
|
||||
<input placeholder="bonjour" other="yes"/>
|
||||
```
|
||||
|
||||
and the following template:
|
||||
|
||||
```xml
|
||||
<div t-translation-context="fr" title="hello">hello</div>
|
||||
<div>Are you sure?</div>
|
||||
<input t-translation-context-placeholder="pt" placeholder="hello" other="yes"/>
|
||||
```
|
||||
|
||||
will be rendered as:
|
||||
|
||||
```xml
|
||||
<div title="bonjour">bonjour</div>
|
||||
<div>Are you sure?</div>
|
||||
<input placeholder="bom dia" other="yes"/>
|
||||
```
|
||||
|
||||
Note that the translation is done during the compilation of the template, not
|
||||
when it is rendered.
|
||||
|
||||
|
||||
@@ -82,6 +82,22 @@ in the sources tab as well.
|
||||
|
||||
<img src="screenshots/function_menu.png"/>
|
||||
|
||||
Using the right-click context menu on a property also allows to observe variables. Observed variables will
|
||||
be sent to a dedicated section of the details window and their value will be refreshed every 200ms. These
|
||||
variables are only shown when they are found and their access path will be kept in memory inside the
|
||||
browser so that it will always persist until the user decides to stop observing the variable. As in the
|
||||
browser's devtools, observed objects are displayed in reduced form and cannot be interacted with. It is
|
||||
still possible to send them to the console or remove them from the list using right-click.
|
||||
|
||||
<img src="screenshots/observe_variables.png"/>
|
||||
|
||||
The last section of the details window is filled with the component's lifecycle hooks. Using right click on
|
||||
them allows to place breakpoints inside the hook (either on its instance or class, hooks like mounted and
|
||||
willStart cannot have instance-based breakpoints because they will never trigger). Conditions in conditional
|
||||
breakpoints will be evaluated in the context of the component's definition.
|
||||
|
||||
<img src="screenshots/hooks.png"/>
|
||||
|
||||
There are several icons available to perform several of the actions described before in the components
|
||||
tree context menu and all these actions are also available by opening the menu by right-clicking on the
|
||||
component's name. Using the left click on the component's name will focus it in the components tree.
|
||||
|
||||
|
Before Width: | Height: | Size: 545 KiB After Width: | Height: | Size: 314 KiB |
|
Before Width: | Height: | Size: 387 KiB After Width: | Height: | Size: 193 KiB |
|
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 320 KiB After Width: | Height: | Size: 188 KiB |
|
After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 206 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 329 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 536 KiB After Width: | Height: | Size: 346 KiB |
|
Before Width: | Height: | Size: 177 KiB After Width: | Height: | Size: 93 KiB |
@@ -1625,6 +1625,13 @@ function makeRootFiber(node) {
|
||||
fibersInError.delete(current);
|
||||
fibersInError.delete(root);
|
||||
current.appliedToDom = false;
|
||||
if (current instanceof RootFiber) {
|
||||
// it is possible that this fiber is a fiber that crashed while being
|
||||
// mounted, so the mounted list is possibly corrupted. We restore it to
|
||||
// its normal initial state (which is empty list or a list with a mount
|
||||
// fiber.
|
||||
current.mounted = current instanceof MountFiber ? [current] : [];
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
@@ -1741,6 +1748,7 @@ class RootFiber extends Fiber {
|
||||
const node = this.node;
|
||||
this.locked = true;
|
||||
let current = undefined;
|
||||
let mountedFibers = this.mounted;
|
||||
try {
|
||||
// Step 1: calling all willPatch lifecycle hooks
|
||||
for (current of this.willPatch) {
|
||||
@@ -1760,7 +1768,6 @@ class RootFiber extends Fiber {
|
||||
node._patch();
|
||||
this.locked = false;
|
||||
// Step 4: calling all mounted lifecycle hooks
|
||||
let mountedFibers = this.mounted;
|
||||
while ((current = mountedFibers.pop())) {
|
||||
current = current;
|
||||
if (current.appliedToDom) {
|
||||
@@ -1781,6 +1788,15 @@ class RootFiber extends Fiber {
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
// if mountedFibers is not empty, this means that a crash occured while
|
||||
// calling the mounted hooks of some component. So, there may still be
|
||||
// some component that have been mounted, but for which the mounted hooks
|
||||
// have not been called. Here, we remove the willUnmount hooks for these
|
||||
// specific component to prevent a worse situation (willUnmount being
|
||||
// called even though mounted has not been called)
|
||||
for (let fiber of mountedFibers) {
|
||||
fiber.node.willUnmount = [];
|
||||
}
|
||||
this.locked = false;
|
||||
node.app.handleError({ fiber: current || this, error: e });
|
||||
}
|
||||
@@ -3722,6 +3738,7 @@ function createContext(parentCtx, params) {
|
||||
index: 0,
|
||||
forceNewBlock: true,
|
||||
translate: parentCtx.translate,
|
||||
translationCtx: parentCtx.translationCtx,
|
||||
tKeyExpr: null,
|
||||
nameSpace: parentCtx.nameSpace,
|
||||
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
||||
@@ -3824,6 +3841,7 @@ class CodeGenerator {
|
||||
forceNewBlock: false,
|
||||
isLast: true,
|
||||
translate: true,
|
||||
translationCtx: "",
|
||||
tKeyExpr: null,
|
||||
});
|
||||
// define blocks and utility functions
|
||||
@@ -3961,9 +3979,9 @@ class CodeGenerator {
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
translate(str) {
|
||||
translate(str, translationCtx) {
|
||||
const match = translationRE.exec(str);
|
||||
return match[1] + this.translateFn(match[2]) + match[3];
|
||||
return match[1] + this.translateFn(match[2], translationCtx) + match[3];
|
||||
}
|
||||
/**
|
||||
* @returns the newly created block name, if any
|
||||
@@ -4004,7 +4022,9 @@ class CodeGenerator {
|
||||
return this.compileTSlot(ast, ctx);
|
||||
case 16 /* TTranslation */:
|
||||
return this.compileTTranslation(ast, ctx);
|
||||
case 17 /* TPortal */:
|
||||
case 17 /* TTranslationContext */:
|
||||
return this.compileTTranslationContext(ast, ctx);
|
||||
case 18 /* TPortal */:
|
||||
return this.compileTPortal(ast, ctx);
|
||||
}
|
||||
}
|
||||
@@ -4042,7 +4062,7 @@ class CodeGenerator {
|
||||
let { block, forceNewBlock } = ctx;
|
||||
let value = ast.value;
|
||||
if (value && ctx.translate !== false) {
|
||||
value = this.translate(value);
|
||||
value = this.translate(value, ctx.translationCtx);
|
||||
}
|
||||
if (!ctx.inPreTag) {
|
||||
value = value.replace(whitespaceRE, " ");
|
||||
@@ -4077,6 +4097,7 @@ class CodeGenerator {
|
||||
return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`;
|
||||
}
|
||||
compileTDomNode(ast, ctx) {
|
||||
var _a;
|
||||
let { block, forceNewBlock } = ctx;
|
||||
const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;
|
||||
let codeIdx = this.target.code.length;
|
||||
@@ -4132,7 +4153,8 @@ class CodeGenerator {
|
||||
}
|
||||
}
|
||||
else if (this.translatableAttributes.includes(key)) {
|
||||
attrs[key] = this.translateFn(ast.attrs[key]);
|
||||
const attrTranslationCtx = ((_a = ast.attrsTranslationCtx) === null || _a === void 0 ? void 0 : _a[key]) || ctx.translationCtx;
|
||||
attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
|
||||
}
|
||||
else {
|
||||
expr = `"${ast.attrs[key]}"`;
|
||||
@@ -4576,7 +4598,7 @@ class CodeGenerator {
|
||||
else {
|
||||
let value;
|
||||
if (ast.defaultValue) {
|
||||
const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue);
|
||||
const defaultValue = toStringExpression(ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue);
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, ${defaultValue})`;
|
||||
}
|
||||
@@ -4610,9 +4632,10 @@ class CodeGenerator {
|
||||
* "some-prop" "state" "'some-prop': ctx['state']"
|
||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||
*/
|
||||
formatProp(name, value) {
|
||||
formatProp(name, value, attrsTranslationCtx, translationCtx) {
|
||||
if (name.endsWith(".translate")) {
|
||||
value = toStringExpression(this.translateFn(value));
|
||||
const attrTranslationCtx = (attrsTranslationCtx === null || attrsTranslationCtx === void 0 ? void 0 : attrsTranslationCtx[name]) || translationCtx;
|
||||
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
|
||||
}
|
||||
else {
|
||||
value = this.captureExpression(value);
|
||||
@@ -4628,14 +4651,14 @@ class CodeGenerator {
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
throw new OwlError(`Invalid prop suffix: ${suffix}`);
|
||||
}
|
||||
}
|
||||
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
||||
return `${name}: ${value || undefined}`;
|
||||
}
|
||||
formatPropObject(obj) {
|
||||
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
|
||||
formatPropObject(obj, attrsTranslationCtx, translationCtx) {
|
||||
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v, attrsTranslationCtx, translationCtx));
|
||||
}
|
||||
getPropString(props, dynProps) {
|
||||
let propString = `{${props.join(",")}}`;
|
||||
@@ -4648,7 +4671,9 @@ class CodeGenerator {
|
||||
let { block } = ctx;
|
||||
// props
|
||||
const hasSlotsProp = "slots" in (ast.props || {});
|
||||
const props = ast.props ? this.formatPropObject(ast.props) : [];
|
||||
const props = ast.props
|
||||
? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
|
||||
: [];
|
||||
// slots
|
||||
let slotDef = "";
|
||||
if (ast.slots) {
|
||||
@@ -4671,7 +4696,7 @@ class CodeGenerator {
|
||||
params.push(`__scope: "${scope}"`);
|
||||
}
|
||||
if (ast.slots[slotName].attrs) {
|
||||
params.push(...this.formatPropObject(ast.slots[slotName].attrs));
|
||||
params.push(...this.formatPropObject(ast.slots[slotName].attrs, ast.slots[slotName].attrsTranslationCtx, ctx.translationCtx));
|
||||
}
|
||||
const slotInfo = `{${params.join(", ")}}`;
|
||||
slotStr.push(`'${slotName}': ${slotInfo}`);
|
||||
@@ -4784,7 +4809,9 @@ class CodeGenerator {
|
||||
if (isMultiple) {
|
||||
key = this.generateComponentKey(key);
|
||||
}
|
||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
||||
const props = ast.attrs
|
||||
? this.formatPropObject(ast.attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
||||
: [];
|
||||
const scope = this.getPropString(props, dynProps);
|
||||
if (ast.defaultContent) {
|
||||
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
||||
@@ -4817,6 +4844,12 @@ class CodeGenerator {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
compileTTranslationContext(ast, ctx) {
|
||||
if (ast.content) {
|
||||
return this.compileAST(ast.content, Object.assign({}, ctx, { translationCtx: ast.translationCtx }));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
compileTPortal(ast, ctx) {
|
||||
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
||||
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
||||
@@ -4886,6 +4919,7 @@ function parseNode(node, ctx) {
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
@@ -4934,10 +4968,10 @@ function parseTCustom(node, ctx) {
|
||||
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
|
||||
}
|
||||
const value = node.getAttribute(attr);
|
||||
const modifier = attr.split(".").length > 1 ? attr.split(".")[1] : undefined;
|
||||
const modifiers = attr.split(".").slice(1);
|
||||
node.removeAttribute(attr);
|
||||
try {
|
||||
customDirective(node, value, modifier);
|
||||
customDirective(node, value, modifiers);
|
||||
}
|
||||
catch (error) {
|
||||
throw new OwlError(`Custom directive "${directiveName}" throw the following error: ${error}`);
|
||||
@@ -4994,6 +5028,7 @@ function parseDOMNode(node, ctx) {
|
||||
node.removeAttribute("t-ref");
|
||||
const nodeAttrsNames = node.getAttributeNames();
|
||||
let attrs = null;
|
||||
let attrsTranslationCtx = null;
|
||||
let on = null;
|
||||
let model = null;
|
||||
for (let attr of nodeAttrsNames) {
|
||||
@@ -5054,6 +5089,11 @@ function parseDOMNode(node, ctx) {
|
||||
else if (attr === "xmlns") {
|
||||
ns = value;
|
||||
}
|
||||
else if (attr.startsWith("t-translation-context-")) {
|
||||
const attrName = attr.slice(22);
|
||||
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||
attrsTranslationCtx[attrName] = value;
|
||||
}
|
||||
else if (attr !== "t-name") {
|
||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
||||
@@ -5075,6 +5115,7 @@ function parseDOMNode(node, ctx) {
|
||||
tag: tagName,
|
||||
dynamicTag,
|
||||
attrs,
|
||||
attrsTranslationCtx,
|
||||
on,
|
||||
ref,
|
||||
content: children,
|
||||
@@ -5215,7 +5256,15 @@ function parseTCall(node, ctx) {
|
||||
if (ast && ast.type === 11 /* TComponent */) {
|
||||
return {
|
||||
...ast,
|
||||
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
|
||||
slots: {
|
||||
default: {
|
||||
content: tcall,
|
||||
scope: null,
|
||||
on: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -5330,9 +5379,15 @@ function parseComponent(node, ctx) {
|
||||
node.removeAttribute("t-slot-scope");
|
||||
let on = null;
|
||||
let props = null;
|
||||
let propsTranslationCtx = null;
|
||||
for (let name of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(name);
|
||||
if (name.startsWith("t-")) {
|
||||
if (name.startsWith("t-translation-context-")) {
|
||||
const attrName = name.slice(22);
|
||||
propsTranslationCtx = propsTranslationCtx || {};
|
||||
propsTranslationCtx[attrName] = value;
|
||||
}
|
||||
else if (name.startsWith("t-")) {
|
||||
if (name.startsWith("t-on-")) {
|
||||
on = on || {};
|
||||
on[name.slice(5)] = value;
|
||||
@@ -5376,6 +5431,7 @@ function parseComponent(node, ctx) {
|
||||
const slotAst = parseNode(slotNode, ctx);
|
||||
let on = null;
|
||||
let attrs = null;
|
||||
let attrsTranslationCtx = null;
|
||||
let scope = null;
|
||||
for (let attributeName of slotNode.getAttributeNames()) {
|
||||
const value = slotNode.getAttribute(attributeName);
|
||||
@@ -5383,6 +5439,11 @@ function parseComponent(node, ctx) {
|
||||
scope = value;
|
||||
continue;
|
||||
}
|
||||
else if (attributeName.startsWith("t-translation-context-")) {
|
||||
const attrName = attributeName.slice(22);
|
||||
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||
attrsTranslationCtx[attrName] = value;
|
||||
}
|
||||
else if (attributeName.startsWith("t-on-")) {
|
||||
on = on || {};
|
||||
on[attributeName.slice(5)] = value;
|
||||
@@ -5393,17 +5454,32 @@ function parseComponent(node, ctx) {
|
||||
}
|
||||
}
|
||||
slots = slots || {};
|
||||
slots[name] = { content: slotAst, on, attrs, scope };
|
||||
slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
|
||||
}
|
||||
// default slot
|
||||
const defaultContent = parseChildNodes(clone, ctx);
|
||||
slots = slots || {};
|
||||
// t-set-slot="default" has priority over content
|
||||
if (defaultContent && !slots.default) {
|
||||
slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
|
||||
slots.default = {
|
||||
content: defaultContent,
|
||||
on,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
scope: defaultSlotScope,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { type: 11 /* TComponent */, name, isDynamic, dynamicProps, props, slots, on };
|
||||
return {
|
||||
type: 11 /* TComponent */,
|
||||
name,
|
||||
isDynamic,
|
||||
dynamicProps,
|
||||
props,
|
||||
propsTranslationCtx,
|
||||
slots,
|
||||
on,
|
||||
};
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Slots
|
||||
@@ -5415,6 +5491,7 @@ function parseTSlot(node, ctx) {
|
||||
const name = node.getAttribute("t-slot");
|
||||
node.removeAttribute("t-slot");
|
||||
let attrs = null;
|
||||
let attrsTranslationCtx = null;
|
||||
let on = null;
|
||||
for (let attributeName of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(attributeName);
|
||||
@@ -5422,6 +5499,11 @@ function parseTSlot(node, ctx) {
|
||||
on = on || {};
|
||||
on[attributeName.slice(5)] = value;
|
||||
}
|
||||
else if (attributeName.startsWith("t-translation-context-")) {
|
||||
const attrName = attributeName.slice(22);
|
||||
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||
attrsTranslationCtx[attrName] = value;
|
||||
}
|
||||
else {
|
||||
attrs = attrs || {};
|
||||
attrs[attributeName] = value;
|
||||
@@ -5431,10 +5513,14 @@ function parseTSlot(node, ctx) {
|
||||
type: 14 /* TSlot */,
|
||||
name,
|
||||
attrs,
|
||||
attrsTranslationCtx,
|
||||
on,
|
||||
defaultContent: parseChildNodes(node, ctx),
|
||||
};
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Translation
|
||||
// -----------------------------------------------------------------------------
|
||||
function parseTTranslation(node, ctx) {
|
||||
if (node.getAttribute("t-translation") !== "off") {
|
||||
return null;
|
||||
@@ -5446,6 +5532,21 @@ function parseTTranslation(node, ctx) {
|
||||
};
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Translation Context
|
||||
// -----------------------------------------------------------------------------
|
||||
function parseTTranslationContext(node, ctx) {
|
||||
const translationCtx = node.getAttribute("t-translation-context");
|
||||
if (!translationCtx) {
|
||||
return null;
|
||||
}
|
||||
node.removeAttribute("t-translation-context");
|
||||
return {
|
||||
type: 17 /* TTranslationContext */,
|
||||
content: parseNode(node, ctx),
|
||||
translationCtx,
|
||||
};
|
||||
}
|
||||
// -----------------------------------------------------------------------------
|
||||
// Portal
|
||||
// -----------------------------------------------------------------------------
|
||||
function parseTPortal(node, ctx) {
|
||||
@@ -5462,7 +5563,7 @@ function parseTPortal(node, ctx) {
|
||||
};
|
||||
}
|
||||
return {
|
||||
type: 17 /* TPortal */,
|
||||
type: 18 /* TPortal */,
|
||||
target,
|
||||
content,
|
||||
};
|
||||
@@ -5604,7 +5705,7 @@ function compile(template, options = {
|
||||
}
|
||||
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
const version = "2.5.0";
|
||||
const version = "2.6.0";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Scheduler
|
||||
@@ -6082,6 +6183,6 @@ TemplateSet.prototype._compileTemplate = function _compileTemplate(name, templat
|
||||
export { App, Component, EventBus, OwlError, __info__, batched, 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 = '2024-11-25T09:30:45.930Z';
|
||||
__info__.hash = '6b24864';
|
||||
__info__.date = '2025-01-15T10:40:24.184Z';
|
||||
__info__.hash = 'a9be149';
|
||||
__info__.url = 'https://github.com/odoo/owl';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@odoo/owl",
|
||||
"version": "2.5.0",
|
||||
"version": "2.6.0",
|
||||
"description": "Odoo Web Library (OWL)",
|
||||
"main": "dist/owl.cjs.js",
|
||||
"module": "dist/owl.es.js",
|
||||
@@ -32,7 +32,10 @@
|
||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
||||
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
||||
"release": "node tools/release.js",
|
||||
"compile_templates": "node tools/compile_xml.js"
|
||||
"compile_templates": "node tools/compile_owl_templates.mjs"
|
||||
},
|
||||
"bin": {
|
||||
"compile_owl_templates": "tools/compile_owl_templates.mjs"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -46,6 +49,7 @@
|
||||
"homepage": "https://github.com/odoo/owl#readme",
|
||||
"devDependencies": {
|
||||
"@types/jest": "^27.0.1",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^14.11.8",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||
"@typescript-eslint/parser": "5.48.1",
|
||||
@@ -97,5 +101,8 @@
|
||||
"prettier": {
|
||||
"printWidth": 100,
|
||||
"endOfLine": "auto"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsdom": "^25.0.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import pkg from "./package.json";
|
||||
import git from "git-rev-sync";
|
||||
import typescript from 'rollup-plugin-typescript2';
|
||||
import typescript from "rollup-plugin-typescript2";
|
||||
import { terser } from "rollup-plugin-terser";
|
||||
import dts from "rollup-plugin-dts";
|
||||
|
||||
@@ -12,7 +12,7 @@ const ES_FILENAME = "dist/owl.es.js";
|
||||
|
||||
if (pkg.module !== ES_FILENAME || pkg.main !== CJS_FILENAME) {
|
||||
throw new Error("package.json has been modified. Build script should be updated accordingly");
|
||||
}
|
||||
}
|
||||
|
||||
const outro = `
|
||||
__info__.date = '${new Date().toISOString()}';
|
||||
@@ -21,39 +21,37 @@ __info__.url = 'https://github.com/odoo/owl';
|
||||
`;
|
||||
|
||||
switch (process.argv[4]) {
|
||||
case "compiler":
|
||||
input = "src/compiler/index.ts",
|
||||
output = [
|
||||
getConfigForFormat('cjs', 'dist/compiler.js', ''),
|
||||
]
|
||||
case "compiler":
|
||||
(input = "src/compiler/index.ts"),
|
||||
(output = [getConfigForFormat("cjs", "dist/compiler.js", "")]);
|
||||
break;
|
||||
case "runtime":
|
||||
input = "src/runtime/index.ts";
|
||||
output = [
|
||||
getConfigForFormat('esm', addSuffix(ES_FILENAME, 'runtime'), outro),
|
||||
getConfigForFormat('cjs', addSuffix(CJS_FILENAME, 'runtime'), outro),
|
||||
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro),
|
||||
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro, true),
|
||||
]
|
||||
getConfigForFormat("esm", addSuffix(ES_FILENAME, "runtime"), outro),
|
||||
getConfigForFormat("cjs", addSuffix(CJS_FILENAME, "runtime"), outro),
|
||||
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro),
|
||||
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro, true),
|
||||
];
|
||||
break;
|
||||
default:
|
||||
input = "src/index.ts",
|
||||
output = [
|
||||
getConfigForFormat('esm', ES_FILENAME, outro),
|
||||
getConfigForFormat('cjs', CJS_FILENAME, outro),
|
||||
getConfigForFormat('iife', IIFE_FILENAME, outro),
|
||||
getConfigForFormat('iife', IIFE_FILENAME, outro, true),
|
||||
]
|
||||
}
|
||||
(input = "src/index.ts"),
|
||||
(output = [
|
||||
getConfigForFormat("esm", ES_FILENAME, outro),
|
||||
getConfigForFormat("cjs", CJS_FILENAME, outro),
|
||||
getConfigForFormat("iife", IIFE_FILENAME, outro),
|
||||
getConfigForFormat("iife", IIFE_FILENAME, outro, true),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate from a string depicting a path a new path for the minified version.
|
||||
* @param {string} pkgFileName file name
|
||||
*/
|
||||
function addSuffix(pkgFileName, suffix) {
|
||||
const parts = pkgFileName.split('.');
|
||||
const parts = pkgFileName.split(".");
|
||||
parts.splice(parts.length - 1, 0, suffix);
|
||||
return parts.join('.');
|
||||
return parts.join(".");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,7 +69,7 @@ function getConfigForFormat(format, generatedFileName, outro, minified = false)
|
||||
outro: outro,
|
||||
freeze: false,
|
||||
plugins: minified ? [terser()] : [],
|
||||
indent: ' ', // indent with 4 spaces
|
||||
indent: " ", // indent with 4 spaces
|
||||
};
|
||||
}
|
||||
|
||||
@@ -81,9 +79,19 @@ export default [
|
||||
output,
|
||||
plugins: [
|
||||
typescript({
|
||||
useTsconfigDeclarationDir: true
|
||||
useTsconfigDeclarationDir: true,
|
||||
}),
|
||||
]
|
||||
],
|
||||
},
|
||||
{
|
||||
input: "src/compiler/standalone/index.ts",
|
||||
output: [{ file: "dist/compile_templates.mjs", format: "es" }],
|
||||
external: ["fs", "fs/promises", "path", "jsdom"],
|
||||
plugins: [
|
||||
typescript({
|
||||
useTsconfigDeclarationDir: true,
|
||||
}),
|
||||
],
|
||||
},
|
||||
{
|
||||
input: "dist/types/index.d.ts",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export type customDirectives = Record<
|
||||
string,
|
||||
(node: Element, value: string, modifier?: string) => void
|
||||
(node: Element, value: string, modifier: string[]) => void
|
||||
>;
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
ASTTOut,
|
||||
ASTTPortal,
|
||||
ASTTranslation,
|
||||
ASTTranslationContext,
|
||||
ASTTSet,
|
||||
ASTType,
|
||||
Attrs,
|
||||
@@ -35,7 +36,7 @@ type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
|
||||
const whitespaceRE = /\s+/g;
|
||||
|
||||
export interface Config {
|
||||
translateFn?: (s: string) => string;
|
||||
translateFn?: (s: string, translationCtx: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
dev?: boolean;
|
||||
}
|
||||
@@ -171,6 +172,7 @@ interface Context {
|
||||
forceNewBlock: boolean;
|
||||
isLast?: boolean;
|
||||
translate: boolean;
|
||||
translationCtx: string;
|
||||
tKeyExpr: string | null;
|
||||
nameSpace?: string;
|
||||
tModelSelectedExpr?: string;
|
||||
@@ -185,6 +187,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>): Context {
|
||||
index: 0,
|
||||
forceNewBlock: true,
|
||||
translate: parentCtx.translate,
|
||||
translationCtx: parentCtx.translationCtx,
|
||||
tKeyExpr: null,
|
||||
nameSpace: parentCtx.nameSpace,
|
||||
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
||||
@@ -263,7 +266,7 @@ export class CodeGenerator {
|
||||
target = new CodeTarget("template");
|
||||
templateName?: string;
|
||||
dev: boolean;
|
||||
translateFn: (s: string) => string;
|
||||
translateFn: (s: string, translationCtx: string) => string;
|
||||
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
|
||||
ast: AST;
|
||||
staticDefs: { id: string; expr: string }[] = [];
|
||||
@@ -303,6 +306,7 @@ export class CodeGenerator {
|
||||
forceNewBlock: false,
|
||||
isLast: true,
|
||||
translate: true,
|
||||
translationCtx: "",
|
||||
tKeyExpr: null,
|
||||
});
|
||||
// define blocks and utility functions
|
||||
@@ -457,9 +461,9 @@ export class CodeGenerator {
|
||||
.join("");
|
||||
}
|
||||
|
||||
translate(str: string): string {
|
||||
translate(str: string, translationCtx: string): string {
|
||||
const match = translationRE.exec(str) as any;
|
||||
return match[1] + this.translateFn(match[2]) + match[3];
|
||||
return match[1] + this.translateFn(match[2], translationCtx) + match[3];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -501,6 +505,8 @@ export class CodeGenerator {
|
||||
return this.compileTSlot(ast, ctx);
|
||||
case ASTType.TTranslation:
|
||||
return this.compileTTranslation(ast, ctx);
|
||||
case ASTType.TTranslationContext:
|
||||
return this.compileTTranslationContext(ast, ctx);
|
||||
case ASTType.TPortal:
|
||||
return this.compileTPortal(ast, ctx);
|
||||
}
|
||||
@@ -542,7 +548,7 @@ export class CodeGenerator {
|
||||
|
||||
let value = ast.value;
|
||||
if (value && ctx.translate !== false) {
|
||||
value = this.translate(value);
|
||||
value = this.translate(value, ctx.translationCtx);
|
||||
}
|
||||
if (!ctx.inPreTag) {
|
||||
value = value.replace(whitespaceRE, " ");
|
||||
@@ -631,7 +637,8 @@ export class CodeGenerator {
|
||||
}
|
||||
}
|
||||
} else if (this.translatableAttributes.includes(key)) {
|
||||
attrs[key] = this.translateFn(ast.attrs[key]);
|
||||
const attrTranslationCtx = ast.attrsTranslationCtx?.[key] || ctx.translationCtx;
|
||||
attrs[key] = this.translateFn(ast.attrs[key], attrTranslationCtx);
|
||||
} else {
|
||||
expr = `"${ast.attrs[key]}"`;
|
||||
attrName = key;
|
||||
@@ -1104,7 +1111,7 @@ export class CodeGenerator {
|
||||
let value: string;
|
||||
if (ast.defaultValue) {
|
||||
const defaultValue = toStringExpression(
|
||||
ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue
|
||||
ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue
|
||||
);
|
||||
if (ast.value) {
|
||||
value = `withDefault(${expr}, ${defaultValue})`;
|
||||
@@ -1139,9 +1146,15 @@ export class CodeGenerator {
|
||||
* "some-prop" "state" "'some-prop': ctx['state']"
|
||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||
*/
|
||||
formatProp(name: string, value: string): string {
|
||||
formatProp(
|
||||
name: string,
|
||||
value: string,
|
||||
attrsTranslationCtx: { [name: string]: string } | null,
|
||||
translationCtx: string
|
||||
): string {
|
||||
if (name.endsWith(".translate")) {
|
||||
value = toStringExpression(this.translateFn(value));
|
||||
const attrTranslationCtx = attrsTranslationCtx?.[name] || translationCtx;
|
||||
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
|
||||
} else {
|
||||
value = this.captureExpression(value);
|
||||
}
|
||||
@@ -1156,15 +1169,21 @@ export class CodeGenerator {
|
||||
case "translate":
|
||||
break;
|
||||
default:
|
||||
throw new OwlError("Invalid prop suffix");
|
||||
throw new OwlError(`Invalid prop suffix: ${suffix}`);
|
||||
}
|
||||
}
|
||||
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
||||
return `${name}: ${value || undefined}`;
|
||||
}
|
||||
|
||||
formatPropObject(obj: { [prop: string]: any }): string[] {
|
||||
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
|
||||
formatPropObject(
|
||||
obj: { [prop: string]: any },
|
||||
attrsTranslationCtx: { [name: string]: string } | null,
|
||||
translationCtx: string
|
||||
): string[] {
|
||||
return Object.entries(obj).map(([k, v]) =>
|
||||
this.formatProp(k, v, attrsTranslationCtx, translationCtx)
|
||||
);
|
||||
}
|
||||
|
||||
getPropString(props: string[], dynProps: string | null): string {
|
||||
@@ -1181,7 +1200,9 @@ export class CodeGenerator {
|
||||
let { block } = ctx;
|
||||
// props
|
||||
const hasSlotsProp = "slots" in (ast.props || {});
|
||||
const props: string[] = ast.props ? this.formatPropObject(ast.props) : [];
|
||||
const props: string[] = ast.props
|
||||
? this.formatPropObject(ast.props, ast.propsTranslationCtx, ctx.translationCtx)
|
||||
: [];
|
||||
|
||||
// slots
|
||||
let slotDef: string = "";
|
||||
@@ -1205,7 +1226,13 @@ export class CodeGenerator {
|
||||
params.push(`__scope: "${scope}"`);
|
||||
}
|
||||
if (ast.slots[slotName].attrs) {
|
||||
params.push(...this.formatPropObject(ast.slots[slotName].attrs!));
|
||||
params.push(
|
||||
...this.formatPropObject(
|
||||
ast.slots[slotName].attrs!,
|
||||
ast.slots[slotName].attrsTranslationCtx,
|
||||
ctx.translationCtx
|
||||
)
|
||||
);
|
||||
}
|
||||
const slotInfo = `{${params.join(", ")}}`;
|
||||
slotStr.push(`'${slotName}': ${slotInfo}`);
|
||||
@@ -1332,7 +1359,9 @@ export class CodeGenerator {
|
||||
key = this.generateComponentKey(key);
|
||||
}
|
||||
|
||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
||||
const props = ast.attrs
|
||||
? this.formatPropObject(ast.attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
||||
: [];
|
||||
const scope = this.getPropString(props, dynProps);
|
||||
if (ast.defaultContent) {
|
||||
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
||||
@@ -1365,6 +1394,15 @@ export class CodeGenerator {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
compileTTranslationContext(ast: ASTTranslationContext, ctx: Context): string | null {
|
||||
if (ast.content) {
|
||||
return this.compileAST(
|
||||
ast.content,
|
||||
Object.assign({}, ctx, { translationCtx: ast.translationCtx })
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
compileTPortal(ast: ASTTPortal, ctx: Context): string {
|
||||
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
||||
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
||||
|
||||
@@ -27,6 +27,7 @@ export const enum ASTType {
|
||||
TSlot,
|
||||
TCallBlock,
|
||||
TTranslation,
|
||||
TTranslationContext,
|
||||
TPortal,
|
||||
}
|
||||
|
||||
@@ -56,6 +57,7 @@ export interface ASTDomNode {
|
||||
tag: string;
|
||||
content: AST[];
|
||||
attrs: Attrs | null;
|
||||
attrsTranslationCtx: Attrs | null;
|
||||
ref: string | null;
|
||||
on: EventHandlers | null;
|
||||
model: TModelInfo | null;
|
||||
@@ -127,6 +129,7 @@ interface SlotDefinition {
|
||||
scope: string | null;
|
||||
on: EventHandlers | null;
|
||||
attrs: Attrs | null;
|
||||
attrsTranslationCtx: Attrs | null;
|
||||
}
|
||||
|
||||
export interface ASTComponent {
|
||||
@@ -136,6 +139,7 @@ export interface ASTComponent {
|
||||
dynamicProps: string | null;
|
||||
on: EventHandlers | null;
|
||||
props: { [name: string]: string } | null;
|
||||
propsTranslationCtx: { [name: string]: string } | null;
|
||||
slots: { [name: string]: SlotDefinition } | null;
|
||||
}
|
||||
|
||||
@@ -143,6 +147,7 @@ export interface ASTSlot {
|
||||
type: ASTType.TSlot;
|
||||
name: string;
|
||||
attrs: Attrs | null;
|
||||
attrsTranslationCtx: Attrs | null;
|
||||
on: EventHandlers | null;
|
||||
defaultContent: AST | null;
|
||||
}
|
||||
@@ -168,6 +173,12 @@ export interface ASTTranslation {
|
||||
content: AST | null;
|
||||
}
|
||||
|
||||
export interface ASTTranslationContext {
|
||||
type: ASTType.TTranslationContext;
|
||||
content: AST | null;
|
||||
translationCtx: string;
|
||||
}
|
||||
|
||||
export interface ASTTPortal {
|
||||
type: ASTType.TPortal;
|
||||
target: string;
|
||||
@@ -192,6 +203,7 @@ export type AST =
|
||||
| ASTLog
|
||||
| ASTDebug
|
||||
| ASTTranslation
|
||||
| ASTTranslationContext
|
||||
| ASTTPortal;
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -245,6 +257,7 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
||||
parseTOutNode(node, ctx) ||
|
||||
parseTKey(node, ctx) ||
|
||||
parseTTranslation(node, ctx) ||
|
||||
parseTTranslationContext(node, ctx) ||
|
||||
parseTSlot(node, ctx) ||
|
||||
parseComponent(node, ctx) ||
|
||||
parseDOMNode(node, ctx) ||
|
||||
@@ -299,10 +312,10 @@ function parseTCustom(node: Element, ctx: ParsingContext): AST | null {
|
||||
throw new OwlError(`Custom directive "${directiveName}" is not defined`);
|
||||
}
|
||||
const value = node.getAttribute(attr)!;
|
||||
const modifier = attr.split(".").length > 1 ? attr.split(".")[1] : undefined;
|
||||
const modifiers = attr.split(".").slice(1);
|
||||
node.removeAttribute(attr);
|
||||
try {
|
||||
customDirective(node, value, modifier);
|
||||
customDirective(node, value, modifiers);
|
||||
} catch (error) {
|
||||
throw new OwlError(
|
||||
`Custom directive "${directiveName}" throw the following error: ${error}`
|
||||
@@ -368,6 +381,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
|
||||
const nodeAttrsNames = node.getAttributeNames();
|
||||
let attrs: ASTDomNode["attrs"] = null;
|
||||
let attrsTranslationCtx: ASTDomNode["attrsTranslationCtx"] = null;
|
||||
let on: EventHandlers | null = null;
|
||||
let model: TModelInfo | null = null;
|
||||
|
||||
@@ -428,6 +442,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
throw new OwlError(`Invalid attribute: '${attr}'`);
|
||||
} else if (attr === "xmlns") {
|
||||
ns = value;
|
||||
} else if (attr.startsWith("t-translation-context-")) {
|
||||
const attrName = attr.slice(22);
|
||||
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||
attrsTranslationCtx[attrName] = value;
|
||||
} else if (attr !== "t-name") {
|
||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
||||
@@ -450,6 +468,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
||||
tag: tagName,
|
||||
dynamicTag,
|
||||
attrs,
|
||||
attrsTranslationCtx,
|
||||
on,
|
||||
ref,
|
||||
content: children,
|
||||
@@ -609,7 +628,15 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (ast && ast.type === ASTType.TComponent) {
|
||||
return {
|
||||
...ast,
|
||||
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
|
||||
slots: {
|
||||
default: {
|
||||
content: tcall,
|
||||
scope: null,
|
||||
on: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -744,9 +771,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
let on: ASTComponent["on"] = null;
|
||||
|
||||
let props: ASTComponent["props"] = null;
|
||||
let propsTranslationCtx: ASTComponent["propsTranslationCtx"] = null;
|
||||
for (let name of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(name)!;
|
||||
if (name.startsWith("t-")) {
|
||||
if (name.startsWith("t-translation-context-")) {
|
||||
const attrName = name.slice(22);
|
||||
propsTranslationCtx = propsTranslationCtx || {};
|
||||
propsTranslationCtx[attrName] = value;
|
||||
} else if (name.startsWith("t-")) {
|
||||
if (name.startsWith("t-on-")) {
|
||||
on = on || {};
|
||||
on[name.slice(5)] = value;
|
||||
@@ -794,12 +826,17 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
const slotAst = parseNode(slotNode, ctx);
|
||||
let on: SlotDefinition["on"] = null;
|
||||
let attrs: Attrs | null = null;
|
||||
let attrsTranslationCtx: Attrs | null = null;
|
||||
let scope: string | null = null;
|
||||
for (let attributeName of slotNode.getAttributeNames()) {
|
||||
const value = slotNode.getAttribute(attributeName)!;
|
||||
if (attributeName === "t-slot-scope") {
|
||||
scope = value;
|
||||
continue;
|
||||
} else if (attributeName.startsWith("t-translation-context-")) {
|
||||
const attrName = attributeName.slice(22);
|
||||
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||
attrsTranslationCtx[attrName] = value;
|
||||
} else if (attributeName.startsWith("t-on-")) {
|
||||
on = on || {};
|
||||
on[attributeName.slice(5)] = value;
|
||||
@@ -809,7 +846,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
}
|
||||
}
|
||||
slots = slots || {};
|
||||
slots[name] = { content: slotAst, on, attrs, scope };
|
||||
slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
|
||||
}
|
||||
|
||||
// default slot
|
||||
@@ -817,10 +854,25 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
||||
slots = slots || {};
|
||||
// t-set-slot="default" has priority over content
|
||||
if (defaultContent && !slots.default) {
|
||||
slots.default = { content: defaultContent, on, attrs: null, scope: defaultSlotScope };
|
||||
slots.default = {
|
||||
content: defaultContent,
|
||||
on,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
scope: defaultSlotScope,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { type: ASTType.TComponent, name, isDynamic, dynamicProps, props, slots, on };
|
||||
return {
|
||||
type: ASTType.TComponent,
|
||||
name,
|
||||
isDynamic,
|
||||
dynamicProps,
|
||||
props,
|
||||
propsTranslationCtx,
|
||||
slots,
|
||||
on,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
@@ -834,12 +886,17 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
||||
const name = node.getAttribute("t-slot")!;
|
||||
node.removeAttribute("t-slot");
|
||||
let attrs: Attrs | null = null;
|
||||
let attrsTranslationCtx: Attrs | null = null;
|
||||
let on: ASTComponent["on"] = null;
|
||||
for (let attributeName of node.getAttributeNames()) {
|
||||
const value = node.getAttribute(attributeName)!;
|
||||
if (attributeName.startsWith("t-on-")) {
|
||||
on = on || {};
|
||||
on[attributeName.slice(5)] = value;
|
||||
} else if (attributeName.startsWith("t-translation-context-")) {
|
||||
const attrName = attributeName.slice(22);
|
||||
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||
attrsTranslationCtx[attrName] = value;
|
||||
} else {
|
||||
attrs = attrs || {};
|
||||
attrs[attributeName] = value;
|
||||
@@ -849,11 +906,16 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
||||
type: ASTType.TSlot,
|
||||
name,
|
||||
attrs,
|
||||
attrsTranslationCtx,
|
||||
on,
|
||||
defaultContent: parseChildNodes(node, ctx),
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Translation
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
||||
if (node.getAttribute("t-translation") !== "off") {
|
||||
return null;
|
||||
@@ -865,6 +927,23 @@ function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Translation Context
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null {
|
||||
const translationCtx = node.getAttribute("t-translation-context");
|
||||
if (!translationCtx) {
|
||||
return null;
|
||||
}
|
||||
node.removeAttribute("t-translation-context");
|
||||
return {
|
||||
type: ASTType.TTranslationContext,
|
||||
content: parseNode(node, ctx),
|
||||
translationCtx,
|
||||
};
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Portal
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// This file exports a function that allows compiling templates ahead of time.
|
||||
// It is used by the "compile_owl_template" command registered in the "bin"
|
||||
// section of owl's package.json
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
import { readdir, readFile, stat } from "fs/promises";
|
||||
import path from "path";
|
||||
import "./setup_jsdom";
|
||||
// Owl imports must be made after setting up jsdom in the global namespace
|
||||
import { compile } from "..";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
async function getXmlFiles(paths: string[]): Promise<string[]> {
|
||||
return (
|
||||
await Promise.all(
|
||||
paths.map(async (file) => {
|
||||
const stats = await stat(path.join(file));
|
||||
if (stats.isDirectory()) {
|
||||
return await getXmlFiles(
|
||||
(await readdir(file)).map((fileName) => path.join(file, fileName))
|
||||
);
|
||||
}
|
||||
if (file.endsWith(".xml")) {
|
||||
return file;
|
||||
}
|
||||
return [];
|
||||
})
|
||||
)
|
||||
).flat();
|
||||
}
|
||||
|
||||
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
||||
const a = "·-_,:;";
|
||||
const p = new RegExp(a.split("").join("|"), "g");
|
||||
|
||||
function slugify(str: string) {
|
||||
return str
|
||||
.replace(/\//g, "") // remove /
|
||||
.replace(/\./g, "_") // Replace . with _
|
||||
.replace(p, (c) => "_") // Replace special characters
|
||||
.replace(/&/g, "_and_") // Replace & with ‘and’
|
||||
.replace(/[^\w\-]+/g, ""); // Remove all non-word characters
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// main
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
export async function compileTemplates(paths: string[]) {
|
||||
const files = await getXmlFiles(paths);
|
||||
process.stdout.write(`Processing ${files.length} files`);
|
||||
let xmlStrings = await Promise.all(files.map((file) => readFile(file, "utf8")));
|
||||
|
||||
const templates = [];
|
||||
const errors = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const fileName = files[i];
|
||||
const fileContent = xmlStrings[i];
|
||||
process.stdout.write(`.`);
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(fileContent, "text/xml");
|
||||
for (const template of doc.querySelectorAll("[t-name]")) {
|
||||
const name = template.getAttribute("t-name");
|
||||
if (template.hasAttribute("owl")) {
|
||||
template.removeAttribute("owl");
|
||||
}
|
||||
const fnName = slugify(name!);
|
||||
try {
|
||||
const fn = compile(template).toString().replace("anonymous", fnName);
|
||||
templates.push(`"${name}": ${fn},\n`);
|
||||
} catch (e) {
|
||||
errors.push({ name, fileName, e });
|
||||
}
|
||||
}
|
||||
}
|
||||
process.stdout.write(`\n`);
|
||||
|
||||
for (let { name, fileName, e } of errors) {
|
||||
console.warn(`Error while compiling '${name}' (in file ${fileName})`);
|
||||
console.error(e);
|
||||
}
|
||||
console.log(`${templates.length} templates compiled`);
|
||||
|
||||
return `export const templates = {\n ${templates.join("\n")} \n}`;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import jsdom from "jsdom";
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// add global DOM stuff for compiler. Needs to be in a separate file so rollup
|
||||
// doesn't hoist the owl imports above this block of code.
|
||||
// -----------------------------------------------------------------------------
|
||||
var document = new jsdom.JSDOM("", {});
|
||||
var window = document.window;
|
||||
global.document = window.document;
|
||||
global.window = window as unknown as Window & typeof globalThis;
|
||||
global.DOMParser = window.DOMParser;
|
||||
global.Element = window.Element;
|
||||
global.Node = window.Node;
|
||||
@@ -30,6 +30,13 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
||||
fibersInError.delete(current);
|
||||
fibersInError.delete(root);
|
||||
current.appliedToDom = false;
|
||||
if (current instanceof RootFiber) {
|
||||
// it is possible that this fiber is a fiber that crashed while being
|
||||
// mounted, so the mounted list is possibly corrupted. We restore it to
|
||||
// its normal initial state (which is empty list or a list with a mount
|
||||
// fiber.
|
||||
current.mounted = current instanceof MountFiber ? [current] : [];
|
||||
}
|
||||
}
|
||||
return current;
|
||||
}
|
||||
@@ -152,6 +159,7 @@ export class RootFiber extends Fiber {
|
||||
const node = this.node;
|
||||
this.locked = true;
|
||||
let current: Fiber | undefined = undefined;
|
||||
let mountedFibers = this.mounted;
|
||||
try {
|
||||
// Step 1: calling all willPatch lifecycle hooks
|
||||
for (current of this.willPatch) {
|
||||
@@ -173,7 +181,6 @@ export class RootFiber extends Fiber {
|
||||
this.locked = false;
|
||||
|
||||
// Step 4: calling all mounted lifecycle hooks
|
||||
let mountedFibers = this.mounted;
|
||||
while ((current = mountedFibers.pop())) {
|
||||
current = current;
|
||||
if (current.appliedToDom) {
|
||||
@@ -194,6 +201,15 @@ export class RootFiber extends Fiber {
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// if mountedFibers is not empty, this means that a crash occured while
|
||||
// calling the mounted hooks of some component. So, there may still be
|
||||
// some component that have been mounted, but for which the mounted hooks
|
||||
// have not been called. Here, we remove the willUnmount hooks for these
|
||||
// specific component to prevent a worse situation (willUnmount being
|
||||
// called even though mounted has not been called)
|
||||
for (let fiber of mountedFibers) {
|
||||
fiber.node.willUnmount = [];
|
||||
}
|
||||
this.locked = false;
|
||||
node.app.handleError({ fiber: current || this, error: e });
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
||||
export interface TemplateSetConfig {
|
||||
dev?: boolean;
|
||||
translatableAttributes?: string[];
|
||||
translateFn?: (s: string) => string;
|
||||
translateFn?: (s: string, translationCtx: string) => string;
|
||||
templates?: string | Document | Record<string, string>;
|
||||
getTemplate?: (s: string) => Element | Function | string | void;
|
||||
customDirectives?: customDirectives;
|
||||
@@ -27,7 +27,7 @@ export class TemplateSet {
|
||||
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
||||
templates: { [name: string]: Template } = {};
|
||||
getRawTemplate?: (s: string) => Element | Function | string | void;
|
||||
translateFn?: (s: string) => string;
|
||||
translateFn?: (s: string, translationCtx: string) => string;
|
||||
translatableAttributes?: string[];
|
||||
Portal = Portal;
|
||||
customDirectives: customDirectives;
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// do not modify manually. This file is generated by the release script.
|
||||
export const version = "2.5.0";
|
||||
export const version = "2.6.0";
|
||||
|
||||
@@ -14,7 +14,7 @@ exports[`t-custom can use t-custom directive on a node 1`] = `
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`t-custom can use t-custom directive with modifier on a node 1`] = `
|
||||
exports[`t-custom can use t-custom directive with modifiers on a node 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
@@ -1,5 +1,128 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`translation context body of t-sets are translated in context 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\\", \`traduit\`);
|
||||
const b2 = text(ctx['label']);
|
||||
return multi([b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation context default slot params and content translated in context 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { callSlot } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
function defaultContent1(ctx, node, key = \\"\\") {
|
||||
return text(\` foo \`);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b3 = callSlot(ctx, node, key, 'default', false, {param: \`param\`,title: \`título\`}, defaultContent1.bind(this));
|
||||
return block1([], [b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation context props with modifier .translate are translated in context 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`ChildComponent\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return comp1({text: \`jeu\`}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation context props with modifier .translate are translated in context 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let txt1 = ctx['props'].text;
|
||||
return block1([txt1]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation context slot attrs and text contents are translated in context 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { capture, markRaw } = helpers;
|
||||
const comp1 = app.createComponent(\`ChildComponent\`, true, true, false, []);
|
||||
|
||||
function slot1(ctx, node, key = \\"\\") {
|
||||
return text(\`jeu\`);
|
||||
}
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const ctx1 = capture(ctx);
|
||||
return comp1({slots: markRaw({'a': {__render: slot1.bind(this), __ctx: ctx1, title: \`título\`}})}, key + \`__1\`, node, this, null);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation context slot attrs and text contents are translated in context 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
let { callSlot } = helpers;
|
||||
|
||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = callSlot(ctx, node, key, 'a', false, {});
|
||||
return block1([], [b2]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation context translation of attributes in context 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block1 = createBlock(\`<div title=\\"titre\\" label=\\"game\\"/>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return block1();
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation context translation of text in context 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
let block2 = createBlock(\`<div>word</div>\`);
|
||||
let block3 = createBlock(\`<div>mot</div>\`);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = block2();
|
||||
const b3 = block3();
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`translation support body of t-sets are translated 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -43,6 +43,7 @@ describe("qweb parser", () => {
|
||||
dynamicTag: null,
|
||||
content: [],
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -70,6 +71,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -84,6 +86,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -98,6 +101,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -109,6 +113,7 @@ describe("qweb parser", () => {
|
||||
tag: "span",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -128,6 +133,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -139,6 +145,7 @@ describe("qweb parser", () => {
|
||||
tag: "span",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -156,6 +163,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -181,6 +189,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -201,6 +210,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -223,6 +233,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -246,6 +257,7 @@ describe("qweb parser", () => {
|
||||
tag: "span",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -262,6 +274,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: { class: "abc" },
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -280,6 +293,7 @@ describe("qweb parser", () => {
|
||||
height: "90px",
|
||||
width: "100px",
|
||||
},
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
attrs: {
|
||||
@@ -290,6 +304,7 @@ describe("qweb parser", () => {
|
||||
stroke: "green",
|
||||
"stroke-width": "1",
|
||||
},
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
@@ -312,6 +327,7 @@ describe("qweb parser", () => {
|
||||
parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`)
|
||||
).toEqual({
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
attrs: {
|
||||
@@ -322,6 +338,7 @@ describe("qweb parser", () => {
|
||||
stroke: "green",
|
||||
"stroke-width": "1",
|
||||
},
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
@@ -348,6 +365,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
content: [
|
||||
@@ -356,6 +374,7 @@ describe("qweb parser", () => {
|
||||
tag: "pre",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
content: [],
|
||||
@@ -391,6 +410,7 @@ describe("qweb parser", () => {
|
||||
tag: "span",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -413,6 +433,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -455,6 +476,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -469,6 +491,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -489,6 +512,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -530,6 +554,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -607,6 +632,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -626,6 +652,7 @@ describe("qweb parser", () => {
|
||||
tag: "h1",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -639,6 +666,7 @@ describe("qweb parser", () => {
|
||||
tag: "h2",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -685,6 +713,7 @@ describe("qweb parser", () => {
|
||||
{
|
||||
type: ASTType.DomNode,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
@@ -705,6 +734,7 @@ describe("qweb parser", () => {
|
||||
{
|
||||
type: ASTType.DomNode,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
@@ -742,6 +772,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -811,6 +842,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -853,6 +885,7 @@ describe("qweb parser", () => {
|
||||
tag: "span",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -887,6 +920,7 @@ describe("qweb parser", () => {
|
||||
tag: "span",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -920,6 +954,7 @@ describe("qweb parser", () => {
|
||||
"t-att-selected": "category.id==options.active_category_id",
|
||||
"t-att-value": "category.id",
|
||||
},
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -940,6 +975,7 @@ describe("qweb parser", () => {
|
||||
).toEqual({
|
||||
type: ASTType.DomNode,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -987,6 +1023,7 @@ describe("qweb parser", () => {
|
||||
ref: null,
|
||||
model: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
ns: null,
|
||||
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
|
||||
},
|
||||
@@ -1010,6 +1047,7 @@ describe("qweb parser", () => {
|
||||
name: "Comp",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
slots: null,
|
||||
on: null,
|
||||
},
|
||||
@@ -1099,6 +1137,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -1139,6 +1178,7 @@ describe("qweb parser", () => {
|
||||
tag: "button",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: { click: "add" },
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -1175,6 +1215,7 @@ describe("qweb parser", () => {
|
||||
tag: "select",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
content: [
|
||||
@@ -1183,6 +1224,7 @@ describe("qweb parser", () => {
|
||||
tag: "option",
|
||||
dynamicTag: null,
|
||||
attrs: { value: "1" },
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
content: [],
|
||||
@@ -1212,6 +1254,7 @@ describe("qweb parser", () => {
|
||||
tag: "select",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
content: [
|
||||
@@ -1220,6 +1263,7 @@ describe("qweb parser", () => {
|
||||
tag: "option",
|
||||
dynamicTag: null,
|
||||
attrs: { "t-att-value": "valueVar" },
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
content: [],
|
||||
@@ -1251,6 +1295,7 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
on: null,
|
||||
slots: null,
|
||||
isDynamic: false,
|
||||
@@ -1263,6 +1308,7 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: { a: "1", b: "'b'" },
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: null,
|
||||
slots: null,
|
||||
@@ -1275,6 +1321,7 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: "state",
|
||||
props: { a: "1" },
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: null,
|
||||
slots: null,
|
||||
@@ -1287,6 +1334,7 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: { click: "someMethod" },
|
||||
slots: null,
|
||||
@@ -1329,12 +1377,14 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: null,
|
||||
slots: {
|
||||
default: {
|
||||
content: { type: ASTType.Text, value: "foo" },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
@@ -1350,12 +1400,14 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: null,
|
||||
slots: {
|
||||
default: {
|
||||
content: { type: ASTType.Text, value: "foo" },
|
||||
attrs: { param: "param" },
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
@@ -1370,6 +1422,7 @@ describe("qweb parser", () => {
|
||||
isDynamic: false,
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
on: null,
|
||||
slots: {
|
||||
default: {
|
||||
@@ -1381,6 +1434,7 @@ describe("qweb parser", () => {
|
||||
tag: "span",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -1392,6 +1446,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -1401,6 +1456,7 @@ describe("qweb parser", () => {
|
||||
],
|
||||
},
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
@@ -1415,9 +1471,11 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
on: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
slots: {
|
||||
mySlot: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
@@ -1434,9 +1492,16 @@ describe("qweb parser", () => {
|
||||
isDynamic: false,
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
on: null,
|
||||
slots: {
|
||||
name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
|
||||
name: {
|
||||
content: { type: ASTType.Text, value: "foo" },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1448,11 +1513,13 @@ describe("qweb parser", () => {
|
||||
isDynamic: false,
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
on: null,
|
||||
slots: {
|
||||
name: {
|
||||
content: { type: ASTType.Text, value: "foo" },
|
||||
attrs: { param: "param" },
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
@@ -1469,12 +1536,14 @@ describe("qweb parser", () => {
|
||||
isDynamic: false,
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
on: null,
|
||||
slots: {
|
||||
name: {
|
||||
content: { type: ASTType.Text, value: "foo" },
|
||||
on: { click: "doStuff" },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
scope: null,
|
||||
},
|
||||
},
|
||||
@@ -1493,16 +1562,24 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: null,
|
||||
slots: {
|
||||
default: {
|
||||
content: { type: ASTType.Text, value: " " },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
name: {
|
||||
content: { type: ASTType.Text, value: "foo" },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1518,11 +1595,24 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: null,
|
||||
slots: {
|
||||
a: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
|
||||
b: { content: { type: ASTType.Text, value: "bar" }, attrs: null, on: null, scope: null },
|
||||
a: {
|
||||
content: { type: ASTType.Text, value: "foo" },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
b: {
|
||||
content: { type: ASTType.Text, value: "bar" },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
@@ -1533,6 +1623,7 @@ describe("qweb parser", () => {
|
||||
name: "myComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: true,
|
||||
on: null,
|
||||
slots: null,
|
||||
@@ -1545,6 +1636,7 @@ describe("qweb parser", () => {
|
||||
name: "mycomponent",
|
||||
dynamicProps: null,
|
||||
props: { a: "1", b: "'b'" },
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: true,
|
||||
on: null,
|
||||
slots: null,
|
||||
@@ -1557,6 +1649,7 @@ describe("qweb parser", () => {
|
||||
name: "mycomponent",
|
||||
dynamicProps: "state",
|
||||
props: { a: "1" },
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: true,
|
||||
on: null,
|
||||
slots: null,
|
||||
@@ -1587,12 +1680,14 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: null,
|
||||
slots: {
|
||||
default: {
|
||||
content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
scope: null,
|
||||
on: null,
|
||||
},
|
||||
@@ -1613,11 +1708,13 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: null,
|
||||
slots: {
|
||||
default: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
content: {
|
||||
@@ -1626,11 +1723,13 @@ describe("qweb parser", () => {
|
||||
name: "Child",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
on: null,
|
||||
slots: {
|
||||
brol: {
|
||||
content: { type: ASTType.Text, value: "coucou" },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
scope: null,
|
||||
on: null,
|
||||
},
|
||||
@@ -1654,11 +1753,13 @@ describe("qweb parser", () => {
|
||||
name: "MyComponent",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
isDynamic: false,
|
||||
on: null,
|
||||
slots: {
|
||||
default: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
content: {
|
||||
@@ -1667,11 +1768,13 @@ describe("qweb parser", () => {
|
||||
name: "Child",
|
||||
dynamicProps: null,
|
||||
props: null,
|
||||
propsTranslationCtx: null,
|
||||
on: null,
|
||||
slots: {
|
||||
brol: {
|
||||
content: { type: ASTType.Text, value: "coucou" },
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
scope: null,
|
||||
},
|
||||
@@ -1691,6 +1794,7 @@ describe("qweb parser", () => {
|
||||
type: ASTType.TSlot,
|
||||
name: "default",
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
defaultContent: null,
|
||||
});
|
||||
@@ -1701,6 +1805,7 @@ describe("qweb parser", () => {
|
||||
type: ASTType.TSlot,
|
||||
name: "header",
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
defaultContent: { type: ASTType.Text, value: "default content" },
|
||||
});
|
||||
@@ -1711,6 +1816,7 @@ describe("qweb parser", () => {
|
||||
type: ASTType.TSlot,
|
||||
name: "default",
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: { "click.prevent": "doSomething" },
|
||||
defaultContent: null,
|
||||
});
|
||||
@@ -1728,6 +1834,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -1746,6 +1853,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
model: null,
|
||||
@@ -1765,6 +1873,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: "name",
|
||||
model: null,
|
||||
@@ -1779,6 +1888,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: "name",
|
||||
model: null,
|
||||
@@ -1795,6 +1905,7 @@ describe("qweb parser", () => {
|
||||
tag: "div",
|
||||
dynamicTag: null,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
on: null,
|
||||
ref: "name",
|
||||
model: null,
|
||||
@@ -1831,6 +1942,7 @@ describe("qweb parser", () => {
|
||||
body: {
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
type: ASTType.Text,
|
||||
@@ -1859,6 +1971,92 @@ describe("qweb parser", () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// t-translation-context
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('t-translation-context="fr"', async () => {
|
||||
expect(parse(`<t t-translation-context="fr">word</t>`)).toEqual({
|
||||
type: ASTType.TTranslationContext,
|
||||
content: {
|
||||
type: ASTType.Text,
|
||||
value: "word",
|
||||
},
|
||||
translationCtx: "fr",
|
||||
});
|
||||
|
||||
expect(parse(`<div t-translation-context="fr">word</div>`)).toEqual({
|
||||
content: {
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [
|
||||
{
|
||||
type: 0,
|
||||
value: "word",
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "div",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
translationCtx: "fr",
|
||||
type: ASTType.TTranslationContext,
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// t-translation-context-attr
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
test('t-translation-context="fr" and t-translation-context-title="pt" for a div attr title', async () => {
|
||||
expect(
|
||||
parse(
|
||||
`<div t-translation-context="fr" title="hello" t-translation-context-title="pt">word</div>`
|
||||
)
|
||||
).toEqual({
|
||||
content: {
|
||||
attrs: { title: "hello" },
|
||||
attrsTranslationCtx: { title: "pt" },
|
||||
content: [
|
||||
{
|
||||
type: 0,
|
||||
value: "word",
|
||||
},
|
||||
],
|
||||
dynamicTag: null,
|
||||
model: null,
|
||||
ns: null,
|
||||
on: null,
|
||||
ref: null,
|
||||
tag: "div",
|
||||
type: ASTType.DomNode,
|
||||
},
|
||||
translationCtx: "fr",
|
||||
type: ASTType.TTranslationContext,
|
||||
});
|
||||
});
|
||||
|
||||
test('t-translation-context-title="fr" for component prop title', async () => {
|
||||
expect(parse(`<Comp title="hello" t-translation-context-title="fr" />`)).toEqual({
|
||||
dynamicProps: null,
|
||||
isDynamic: false,
|
||||
name: "Comp",
|
||||
on: null,
|
||||
props: {
|
||||
title: "hello",
|
||||
},
|
||||
propsTranslationCtx: {
|
||||
title: "fr",
|
||||
},
|
||||
slots: null,
|
||||
type: ASTType.TComponent,
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// t-model
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1866,6 +2064,7 @@ describe("qweb parser", () => {
|
||||
expect(parse(`<input t-model="state.stuff" />`)).toEqual({
|
||||
type: ASTType.DomNode,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
on: null,
|
||||
ref: null,
|
||||
@@ -1886,6 +2085,7 @@ describe("qweb parser", () => {
|
||||
expect(parse(`<input t-model="state['stuff']" />`)).toEqual({
|
||||
type: ASTType.DomNode,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
on: null,
|
||||
ref: null,
|
||||
@@ -1906,6 +2106,7 @@ describe("qweb parser", () => {
|
||||
expect(parse(`<input t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
||||
type: ASTType.DomNode,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
on: null,
|
||||
ref: null,
|
||||
@@ -1927,6 +2128,7 @@ describe("qweb parser", () => {
|
||||
expect(parse(`<textarea t-model="state.stuff" />`)).toEqual({
|
||||
type: ASTType.DomNode,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
on: null,
|
||||
ref: null,
|
||||
@@ -1947,6 +2149,7 @@ describe("qweb parser", () => {
|
||||
expect(parse(`<input type="checkbox" t-model="state.stuff" />`)).toEqual({
|
||||
type: ASTType.DomNode,
|
||||
attrs: { type: "checkbox" },
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
on: null,
|
||||
ref: null,
|
||||
@@ -1967,6 +2170,7 @@ describe("qweb parser", () => {
|
||||
expect(parse(`<input type="radio" t-model="state.stuff" />`)).toEqual({
|
||||
type: ASTType.DomNode,
|
||||
attrs: { type: "radio" },
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
on: null,
|
||||
ref: null,
|
||||
@@ -1987,6 +2191,7 @@ describe("qweb parser", () => {
|
||||
expect(parse(`<input type="radio" t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
||||
type: ASTType.DomNode,
|
||||
attrs: { type: "radio" },
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
on: null,
|
||||
ref: null,
|
||||
@@ -2012,6 +2217,7 @@ describe("qweb parser", () => {
|
||||
expect(parse(`<div t-tag="theTag" />`)).toEqual({
|
||||
type: ASTType.DomNode,
|
||||
attrs: null,
|
||||
attrsTranslationCtx: null,
|
||||
content: [],
|
||||
on: null,
|
||||
ref: null,
|
||||
|
||||
@@ -31,25 +31,27 @@ describe("t-custom", () => {
|
||||
expect(steps).toEqual(["clicked"]);
|
||||
});
|
||||
|
||||
test("can use t-custom directive with modifier on a node", async () => {
|
||||
test("can use t-custom directive with modifiers on a node", async () => {
|
||||
const steps: string[] = [];
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`<div t-custom-plop.mouse="click" class="my-div"/>`;
|
||||
static template = xml`<div t-custom-plop.mouse.stop="click" class="my-div"/>`;
|
||||
click() {
|
||||
steps.push("clicked");
|
||||
}
|
||||
}
|
||||
const app = new App(SomeComponent, {
|
||||
customDirectives: {
|
||||
plop: (node, value, modifier) => {
|
||||
plop: (node, value, modifiers) => {
|
||||
node.setAttribute("t-on-click", value);
|
||||
steps.push(modifier || "");
|
||||
for (let mod of modifiers) {
|
||||
steps.push(mod);
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
await app.mount(fixture);
|
||||
expect(fixture.innerHTML).toBe(`<div class="my-div"></div>`);
|
||||
fixture.querySelector("div")!.click();
|
||||
expect(steps).toEqual(["mouse", "clicked"]);
|
||||
expect(steps).toEqual(["mouse", "stop", "clicked"]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ describe("translation support", () => {
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("<div> mot </div>");
|
||||
expect(translateFn).toHaveBeenCalledWith("word");
|
||||
expect(translateFn).toHaveBeenCalledWith("word", "");
|
||||
});
|
||||
|
||||
test("translation works, even if initial string has inner consecutive white space", async () => {
|
||||
@@ -97,7 +97,7 @@ describe("translation support", () => {
|
||||
const translateFn = jest.fn((expr: string) => (expr === "some word" ? "un mot" : expr));
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(translateFn).toHaveBeenCalledWith("some word");
|
||||
expect(translateFn).toHaveBeenCalledWith("some word", "");
|
||||
expect(fixture.innerHTML).toBe("<div>un mot</div>");
|
||||
});
|
||||
|
||||
@@ -171,3 +171,126 @@ describe("translation support", () => {
|
||||
expect(fixture.innerHTML).toBe("translated");
|
||||
});
|
||||
});
|
||||
|
||||
describe("translation context", () => {
|
||||
test("translation of text in context", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>word</div>
|
||||
<div t-translation-context="fr">word</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||
translationCtx === "fr" ? (expr === "word" ? "mot" : expr) : expr
|
||||
);
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("<div>word</div><div>mot</div>");
|
||||
expect(translateFn).toHaveBeenCalledWith("word", "");
|
||||
expect(translateFn).toHaveBeenCalledWith("word", "fr");
|
||||
});
|
||||
test("translation of attributes in context", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div t-translation-context="en" t-translation-context-title="fr" title="title" label="game"/>
|
||||
`;
|
||||
}
|
||||
|
||||
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||
translationCtx === "fr" ? (expr === "title" ? "titre" : expr) : expr
|
||||
);
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe(`<div title="titre" label="game"></div>`);
|
||||
expect(translateFn).toHaveBeenCalledWith("title", "fr");
|
||||
expect(translateFn).toHaveBeenCalledWith("game", "en");
|
||||
});
|
||||
test("body of t-sets are translated in context", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<t t-set="label" t-translation-context="fr">untranslated</t>
|
||||
<t t-esc="label"/>`;
|
||||
}
|
||||
|
||||
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||
translationCtx === "fr" ? "traduit" : expr
|
||||
);
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("traduit");
|
||||
expect(translateFn).toHaveBeenCalledWith("untranslated", "fr");
|
||||
});
|
||||
test("props with modifier .translate are translated in context", async () => {
|
||||
class ChildComponent extends Component {
|
||||
static props = ["text"];
|
||||
static template = xml`<span t-esc="props.text"/>`;
|
||||
}
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static components = { ChildComponent };
|
||||
static template = xml`
|
||||
<ChildComponent text.translate="game" t-translation-context-text.translate="fr" />`;
|
||||
}
|
||||
|
||||
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||
translationCtx === "fr" ? "jeu" : expr
|
||||
);
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("<span>jeu</span>");
|
||||
expect(translateFn).toHaveBeenCalledWith("game", "fr");
|
||||
});
|
||||
test("slot attrs and text contents are translated in context", async () => {
|
||||
class ChildComponent extends Component {
|
||||
static template = xml`
|
||||
<div t-translation-context="ja">
|
||||
<t t-slot="a"/>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
class SomeComponent extends Component {
|
||||
static components = { ChildComponent };
|
||||
static template = xml`
|
||||
<ChildComponent t-translation-context="fr">
|
||||
<t t-set-slot="a" title.translate="title" t-translation-context-title.translate="pt">game</t>
|
||||
</ChildComponent>
|
||||
`;
|
||||
}
|
||||
|
||||
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||
translationCtx === "fr" ? "jeu" : translationCtx === "pt" ? "título" : expr
|
||||
);
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("<div>jeu</div>");
|
||||
expect(translateFn).toHaveBeenCalledWith("game", "fr");
|
||||
expect(translateFn).toHaveBeenCalledWith("title", "pt");
|
||||
});
|
||||
test("default slot params and content translated in context", async () => {
|
||||
class SomeComponent extends Component {
|
||||
static template = xml`
|
||||
<div>
|
||||
<t
|
||||
t-slot="default"
|
||||
t-translation-context="fr"
|
||||
param.translate="param"
|
||||
title.translate="title"
|
||||
t-translation-context-title.translate="pt"
|
||||
>
|
||||
foo
|
||||
</t>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
const translateFn = jest.fn((expr: string, translationCtx: string) =>
|
||||
translationCtx === "pt" ? "título" : expr
|
||||
);
|
||||
|
||||
await mount(SomeComponent, fixture, { translateFn });
|
||||
expect(fixture.innerHTML).toBe("<div> foo </div>");
|
||||
expect(translateFn).toHaveBeenCalledWith("foo", "fr");
|
||||
expect(translateFn).toHaveBeenCalledWith("param", "fr");
|
||||
expect(translateFn).toHaveBeenCalledWith("title", "pt");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1191,6 +1191,135 @@ exports[`can catch errors error in mounted on a component with a sibling (proper
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(null, false, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const Comp1 = ctx['component'];
|
||||
return toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery 2`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
const comp2 = app.createComponent(\`Boom\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`parent\`);
|
||||
const b3 = comp1({}, key + \`__1\`, node, this, null);
|
||||
const b4 = comp2({}, key + \`__2\`, node, this, null);
|
||||
return multi([b2, b3, b4]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`abc\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`boom\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery 5`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`def\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery, variation 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(null, false, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
let b2, b3;
|
||||
b2 = text(\`R\`);
|
||||
if (ctx['state'].gogogo) {
|
||||
const Comp1 = ctx['component'];
|
||||
b3 = toggler(Comp1, comp1({}, (Comp1).name + key + \`__1\`, node, this, Comp1));
|
||||
}
|
||||
return multi([b2, b3]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery, variation 3`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
const comp1 = app.createComponent(\`Child\`, true, false, false, []);
|
||||
const comp2 = app.createComponent(\`Boom\`, true, false, false, []);
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
const b2 = text(\`parent\`);
|
||||
const b3 = comp1({}, key + \`__1\`, node, this, null);
|
||||
const b4 = comp2({}, key + \`__2\`, node, this, null);
|
||||
return multi([b2, b3, b4]);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery, variation 4`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`abc\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery, variation 5`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`boom\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors error in onMounted, graceful recovery, variation 6`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||
|
||||
return function template(ctx, node, key = \\"\\") {
|
||||
return text(\`def\`);
|
||||
}
|
||||
}"
|
||||
`;
|
||||
|
||||
exports[`can catch errors onError in class inheritance is called if rethrown 1`] = `
|
||||
"function anonymous(app, bdom, helpers
|
||||
) {
|
||||
|
||||
@@ -1678,4 +1678,198 @@ describe("can catch errors", () => {
|
||||
`);
|
||||
expect(fixture.innerHTML).toBe("2");
|
||||
});
|
||||
|
||||
test("error in onMounted, graceful recovery", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`abc`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class OtherChild extends Component {
|
||||
static template = xml`def`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class Boom extends Component {
|
||||
static template = xml`boom`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
onMounted(() => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`parent<Child/><Boom/>`;
|
||||
static components = { Child, Boom };
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = xml`<t t-component="component"/>`;
|
||||
|
||||
component: any = Parent;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
onError(() => {
|
||||
logStep("error");
|
||||
this.component = OtherChild;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await mount(Root, fixture);
|
||||
expect(fixture.innerHTML).toBe("def");
|
||||
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Root:setup",
|
||||
"Root:willStart",
|
||||
"Root:willRender",
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Root:rendered",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Boom:setup",
|
||||
"Boom:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Boom:willRender",
|
||||
"Boom:rendered",
|
||||
"Boom:mounted",
|
||||
"error",
|
||||
"Root:willRender",
|
||||
"OtherChild:setup",
|
||||
"OtherChild:willStart",
|
||||
"Root:rendered",
|
||||
"OtherChild:willRender",
|
||||
"OtherChild:rendered",
|
||||
"OtherChild:mounted",
|
||||
"Root:mounted",
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
test("error in onMounted, graceful recovery, variation", async () => {
|
||||
class Child extends Component {
|
||||
static template = xml`abc`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class OtherChild extends Component {
|
||||
static template = xml`def`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class Boom extends Component {
|
||||
static template = xml`boom`;
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
onMounted(() => {
|
||||
throw new Error("boom");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class Parent extends Component {
|
||||
static template = xml`parent<Child/><Boom/>`;
|
||||
static components = { Child, Boom };
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
}
|
||||
}
|
||||
|
||||
class Root extends Component {
|
||||
static template = xml`R<t t-if="state.gogogo" t-component="component"/>`;
|
||||
|
||||
component: any = Parent;
|
||||
state = useState({ gogogo: false });
|
||||
|
||||
setup() {
|
||||
useLogLifecycle();
|
||||
onError(() => {
|
||||
logStep("error");
|
||||
this.component = OtherChild;
|
||||
this.render();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const root = await mount(Root, fixture);
|
||||
expect(fixture.innerHTML).toBe("R");
|
||||
|
||||
// standard mounting process
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Root:setup",
|
||||
"Root:willStart",
|
||||
"Root:willRender",
|
||||
"Root:rendered",
|
||||
"Root:mounted",
|
||||
]
|
||||
`);
|
||||
|
||||
root.state.gogogo = true;
|
||||
await nextTick();
|
||||
|
||||
expect(fixture.innerHTML).toBe("Rparentabcboom");
|
||||
// rerender, root creates sub components, it crashes, tries to recover
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"Root:willRender",
|
||||
"Parent:setup",
|
||||
"Parent:willStart",
|
||||
"Root:rendered",
|
||||
"Parent:willRender",
|
||||
"Child:setup",
|
||||
"Child:willStart",
|
||||
"Boom:setup",
|
||||
"Boom:willStart",
|
||||
"Parent:rendered",
|
||||
"Child:willRender",
|
||||
"Child:rendered",
|
||||
"Boom:willRender",
|
||||
"Boom:rendered",
|
||||
"Root:willPatch",
|
||||
"Boom:mounted",
|
||||
"error",
|
||||
"Root:willRender",
|
||||
"OtherChild:setup",
|
||||
"OtherChild:willStart",
|
||||
"Root:rendered",
|
||||
]
|
||||
`);
|
||||
|
||||
await nextTick();
|
||||
expect(fixture.innerHTML).toBe("Rdef");
|
||||
|
||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||
Array [
|
||||
"OtherChild:willRender",
|
||||
"OtherChild:rendered",
|
||||
"Root:willPatch",
|
||||
"Child:willDestroy",
|
||||
"Boom:willUnmount",
|
||||
"Boom:willDestroy",
|
||||
"Parent:willDestroy",
|
||||
"OtherChild:mounted",
|
||||
"Root:patched",
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -339,7 +339,7 @@ test("throw if prop uses an unknown suffix", async () => {
|
||||
|
||||
await expect(async () => {
|
||||
await mount(Parent, fixture);
|
||||
}).rejects.toThrowError("Invalid prop suffix");
|
||||
}).rejects.toThrowError("Invalid prop suffix: somesuffix");
|
||||
});
|
||||
|
||||
test(".alike suffix in a simple case", async () => {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// this is the "compile_owl_templates" command that owl makes available when
|
||||
// installed as a node_module.
|
||||
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
||||
import { dirname } from "path";
|
||||
import { compileTemplates } from "../dist/compile_templates.mjs";
|
||||
import { parseArgs } from "util";
|
||||
|
||||
const { values, positionals } = parseArgs({
|
||||
allowPositionals: true,
|
||||
options: {
|
||||
output: {
|
||||
type: "string",
|
||||
short: "o",
|
||||
default: "templates.js",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (positionals.length) {
|
||||
const result = await compileTemplates(positionals);
|
||||
const outputPath = values.output;
|
||||
const dir = dirname(outputPath);
|
||||
if (!existsSync(dir)) {
|
||||
mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
writeFileSync(outputPath, result);
|
||||
} else {
|
||||
console.log("Please provide a path");
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const jsdom = require("jsdom");
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// add global DOM stuff for compiler
|
||||
// -----------------------------------------------------------------------------
|
||||
var document = new jsdom.JSDOM("", {});
|
||||
var window = document.window;
|
||||
global.document = window.document;
|
||||
global.window = window;
|
||||
global.DOMParser = window.DOMParser;
|
||||
global.Element = window.Element;
|
||||
global.Node = window.Node;
|
||||
// this needs to be below the jsdom stuff
|
||||
const { compile } = require("../dist/compiler.js");
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// helpers
|
||||
// -----------------------------------------------------------------------------
|
||||
async function getXmlFiles(dir) {
|
||||
let xmls = [];
|
||||
const files = await fs.promises.readdir(dir);
|
||||
const filesStats = await Promise.all(files.map((file) => fs.promises.stat(path.join(dir, file))));
|
||||
for (let i in files) {
|
||||
const name = path.join(dir, files[i]);
|
||||
if (filesStats[i].isDirectory()) {
|
||||
xmls = xmls.concat(await getXmlFiles(name));
|
||||
} else {
|
||||
if (name.endsWith(".xml")) {
|
||||
xmls.push(name);
|
||||
}
|
||||
}
|
||||
}
|
||||
return xmls;
|
||||
}
|
||||
|
||||
function writeToFile(filepath, data) {
|
||||
if (!fs.existsSync(path.dirname(filepath))) {
|
||||
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
||||
}
|
||||
fs.writeFile(filepath, data, (err) => {
|
||||
if (err) {
|
||||
process.stdout.write(`Error while writing file ${filepath}: ${err}`);
|
||||
return;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// adapted from https://medium.com/@mhagemann/the-ultimate-way-to-slugify-a-url-string-in-javascript-b8e4a0d849e1
|
||||
const a = "·-_,:;";
|
||||
const p = new RegExp(a.split("").join("|"), "g");
|
||||
|
||||
function slugify(str) {
|
||||
return str
|
||||
.replace(/\//g, "") // remove /
|
||||
.replace(/\./g, "_") // Replace . with _
|
||||
.replace(p, (c) => '_') // Replace special characters
|
||||
.replace(/&/g, "_and_") // Replace & with ‘and’
|
||||
.replace(/[^\w\-]+/g, "") // Remove all non-word characters
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// main
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
async function compileTemplates(files) {
|
||||
process.stdout.write(`Processing ${files.length} files`);
|
||||
let xmlStrings = await Promise.all(files.map((file) => fs.promises.readFile(file, "utf8")));
|
||||
|
||||
const templates = [];
|
||||
const errors = [];
|
||||
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
const fileName = files[i];
|
||||
const fileContent = xmlStrings[i];
|
||||
process.stdout.write(`.`);
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(fileContent, "text/xml");
|
||||
for (const template of doc.querySelectorAll("[t-name]")) {
|
||||
const name = template.getAttribute("t-name");
|
||||
if (template.hasAttribute("owl")) {
|
||||
template.removeAttribute("owl")
|
||||
}
|
||||
const fnName = slugify(name);
|
||||
try {
|
||||
const fn = compile(template).toString().replace('anonymous', fnName);
|
||||
templates.push(`"${name}": ${fn},\n`);
|
||||
} catch (e) {
|
||||
errors.push({ name, fileName, e });
|
||||
}
|
||||
}
|
||||
}
|
||||
process.stdout.write(`\n`);
|
||||
|
||||
for (let { name, fileName, e } of errors) {
|
||||
console.warn(`Error while compiling '${name}' (in file ${fileName})`);
|
||||
console.error(e);
|
||||
}
|
||||
console.log(`${templates.length} templates compiled`);
|
||||
|
||||
return `export const templates = {\n ${templates.join("\n")} \n}`;
|
||||
}
|
||||
|
||||
const templatesPath = process.argv[2];
|
||||
if (templatesPath && templatesPath.length) {
|
||||
getXmlFiles(templatesPath).then(async (files) => {
|
||||
const result = await compileTemplates(files);
|
||||
writeToFile("templates.js", result);
|
||||
});
|
||||
} else {
|
||||
console.log("Please provide a path");
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Owl devtools",
|
||||
"version": "1.2.2",
|
||||
"version": "1.3.0",
|
||||
"manifest_version": 3,
|
||||
"description": "Chrome devtools extension for Odoo Owl framework",
|
||||
"icons": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "Owl devtools",
|
||||
"version": "1.0.0",
|
||||
"version": "1.1.0",
|
||||
"description": "Firefox devtools extension for Odoo Owl framework",
|
||||
"manifest_version": 2,
|
||||
"browser_specific_settings": {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { useStore } from "../store/store";
|
||||
|
||||
const { Component, useEffect, useRef } = owl;
|
||||
|
||||
export class ContextMenu extends Component {
|
||||
static template = "devtools.ContextMenu";
|
||||
static props = {
|
||||
items: Array,
|
||||
};
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.contextMenu = useRef("contextmenu");
|
||||
useEffect(
|
||||
(position) => {
|
||||
const menu = this.contextMenu.el;
|
||||
const menuWidth = menu.offsetWidth;
|
||||
const menuHeight = menu.offsetHeight;
|
||||
let { x, y } = position;
|
||||
if (x + menuWidth > window.innerWidth) {
|
||||
x = window.innerWidth - menuWidth;
|
||||
}
|
||||
if (y + menuHeight > window.innerHeight) {
|
||||
y = window.innerHeight - menuHeight;
|
||||
}
|
||||
menu.style.left = x + "px";
|
||||
// Need 25px offset because of the main navbar from the browser devtools
|
||||
menu.style.top = y + "px";
|
||||
},
|
||||
() => [this.store.contextMenu?.position]
|
||||
);
|
||||
}
|
||||
onClickItem(action) {
|
||||
action();
|
||||
this.store.contextMenu = null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.ContextMenu">
|
||||
<div class="custom-menu" t-ref="contextmenu">
|
||||
<ul class="my-1">
|
||||
<li t-foreach="props.items" t-as="item" t-key="item_index" t-if="item.show" t-esc="item.title" t-on-click.stop="() => this.onClickItem(item.action)" class="custom-menu-item py-1 px-4"/>
|
||||
</ul>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
@@ -1,4 +1,4 @@
|
||||
const { Component, useRef, useEffect } = owl;
|
||||
const { Component } = owl;
|
||||
import { useStore } from "../../../store/store";
|
||||
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
||||
|
||||
@@ -7,23 +7,64 @@ export class DetailsWindow extends Component {
|
||||
static components = { ObjectTreeElement };
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.contextMenu = useRef("contextmenu");
|
||||
this.contextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent;
|
||||
// Open the context menu when the ids match
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.contextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
||||
}
|
||||
}
|
||||
|
||||
get contextMenuItems() {
|
||||
return [
|
||||
{
|
||||
title: "Inspect source code",
|
||||
show: true,
|
||||
action: () => this.store.inspectComponent("source", this.store.activeComponent.path),
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
{
|
||||
title: "Store as global variable",
|
||||
show: this.store.activeComponent.path.length !== 1,
|
||||
action: () =>
|
||||
this.store.logObjectInConsole([
|
||||
...this.store.activeComponent.path,
|
||||
{ type: "item", value: "component" },
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "Inspect in Elements tab",
|
||||
show: this.store.activeComponent.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("DOM", this.store.activeComponent.path),
|
||||
},
|
||||
{
|
||||
title: "Force rerender",
|
||||
show: this.store.activeComponent.path.length !== 1,
|
||||
action: () => this.store.refreshComponent(this.store.activeComponent.path),
|
||||
},
|
||||
{
|
||||
title: "Store observed states as global variable",
|
||||
show: this.store.activeComponent.path.length !== 1,
|
||||
action: () =>
|
||||
this.store.logObjectInConsole([
|
||||
...this.store.activeComponent.path,
|
||||
{ type: "item", value: "subscriptions" },
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "Inspect compiled template",
|
||||
show: this.store.activeComponent.path.length !== 1,
|
||||
action: () =>
|
||||
this.store.inspectComponent("compiled template", this.store.activeComponent.path),
|
||||
},
|
||||
{
|
||||
title: "Log raw template",
|
||||
show: this.store.activeComponent.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("raw template", this.store.activeComponent.path),
|
||||
},
|
||||
{
|
||||
title: "Store as global variable",
|
||||
show: this.store.activeComponent.path.length === 1,
|
||||
action: () => this.store.logObjectInConsole([...this.store.activeComponent.path]),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
openMenu(ev) {
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
||||
this.store.openContextMenu(ev, this.contextMenuItems);
|
||||
}
|
||||
|
||||
toggleCategory(ev, category) {
|
||||
|
||||
@@ -20,6 +20,17 @@
|
||||
</t>
|
||||
</div>
|
||||
<div class="details-container">
|
||||
<div t-if="store.observedVariables.length and store.observedVariables.some((v) => v.visible)" id="observedVariables" class="details-panel ps-2 py-1">
|
||||
<div class="d-flex mb-2">
|
||||
<div class="w-100">
|
||||
<b class="ps-2">observed variables</b>
|
||||
</div>
|
||||
<i title="Remove observed variables" class="fa fa-times utility-icon p-1" t-on-click.stop="() => this.store.clearObservedVariable()"></i>
|
||||
</div>
|
||||
<t t-foreach="store.observedVariables" t-as="observed" t-key="observed_index" t-if="observed.visible">
|
||||
<ObjectTreeElement object="observed" index="observed_index"/>
|
||||
</t>
|
||||
</div>
|
||||
<div t-if="store.activeComponent.env.children.length > 0" id="env" class="details-panel ps-2 py-1">
|
||||
<div class="d-flex mb-2">
|
||||
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'env')">
|
||||
@@ -59,7 +70,7 @@
|
||||
</t>
|
||||
</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="w-100 text-truncate" t-on-click.stop="(ev) => this.toggleCategory(ev, 'instance')">
|
||||
<i class="fa mx-1 pointer-icon"
|
||||
@@ -75,22 +86,19 @@
|
||||
<ObjectTreeElement object="instance"/>
|
||||
</t>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
||||
<ul class="my-1">
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('source', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||
<t t-if="store.activeComponent.path.length !== 1">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('DOM', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||
<li t-on-click.stop="() => this.store.refreshComponent(store.activeComponent.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('raw template', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||
<div t-if="store.activeComponent.hooks?.children.length > 0" id="hooks" class="details-panel ps-2 py-1">
|
||||
<div class="d-flex mb-2">
|
||||
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'hooks')">
|
||||
<i class="fa mx-1 pointer-icon"
|
||||
t-att-class="{'fa-caret-right': !store.activeComponent.hooks.toggled, 'fa-caret-down': store.activeComponent.hooks.toggled}"
|
||||
/><b>hooks</b>
|
||||
</div>
|
||||
<i title="Remove breakpoints" class="fa fa-times utility-icon p-1" t-on-click.stop="() => this.store.removeBreakpoints()"></i>
|
||||
</div>
|
||||
<t t-if="store.activeComponent.hooks.toggled" t-foreach="store.activeComponent.hooks.children" t-as="hook" t-key="hook_index">
|
||||
<ObjectTreeElement object="hook"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...store.activeComponent.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
</templates>
|
||||
|
||||
@@ -13,19 +13,8 @@ export class ObjectTreeElement extends Component {
|
||||
menuTop: 0,
|
||||
menuLeft: 0,
|
||||
});
|
||||
this.contextMenu = useRef("contextmenu");
|
||||
const inputRef = useRef("input");
|
||||
this.store = useStore();
|
||||
this.contextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent,
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.contextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
||||
}
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
useEffect(
|
||||
(editMode) => {
|
||||
// Focus on the input when it is created
|
||||
@@ -63,16 +52,76 @@ export class ObjectTreeElement extends Component {
|
||||
return this.props.object.depth * 0.8 + 0.3;
|
||||
}
|
||||
|
||||
get contextMenuItems() {
|
||||
return [
|
||||
{
|
||||
title: "Store as global variable",
|
||||
show: true,
|
||||
action: () => this.store.logObjectInConsole(this.props.object.path),
|
||||
},
|
||||
{
|
||||
title: "Inspect function source code",
|
||||
show: this.props.object.contentType === "function",
|
||||
action: () => this.store.inspectFunctionSource(this.props.object.path),
|
||||
},
|
||||
{
|
||||
title: "Observe variable",
|
||||
show: this.props.object.objectType !== "observed",
|
||||
action: () => this.store.observeVariable(this.props.object.path),
|
||||
},
|
||||
{
|
||||
title: "Unobserve variable",
|
||||
show: this.props.object.objectType === "observed",
|
||||
action: () => this.store.clearObservedVariable(this.props.index),
|
||||
},
|
||||
{
|
||||
title: "Inject breakpoint on component",
|
||||
show: this.props.object.contentType === "array" && this.props.object.objectType === "hook",
|
||||
action: () =>
|
||||
this.store.injectBreakpoint(this.props.object.name, this.store.activeComponent.path),
|
||||
},
|
||||
{
|
||||
title: "Inject conditional breakpoint on component",
|
||||
show: this.props.object.contentType === "array" && this.props.object.objectType === "hook",
|
||||
action: () => {
|
||||
const condition = window.prompt("Enter the condition");
|
||||
if (condition) {
|
||||
this.store.injectBreakpoint(
|
||||
this.props.object.name,
|
||||
this.store.activeComponent.path,
|
||||
false,
|
||||
condition
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
title: "Inject breakpoint on instance",
|
||||
show:
|
||||
this.props.object.contentType === "array" &&
|
||||
this.props.object.objectType === "hook" &&
|
||||
!["mounted", "willStart"].includes(this.props.object.name),
|
||||
action: () =>
|
||||
this.store.injectBreakpoint(
|
||||
this.props.object.name,
|
||||
this.store.activeComponent.path,
|
||||
true
|
||||
),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
openMenu(ev) {
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
||||
this.store.openContextMenu(ev, this.contextMenuItems);
|
||||
}
|
||||
|
||||
setupEditMode() {
|
||||
if (!this.state.editMode) {
|
||||
if (!this.props.object.hasChildren) {
|
||||
this.state.editMode = true;
|
||||
}
|
||||
if (
|
||||
!this.state.editMode &&
|
||||
!this.props.object.hasChildren &&
|
||||
!(this.props.object.objectType === "observed")
|
||||
) {
|
||||
this.state.editMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="m-0 p-0 text-nowrap w-100 object-line"
|
||||
t-att-class="props.class + (props.object.hasChildren ? ' bg-feedback' : '')"
|
||||
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
||||
t-on-contextmenu.prevent="openMenu"
|
||||
t-on-contextmenu.prevent="openMenu"
|
||||
>
|
||||
<div t-attf-style="padding-left: {{objectPadding}}rem">
|
||||
<i class="fa px-1 pointer-icon caret"
|
||||
@@ -12,7 +12,7 @@
|
||||
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
|
||||
/>
|
||||
<t t-esc="props.object.name"/>
|
||||
<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'">
|
||||
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
|
||||
<t t-esc="props.object.content"/>
|
||||
@@ -20,25 +20,17 @@
|
||||
</t>
|
||||
<t t-else="">
|
||||
<span class="object-content" t-att-class="objectLineClass" t-on-dblclick.stop="setupEditMode">
|
||||
<t t-if="state.editMode">
|
||||
<t t-if="state.editMode">
|
||||
<input t-attf-id="objectEditionInput/{{pathAsString}}" t-ref="input" type="text" placeholder="" t-att-value="props.object.content" t-on-keydown.stop="editObject"/>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<t t-esc="props.object.content"/>
|
||||
</t>
|
||||
</span>
|
||||
</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">
|
||||
<ul class="my-1">
|
||||
<li t-on-click="() => this.store.logObjectInConsole(this.props.object.path)" class="custom-menu-item py-1 px-4 text-nowrap">Store as global variable</li>
|
||||
<t t-if='props.object.contentType == "function"'>
|
||||
<li t-on-click="() => this.store.inspectFunctionSource(this.props.object.path)" class="custom-menu-item py-1 px-4 text-nowrap">Inspect function source code</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-if="props.object.toggled" t-key="contextMenuId">
|
||||
<t t-foreach="props.object.children" t-as="child" t-key="child_index">
|
||||
<ObjectTreeElement object="child" class="this.classFor(child)"/>
|
||||
|
||||
@@ -16,10 +16,7 @@ export class TreeElement extends Component {
|
||||
searched: false,
|
||||
});
|
||||
this.store = useStore();
|
||||
this.contextMenu = useRef("contextmenu");
|
||||
this.element = useRef("element");
|
||||
this.contextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent;
|
||||
this.stringifiedPath = JSON.stringify(this.props.component.path);
|
||||
// Scroll to the selected element when it changes
|
||||
onMounted(() => {
|
||||
@@ -38,15 +35,6 @@ export class TreeElement extends Component {
|
||||
},
|
||||
() => [this.props.component.selected]
|
||||
);
|
||||
// Open the context menu when the ids match
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.contextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
||||
}
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
// Effect to apply a short highlight effect to the component when it is rendered
|
||||
useEffect(
|
||||
() => {
|
||||
@@ -86,9 +74,86 @@ export class TreeElement extends Component {
|
||||
return minimizeKey(this.props.component.key);
|
||||
}
|
||||
|
||||
get contextMenuItems() {
|
||||
return [
|
||||
{
|
||||
title: "Expand children",
|
||||
show: true,
|
||||
action: () => this.store.toggleComponentAndChildren(this.props.component, true),
|
||||
},
|
||||
{
|
||||
title: "Fold all children",
|
||||
show: true,
|
||||
action: () => this.store.toggleComponentAndChildren(this.props.component, false),
|
||||
},
|
||||
{
|
||||
title: "Fold direct children",
|
||||
show: true,
|
||||
action: () => this.store.foldDirectChildren(this.props.component),
|
||||
},
|
||||
{
|
||||
title: "Inspect source code",
|
||||
show: true,
|
||||
action: () => this.store.inspectComponent("source", this.props.component.path),
|
||||
},
|
||||
{
|
||||
title: "Store as global variable",
|
||||
show: this.props.component.path.length !== 1,
|
||||
action: () =>
|
||||
this.store.logObjectInConsole([
|
||||
...this.props.component.path,
|
||||
{ type: "item", value: "component" },
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "Inspect in Elements tab",
|
||||
show: this.props.component.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("DOM", this.props.component.path),
|
||||
},
|
||||
{
|
||||
title: "Force rerender",
|
||||
show: this.props.component.path.length !== 1,
|
||||
action: () => this.store.refreshComponent(this.props.component.path),
|
||||
},
|
||||
{
|
||||
title: "Store observed states as global variable",
|
||||
show: this.props.component.path.length !== 1,
|
||||
action: () =>
|
||||
this.store.logObjectInConsole([
|
||||
...this.props.component.path,
|
||||
{ type: "item", value: "subscriptions" },
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "Inspect compiled template",
|
||||
show: this.props.component.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("compiled template", this.props.component.path),
|
||||
},
|
||||
{
|
||||
title: "Log raw template",
|
||||
show: this.props.component.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("raw template", this.props.component.path),
|
||||
},
|
||||
{
|
||||
title: "Store as global variable",
|
||||
show: this.props.component.path.length === 1,
|
||||
action: () => this.store.logObjectInConsole([...this.props.component.path]),
|
||||
},
|
||||
{
|
||||
title: "Don't fold component by default",
|
||||
show: this.store.settings.componentsToggleBlacklist.has(this.props.component.name),
|
||||
action: () => this.toggleComponentToBlacklist(),
|
||||
},
|
||||
{
|
||||
title: "Fold component by default",
|
||||
show: !this.store.settings.componentsToggleBlacklist.has(this.props.component.name),
|
||||
action: () => this.toggleComponentToBlacklist(),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
openMenu(ev) {
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
||||
this.store.openContextMenu(ev, this.contextMenuItems);
|
||||
}
|
||||
|
||||
// Expand/fold the component node
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.TreeElement" owl="1">
|
||||
<div t-ref="element"
|
||||
t-att-class="{'component-selected': props.component.selected,'component-highlighted': props.component.highlighted}"
|
||||
class="tree-component m-0 p-0 w-100 text-nowrap user-select-none"
|
||||
t-on-contextmenu.prevent="openMenu"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.component.path)"
|
||||
t-att-class="{'component-selected': props.component.selected,'component-highlighted': props.component.highlighted}"
|
||||
class="tree-component m-0 p-0 w-100 text-nowrap user-select-none"
|
||||
t-on-contextmenu.prevent="openMenu"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.component.path)"
|
||||
t-on-click.stop="toggleComponent"
|
||||
>
|
||||
<div class="component-wrapper" t-attf-style="padding-left: {{componentPadding}}rem">
|
||||
<i class="fa px-1 pointer-icon caret"
|
||||
t-att-class="{'fa-caret-right': !props.component.toggled, 'fa-caret-down': props.component.toggled}"
|
||||
t-att-class="{'fa-caret-right': !props.component.toggled, 'fa-caret-down': props.component.toggled}"
|
||||
t-on-click.stop="toggleDisplay"
|
||||
t-attf-style="{{props.component.children.length > 0 ? '' : 'visibility: hidden;'}}"
|
||||
/>
|
||||
@@ -26,29 +26,6 @@
|
||||
<span t-if="props.component.depth">></span>
|
||||
<span class="version" t-else="">owl=<t t-esc="props.component.version"/></span>
|
||||
</div>
|
||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
||||
<ul class="my-1">
|
||||
<li t-on-click.stop="() => this.store.toggleComponentAndChildren(props.component, true)" class="custom-menu-item py-1 px-4">Expand children</li>
|
||||
<li t-on-click.stop="() => this.store.toggleComponentAndChildren(props.component, false)" class="custom-menu-item py-1 px-4">Fold all children</li>
|
||||
<li t-on-click.stop="() => this.store.foldDirectChildren(props.component)" class="custom-menu-item py-1 px-4">Fold direct children</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('source', props.component.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||
<t t-if="props.component.path.length !== 1">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.component.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||
<li t-on-click.stop="() => this.store.refreshComponent(props.component.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.component.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.component.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.component.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
</t>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
<t t-if="props.component.toggled">
|
||||
<t t-foreach="props.component.children" t-as="child" t-key="child.key">
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
const { Component } = owl;
|
||||
import { ComponentsTab } from "./components_tab/components_tab";
|
||||
import { Tab } from "./tab/tab";
|
||||
import { ProfilerTab } from "./profiler_tab/profiler_tab";
|
||||
import { ContextMenu } from "../context_menu/context_menu";
|
||||
import { useStore } from "../store/store";
|
||||
import { ComponentsTab } from "./components_tab/components_tab";
|
||||
import { ProfilerTab } from "./profiler_tab/profiler_tab";
|
||||
import { Tab } from "./tab/tab";
|
||||
|
||||
export class DevtoolsWindow extends Component {
|
||||
static props = [];
|
||||
static template = "devtools.DevtoolsWindow";
|
||||
static components = { ComponentsTab, Tab, ProfilerTab };
|
||||
static components = { ComponentsTab, Tab, ProfilerTab, ContextMenu };
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
Owl is not loaded on this page.
|
||||
</div>
|
||||
</t>
|
||||
<ContextMenu t-if="store.contextMenu" items="store.contextMenu.items"/>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -1,24 +1,13 @@
|
||||
import { minimizeKey } from "../../../../utils";
|
||||
import { useStore } from "../../../store/store";
|
||||
|
||||
const { Component, useEffect, useRef } = owl;
|
||||
const { Component } = owl;
|
||||
|
||||
export class Event extends Component {
|
||||
static template = "devtools.Event";
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.componentContextMenu = useRef("componentContextmenu");
|
||||
this.componentContextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent,
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.componentContextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.componentContextMenu.el);
|
||||
}
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
}
|
||||
|
||||
// Formatting for displaying the key of the component
|
||||
@@ -54,13 +43,65 @@ export class Event extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
openComponentMenu(ev) {
|
||||
get contextMenuItems() {
|
||||
return [
|
||||
{
|
||||
title: "Inspect source code",
|
||||
show: true,
|
||||
action: () => this.store.inspectComponent("source", this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Store as global variable",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () =>
|
||||
this.store.logObjectInConsole([
|
||||
...this.props.event.path,
|
||||
{ type: "item", value: "component" },
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "Inspect in Elements tab",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("DOM", this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Force rerender",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () => this.store.refreshComponent(this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Store observed states as global variable",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () =>
|
||||
this.store.logObjectInConsole([
|
||||
...this.props.event.path,
|
||||
{ type: "item", value: "subscriptions" },
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "Inspect compiled template",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("compiled template", this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Log raw template",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("raw template", this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Store as global variable",
|
||||
show: this.props.event.path.length === 1,
|
||||
action: () => this.store.logObjectInConsole([...this.props.event.path]),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
openMenu(ev) {
|
||||
if (this.props.event.type === "destroy") {
|
||||
return;
|
||||
} else {
|
||||
ev.preventDefault();
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.componentContextMenuId;
|
||||
this.store.openContextMenu(ev, this.contextMenuItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
t-att-class="{'fa-caret-right': !props.event.toggled, 'fa-caret-down': props.event.toggled}"
|
||||
t-attf-style="visibility: {{props.event.origin ? '' : 'hidden'}};"
|
||||
/>
|
||||
<t t-esc="props.event.type"/>:
|
||||
<t t-esc="props.event.type"/>:
|
||||
<<span style="cursor:pointer; color: var(--component-color);"
|
||||
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||
t-on-contextmenu="openComponentMenu"
|
||||
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||
t-on-contextmenu="openMenu"
|
||||
t-esc="props.event.component"
|
||||
/>
|
||||
<t t-if="minimizedKey.length > 0">
|
||||
@@ -29,10 +29,10 @@
|
||||
<div class="my-0 pt-1 object-line">
|
||||
<i class="fa fa-caret-right mx-1 pe-2" style="visibility: hidden;"></i>
|
||||
<span>
|
||||
origin:
|
||||
<<span style="cursor:pointer; color: var(--component-color);"
|
||||
t-on-click.stop="() => this.store.selectComponent(props.event.origin.path)"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.origin.path)"
|
||||
origin:
|
||||
<<span style="cursor:pointer; color: var(--component-color);"
|
||||
t-on-click.stop="() => this.store.selectComponent(props.event.origin.path)"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.origin.path)"
|
||||
t-esc="props.event.origin.component"
|
||||
/>
|
||||
<t t-if="originMinimizedKey.length > 0">
|
||||
@@ -43,22 +43,6 @@
|
||||
</span>
|
||||
</div>
|
||||
</t>
|
||||
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-ref="componentContextmenu">
|
||||
<ul class="my-1">
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('source', props.event.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||
<t t-if="props.event.path.length !== 1">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.event.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||
<li t-on-click.stop="() => this.store.refreshComponent(props.event.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.event.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.event.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</t>
|
||||
</templates>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { minimizeKey } from "../../../../utils";
|
||||
import { useStore } from "../../../store/store";
|
||||
|
||||
const { Component, useRef, useEffect } = owl;
|
||||
const { Component } = owl;
|
||||
|
||||
export class EventNode extends Component {
|
||||
static template = "devtools.EventNode";
|
||||
@@ -10,33 +10,89 @@ export class EventNode extends Component {
|
||||
|
||||
setup() {
|
||||
this.store = useStore();
|
||||
this.nodeContextMenu = useRef("nodeContextMenu");
|
||||
this.nodeContextMenuId = this.store.contextMenu.id++;
|
||||
this.componentContextMenu = useRef("componentContextmenu");
|
||||
this.componentContextMenuId = this.store.contextMenu.id++;
|
||||
this.contextMenuEvent,
|
||||
useEffect(
|
||||
(menuId) => {
|
||||
if (menuId === this.nodeContextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.nodeContextMenu.el);
|
||||
}
|
||||
if (menuId === this.componentContextMenuId) {
|
||||
this.store.contextMenu.open(this.contextMenuEvent, this.componentContextMenu.el);
|
||||
}
|
||||
},
|
||||
() => [this.store.contextMenu.activeMenu]
|
||||
);
|
||||
}
|
||||
|
||||
get eventPadding() {
|
||||
return this.props.event.depth * 0.8 + 0.3;
|
||||
}
|
||||
|
||||
get nodeContextMenuItems() {
|
||||
return [
|
||||
{
|
||||
title: "Expand children",
|
||||
show: true,
|
||||
action: () => this.store.toggleEventAndChildren(this.props.event, true),
|
||||
},
|
||||
{
|
||||
title: "Fold all children",
|
||||
show: true,
|
||||
action: () => this.store.toggleEventAndChildren(this.props.event, false),
|
||||
},
|
||||
{
|
||||
title: "Fold direct children",
|
||||
show: true,
|
||||
action: () => this.store.foldDirectChildren(this.props.event),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
get componentContextMenuItems() {
|
||||
return [
|
||||
{
|
||||
title: "Inspect source code",
|
||||
show: true,
|
||||
action: () => this.store.inspectComponent("source", this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Store as global variable",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () =>
|
||||
this.store.logObjectInConsole([
|
||||
...this.props.event.path,
|
||||
{ type: "item", value: "component" },
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "Inspect in Elements tab",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("DOM", this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Force rerender",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () => this.store.refreshComponent(this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Store observed states as global variable",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () =>
|
||||
this.store.logObjectInConsole([
|
||||
...this.props.event.path,
|
||||
{ type: "item", value: "subscriptions" },
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: "Inspect compiled template",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("compiled template", this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Log raw template",
|
||||
show: this.props.event.path.length !== 1,
|
||||
action: () => this.store.inspectComponent("raw template", this.props.event.path),
|
||||
},
|
||||
{
|
||||
title: "Store as global variable",
|
||||
show: this.props.event.path.length === 1,
|
||||
action: () => this.store.logObjectInConsole([...this.props.event.path]),
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
openNodeMenu(ev) {
|
||||
if (this.props.event.children.length) {
|
||||
ev.preventDefault();
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.nodeContextMenuId;
|
||||
this.store.openContextMenu(ev, this.nodeContextMenuItems);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,8 +101,7 @@ export class EventNode extends Component {
|
||||
return;
|
||||
} else {
|
||||
ev.preventDefault();
|
||||
this.contextMenuEvent = ev;
|
||||
this.store.contextMenu.activeMenu = this.componentContextMenuId;
|
||||
this.store.openContextMenu(ev, this.componentContextMenuItems);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<templates xml:space="preserve">
|
||||
<t t-name="devtools.EventNode" owl="1">
|
||||
<div class="my-0 p-0 object-line"
|
||||
t-on-click.stop="toggleDisplay"
|
||||
t-on-contextmenu="openNodeMenu"
|
||||
t-on-click.stop="toggleDisplay"
|
||||
t-on-contextmenu="openNodeMenu"
|
||||
>
|
||||
<div class="text-nowrap" t-attf-style="padding-left: {{eventPadding}}rem">
|
||||
<i class="fa px-1 pointer-icon caret"
|
||||
@@ -11,11 +11,11 @@
|
||||
t-attf-style="visibility: {{props.event.children.length > 0 ? '' : 'hidden'}};"
|
||||
/>
|
||||
<span>
|
||||
<t t-esc="props.event.type"/>:
|
||||
<<span style="cursor:pointer; color: var(--component-color);"
|
||||
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||
t-on-contextmenu.stop="openComponentMenu"
|
||||
<t t-esc="props.event.type"/>:
|
||||
<<span style="cursor:pointer; color: var(--component-color);"
|
||||
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||
t-on-contextmenu.stop="openComponentMenu"
|
||||
t-esc="props.event.component"/>
|
||||
<t t-if="minimizedKey.length > 0">
|
||||
<span t-if="minimizedKey.length > 0" style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||
@@ -28,29 +28,6 @@
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div t-if="store.contextMenu.activeMenu === nodeContextMenuId" class="custom-menu" t-ref="nodeContextMenu">
|
||||
<ul class="my-1">
|
||||
<li t-on-click.stop="() => this.store.toggleEventAndChildren(props.event, true)" class="custom-menu-item py-1 px-4">Expand children</li>
|
||||
<li t-on-click.stop="() => this.store.toggleEventAndChildren(props.event, false)" class="custom-menu-item py-1 px-4">Fold all children</li>
|
||||
<li t-on-click.stop="() => this.store.foldDirectChildren(props.event)" class="custom-menu-item py-1 px-4">Fold direct children</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div t-if="store.contextMenu.activeMenu === componentContextMenuId" class="custom-menu" t-ref="componentContextmenu">
|
||||
<ul class="my-1">
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('source', props.event.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
||||
<t t-if="props.event.path.length !== 1">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'component'}])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('DOM', props.event.path)" class="custom-menu-item py-1 px-4">Inspect in Elements tab</li>
|
||||
<li t-on-click.stop="() => this.store.refreshComponent(props.event.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path, { type: 'item', value: 'subscriptions'}])" class="custom-menu-item py-1 px-4">Store observed states as global variable</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('compiled template', props.event.path)" class="custom-menu-item py-1 px-4">Inspect compiled template</li>
|
||||
<li t-on-click.stop="() => this.store.inspectComponent('raw template', props.event.path)" class="custom-menu-item py-1 px-4">Log raw template</li>
|
||||
</t>
|
||||
<t t-else="">
|
||||
<li t-on-click.stop="() => this.store.logObjectInConsole([...props.event.path])" class="custom-menu-item py-1 px-4">Store as global variable</li>
|
||||
</t>
|
||||
</ul>
|
||||
</div>
|
||||
<t t-if="props.event.toggled">
|
||||
<t t-foreach="props.event.children" t-as="child" t-key="child.id">
|
||||
<EventNode event="child"/>
|
||||
|
||||
@@ -11,30 +11,7 @@ export const store = reactive({
|
||||
darkmode: false,
|
||||
componentsToggleBlacklist: new Set(),
|
||||
},
|
||||
contextMenu: {
|
||||
id: 0,
|
||||
activeMenu: -1,
|
||||
// Opens the context menu corresponding with the given menu html element
|
||||
open(event, menu) {
|
||||
const menuWidth = menu.offsetWidth;
|
||||
const menuHeight = menu.offsetHeight;
|
||||
let x = event.clientX;
|
||||
let y = event.clientY;
|
||||
if (x + menuWidth > window.innerWidth) {
|
||||
x = window.innerWidth - menuWidth;
|
||||
}
|
||||
if (y + menuHeight > window.innerHeight) {
|
||||
y = window.innerHeight - menuHeight;
|
||||
}
|
||||
menu.style.left = x + "px";
|
||||
// Need 25px offset because of the main navbar from the browser devtools
|
||||
menu.style.top = y - 25 + "px";
|
||||
},
|
||||
// Close the currently displayed context menu
|
||||
close() {
|
||||
this.activeMenu = -1;
|
||||
},
|
||||
},
|
||||
contextMenu: null,
|
||||
isFirefox: IS_FIREFOX,
|
||||
frameUrls: ["top"],
|
||||
activeFrame: "top",
|
||||
@@ -56,9 +33,11 @@ export const store = reactive({
|
||||
props: { toggled: true, children: [] },
|
||||
env: { toggled: false, children: [] },
|
||||
instance: { toggled: true, children: [] },
|
||||
hooks: { toggled: true, children: [] },
|
||||
version: "1.0",
|
||||
},
|
||||
selectedElement: null,
|
||||
observedVariables: [],
|
||||
componentSearch: {
|
||||
search: "",
|
||||
searchResults: [],
|
||||
@@ -93,6 +72,16 @@ export const store = reactive({
|
||||
evalFunctionInWindow("disableHTMLSelector", [], this.activeFrame);
|
||||
},
|
||||
|
||||
openContextMenu(event, items) {
|
||||
this.contextMenu = {
|
||||
position: {
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
},
|
||||
items,
|
||||
};
|
||||
},
|
||||
|
||||
// Load all data related to the components tree using the global hook loaded on the page
|
||||
// Use fromOld to specify if we want to keep most of the toggled/selected data of the old tree
|
||||
// when generating the new one
|
||||
@@ -577,6 +566,42 @@ export const store = reactive({
|
||||
}
|
||||
},
|
||||
|
||||
async injectBreakpoint(hook, path, instanceOnly = false, condition = "1") {
|
||||
path = [...path];
|
||||
await evalFunctionInWindow(
|
||||
"injectBreakpoint",
|
||||
[hook, path, instanceOnly, condition],
|
||||
this.activeFrame
|
||||
);
|
||||
await this.loadComponentsTree(true);
|
||||
},
|
||||
|
||||
async removeBreakpoints() {
|
||||
await evalFunctionInWindow("removeBreakpoints", [], this.activeFrame);
|
||||
await this.loadComponentsTree(true);
|
||||
},
|
||||
|
||||
async observeVariable(path) {
|
||||
this.observedVariables.push({ path: [...path], visible: false });
|
||||
this.observedVariables = await evalFunctionInWindow(
|
||||
"getObservedVariables",
|
||||
[store.observedVariables],
|
||||
store.activeFrame
|
||||
);
|
||||
await browserInstance.storage.local.set({
|
||||
observedVariables: toRaw(this.observedVariables).map((o) => o.path),
|
||||
});
|
||||
},
|
||||
|
||||
clearObservedVariable(index) {
|
||||
if (index !== undefined) {
|
||||
this.observedVariables.splice(index, 1);
|
||||
} else {
|
||||
this.observedVariables = [];
|
||||
}
|
||||
browserInstance.storage.local.set({ observedVariables: toRaw(this.observedVariables) });
|
||||
},
|
||||
|
||||
// Trigger the highlight on the component in the page
|
||||
highlightComponent(path) {
|
||||
evalFunctionInWindow("highlightComponent", [path], this.activeFrame);
|
||||
@@ -646,8 +671,9 @@ async function init() {
|
||||
store.updateIFrameList();
|
||||
|
||||
// Global listeners to close the currently shown context menu when the user clicks or opens another
|
||||
document.addEventListener("click", () => store.contextMenu.close(), { capture: true });
|
||||
document.addEventListener("contextmenu", () => store.contextMenu.close(), { capture: true });
|
||||
document.addEventListener("click", () => (store.contextMenu = null), { capture: true });
|
||||
document.addEventListener("contextmenu", () => (store.contextMenu = null), { capture: true });
|
||||
window.addEventListener("blur", () => (store.contextMenu = null), { capture: true });
|
||||
|
||||
// Make sure the events recorder is at its initial state in every frame
|
||||
for (const frame of store.frameUrls) {
|
||||
@@ -666,6 +692,16 @@ async function init() {
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
// Refresh observed variables values every 200 ms
|
||||
setInterval(async () => {
|
||||
if (store.owlStatus) {
|
||||
store.observedVariables = await evalFunctionInWindow(
|
||||
"getObservedVariables",
|
||||
[[...store.observedVariables]],
|
||||
store.activeFrame
|
||||
);
|
||||
}
|
||||
}, 200);
|
||||
}
|
||||
|
||||
let rootRendersTimeout = false;
|
||||
@@ -760,6 +796,16 @@ async function loadSettings() {
|
||||
storage.owlDevtoolsComponentsToggleBlacklist
|
||||
);
|
||||
}
|
||||
// Observed variables
|
||||
if (storage.observedVariables) {
|
||||
store.observedVariables = [];
|
||||
for (const path of storage.observedVariables) {
|
||||
store.observedVariables.push({
|
||||
path: [...path],
|
||||
visible: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Function to handle and store a batch of events coming from the page
|
||||
|
||||
@@ -51,6 +51,8 @@
|
||||
this.requestedFrame = false;
|
||||
this.enabledSelector = false;
|
||||
this.eventsBatch = [];
|
||||
this.breakpointsClassMap = new Map();
|
||||
this.breakpointsHookMap = new Map();
|
||||
// Object which defines how different types of data should be displayed when passed to the devtools
|
||||
this.serializer = {
|
||||
// Defines how leaf object nodes should be displayed in the extension when inside a bigger structure
|
||||
@@ -58,15 +60,16 @@
|
||||
// The asConstructorName parameter can be passed to change the display of objects and functions to
|
||||
// be their constructor name (useful for prototype, map and set display)
|
||||
serializeItem(value, asConstructorName = false) {
|
||||
if (typeof value === "array") {
|
||||
return "Array(" + value.length + ")";
|
||||
} else if (typeof value === "object") {
|
||||
if (typeof value === "object") {
|
||||
if (value == null) {
|
||||
return "null";
|
||||
}
|
||||
if (asConstructorName) {
|
||||
return value.constructor.name;
|
||||
}
|
||||
if (value instanceof Array) {
|
||||
return "Array(" + value.length + ")";
|
||||
}
|
||||
return "{...}";
|
||||
} else if (typeof value === "undefined") {
|
||||
return "undefined";
|
||||
@@ -116,12 +119,17 @@
|
||||
} else if (obj instanceof Number) {
|
||||
result[0] = obj.toString();
|
||||
} else {
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
for (const key of Reflect.ownKeys(obj)) {
|
||||
if (length > 25) {
|
||||
result.push("...");
|
||||
break;
|
||||
}
|
||||
const element = key + ": " + this.serializeItem(value);
|
||||
let element;
|
||||
if (Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")) {
|
||||
element = key.toString() + ": (...)";
|
||||
} else {
|
||||
element = key.toString() + ": " + this.serializeItem(obj[key]);
|
||||
}
|
||||
length += element.length;
|
||||
result.push(element);
|
||||
}
|
||||
@@ -135,7 +143,7 @@
|
||||
result.push(element);
|
||||
}
|
||||
}
|
||||
if (obj.constructor.name !== "Object") {
|
||||
if (obj.constructor && obj.constructor.name !== "Object") {
|
||||
return obj.constructor.name + " {" + result.join(", ") + "}";
|
||||
}
|
||||
return "{" + result.join(", ") + "}";
|
||||
@@ -695,7 +703,7 @@
|
||||
try {
|
||||
obj = Object.getOwnPropertyDescriptor(obj, key.value).get.call(obj);
|
||||
} catch (e) {
|
||||
obj = e.toString();
|
||||
obj = "Exception: " + e.toString();
|
||||
}
|
||||
break;
|
||||
case "prototype getter":
|
||||
@@ -713,13 +721,13 @@
|
||||
try {
|
||||
obj = Object.getOwnPropertyDescriptor(obj, key.value).get;
|
||||
} catch (e) {
|
||||
obj = e.toString();
|
||||
obj = "Exception: " + e.toString();
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
obj = obj[key.value];
|
||||
} catch (e) {
|
||||
obj = e.toString();
|
||||
obj = "Exception: " + e.toString();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -732,15 +740,20 @@
|
||||
|
||||
// Returns the asked property given its global path
|
||||
getObjectProperty(path) {
|
||||
// Just return the corresponding app if path is of length 1
|
||||
if (path.length === 1) {
|
||||
// Just return the corresponding app if path is of length 1 and not simplified
|
||||
if (path.length === 1 && !isNaN(path[0])) {
|
||||
return [...this.apps][path[0]];
|
||||
}
|
||||
// Path to the component node is only strings, becomes objects for properties
|
||||
const index = path.findIndex((key) => typeof key !== "string");
|
||||
const componentNode = this.getComponentNode(path.slice(0, index));
|
||||
const obj = this.getObject(componentNode, path.slice(index));
|
||||
return obj;
|
||||
if (index === -1) {
|
||||
return this.getComponentNode(path);
|
||||
}
|
||||
const componentNode = this.getComponentNode(index === 1 ? [path[0]] : path.slice(0, index));
|
||||
if (componentNode) {
|
||||
return this.getObject(componentNode, path.slice(index));
|
||||
}
|
||||
return "Exception: component not found";
|
||||
}
|
||||
|
||||
// Returns a modified version of an object node that has compatible format with the devtools ObjectTreeElement component
|
||||
@@ -788,6 +801,11 @@
|
||||
child.name = "value";
|
||||
obj = parentObj[1];
|
||||
break;
|
||||
case "getter":
|
||||
child.name = key.value;
|
||||
obj = parentObj[key.value];
|
||||
break;
|
||||
case "prototype getter":
|
||||
case "set entry":
|
||||
case "map entry":
|
||||
case "item":
|
||||
@@ -803,6 +821,8 @@
|
||||
parentObj
|
||||
).findIndex((sym) => sym === key.value);
|
||||
child.path[child.path.length - 1].value = key.value.toString();
|
||||
} else if (key.hasOwnProperty("symbolIndex")) {
|
||||
obj = parentObj[Object.getOwnPropertySymbols(parentObj)[key.symbolIndex]];
|
||||
} else {
|
||||
obj = parentObj[key.value];
|
||||
}
|
||||
@@ -834,7 +854,7 @@
|
||||
child.contentType = "set";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj.constructor.name === "Array":
|
||||
case obj.constructor?.name === "Array":
|
||||
child.contentType = "array";
|
||||
child.hasChildren = obj.length > 0;
|
||||
break;
|
||||
@@ -842,11 +862,11 @@
|
||||
child.contentType = "function";
|
||||
child.hasChildren = true;
|
||||
break;
|
||||
case obj instanceof Object:
|
||||
case typeof obj === "object":
|
||||
child.contentType = "object";
|
||||
child.hasChildren =
|
||||
Object.keys(obj).length ||
|
||||
Object.getOwnPropertySymbols(obj).length ||
|
||||
Object.keys(obj).length > 0 ||
|
||||
Object.getOwnPropertySymbols(obj).length > 0 ||
|
||||
obj.constructor.name !== "Object";
|
||||
break;
|
||||
default:
|
||||
@@ -890,12 +910,15 @@
|
||||
path.shift();
|
||||
}
|
||||
// the value is either "props" or "env" here
|
||||
if (objType !== "instance") {
|
||||
if (objType !== "instance" && objType !== "hook") {
|
||||
obj = oldTree[path[0].value].children;
|
||||
path.shift();
|
||||
// there is nothing otherwise but extension side it is in instance
|
||||
} else {
|
||||
} else if (objType === "instance") {
|
||||
obj = oldTree.instance.children;
|
||||
} else {
|
||||
obj = oldTree.hooks.children;
|
||||
path.shift();
|
||||
}
|
||||
// the first element here is directly in an array instead of a children array
|
||||
obj = obj[path[0].childIndex];
|
||||
@@ -1026,36 +1049,40 @@
|
||||
break;
|
||||
case "object":
|
||||
case "function":
|
||||
Reflect.ownKeys(obj).forEach((key) => {
|
||||
if (
|
||||
key !== "__proto__" &&
|
||||
Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
|
||||
) {
|
||||
let child = {
|
||||
name: key,
|
||||
depth: depth,
|
||||
toggled: false,
|
||||
objectType: objType,
|
||||
path: [...path, { type: "getter", value: key, childIndex: children.length }],
|
||||
contentType: "getter",
|
||||
content: "(...)",
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
};
|
||||
children.push(child);
|
||||
}
|
||||
const child = this.serializeObjectChild(
|
||||
obj,
|
||||
{ type: "item", value: key, childIndex: children.length },
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) children.push(child);
|
||||
index++;
|
||||
});
|
||||
Reflect.ownKeys(obj)
|
||||
.sort(compareKeys)
|
||||
.forEach((key) => {
|
||||
if (
|
||||
key !== "__proto__" &&
|
||||
Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
|
||||
) {
|
||||
let child = {
|
||||
name: key,
|
||||
depth: depth,
|
||||
toggled: false,
|
||||
objectType: objType,
|
||||
path: [...path, { type: "getter", value: key, childIndex: children.length }],
|
||||
contentType: "getter",
|
||||
content: "(...)",
|
||||
hasChildren: false,
|
||||
children: [],
|
||||
};
|
||||
children.push(child);
|
||||
}
|
||||
const child = this.serializeObjectChild(
|
||||
obj,
|
||||
{ type: "item", value: key, childIndex: children.length },
|
||||
depth,
|
||||
objType,
|
||||
path,
|
||||
oldBranch?.children[index],
|
||||
oldTree
|
||||
);
|
||||
if (child) {
|
||||
children.push(child);
|
||||
}
|
||||
index++;
|
||||
});
|
||||
}
|
||||
let proto = Object.getPrototypeOf(obj);
|
||||
while (proto) {
|
||||
@@ -1099,25 +1126,44 @@
|
||||
}
|
||||
// Returns the Component node given its path and the root component node
|
||||
getComponentNode(path) {
|
||||
// The node is an app and not a component
|
||||
if (path.length === 1) {
|
||||
// All paths that consists in an array containing a single stringified number lead to an app
|
||||
if (path.length === 1 && !isNaN(path[0])) {
|
||||
return [...this.apps][path[0]];
|
||||
}
|
||||
// The second element in the path will always be the root of the app
|
||||
let node = [...this.apps][path[0]]?.root;
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 2; i < path.length; i++) {
|
||||
// From this point onwards, it is an object path inside the component node
|
||||
if (typeof path[i] !== "string") {
|
||||
break;
|
||||
}
|
||||
if (node.children.hasOwnProperty(path[i])) {
|
||||
node = node.children[path[i]];
|
||||
} else {
|
||||
let node;
|
||||
// If the path is longer and its first item is indeed an app number, it is a regular path
|
||||
if (!isNaN(path[0])) {
|
||||
// The second element in the path will always be the root of the app
|
||||
node = [...this.apps][path[0]]?.root;
|
||||
if (!node) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 2; i < path.length; i++) {
|
||||
// From this point onwards, it is an object path inside the component node
|
||||
if (typeof path[i] !== "string") {
|
||||
break;
|
||||
}
|
||||
if (node.children.hasOwnProperty(path[i])) {
|
||||
node = node.children[path[i]];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
// If the first path item is a more complex string, it is a simplified path where elements
|
||||
// are a series of component names and indexes separated by slashes
|
||||
} else {
|
||||
const simplifiedPathArray = path[0].split("/");
|
||||
node = [...this.apps][simplifiedPathArray[0]]?.root;
|
||||
if (node.name !== simplifiedPathArray[1]) {
|
||||
return null;
|
||||
}
|
||||
for (let i = 2; i < simplifiedPathArray.length; i += 2) {
|
||||
const key = Reflect.ownKeys(node.children)[simplifiedPathArray[i]];
|
||||
node = node.children[key];
|
||||
if (node.name !== simplifiedPathArray[i + 1]) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
return node;
|
||||
}
|
||||
@@ -1155,42 +1201,46 @@
|
||||
const propsPath = isApp
|
||||
? [...path, { type: "item", value: "props" }]
|
||||
: [...path, { type: "item", value: "component" }, { type: "item", value: "props" }];
|
||||
Reflect.ownKeys(props).forEach((key) => {
|
||||
let oldBranch = oldTree?.props.children[component.props.children.length];
|
||||
const property = this.serializeObjectChild(
|
||||
props,
|
||||
{ type: "item", value: key, childIndex: component.props.children.length },
|
||||
0,
|
||||
"props",
|
||||
propsPath,
|
||||
oldBranch,
|
||||
oldTree
|
||||
);
|
||||
if (property) {
|
||||
component.props.children.push(property);
|
||||
}
|
||||
});
|
||||
Reflect.ownKeys(props)
|
||||
.sort(compareKeys)
|
||||
.forEach((key) => {
|
||||
let oldBranch = oldTree?.props.children[component.props.children.length];
|
||||
const property = this.serializeObjectChild(
|
||||
props,
|
||||
{ type: "item", value: key, childIndex: component.props.children.length },
|
||||
0,
|
||||
"props",
|
||||
propsPath,
|
||||
oldBranch,
|
||||
oldTree
|
||||
);
|
||||
if (property) {
|
||||
component.props.children.push(property);
|
||||
}
|
||||
});
|
||||
// Load env of the component
|
||||
const env = isApp ? node.env : node.component.env;
|
||||
component.env = { toggled: oldTree ? oldTree.env.toggled : false, children: [] };
|
||||
const envPath = isApp
|
||||
? [...path, { type: "item", value: "env" }]
|
||||
: [...path, { type: "item", value: "component" }, { type: "item", value: "env" }];
|
||||
Reflect.ownKeys(env).forEach((key) => {
|
||||
let oldBranch = oldTree?.env.children[component.env.children.length];
|
||||
const envElement = this.serializeObjectChild(
|
||||
env,
|
||||
{ type: "item", value: key, childIndex: component.env.children.length },
|
||||
0,
|
||||
"env",
|
||||
envPath,
|
||||
oldBranch,
|
||||
oldTree
|
||||
);
|
||||
if (envElement) {
|
||||
component.env.children.push(envElement);
|
||||
}
|
||||
});
|
||||
Reflect.ownKeys(env)
|
||||
.sort(compareKeys)
|
||||
.forEach((key) => {
|
||||
let oldBranch = oldTree?.env.children[component.env.children.length];
|
||||
const envElement = this.serializeObjectChild(
|
||||
env,
|
||||
{ type: "item", value: key, childIndex: component.env.children.length },
|
||||
0,
|
||||
"env",
|
||||
envPath,
|
||||
oldBranch,
|
||||
oldTree
|
||||
);
|
||||
if (envElement) {
|
||||
component.env.children.push(envElement);
|
||||
}
|
||||
});
|
||||
// Load env getters
|
||||
let obj = Object.getPrototypeOf(env);
|
||||
Reflect.ownKeys(obj).forEach((key) => {
|
||||
@@ -1229,23 +1279,25 @@
|
||||
const instance = isApp ? node : node.component;
|
||||
component.instance = { toggled: oldTree ? oldTree.instance.toggled : true, children: [] };
|
||||
const instancePath = isApp ? path : [...path, { type: "item", value: "component" }];
|
||||
Reflect.ownKeys(instance).forEach((key) => {
|
||||
if (!["env", "props"].includes(key)) {
|
||||
let oldBranch = oldTree?.instance.children[component.instance.children.length];
|
||||
const instanceElement = this.serializeObjectChild(
|
||||
instance,
|
||||
{ type: "item", value: key, childIndex: component.instance.children.length },
|
||||
0,
|
||||
"instance",
|
||||
instancePath,
|
||||
oldBranch,
|
||||
oldTree
|
||||
);
|
||||
if (instanceElement) {
|
||||
component.instance.children.push(instanceElement);
|
||||
Reflect.ownKeys(instance)
|
||||
.sort(compareKeys)
|
||||
.forEach((key) => {
|
||||
if (!["env", "props"].includes(key)) {
|
||||
let oldBranch = oldTree?.instance.children[component.instance.children.length];
|
||||
const instanceElement = this.serializeObjectChild(
|
||||
instance,
|
||||
{ type: "item", value: key, childIndex: component.instance.children.length },
|
||||
0,
|
||||
"instance",
|
||||
instancePath,
|
||||
oldBranch,
|
||||
oldTree
|
||||
);
|
||||
if (instanceElement) {
|
||||
component.instance.children.push(instanceElement);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
// Load instance getters
|
||||
obj = Object.getPrototypeOf(instance);
|
||||
while (obj) {
|
||||
@@ -1361,6 +1413,39 @@
|
||||
component.subscriptions.children.push(subscription);
|
||||
});
|
||||
}
|
||||
// Load hooks of the component
|
||||
if (!isApp) {
|
||||
component.hooks = { toggled: oldTree ? oldTree.hooks.toggled : true, children: [] };
|
||||
const hooksList = [
|
||||
"mounted",
|
||||
"patched",
|
||||
"willDestroy",
|
||||
"willPatch",
|
||||
"willStart",
|
||||
"willUnmount",
|
||||
"willUpdateProps",
|
||||
];
|
||||
const hooksPath = [...instancePath, { type: "item", value: "__owl__" }];
|
||||
Reflect.ownKeys(instance.__owl__)
|
||||
.sort(compareKeys)
|
||||
.forEach((key) => {
|
||||
if (hooksList.includes(key)) {
|
||||
let oldBranch = oldTree?.hooks.children[component.hooks.children.length];
|
||||
const property = this.serializeObjectChild(
|
||||
instance.__owl__,
|
||||
{ type: "item", value: key, childIndex: component.hooks.children.length },
|
||||
0,
|
||||
"hook",
|
||||
hooksPath,
|
||||
oldBranch,
|
||||
oldTree
|
||||
);
|
||||
if (property) {
|
||||
component.hooks.children.push(property);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return component;
|
||||
}
|
||||
// Replace the content of a parsed getter object with the result of the corresponding get method
|
||||
@@ -1635,6 +1720,37 @@
|
||||
}
|
||||
return children;
|
||||
}
|
||||
getObservedVariables(current) {
|
||||
const res = [...current];
|
||||
for (let i = 0; i < current.length; i++) {
|
||||
const path = current[i].path;
|
||||
const parent = this.getObjectProperty(path.slice(0, path.length - 1));
|
||||
if (parent && !(typeof parent === "string" && parent.startsWith("Exception: "))) {
|
||||
const result = this.serializeObjectChild(
|
||||
parent,
|
||||
path.at(-1),
|
||||
0,
|
||||
"observed",
|
||||
path.slice(0, path.length - 1),
|
||||
{},
|
||||
{}
|
||||
);
|
||||
result.hasChildren = false;
|
||||
result.visible = true;
|
||||
const index = path.findIndex((key) => typeof key !== "string");
|
||||
if (index > 1) {
|
||||
const componentNode = this.getComponentNode(path.slice(0, index));
|
||||
result.path = [this.getComponentSimplifiedPath(componentNode)].concat(
|
||||
path.slice(index)
|
||||
);
|
||||
}
|
||||
res[i] = result;
|
||||
} else {
|
||||
res[i].visible = false;
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
// Returns the path of the given component node
|
||||
getComponentPath(componentNode) {
|
||||
let path = [];
|
||||
@@ -1651,6 +1767,23 @@
|
||||
path.unshift(index.toString());
|
||||
return path;
|
||||
}
|
||||
// Returns the simplified path of the given component node (using component names and indexes)
|
||||
getComponentSimplifiedPath(componentNode) {
|
||||
let path = componentNode.name;
|
||||
if (componentNode.parentKey) {
|
||||
while (componentNode.parent) {
|
||||
const previousKey = componentNode.parentKey;
|
||||
componentNode = componentNode.parent;
|
||||
path = `${componentNode.name}/${Reflect.ownKeys(componentNode.children).indexOf(
|
||||
previousKey
|
||||
)}/${path}`;
|
||||
}
|
||||
}
|
||||
const appsArray = [...this.apps];
|
||||
let index = appsArray.findIndex((app) => app === componentNode.app);
|
||||
path = index.toString() + (path.length ? `/${path}` : "");
|
||||
return path;
|
||||
}
|
||||
// Store the object into a temp window variable and log it to the console
|
||||
sendObjectToConsole(path) {
|
||||
const obj = this.getObjectProperty(path);
|
||||
@@ -1698,6 +1831,48 @@
|
||||
}
|
||||
}
|
||||
|
||||
injectBreakpoint(hook, path, instanceOnly, condition) {
|
||||
const componentNode = this.getObjectProperty(path);
|
||||
const injectFunctionInHook = (comp, hook, fn) => {
|
||||
comp[hook].push(fn);
|
||||
};
|
||||
const originalHook = [...componentNode.component.__owl__[hook]];
|
||||
injectFunctionInHook(componentNode.component.__owl__, hook, () => {
|
||||
debugger;
|
||||
});
|
||||
if (!this.breakpointsHookMap.get([componentNode.component.__owl__, hook])) {
|
||||
this.breakpointsHookMap.set([componentNode.component.__owl__, hook], originalHook);
|
||||
}
|
||||
if (!instanceOnly) {
|
||||
const componentClass = componentNode.component.constructor;
|
||||
const originalSetup = componentClass.prototype.setup;
|
||||
if (!this.breakpointsClassMap.get(componentClass)) {
|
||||
this.breakpointsClassMap.set(componentClass, originalSetup);
|
||||
}
|
||||
componentClass.prototype.setup = function () {
|
||||
const debuggerFunc = () => {
|
||||
if (eval(condition)) {
|
||||
this;
|
||||
debugger;
|
||||
}
|
||||
};
|
||||
injectFunctionInHook(this.__owl__, hook, debuggerFunc);
|
||||
originalSetup.call(this, ...arguments);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
removeBreakpoints() {
|
||||
for (const [component, setup] of this.breakpointsClassMap) {
|
||||
component.prototype.setup = setup;
|
||||
}
|
||||
this.breakpointsClassMap.clear();
|
||||
for (const [ref, originalHook] of this.breakpointsHookMap) {
|
||||
ref[0][ref[1]] = originalHook;
|
||||
}
|
||||
this.breakpointsHookMap.clear();
|
||||
}
|
||||
|
||||
targetName(target, node) {
|
||||
// check on component
|
||||
const { component } = node;
|
||||
@@ -1747,6 +1922,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
function compareKeys(a, b) {
|
||||
const isSymbolA = typeof a === "symbol";
|
||||
const isSymbolB = typeof b === "symbol";
|
||||
|
||||
if (isSymbolA && !isSymbolB) {
|
||||
return 1; // Place Symbols at the end
|
||||
} else if (!isSymbolA && isSymbolB) {
|
||||
return -1; // Place non-Symbols at the beginning
|
||||
} else {
|
||||
return String(a).localeCompare(String(b), undefined, { numeric: true }); // Sort other keys alphabetically
|
||||
}
|
||||
}
|
||||
|
||||
function checkOwlStatus() {
|
||||
let owlStatus = 2;
|
||||
if (!window.__OWL__DEVTOOLS_GLOBAL_HOOK__) {
|
||||
|
||||