Compare commits
44 Commits
v2.5.1
..
signal-nby
| Author | SHA1 | Date | |
|---|---|---|---|
| d0cdc489b1 | |||
| a309ec498b | |||
| 1ddf2ceff9 | |||
| 38574a1f3a | |||
| 6f2600ba5f | |||
| ed9db6ea2a | |||
| 0c1f9f3ffa | |||
| 4385969e2e | |||
| e31d195bb7 | |||
| 836fdada7f | |||
| 22580661c4 | |||
| adb9d405bb | |||
| 8d12bf17dc | |||
| c74c66ab3b | |||
| 5187f01c44 | |||
| 521111644c | |||
| c2728c9daf | |||
| 871dad6a13 | |||
| b620502a0f | |||
| 89cb00cc83 | |||
| 56041bc133 | |||
| e788e361c7 | |||
| 9d378b0e7b | |||
| fd3c194525 | |||
| ac9ccb81ca | |||
| 2b5cea944b | |||
| cf8039f643 | |||
| aec2373e6d | |||
| a9be149e1e | |||
| b365ea5c9c | |||
| b31fa81083 | |||
| 9b656fd9e4 | |||
| 17f4823b13 | |||
| d3bc101177 | |||
| 4287beae19 | |||
| a073c68685 | |||
| 7078d64049 | |||
| ccd31f12d9 | |||
| aff4019cde | |||
| 5ecb4809ff | |||
| c2daecc07b | |||
| fcda17c8e9 | |||
| aeed79c7e5 | |||
| 1c5b6f2573 |
@@ -320,6 +320,28 @@ class ComponentB extends owl.Component {
|
|||||||
|
|
||||||
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
Note: the props validation code is done by using the [validate utility function](utils.md#validate).
|
||||||
|
|
||||||
|
### `slots` prop
|
||||||
|
|
||||||
|
If a component that uses [slots](slots.md) also lists or validates its props, then
|
||||||
|
you will have to explicitely allow the `slots` prop (with an `Object` type), or
|
||||||
|
allow extra props using the `*` notation mentioned above. This is because slots
|
||||||
|
are provided to a component [as props](slots.md#slots-and-props).
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```js
|
||||||
|
class MyComponent extends Component {
|
||||||
|
static props = [someProp, slots?];
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyComponentWithValidation extends Component {
|
||||||
|
static props = {
|
||||||
|
someProp: {type: Number, optional: true},
|
||||||
|
slots : {type: Object, optional: true},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Good Practices
|
## Good Practices
|
||||||
|
|
||||||
A `props` object is a collection of values that come from the parent. As such,
|
A `props` object is a collection of values that come from the parent. As such,
|
||||||
|
|||||||
@@ -56,17 +56,19 @@ extensions.
|
|||||||
|
|
||||||
For reference, here is a list of all standard QWeb directives:
|
For reference, here is a list of all standard QWeb directives:
|
||||||
|
|
||||||
| Name | Description |
|
| Name | Description |
|
||||||
| ------------------------------ | --------------------------------------------------------------- |
|
| ------------------------------ | ----------------------------------------------------------------------- |
|
||||||
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
| `t-esc` | [Outputting safely a value](#outputting-data) |
|
||||||
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
| `t-out` | [Outputting value, possibly without escaping](#outputting-data) |
|
||||||
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
| `t-set`, `t-value` | [Setting variables](#setting-variables) |
|
||||||
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
| `t-if`, `t-elif`, `t-else`, | [conditionally rendering](#conditionals) |
|
||||||
| `t-foreach`, `t-as` | [Loops](#loops) |
|
| `t-foreach`, `t-as` | [Loops](#loops) |
|
||||||
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
| `t-att`, `t-attf-*`, `t-att-*` | [Dynamic attributes](#dynamic-attributes) |
|
||||||
| `t-call` | [Rendering sub templates](#sub-templates) |
|
| `t-call` | [Rendering sub templates](#sub-templates) |
|
||||||
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
| `t-debug`, `t-log` | [Debugging](#debugging) |
|
||||||
| `t-translation` | [Disabling the translation of a node](translations.md) |
|
| `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
|
The component system in Owl requires additional directives, to express various
|
||||||
needs. Here is a list of all Owl specific directives:
|
needs. Here is a list of all Owl specific directives:
|
||||||
@@ -191,6 +193,15 @@ The first `t-out` will act as a `t-esc` directive, which means that the content
|
|||||||
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
of `value1` will be escaped. However, since `value2` has been tagged as a markup,
|
||||||
this will be injected as html.
|
this will be injected as html.
|
||||||
|
|
||||||
|
`markup` can also be used as a tag function, allowing the interpolated values to
|
||||||
|
be safely escaped:
|
||||||
|
|
||||||
|
```js
|
||||||
|
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||||
|
// <b><script>alert('💥💥')</script></b>
|
||||||
|
const value = markup`<b>${maliciousInput}</b>`;
|
||||||
|
```
|
||||||
|
|
||||||
### Setting Variables
|
### Setting Variables
|
||||||
|
|
||||||
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
QWeb allows creating variables from within the template, to memoize a computation (to use it multiple times), give a piece of data a clearer name, ...
|
||||||
|
|||||||
@@ -1,17 +1,28 @@
|
|||||||
# 🦉 Translations 🦉
|
# 🦉 Translations 🦉
|
||||||
|
|
||||||
If properly setup, Owl can translate all rendered templates. To do
|
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:
|
For example:
|
||||||
|
|
||||||
```js
|
```js
|
||||||
const translations = {
|
const translations = {
|
||||||
hello: "bonjour",
|
fr: {
|
||||||
yes: "oui",
|
hello: "bonjour",
|
||||||
no: "non",
|
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 });
|
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`,
|
`placeholder`, `label` and `alt`,
|
||||||
- translating text nodes can be disabled with the special attribute `t-translation`,
|
- translating text nodes can be disabled with the special attribute `t-translation`,
|
||||||
if its value is `off`.
|
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:
|
So, with the above `translateFn`, the following templates:
|
||||||
|
|
||||||
@@ -46,6 +62,22 @@ will be rendered as:
|
|||||||
<input placeholder="bonjour" other="yes"/>
|
<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
|
Note that the translation is done during the compilation of the template, not
|
||||||
when it is rendered.
|
when it is rendered.
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,22 @@ in the sources tab as well.
|
|||||||
|
|
||||||
<img src="screenshots/function_menu.png"/>
|
<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
|
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
|
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.
|
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 |
@@ -276,13 +276,39 @@ function inOwnerDocument(el) {
|
|||||||
const rootNode = el.getRootNode();
|
const rootNode = el.getRootNode();
|
||||||
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* Determine whether the given element is contained in a specific root documnet:
|
||||||
|
* either directly or with a shadow root in between or in an iframe.
|
||||||
|
*/
|
||||||
|
function isAttachedToDocument(element, documentElement) {
|
||||||
|
let current = element;
|
||||||
|
const shadowRoot = documentElement.defaultView.ShadowRoot;
|
||||||
|
while (current) {
|
||||||
|
if (current === documentElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (current.parentNode) {
|
||||||
|
current = current.parentNode;
|
||||||
|
}
|
||||||
|
else if (current instanceof shadowRoot && current.host) {
|
||||||
|
current = current.host;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
function validateTarget(target) {
|
function validateTarget(target) {
|
||||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||||
const document = target && target.ownerDocument;
|
const document = target && target.ownerDocument;
|
||||||
if (document) {
|
if (document) {
|
||||||
|
if (!document.defaultView) {
|
||||||
|
throw new OwlError("Cannot mount a component: the target document is not attached to a window (defaultView is missing)");
|
||||||
|
}
|
||||||
const HTMLElement = document.defaultView.HTMLElement;
|
const HTMLElement = document.defaultView.HTMLElement;
|
||||||
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||||
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
if (!isAttachedToDocument(target, document)) {
|
||||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -319,12 +345,40 @@ async function loadFile(url) {
|
|||||||
*/
|
*/
|
||||||
class Markup extends String {
|
class Markup extends String {
|
||||||
}
|
}
|
||||||
/*
|
function htmlEscape(str) {
|
||||||
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
if (str instanceof Markup) {
|
||||||
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
return str;
|
||||||
*/
|
}
|
||||||
function markup(value) {
|
if (str === undefined) {
|
||||||
return new Markup(value);
|
return markup("");
|
||||||
|
}
|
||||||
|
if (typeof str === "number") {
|
||||||
|
return markup(String(str));
|
||||||
|
}
|
||||||
|
[
|
||||||
|
["&", "&"],
|
||||||
|
["<", "<"],
|
||||||
|
[">", ">"],
|
||||||
|
["'", "'"],
|
||||||
|
['"', """],
|
||||||
|
["`", "`"],
|
||||||
|
].forEach((pairs) => {
|
||||||
|
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
|
||||||
|
});
|
||||||
|
return markup(str);
|
||||||
|
}
|
||||||
|
function markup(valueOrStrings, ...placeholders) {
|
||||||
|
if (!Array.isArray(valueOrStrings)) {
|
||||||
|
return new Markup(valueOrStrings);
|
||||||
|
}
|
||||||
|
const strings = valueOrStrings;
|
||||||
|
let acc = "";
|
||||||
|
let i = 0;
|
||||||
|
for (; i < placeholders.length; ++i) {
|
||||||
|
acc += strings[i] + htmlEscape(placeholders[i]);
|
||||||
|
}
|
||||||
|
acc += strings[i];
|
||||||
|
return new Markup(acc);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEventHandler(rawEvent) {
|
function createEventHandler(rawEvent) {
|
||||||
@@ -1625,6 +1679,13 @@ function makeRootFiber(node) {
|
|||||||
fibersInError.delete(current);
|
fibersInError.delete(current);
|
||||||
fibersInError.delete(root);
|
fibersInError.delete(root);
|
||||||
current.appliedToDom = false;
|
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;
|
return current;
|
||||||
}
|
}
|
||||||
@@ -1741,6 +1802,7 @@ class RootFiber extends Fiber {
|
|||||||
const node = this.node;
|
const node = this.node;
|
||||||
this.locked = true;
|
this.locked = true;
|
||||||
let current = undefined;
|
let current = undefined;
|
||||||
|
let mountedFibers = this.mounted;
|
||||||
try {
|
try {
|
||||||
// Step 1: calling all willPatch lifecycle hooks
|
// Step 1: calling all willPatch lifecycle hooks
|
||||||
for (current of this.willPatch) {
|
for (current of this.willPatch) {
|
||||||
@@ -1760,7 +1822,6 @@ class RootFiber extends Fiber {
|
|||||||
node._patch();
|
node._patch();
|
||||||
this.locked = false;
|
this.locked = false;
|
||||||
// Step 4: calling all mounted lifecycle hooks
|
// Step 4: calling all mounted lifecycle hooks
|
||||||
let mountedFibers = this.mounted;
|
|
||||||
while ((current = mountedFibers.pop())) {
|
while ((current = mountedFibers.pop())) {
|
||||||
current = current;
|
current = current;
|
||||||
if (current.appliedToDom) {
|
if (current.appliedToDom) {
|
||||||
@@ -1781,6 +1842,15 @@ class RootFiber extends Fiber {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (e) {
|
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;
|
this.locked = false;
|
||||||
node.app.handleError({ fiber: current || this, error: e });
|
node.app.handleError({ fiber: current || this, error: e });
|
||||||
}
|
}
|
||||||
@@ -3722,6 +3792,7 @@ function createContext(parentCtx, params) {
|
|||||||
index: 0,
|
index: 0,
|
||||||
forceNewBlock: true,
|
forceNewBlock: true,
|
||||||
translate: parentCtx.translate,
|
translate: parentCtx.translate,
|
||||||
|
translationCtx: parentCtx.translationCtx,
|
||||||
tKeyExpr: null,
|
tKeyExpr: null,
|
||||||
nameSpace: parentCtx.nameSpace,
|
nameSpace: parentCtx.nameSpace,
|
||||||
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
||||||
@@ -3779,7 +3850,16 @@ class CodeTarget {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
const TRANSLATABLE_ATTRS = [
|
||||||
|
"alt",
|
||||||
|
"aria-label",
|
||||||
|
"aria-placeholder",
|
||||||
|
"aria-roledescription",
|
||||||
|
"aria-valuetext",
|
||||||
|
"label",
|
||||||
|
"placeholder",
|
||||||
|
"title",
|
||||||
|
];
|
||||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||||
class CodeGenerator {
|
class CodeGenerator {
|
||||||
constructor(ast, options) {
|
constructor(ast, options) {
|
||||||
@@ -3824,6 +3904,7 @@ class CodeGenerator {
|
|||||||
forceNewBlock: false,
|
forceNewBlock: false,
|
||||||
isLast: true,
|
isLast: true,
|
||||||
translate: true,
|
translate: true,
|
||||||
|
translationCtx: "",
|
||||||
tKeyExpr: null,
|
tKeyExpr: null,
|
||||||
});
|
});
|
||||||
// define blocks and utility functions
|
// define blocks and utility functions
|
||||||
@@ -3961,9 +4042,9 @@ class CodeGenerator {
|
|||||||
})
|
})
|
||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
translate(str) {
|
translate(str, translationCtx) {
|
||||||
const match = translationRE.exec(str);
|
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
|
* @returns the newly created block name, if any
|
||||||
@@ -4004,7 +4085,9 @@ class CodeGenerator {
|
|||||||
return this.compileTSlot(ast, ctx);
|
return this.compileTSlot(ast, ctx);
|
||||||
case 16 /* TTranslation */:
|
case 16 /* TTranslation */:
|
||||||
return this.compileTTranslation(ast, ctx);
|
return this.compileTTranslation(ast, ctx);
|
||||||
case 17 /* TPortal */:
|
case 17 /* TTranslationContext */:
|
||||||
|
return this.compileTTranslationContext(ast, ctx);
|
||||||
|
case 18 /* TPortal */:
|
||||||
return this.compileTPortal(ast, ctx);
|
return this.compileTPortal(ast, ctx);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4042,7 +4125,7 @@ class CodeGenerator {
|
|||||||
let { block, forceNewBlock } = ctx;
|
let { block, forceNewBlock } = ctx;
|
||||||
let value = ast.value;
|
let value = ast.value;
|
||||||
if (value && ctx.translate !== false) {
|
if (value && ctx.translate !== false) {
|
||||||
value = this.translate(value);
|
value = this.translate(value, ctx.translationCtx);
|
||||||
}
|
}
|
||||||
if (!ctx.inPreTag) {
|
if (!ctx.inPreTag) {
|
||||||
value = value.replace(whitespaceRE, " ");
|
value = value.replace(whitespaceRE, " ");
|
||||||
@@ -4077,6 +4160,7 @@ class CodeGenerator {
|
|||||||
return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`;
|
return `[${modifiersCode}${this.captureExpression(handler)}, ctx]`;
|
||||||
}
|
}
|
||||||
compileTDomNode(ast, ctx) {
|
compileTDomNode(ast, ctx) {
|
||||||
|
var _a;
|
||||||
let { block, forceNewBlock } = ctx;
|
let { block, forceNewBlock } = ctx;
|
||||||
const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;
|
const isNewBlock = !block || forceNewBlock || ast.dynamicTag !== null || ast.ns;
|
||||||
let codeIdx = this.target.code.length;
|
let codeIdx = this.target.code.length;
|
||||||
@@ -4132,7 +4216,8 @@ class CodeGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (this.translatableAttributes.includes(key)) {
|
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 {
|
else {
|
||||||
expr = `"${ast.attrs[key]}"`;
|
expr = `"${ast.attrs[key]}"`;
|
||||||
@@ -4454,7 +4539,7 @@ class CodeGenerator {
|
|||||||
const isNewBlock = !block || forceNewBlock;
|
const isNewBlock = !block || forceNewBlock;
|
||||||
let codeIdx = this.target.code.length;
|
let codeIdx = this.target.code.length;
|
||||||
if (isNewBlock) {
|
if (isNewBlock) {
|
||||||
const n = ast.content.filter((c) => c.type !== 6 /* TSet */).length;
|
const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
|
||||||
let result = null;
|
let result = null;
|
||||||
if (n <= 1) {
|
if (n <= 1) {
|
||||||
for (let child of ast.content) {
|
for (let child of ast.content) {
|
||||||
@@ -4468,15 +4553,15 @@ class CodeGenerator {
|
|||||||
let index = 0;
|
let index = 0;
|
||||||
for (let i = 0, l = ast.content.length; i < l; i++) {
|
for (let i = 0, l = ast.content.length; i < l; i++) {
|
||||||
const child = ast.content[i];
|
const child = ast.content[i];
|
||||||
const isTSet = child.type === 6 /* TSet */;
|
const forceNewBlock = !child.hasNoRepresentation;
|
||||||
const subCtx = createContext(ctx, {
|
const subCtx = createContext(ctx, {
|
||||||
block,
|
block,
|
||||||
index,
|
index,
|
||||||
forceNewBlock: !isTSet,
|
forceNewBlock,
|
||||||
isLast: ctx.isLast && i === l - 1,
|
isLast: ctx.isLast && i === l - 1,
|
||||||
});
|
});
|
||||||
this.compileAST(child, subCtx);
|
this.compileAST(child, subCtx);
|
||||||
if (!isTSet) {
|
if (forceNewBlock) {
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4576,7 +4661,7 @@ class CodeGenerator {
|
|||||||
else {
|
else {
|
||||||
let value;
|
let value;
|
||||||
if (ast.defaultValue) {
|
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) {
|
if (ast.value) {
|
||||||
value = `withDefault(${expr}, ${defaultValue})`;
|
value = `withDefault(${expr}, ${defaultValue})`;
|
||||||
}
|
}
|
||||||
@@ -4610,9 +4695,10 @@ class CodeGenerator {
|
|||||||
* "some-prop" "state" "'some-prop': ctx['state']"
|
* "some-prop" "state" "'some-prop': ctx['state']"
|
||||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
||||||
*/
|
*/
|
||||||
formatProp(name, value) {
|
formatProp(name, value, attrsTranslationCtx, translationCtx) {
|
||||||
if (name.endsWith(".translate")) {
|
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 {
|
else {
|
||||||
value = this.captureExpression(value);
|
value = this.captureExpression(value);
|
||||||
@@ -4628,14 +4714,14 @@ class CodeGenerator {
|
|||||||
case "translate":
|
case "translate":
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new OwlError("Invalid prop suffix");
|
throw new OwlError(`Invalid prop suffix: ${suffix}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
||||||
return `${name}: ${value || undefined}`;
|
return `${name}: ${value || undefined}`;
|
||||||
}
|
}
|
||||||
formatPropObject(obj) {
|
formatPropObject(obj, attrsTranslationCtx, translationCtx) {
|
||||||
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
|
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v, attrsTranslationCtx, translationCtx));
|
||||||
}
|
}
|
||||||
getPropString(props, dynProps) {
|
getPropString(props, dynProps) {
|
||||||
let propString = `{${props.join(",")}}`;
|
let propString = `{${props.join(",")}}`;
|
||||||
@@ -4648,7 +4734,9 @@ class CodeGenerator {
|
|||||||
let { block } = ctx;
|
let { block } = ctx;
|
||||||
// props
|
// props
|
||||||
const hasSlotsProp = "slots" in (ast.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
|
// slots
|
||||||
let slotDef = "";
|
let slotDef = "";
|
||||||
if (ast.slots) {
|
if (ast.slots) {
|
||||||
@@ -4671,7 +4759,7 @@ class CodeGenerator {
|
|||||||
params.push(`__scope: "${scope}"`);
|
params.push(`__scope: "${scope}"`);
|
||||||
}
|
}
|
||||||
if (ast.slots[slotName].attrs) {
|
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(", ")}}`;
|
const slotInfo = `{${params.join(", ")}}`;
|
||||||
slotStr.push(`'${slotName}': ${slotInfo}`);
|
slotStr.push(`'${slotName}': ${slotInfo}`);
|
||||||
@@ -4776,15 +4864,16 @@ class CodeGenerator {
|
|||||||
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
||||||
this.slotNames.add(ast.name);
|
this.slotNames.add(ast.name);
|
||||||
}
|
}
|
||||||
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
|
const attrs = { ...ast.attrs };
|
||||||
if (ast.attrs) {
|
const dynProps = attrs["t-props"];
|
||||||
delete ast.attrs["t-props"];
|
delete attrs["t-props"];
|
||||||
}
|
|
||||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
key = this.generateComponentKey(key);
|
key = this.generateComponentKey(key);
|
||||||
}
|
}
|
||||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
const props = ast.attrs
|
||||||
|
? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
||||||
|
: [];
|
||||||
const scope = this.getPropString(props, dynProps);
|
const scope = this.getPropString(props, dynProps);
|
||||||
if (ast.defaultContent) {
|
if (ast.defaultContent) {
|
||||||
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
||||||
@@ -4817,6 +4906,12 @@ class CodeGenerator {
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
compileTTranslationContext(ast, ctx) {
|
||||||
|
if (ast.content) {
|
||||||
|
return this.compileAST(ast.content, Object.assign({}, ctx, { translationCtx: ast.translationCtx }));
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
compileTPortal(ast, ctx) {
|
compileTPortal(ast, ctx) {
|
||||||
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
||||||
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
||||||
@@ -4882,10 +4977,11 @@ function parseNode(node, ctx) {
|
|||||||
parseTPortal(node, ctx) ||
|
parseTPortal(node, ctx) ||
|
||||||
parseTCall(node, ctx) ||
|
parseTCall(node, ctx) ||
|
||||||
parseTCallBlock(node) ||
|
parseTCallBlock(node) ||
|
||||||
|
parseTTranslation(node, ctx) ||
|
||||||
|
parseTTranslationContext(node, ctx) ||
|
||||||
|
parseTKey(node, ctx) ||
|
||||||
parseTEscNode(node, ctx) ||
|
parseTEscNode(node, ctx) ||
|
||||||
parseTOutNode(node, ctx) ||
|
parseTOutNode(node, ctx) ||
|
||||||
parseTKey(node, ctx) ||
|
|
||||||
parseTTranslation(node, ctx) ||
|
|
||||||
parseTSlot(node, ctx) ||
|
parseTSlot(node, ctx) ||
|
||||||
parseComponent(node, ctx) ||
|
parseComponent(node, ctx) ||
|
||||||
parseDOMNode(node, ctx) ||
|
parseDOMNode(node, ctx) ||
|
||||||
@@ -4953,19 +5049,29 @@ function parseTCustom(node, ctx) {
|
|||||||
function parseTDebugLog(node, ctx) {
|
function parseTDebugLog(node, ctx) {
|
||||||
if (node.hasAttribute("t-debug")) {
|
if (node.hasAttribute("t-debug")) {
|
||||||
node.removeAttribute("t-debug");
|
node.removeAttribute("t-debug");
|
||||||
return {
|
const content = parseNode(node, ctx);
|
||||||
|
const ast = {
|
||||||
type: 12 /* TDebug */,
|
type: 12 /* TDebug */,
|
||||||
content: parseNode(node, ctx),
|
content,
|
||||||
};
|
};
|
||||||
|
if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
}
|
}
|
||||||
if (node.hasAttribute("t-log")) {
|
if (node.hasAttribute("t-log")) {
|
||||||
const expr = node.getAttribute("t-log");
|
const expr = node.getAttribute("t-log");
|
||||||
node.removeAttribute("t-log");
|
node.removeAttribute("t-log");
|
||||||
return {
|
const content = parseNode(node, ctx);
|
||||||
|
const ast = {
|
||||||
type: 13 /* TLog */,
|
type: 13 /* TLog */,
|
||||||
expr,
|
expr,
|
||||||
content: parseNode(node, ctx),
|
content,
|
||||||
};
|
};
|
||||||
|
if (content === null || content === void 0 ? void 0 : content.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -4994,6 +5100,7 @@ function parseDOMNode(node, ctx) {
|
|||||||
node.removeAttribute("t-ref");
|
node.removeAttribute("t-ref");
|
||||||
const nodeAttrsNames = node.getAttributeNames();
|
const nodeAttrsNames = node.getAttributeNames();
|
||||||
let attrs = null;
|
let attrs = null;
|
||||||
|
let attrsTranslationCtx = null;
|
||||||
let on = null;
|
let on = null;
|
||||||
let model = null;
|
let model = null;
|
||||||
for (let attr of nodeAttrsNames) {
|
for (let attr of nodeAttrsNames) {
|
||||||
@@ -5054,6 +5161,11 @@ function parseDOMNode(node, ctx) {
|
|||||||
else if (attr === "xmlns") {
|
else if (attr === "xmlns") {
|
||||||
ns = value;
|
ns = value;
|
||||||
}
|
}
|
||||||
|
else if (attr.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attr.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
|
}
|
||||||
else if (attr !== "t-name") {
|
else if (attr !== "t-name") {
|
||||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||||
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
||||||
@@ -5075,6 +5187,7 @@ function parseDOMNode(node, ctx) {
|
|||||||
tag: tagName,
|
tag: tagName,
|
||||||
dynamicTag,
|
dynamicTag,
|
||||||
attrs,
|
attrs,
|
||||||
|
attrsTranslationCtx,
|
||||||
on,
|
on,
|
||||||
ref,
|
ref,
|
||||||
content: children,
|
content: children,
|
||||||
@@ -5188,11 +5301,19 @@ function parseTKey(node, ctx) {
|
|||||||
}
|
}
|
||||||
const key = node.getAttribute("t-key");
|
const key = node.getAttribute("t-key");
|
||||||
node.removeAttribute("t-key");
|
node.removeAttribute("t-key");
|
||||||
const body = parseNode(node, ctx);
|
const content = parseNode(node, ctx);
|
||||||
if (!body) {
|
if (!content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { type: 10 /* TKey */, expr: key, content: body };
|
const ast = {
|
||||||
|
type: 10 /* TKey */,
|
||||||
|
expr: key,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
if (content.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
}
|
}
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// t-call
|
// t-call
|
||||||
@@ -5215,7 +5336,15 @@ function parseTCall(node, ctx) {
|
|||||||
if (ast && ast.type === 11 /* TComponent */) {
|
if (ast && ast.type === 11 /* TComponent */) {
|
||||||
return {
|
return {
|
||||||
...ast,
|
...ast,
|
||||||
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
|
slots: {
|
||||||
|
default: {
|
||||||
|
content: tcall,
|
||||||
|
scope: null,
|
||||||
|
on: null,
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5293,7 +5422,7 @@ function parseTSetNode(node, ctx) {
|
|||||||
if (node.textContent !== node.innerHTML) {
|
if (node.textContent !== node.innerHTML) {
|
||||||
body = parseChildren(node, ctx);
|
body = parseChildren(node, ctx);
|
||||||
}
|
}
|
||||||
return { type: 6 /* TSet */, name, value, defaultValue, body };
|
return { type: 6 /* TSet */, name, value, defaultValue, body, hasNoRepresentation: true };
|
||||||
}
|
}
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Components
|
// Components
|
||||||
@@ -5330,9 +5459,15 @@ function parseComponent(node, ctx) {
|
|||||||
node.removeAttribute("t-slot-scope");
|
node.removeAttribute("t-slot-scope");
|
||||||
let on = null;
|
let on = null;
|
||||||
let props = null;
|
let props = null;
|
||||||
|
let propsTranslationCtx = null;
|
||||||
for (let name of node.getAttributeNames()) {
|
for (let name of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(name);
|
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-")) {
|
if (name.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[name.slice(5)] = value;
|
on[name.slice(5)] = value;
|
||||||
@@ -5376,6 +5511,7 @@ function parseComponent(node, ctx) {
|
|||||||
const slotAst = parseNode(slotNode, ctx);
|
const slotAst = parseNode(slotNode, ctx);
|
||||||
let on = null;
|
let on = null;
|
||||||
let attrs = null;
|
let attrs = null;
|
||||||
|
let attrsTranslationCtx = null;
|
||||||
let scope = null;
|
let scope = null;
|
||||||
for (let attributeName of slotNode.getAttributeNames()) {
|
for (let attributeName of slotNode.getAttributeNames()) {
|
||||||
const value = slotNode.getAttribute(attributeName);
|
const value = slotNode.getAttribute(attributeName);
|
||||||
@@ -5383,6 +5519,11 @@ function parseComponent(node, ctx) {
|
|||||||
scope = value;
|
scope = value;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
else if (attributeName.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attributeName.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
|
}
|
||||||
else if (attributeName.startsWith("t-on-")) {
|
else if (attributeName.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attributeName.slice(5)] = value;
|
on[attributeName.slice(5)] = value;
|
||||||
@@ -5393,17 +5534,32 @@ function parseComponent(node, ctx) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
slots = slots || {};
|
slots = slots || {};
|
||||||
slots[name] = { content: slotAst, on, attrs, scope };
|
slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
|
||||||
}
|
}
|
||||||
// default slot
|
// default slot
|
||||||
const defaultContent = parseChildNodes(clone, ctx);
|
const defaultContent = parseChildNodes(clone, ctx);
|
||||||
slots = slots || {};
|
slots = slots || {};
|
||||||
// t-set-slot="default" has priority over content
|
// t-set-slot="default" has priority over content
|
||||||
if (defaultContent && !slots.default) {
|
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
|
// Slots
|
||||||
@@ -5415,6 +5571,7 @@ function parseTSlot(node, ctx) {
|
|||||||
const name = node.getAttribute("t-slot");
|
const name = node.getAttribute("t-slot");
|
||||||
node.removeAttribute("t-slot");
|
node.removeAttribute("t-slot");
|
||||||
let attrs = null;
|
let attrs = null;
|
||||||
|
let attrsTranslationCtx = null;
|
||||||
let on = null;
|
let on = null;
|
||||||
for (let attributeName of node.getAttributeNames()) {
|
for (let attributeName of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(attributeName);
|
const value = node.getAttribute(attributeName);
|
||||||
@@ -5422,6 +5579,11 @@ function parseTSlot(node, ctx) {
|
|||||||
on = on || {};
|
on = on || {};
|
||||||
on[attributeName.slice(5)] = value;
|
on[attributeName.slice(5)] = value;
|
||||||
}
|
}
|
||||||
|
else if (attributeName.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attributeName.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
|
}
|
||||||
else {
|
else {
|
||||||
attrs = attrs || {};
|
attrs = attrs || {};
|
||||||
attrs[attributeName] = value;
|
attrs[attributeName] = value;
|
||||||
@@ -5431,19 +5593,59 @@ function parseTSlot(node, ctx) {
|
|||||||
type: 14 /* TSlot */,
|
type: 14 /* TSlot */,
|
||||||
name,
|
name,
|
||||||
attrs,
|
attrs,
|
||||||
|
attrsTranslationCtx,
|
||||||
on,
|
on,
|
||||||
defaultContent: parseChildNodes(node, ctx),
|
defaultContent: parseChildNodes(node, ctx),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Translation
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
function wrapInTTranslationAST(r) {
|
||||||
|
const ast = { type: 16 /* TTranslation */, content: r };
|
||||||
|
if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
function parseTTranslation(node, ctx) {
|
function parseTTranslation(node, ctx) {
|
||||||
if (node.getAttribute("t-translation") !== "off") {
|
if (node.getAttribute("t-translation") !== "off") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
node.removeAttribute("t-translation");
|
node.removeAttribute("t-translation");
|
||||||
return {
|
const result = parseNode(node, ctx);
|
||||||
type: 16 /* TTranslation */,
|
if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
|
||||||
content: parseNode(node, ctx),
|
const children = result.content.map(wrapInTTranslationAST);
|
||||||
|
return makeASTMulti(children);
|
||||||
|
}
|
||||||
|
return wrapInTTranslationAST(result);
|
||||||
|
}
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Translation Context
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
function wrapInTTranslationContextAST(r, translationCtx) {
|
||||||
|
const ast = {
|
||||||
|
type: 17 /* TTranslationContext */,
|
||||||
|
content: r,
|
||||||
|
translationCtx,
|
||||||
};
|
};
|
||||||
|
if (r === null || r === void 0 ? void 0 : r.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
function parseTTranslationContext(node, ctx) {
|
||||||
|
const translationCtx = node.getAttribute("t-translation-context");
|
||||||
|
if (!translationCtx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
node.removeAttribute("t-translation-context");
|
||||||
|
const result = parseNode(node, ctx);
|
||||||
|
if ((result === null || result === void 0 ? void 0 : result.type) === 3 /* Multi */) {
|
||||||
|
const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
|
||||||
|
return makeASTMulti(children);
|
||||||
|
}
|
||||||
|
return wrapInTTranslationContextAST(result, translationCtx);
|
||||||
}
|
}
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Portal
|
// Portal
|
||||||
@@ -5462,7 +5664,7 @@ function parseTPortal(node, ctx) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
type: 17 /* TPortal */,
|
type: 18 /* TPortal */,
|
||||||
target,
|
target,
|
||||||
content,
|
content,
|
||||||
};
|
};
|
||||||
@@ -5488,6 +5690,13 @@ function parseChildren(node, ctx) {
|
|||||||
}
|
}
|
||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
function makeASTMulti(children) {
|
||||||
|
const ast = { type: 3 /* Multi */, content: children };
|
||||||
|
if (children.every((c) => c.hasNoRepresentation)) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Parse all the child nodes of a given node and return an ast if possible.
|
* Parse all the child nodes of a given node and return an ast if possible.
|
||||||
* In the case there are multiple children, they are wrapped in a astmulti.
|
* In the case there are multiple children, they are wrapped in a astmulti.
|
||||||
@@ -5500,7 +5709,7 @@ function parseChildNodes(node, ctx) {
|
|||||||
case 1:
|
case 1:
|
||||||
return children[0];
|
return children[0];
|
||||||
default:
|
default:
|
||||||
return { type: 3 /* Multi */, content: children };
|
return makeASTMulti(children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -5604,7 +5813,7 @@ function compile(template, options = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// do not modify manually. This file is generated by the release script.
|
// do not modify manually. This file is generated by the release script.
|
||||||
const version = "2.5.1";
|
const version = "2.8.1";
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
// Scheduler
|
// Scheduler
|
||||||
@@ -5699,13 +5908,6 @@ class Scheduler {
|
|||||||
Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
|
Scheduler.requestAnimationFrame = window.requestAnimationFrame.bind(window);
|
||||||
|
|
||||||
let hasBeenLogged = false;
|
let hasBeenLogged = false;
|
||||||
const DEV_MSG = () => {
|
|
||||||
const hash = window.owl ? window.owl.__info__.hash : "master";
|
|
||||||
return `Owl is running in 'dev' mode.
|
|
||||||
|
|
||||||
This is not suitable for production use.
|
|
||||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
|
||||||
};
|
|
||||||
const apps = new Set();
|
const apps = new Set();
|
||||||
window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = { apps, Fiber, RootFiber, toRaw, reactive });
|
window.__OWL_DEVTOOLS__ || (window.__OWL_DEVTOOLS__ = { apps, Fiber, RootFiber, toRaw, reactive });
|
||||||
class App extends TemplateSet {
|
class App extends TemplateSet {
|
||||||
@@ -5722,7 +5924,7 @@ class App extends TemplateSet {
|
|||||||
}
|
}
|
||||||
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
||||||
if (this.dev && !config.test && !hasBeenLogged) {
|
if (this.dev && !config.test && !hasBeenLogged) {
|
||||||
console.info(DEV_MSG());
|
console.info(`Owl is running in 'dev' mode.`);
|
||||||
hasBeenLogged = true;
|
hasBeenLogged = true;
|
||||||
}
|
}
|
||||||
const env = config.env || {};
|
const env = config.env || {};
|
||||||
@@ -6079,9 +6281,9 @@ 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 };
|
export { App, Component, EventBus, OwlError, __info__, batched, blockDom, htmlEscape, 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-26T08:42:41.633Z';
|
__info__.date = '2025-09-23T07:17:45.055Z';
|
||||||
__info__.hash = '7fc552e';
|
__info__.hash = '5211116';
|
||||||
__info__.url = 'https://github.com/odoo/owl';
|
__info__.url = 'https://github.com/odoo/owl';
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@odoo/owl",
|
"name": "@odoo/owl",
|
||||||
"version": "2.5.1",
|
"version": "2.8.1",
|
||||||
"description": "Odoo Web Library (OWL)",
|
"description": "Odoo Web Library (OWL)",
|
||||||
"main": "dist/owl.cjs.js",
|
"main": "dist/owl.cjs.js",
|
||||||
"module": "dist/owl.es.js",
|
"module": "dist/owl.es.js",
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
|
"build:devtools-chrome": "npm run dev:devtools-chrome -- --config-env=production",
|
||||||
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
|
"build:devtools-firefox": "npm run dev:devtools-firefox -- --config-env=production",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
"test:debug": "node node_modules/.bin/jest --runInBand --watch --testTimeout=5000000",
|
||||||
"test:watch": "jest --watch",
|
"test:watch": "jest --watch",
|
||||||
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
|
"playground:serve": "python3 tools/playground_server.py || python tools/playground_server.py",
|
||||||
"playground": "npm run build && npm run playground:serve",
|
"playground": "npm run build && npm run playground:serve",
|
||||||
@@ -32,7 +32,10 @@
|
|||||||
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
"check-formatting": "prettier {src/*.ts,src/**/*.ts,tests/*.ts,tests/**/*.ts,doc/*.md,doc/**/*.md,tools/devtools/**/*.js} --check",
|
||||||
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
"lint": "eslint src/**/*.ts tests/**/*.ts",
|
||||||
"release": "node tools/release.js",
|
"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": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
@@ -46,6 +49,7 @@
|
|||||||
"homepage": "https://github.com/odoo/owl#readme",
|
"homepage": "https://github.com/odoo/owl#readme",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
|
"@types/jsdom": "^21.1.7",
|
||||||
"@types/node": "^14.11.8",
|
"@types/node": "^14.11.8",
|
||||||
"@typescript-eslint/eslint-plugin": "5.48.1",
|
"@typescript-eslint/eslint-plugin": "5.48.1",
|
||||||
"@typescript-eslint/parser": "5.48.1",
|
"@typescript-eslint/parser": "5.48.1",
|
||||||
@@ -97,5 +101,8 @@
|
|||||||
"prettier": {
|
"prettier": {
|
||||||
"printWidth": 100,
|
"printWidth": 100,
|
||||||
"endOfLine": "auto"
|
"endOfLine": "auto"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"jsdom": "^25.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import pkg from "./package.json";
|
import pkg from "./package.json";
|
||||||
import git from "git-rev-sync";
|
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 { terser } from "rollup-plugin-terser";
|
||||||
import dts from "rollup-plugin-dts";
|
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) {
|
if (pkg.module !== ES_FILENAME || pkg.main !== CJS_FILENAME) {
|
||||||
throw new Error("package.json has been modified. Build script should be updated accordingly");
|
throw new Error("package.json has been modified. Build script should be updated accordingly");
|
||||||
}
|
}
|
||||||
|
|
||||||
const outro = `
|
const outro = `
|
||||||
__info__.date = '${new Date().toISOString()}';
|
__info__.date = '${new Date().toISOString()}';
|
||||||
@@ -21,39 +21,37 @@ __info__.url = 'https://github.com/odoo/owl';
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
switch (process.argv[4]) {
|
switch (process.argv[4]) {
|
||||||
case "compiler":
|
case "compiler":
|
||||||
input = "src/compiler/index.ts",
|
(input = "src/compiler/index.ts"),
|
||||||
output = [
|
(output = [getConfigForFormat("cjs", "dist/compiler.js", "")]);
|
||||||
getConfigForFormat('cjs', 'dist/compiler.js', ''),
|
|
||||||
]
|
|
||||||
break;
|
break;
|
||||||
case "runtime":
|
case "runtime":
|
||||||
input = "src/runtime/index.ts";
|
input = "src/runtime/index.ts";
|
||||||
output = [
|
output = [
|
||||||
getConfigForFormat('esm', addSuffix(ES_FILENAME, 'runtime'), outro),
|
getConfigForFormat("esm", addSuffix(ES_FILENAME, "runtime"), outro),
|
||||||
getConfigForFormat('cjs', addSuffix(CJS_FILENAME, 'runtime'), outro),
|
getConfigForFormat("cjs", addSuffix(CJS_FILENAME, "runtime"), outro),
|
||||||
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro),
|
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro),
|
||||||
getConfigForFormat('iife', addSuffix(IIFE_FILENAME, 'runtime'), outro, true),
|
getConfigForFormat("iife", addSuffix(IIFE_FILENAME, "runtime"), outro, true),
|
||||||
]
|
];
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
input = "src/index.ts",
|
(input = "src/index.ts"),
|
||||||
output = [
|
(output = [
|
||||||
getConfigForFormat('esm', ES_FILENAME, outro),
|
getConfigForFormat("esm", ES_FILENAME, outro),
|
||||||
getConfigForFormat('cjs', CJS_FILENAME, outro),
|
getConfigForFormat("cjs", CJS_FILENAME, outro),
|
||||||
getConfigForFormat('iife', IIFE_FILENAME, outro),
|
getConfigForFormat("iife", IIFE_FILENAME, outro),
|
||||||
getConfigForFormat('iife', IIFE_FILENAME, outro, true),
|
getConfigForFormat("iife", IIFE_FILENAME, outro, true),
|
||||||
]
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Generate from a string depicting a path a new path for the minified version.
|
* Generate from a string depicting a path a new path for the minified version.
|
||||||
* @param {string} pkgFileName file name
|
* @param {string} pkgFileName file name
|
||||||
*/
|
*/
|
||||||
function addSuffix(pkgFileName, suffix) {
|
function addSuffix(pkgFileName, suffix) {
|
||||||
const parts = pkgFileName.split('.');
|
const parts = pkgFileName.split(".");
|
||||||
parts.splice(parts.length - 1, 0, suffix);
|
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,
|
outro: outro,
|
||||||
freeze: false,
|
freeze: false,
|
||||||
plugins: minified ? [terser()] : [],
|
plugins: minified ? [terser()] : [],
|
||||||
indent: ' ', // indent with 4 spaces
|
indent: " ", // indent with 4 spaces
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +79,19 @@ export default [
|
|||||||
output,
|
output,
|
||||||
plugins: [
|
plugins: [
|
||||||
typescript({
|
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",
|
input: "dist/types/index.d.ts",
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# encountered issues
|
||||||
|
## dropdown issue
|
||||||
|
- there was a problem that writing in a state while the effect was updated.
|
||||||
|
- the tracking of signal being written were dropped because we cleared it
|
||||||
|
after re-running the effect that made a write.
|
||||||
|
- solution: clear the tracked signal before re-executing the effects
|
||||||
|
- reading signal A while also writing signal A makes an infinite loop
|
||||||
|
- current solution: use toRaw in order to not track the read
|
||||||
|
- possible better solution to explore: do not track read if there is a write in a effect.
|
||||||
|
## website issue
|
||||||
|
- a rpc request was made on onWillStart, onWillStart was tracking reads. (see WebsiteBuilderClientAction)
|
||||||
|
- The read subsequently made a write, that re-triggered the onWillStart.
|
||||||
|
- A similar situation happened with onWillUpdateProps (see Transition)
|
||||||
|
- solution: prevent tracking reads in onWillStart and onWillUpdateProps
|
||||||
|
|
||||||
|
# future
|
||||||
|
- worker for computation?
|
||||||
|
- cap'n web
|
||||||
@@ -1,4 +1,28 @@
|
|||||||
|
export type ExecutionContext = {
|
||||||
|
onReadAtom: (atom: Atom) => void;
|
||||||
|
unsubcribe?: (scheduledContexts: Set<ExecutionContext>) => void;
|
||||||
|
update?: Function;
|
||||||
|
atoms?: Set<Atom>;
|
||||||
|
meta?: any;
|
||||||
|
// getParent: () => ExecutionContext | undefined;
|
||||||
|
// getChildren: () => ExecutionContext[];
|
||||||
|
// schedule: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
export type customDirectives = Record<
|
export type customDirectives = Record<
|
||||||
string,
|
string,
|
||||||
(node: Element, value: string, modifier: string[]) => void
|
(node: Element, value: string, modifier: string[]) => void
|
||||||
>;
|
>;
|
||||||
|
|
||||||
|
export type Atom = {
|
||||||
|
executionContexts: Set<ExecutionContext>;
|
||||||
|
dependents: Set<DerivedAtom>;
|
||||||
|
getValue: () => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type OldValue = any;
|
||||||
|
|
||||||
|
export type DerivedAtom = Atom & {
|
||||||
|
dependencies: Map<Atom, OldValue>;
|
||||||
|
computed: boolean;
|
||||||
|
};
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
ASTTOut,
|
ASTTOut,
|
||||||
ASTTPortal,
|
ASTTPortal,
|
||||||
ASTTranslation,
|
ASTTranslation,
|
||||||
|
ASTTranslationContext,
|
||||||
ASTTSet,
|
ASTTSet,
|
||||||
ASTType,
|
ASTType,
|
||||||
Attrs,
|
Attrs,
|
||||||
@@ -35,7 +36,7 @@ type BlockType = "block" | "text" | "multi" | "list" | "html" | "comment";
|
|||||||
const whitespaceRE = /\s+/g;
|
const whitespaceRE = /\s+/g;
|
||||||
|
|
||||||
export interface Config {
|
export interface Config {
|
||||||
translateFn?: (s: string) => string;
|
translateFn?: (s: string, translationCtx: string) => string;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
dev?: boolean;
|
dev?: boolean;
|
||||||
}
|
}
|
||||||
@@ -171,6 +172,7 @@ interface Context {
|
|||||||
forceNewBlock: boolean;
|
forceNewBlock: boolean;
|
||||||
isLast?: boolean;
|
isLast?: boolean;
|
||||||
translate: boolean;
|
translate: boolean;
|
||||||
|
translationCtx: string;
|
||||||
tKeyExpr: string | null;
|
tKeyExpr: string | null;
|
||||||
nameSpace?: string;
|
nameSpace?: string;
|
||||||
tModelSelectedExpr?: string;
|
tModelSelectedExpr?: string;
|
||||||
@@ -185,6 +187,7 @@ function createContext(parentCtx: Context, params?: Partial<Context>): Context {
|
|||||||
index: 0,
|
index: 0,
|
||||||
forceNewBlock: true,
|
forceNewBlock: true,
|
||||||
translate: parentCtx.translate,
|
translate: parentCtx.translate,
|
||||||
|
translationCtx: parentCtx.translationCtx,
|
||||||
tKeyExpr: null,
|
tKeyExpr: null,
|
||||||
nameSpace: parentCtx.nameSpace,
|
nameSpace: parentCtx.nameSpace,
|
||||||
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
tModelSelectedExpr: parentCtx.tModelSelectedExpr,
|
||||||
@@ -251,7 +254,16 @@ class CodeTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TRANSLATABLE_ATTRS = ["label", "title", "placeholder", "alt"];
|
const TRANSLATABLE_ATTRS = [
|
||||||
|
"alt",
|
||||||
|
"aria-label",
|
||||||
|
"aria-placeholder",
|
||||||
|
"aria-roledescription",
|
||||||
|
"aria-valuetext",
|
||||||
|
"label",
|
||||||
|
"placeholder",
|
||||||
|
"title",
|
||||||
|
];
|
||||||
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
const translationRE = /^(\s*)([\s\S]+?)(\s*)$/;
|
||||||
|
|
||||||
export class CodeGenerator {
|
export class CodeGenerator {
|
||||||
@@ -263,7 +275,7 @@ export class CodeGenerator {
|
|||||||
target = new CodeTarget("template");
|
target = new CodeTarget("template");
|
||||||
templateName?: string;
|
templateName?: string;
|
||||||
dev: boolean;
|
dev: boolean;
|
||||||
translateFn: (s: string) => string;
|
translateFn: (s: string, translationCtx: string) => string;
|
||||||
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
|
translatableAttributes: string[] = TRANSLATABLE_ATTRS;
|
||||||
ast: AST;
|
ast: AST;
|
||||||
staticDefs: { id: string; expr: string }[] = [];
|
staticDefs: { id: string; expr: string }[] = [];
|
||||||
@@ -303,6 +315,7 @@ export class CodeGenerator {
|
|||||||
forceNewBlock: false,
|
forceNewBlock: false,
|
||||||
isLast: true,
|
isLast: true,
|
||||||
translate: true,
|
translate: true,
|
||||||
|
translationCtx: "",
|
||||||
tKeyExpr: null,
|
tKeyExpr: null,
|
||||||
});
|
});
|
||||||
// define blocks and utility functions
|
// define blocks and utility functions
|
||||||
@@ -457,9 +470,9 @@ export class CodeGenerator {
|
|||||||
.join("");
|
.join("");
|
||||||
}
|
}
|
||||||
|
|
||||||
translate(str: string): string {
|
translate(str: string, translationCtx: string): string {
|
||||||
const match = translationRE.exec(str) as any;
|
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 +514,8 @@ export class CodeGenerator {
|
|||||||
return this.compileTSlot(ast, ctx);
|
return this.compileTSlot(ast, ctx);
|
||||||
case ASTType.TTranslation:
|
case ASTType.TTranslation:
|
||||||
return this.compileTTranslation(ast, ctx);
|
return this.compileTTranslation(ast, ctx);
|
||||||
|
case ASTType.TTranslationContext:
|
||||||
|
return this.compileTTranslationContext(ast, ctx);
|
||||||
case ASTType.TPortal:
|
case ASTType.TPortal:
|
||||||
return this.compileTPortal(ast, ctx);
|
return this.compileTPortal(ast, ctx);
|
||||||
}
|
}
|
||||||
@@ -542,7 +557,7 @@ export class CodeGenerator {
|
|||||||
|
|
||||||
let value = ast.value;
|
let value = ast.value;
|
||||||
if (value && ctx.translate !== false) {
|
if (value && ctx.translate !== false) {
|
||||||
value = this.translate(value);
|
value = this.translate(value, ctx.translationCtx);
|
||||||
}
|
}
|
||||||
if (!ctx.inPreTag) {
|
if (!ctx.inPreTag) {
|
||||||
value = value.replace(whitespaceRE, " ");
|
value = value.replace(whitespaceRE, " ");
|
||||||
@@ -631,7 +646,8 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (this.translatableAttributes.includes(key)) {
|
} 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 {
|
} else {
|
||||||
expr = `"${ast.attrs[key]}"`;
|
expr = `"${ast.attrs[key]}"`;
|
||||||
attrName = key;
|
attrName = key;
|
||||||
@@ -979,7 +995,7 @@ export class CodeGenerator {
|
|||||||
const isNewBlock = !block || forceNewBlock;
|
const isNewBlock = !block || forceNewBlock;
|
||||||
let codeIdx = this.target.code.length;
|
let codeIdx = this.target.code.length;
|
||||||
if (isNewBlock) {
|
if (isNewBlock) {
|
||||||
const n = ast.content.filter((c) => c.type !== ASTType.TSet).length;
|
const n = ast.content.filter((c) => !c.hasNoRepresentation).length;
|
||||||
let result: string | null = null;
|
let result: string | null = null;
|
||||||
if (n <= 1) {
|
if (n <= 1) {
|
||||||
for (let child of ast.content) {
|
for (let child of ast.content) {
|
||||||
@@ -993,15 +1009,15 @@ export class CodeGenerator {
|
|||||||
let index = 0;
|
let index = 0;
|
||||||
for (let i = 0, l = ast.content.length; i < l; i++) {
|
for (let i = 0, l = ast.content.length; i < l; i++) {
|
||||||
const child = ast.content[i];
|
const child = ast.content[i];
|
||||||
const isTSet = child.type === ASTType.TSet;
|
const forceNewBlock = !child.hasNoRepresentation;
|
||||||
const subCtx = createContext(ctx, {
|
const subCtx = createContext(ctx, {
|
||||||
block,
|
block,
|
||||||
index,
|
index,
|
||||||
forceNewBlock: !isTSet,
|
forceNewBlock,
|
||||||
isLast: ctx.isLast && i === l - 1,
|
isLast: ctx.isLast && i === l - 1,
|
||||||
});
|
});
|
||||||
this.compileAST(child, subCtx);
|
this.compileAST(child, subCtx);
|
||||||
if (!isTSet) {
|
if (forceNewBlock) {
|
||||||
index++;
|
index++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1104,7 +1120,7 @@ export class CodeGenerator {
|
|||||||
let value: string;
|
let value: string;
|
||||||
if (ast.defaultValue) {
|
if (ast.defaultValue) {
|
||||||
const defaultValue = toStringExpression(
|
const defaultValue = toStringExpression(
|
||||||
ctx.translate ? this.translate(ast.defaultValue) : ast.defaultValue
|
ctx.translate ? this.translate(ast.defaultValue, ctx.translationCtx) : ast.defaultValue
|
||||||
);
|
);
|
||||||
if (ast.value) {
|
if (ast.value) {
|
||||||
value = `withDefault(${expr}, ${defaultValue})`;
|
value = `withDefault(${expr}, ${defaultValue})`;
|
||||||
@@ -1139,9 +1155,15 @@ export class CodeGenerator {
|
|||||||
* "some-prop" "state" "'some-prop': ctx['state']"
|
* "some-prop" "state" "'some-prop': ctx['state']"
|
||||||
* "onClick.bind" "onClick" "onClick: bind(ctx, ctx['onClick'])"
|
* "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")) {
|
if (name.endsWith(".translate")) {
|
||||||
value = toStringExpression(this.translateFn(value));
|
const attrTranslationCtx = attrsTranslationCtx?.[name] || translationCtx;
|
||||||
|
value = toStringExpression(this.translateFn(value, attrTranslationCtx));
|
||||||
} else {
|
} else {
|
||||||
value = this.captureExpression(value);
|
value = this.captureExpression(value);
|
||||||
}
|
}
|
||||||
@@ -1156,15 +1178,21 @@ export class CodeGenerator {
|
|||||||
case "translate":
|
case "translate":
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new OwlError("Invalid prop suffix");
|
throw new OwlError(`Invalid prop suffix: ${suffix}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
name = /^[a-z_]+$/i.test(name) ? name : `'${name}'`;
|
||||||
return `${name}: ${value || undefined}`;
|
return `${name}: ${value || undefined}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
formatPropObject(obj: { [prop: string]: any }): string[] {
|
formatPropObject(
|
||||||
return Object.entries(obj).map(([k, v]) => this.formatProp(k, v));
|
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 {
|
getPropString(props: string[], dynProps: string | null): string {
|
||||||
@@ -1181,7 +1209,9 @@ export class CodeGenerator {
|
|||||||
let { block } = ctx;
|
let { block } = ctx;
|
||||||
// props
|
// props
|
||||||
const hasSlotsProp = "slots" in (ast.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
|
// slots
|
||||||
let slotDef: string = "";
|
let slotDef: string = "";
|
||||||
@@ -1205,7 +1235,13 @@ export class CodeGenerator {
|
|||||||
params.push(`__scope: "${scope}"`);
|
params.push(`__scope: "${scope}"`);
|
||||||
}
|
}
|
||||||
if (ast.slots[slotName].attrs) {
|
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(", ")}}`;
|
const slotInfo = `{${params.join(", ")}}`;
|
||||||
slotStr.push(`'${slotName}': ${slotInfo}`);
|
slotStr.push(`'${slotName}': ${slotInfo}`);
|
||||||
@@ -1323,16 +1359,17 @@ export class CodeGenerator {
|
|||||||
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
isMultiple = isMultiple || this.slotNames.has(ast.name);
|
||||||
this.slotNames.add(ast.name);
|
this.slotNames.add(ast.name);
|
||||||
}
|
}
|
||||||
const dynProps = ast.attrs ? ast.attrs["t-props"] : null;
|
const attrs = { ...ast.attrs };
|
||||||
if (ast.attrs) {
|
const dynProps = attrs["t-props"];
|
||||||
delete ast.attrs["t-props"];
|
delete attrs["t-props"];
|
||||||
}
|
|
||||||
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
let key = this.target.loopLevel ? `key${this.target.loopLevel}` : "key";
|
||||||
if (isMultiple) {
|
if (isMultiple) {
|
||||||
key = this.generateComponentKey(key);
|
key = this.generateComponentKey(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = ast.attrs ? this.formatPropObject(ast.attrs) : [];
|
const props = ast.attrs
|
||||||
|
? this.formatPropObject(attrs, ast.attrsTranslationCtx, ctx.translationCtx)
|
||||||
|
: [];
|
||||||
const scope = this.getPropString(props, dynProps);
|
const scope = this.getPropString(props, dynProps);
|
||||||
if (ast.defaultContent) {
|
if (ast.defaultContent) {
|
||||||
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
const name = this.compileInNewTarget("defaultContent", ast.defaultContent, ctx);
|
||||||
@@ -1365,6 +1402,15 @@ export class CodeGenerator {
|
|||||||
}
|
}
|
||||||
return null;
|
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 {
|
compileTPortal(ast: ASTTPortal, ctx: Context): string {
|
||||||
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
if (!this.staticDefs.find((d) => d.id === "Portal")) {
|
||||||
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
this.staticDefs.push({ id: "Portal", expr: `app.Portal` });
|
||||||
|
|||||||
@@ -27,15 +27,21 @@ export const enum ASTType {
|
|||||||
TSlot,
|
TSlot,
|
||||||
TCallBlock,
|
TCallBlock,
|
||||||
TTranslation,
|
TTranslation,
|
||||||
|
TTranslationContext,
|
||||||
TPortal,
|
TPortal,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTText {
|
export interface BaseAST {
|
||||||
|
type: ASTType;
|
||||||
|
hasNoRepresentation?: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ASTText extends BaseAST {
|
||||||
type: ASTType.Text;
|
type: ASTType.Text;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTComment {
|
export interface ASTComment extends BaseAST {
|
||||||
type: ASTType.Comment;
|
type: ASTType.Comment;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
@@ -51,11 +57,12 @@ interface TModelInfo {
|
|||||||
specialInitTargetAttr: string | null;
|
specialInitTargetAttr: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTDomNode {
|
export interface ASTDomNode extends BaseAST {
|
||||||
type: ASTType.DomNode;
|
type: ASTType.DomNode;
|
||||||
tag: string;
|
tag: string;
|
||||||
content: AST[];
|
content: AST[];
|
||||||
attrs: Attrs | null;
|
attrs: Attrs | null;
|
||||||
|
attrsTranslationCtx: Attrs | null;
|
||||||
ref: string | null;
|
ref: string | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
model: TModelInfo | null;
|
model: TModelInfo | null;
|
||||||
@@ -63,24 +70,24 @@ export interface ASTDomNode {
|
|||||||
ns: string | null;
|
ns: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTMulti {
|
export interface ASTMulti extends BaseAST {
|
||||||
type: ASTType.Multi;
|
type: ASTType.Multi;
|
||||||
content: AST[];
|
content: AST[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTEsc {
|
export interface ASTTEsc extends BaseAST {
|
||||||
type: ASTType.TEsc;
|
type: ASTType.TEsc;
|
||||||
expr: string;
|
expr: string;
|
||||||
defaultValue: string;
|
defaultValue: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTOut {
|
export interface ASTTOut extends BaseAST {
|
||||||
type: ASTType.TOut;
|
type: ASTType.TOut;
|
||||||
expr: string;
|
expr: string;
|
||||||
body: AST[] | null;
|
body: AST[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTif {
|
export interface ASTTif extends BaseAST {
|
||||||
type: ASTType.TIf;
|
type: ASTType.TIf;
|
||||||
condition: string;
|
condition: string;
|
||||||
content: AST;
|
content: AST;
|
||||||
@@ -88,15 +95,16 @@ export interface ASTTif {
|
|||||||
tElse: AST | null;
|
tElse: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTSet {
|
export interface ASTTSet extends BaseAST {
|
||||||
type: ASTType.TSet;
|
type: ASTType.TSet;
|
||||||
name: string;
|
name: string;
|
||||||
value: string | null; // value defined in attribute
|
value: string | null; // value defined in attribute
|
||||||
defaultValue: string | null; // value defined in body, if text
|
defaultValue: string | null; // value defined in body, if text
|
||||||
body: AST[] | null; // content of body if not text
|
body: AST[] | null; // content of body if not text
|
||||||
|
hasNoRepresentation: true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTForEach {
|
export interface ASTTForEach extends BaseAST {
|
||||||
type: ASTType.TForEach;
|
type: ASTType.TForEach;
|
||||||
collection: string;
|
collection: string;
|
||||||
elem: string;
|
elem: string;
|
||||||
@@ -109,13 +117,13 @@ export interface ASTTForEach {
|
|||||||
key: string | null;
|
key: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTKey {
|
export interface ASTTKey extends BaseAST {
|
||||||
type: ASTType.TKey;
|
type: ASTType.TKey;
|
||||||
expr: string;
|
expr: string;
|
||||||
content: AST;
|
content: AST;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTCall {
|
export interface ASTTCall extends BaseAST {
|
||||||
type: ASTType.TCall;
|
type: ASTType.TCall;
|
||||||
name: string;
|
name: string;
|
||||||
body: AST[] | null;
|
body: AST[] | null;
|
||||||
@@ -127,48 +135,57 @@ interface SlotDefinition {
|
|||||||
scope: string | null;
|
scope: string | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
attrs: Attrs | null;
|
attrs: Attrs | null;
|
||||||
|
attrsTranslationCtx: Attrs | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTComponent {
|
export interface ASTComponent extends BaseAST {
|
||||||
type: ASTType.TComponent;
|
type: ASTType.TComponent;
|
||||||
name: string;
|
name: string;
|
||||||
isDynamic: boolean;
|
isDynamic: boolean;
|
||||||
dynamicProps: string | null;
|
dynamicProps: string | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
props: { [name: string]: string } | null;
|
props: { [name: string]: string } | null;
|
||||||
|
propsTranslationCtx: { [name: string]: string } | null;
|
||||||
slots: { [name: string]: SlotDefinition } | null;
|
slots: { [name: string]: SlotDefinition } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTSlot {
|
export interface ASTSlot extends BaseAST {
|
||||||
type: ASTType.TSlot;
|
type: ASTType.TSlot;
|
||||||
name: string;
|
name: string;
|
||||||
attrs: Attrs | null;
|
attrs: Attrs | null;
|
||||||
|
attrsTranslationCtx: Attrs | null;
|
||||||
on: EventHandlers | null;
|
on: EventHandlers | null;
|
||||||
defaultContent: AST | null;
|
defaultContent: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTCallBlock {
|
export interface ASTTCallBlock extends BaseAST {
|
||||||
type: ASTType.TCallBlock;
|
type: ASTType.TCallBlock;
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTDebug {
|
export interface ASTDebug extends BaseAST {
|
||||||
type: ASTType.TDebug;
|
type: ASTType.TDebug;
|
||||||
content: AST | null;
|
content: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTLog {
|
export interface ASTLog extends BaseAST {
|
||||||
type: ASTType.TLog;
|
type: ASTType.TLog;
|
||||||
expr: string;
|
expr: string;
|
||||||
content: AST | null;
|
content: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTranslation {
|
export interface ASTTranslation extends BaseAST {
|
||||||
type: ASTType.TTranslation;
|
type: ASTType.TTranslation;
|
||||||
content: AST | null;
|
content: AST | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASTTPortal {
|
export interface ASTTranslationContext extends BaseAST {
|
||||||
|
type: ASTType.TTranslationContext;
|
||||||
|
content: AST | null;
|
||||||
|
translationCtx: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ASTTPortal extends BaseAST {
|
||||||
type: ASTType.TPortal;
|
type: ASTType.TPortal;
|
||||||
target: string;
|
target: string;
|
||||||
content: AST;
|
content: AST;
|
||||||
@@ -192,6 +209,7 @@ export type AST =
|
|||||||
| ASTLog
|
| ASTLog
|
||||||
| ASTDebug
|
| ASTDebug
|
||||||
| ASTTranslation
|
| ASTTranslation
|
||||||
|
| ASTTranslationContext
|
||||||
| ASTTPortal;
|
| ASTTPortal;
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -241,10 +259,11 @@ function parseNode(node: Node, ctx: ParsingContext): AST | null {
|
|||||||
parseTPortal(node, ctx) ||
|
parseTPortal(node, ctx) ||
|
||||||
parseTCall(node, ctx) ||
|
parseTCall(node, ctx) ||
|
||||||
parseTCallBlock(node, ctx) ||
|
parseTCallBlock(node, ctx) ||
|
||||||
|
parseTTranslation(node, ctx) ||
|
||||||
|
parseTTranslationContext(node, ctx) ||
|
||||||
|
parseTKey(node, ctx) ||
|
||||||
parseTEscNode(node, ctx) ||
|
parseTEscNode(node, ctx) ||
|
||||||
parseTOutNode(node, ctx) ||
|
parseTOutNode(node, ctx) ||
|
||||||
parseTKey(node, ctx) ||
|
|
||||||
parseTTranslation(node, ctx) ||
|
|
||||||
parseTSlot(node, ctx) ||
|
parseTSlot(node, ctx) ||
|
||||||
parseComponent(node, ctx) ||
|
parseComponent(node, ctx) ||
|
||||||
parseDOMNode(node, ctx) ||
|
parseDOMNode(node, ctx) ||
|
||||||
@@ -321,20 +340,30 @@ function parseTCustom(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
|
function parseTDebugLog(node: Element, ctx: ParsingContext): AST | null {
|
||||||
if (node.hasAttribute("t-debug")) {
|
if (node.hasAttribute("t-debug")) {
|
||||||
node.removeAttribute("t-debug");
|
node.removeAttribute("t-debug");
|
||||||
return {
|
const content = parseNode(node, ctx);
|
||||||
|
const ast: ASTDebug = {
|
||||||
type: ASTType.TDebug,
|
type: ASTType.TDebug,
|
||||||
content: parseNode(node, ctx),
|
content,
|
||||||
};
|
};
|
||||||
|
if (content?.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node.hasAttribute("t-log")) {
|
if (node.hasAttribute("t-log")) {
|
||||||
const expr = node.getAttribute("t-log")!;
|
const expr = node.getAttribute("t-log")!;
|
||||||
node.removeAttribute("t-log");
|
node.removeAttribute("t-log");
|
||||||
return {
|
const content = parseNode(node, ctx);
|
||||||
|
const ast: ASTLog = {
|
||||||
type: ASTType.TLog,
|
type: ASTType.TLog,
|
||||||
expr,
|
expr,
|
||||||
content: parseNode(node, ctx),
|
content,
|
||||||
};
|
};
|
||||||
|
if (content?.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -368,6 +397,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
|
|
||||||
const nodeAttrsNames = node.getAttributeNames();
|
const nodeAttrsNames = node.getAttributeNames();
|
||||||
let attrs: ASTDomNode["attrs"] = null;
|
let attrs: ASTDomNode["attrs"] = null;
|
||||||
|
let attrsTranslationCtx: ASTDomNode["attrsTranslationCtx"] = null;
|
||||||
let on: EventHandlers | null = null;
|
let on: EventHandlers | null = null;
|
||||||
let model: TModelInfo | null = null;
|
let model: TModelInfo | null = null;
|
||||||
|
|
||||||
@@ -428,6 +458,10 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
throw new OwlError(`Invalid attribute: '${attr}'`);
|
throw new OwlError(`Invalid attribute: '${attr}'`);
|
||||||
} else if (attr === "xmlns") {
|
} else if (attr === "xmlns") {
|
||||||
ns = value;
|
ns = value;
|
||||||
|
} else if (attr.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attr.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
} else if (attr !== "t-name") {
|
} else if (attr !== "t-name") {
|
||||||
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
if (attr.startsWith("t-") && !attr.startsWith("t-att")) {
|
||||||
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
throw new OwlError(`Unknown QWeb directive: '${attr}'`);
|
||||||
@@ -450,6 +484,7 @@ function parseDOMNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
tag: tagName,
|
tag: tagName,
|
||||||
dynamicTag,
|
dynamicTag,
|
||||||
attrs,
|
attrs,
|
||||||
|
attrsTranslationCtx,
|
||||||
on,
|
on,
|
||||||
ref,
|
ref,
|
||||||
content: children,
|
content: children,
|
||||||
@@ -579,11 +614,19 @@ function parseTKey(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
}
|
}
|
||||||
const key = node.getAttribute("t-key")!;
|
const key = node.getAttribute("t-key")!;
|
||||||
node.removeAttribute("t-key");
|
node.removeAttribute("t-key");
|
||||||
const body = parseNode(node, ctx);
|
const content = parseNode(node, ctx);
|
||||||
if (!body) {
|
if (!content) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { type: ASTType.TKey, expr: key, content: body };
|
const ast: ASTTKey = {
|
||||||
|
type: ASTType.TKey,
|
||||||
|
expr: key,
|
||||||
|
content,
|
||||||
|
};
|
||||||
|
if (content.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -609,7 +652,15 @@ function parseTCall(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
if (ast && ast.type === ASTType.TComponent) {
|
if (ast && ast.type === ASTType.TComponent) {
|
||||||
return {
|
return {
|
||||||
...ast,
|
...ast,
|
||||||
slots: { default: { content: tcall, scope: null, on: null, attrs: null } },
|
slots: {
|
||||||
|
default: {
|
||||||
|
content: tcall,
|
||||||
|
scope: null,
|
||||||
|
on: null,
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -697,7 +748,7 @@ function parseTSetNode(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
if (node.textContent !== node.innerHTML) {
|
if (node.textContent !== node.innerHTML) {
|
||||||
body = parseChildren(node, ctx);
|
body = parseChildren(node, ctx);
|
||||||
}
|
}
|
||||||
return { type: ASTType.TSet, name, value, defaultValue, body };
|
return { type: ASTType.TSet, name, value, defaultValue, body, hasNoRepresentation: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -744,9 +795,14 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
let on: ASTComponent["on"] = null;
|
let on: ASTComponent["on"] = null;
|
||||||
|
|
||||||
let props: ASTComponent["props"] = null;
|
let props: ASTComponent["props"] = null;
|
||||||
|
let propsTranslationCtx: ASTComponent["propsTranslationCtx"] = null;
|
||||||
for (let name of node.getAttributeNames()) {
|
for (let name of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(name)!;
|
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-")) {
|
if (name.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[name.slice(5)] = value;
|
on[name.slice(5)] = value;
|
||||||
@@ -794,12 +850,17 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
const slotAst = parseNode(slotNode, ctx);
|
const slotAst = parseNode(slotNode, ctx);
|
||||||
let on: SlotDefinition["on"] = null;
|
let on: SlotDefinition["on"] = null;
|
||||||
let attrs: Attrs | null = null;
|
let attrs: Attrs | null = null;
|
||||||
|
let attrsTranslationCtx: Attrs | null = null;
|
||||||
let scope: string | null = null;
|
let scope: string | null = null;
|
||||||
for (let attributeName of slotNode.getAttributeNames()) {
|
for (let attributeName of slotNode.getAttributeNames()) {
|
||||||
const value = slotNode.getAttribute(attributeName)!;
|
const value = slotNode.getAttribute(attributeName)!;
|
||||||
if (attributeName === "t-slot-scope") {
|
if (attributeName === "t-slot-scope") {
|
||||||
scope = value;
|
scope = value;
|
||||||
continue;
|
continue;
|
||||||
|
} else if (attributeName.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attributeName.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
} else if (attributeName.startsWith("t-on-")) {
|
} else if (attributeName.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attributeName.slice(5)] = value;
|
on[attributeName.slice(5)] = value;
|
||||||
@@ -809,7 +870,7 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
slots = slots || {};
|
slots = slots || {};
|
||||||
slots[name] = { content: slotAst, on, attrs, scope };
|
slots[name] = { content: slotAst, on, attrs, attrsTranslationCtx, scope };
|
||||||
}
|
}
|
||||||
|
|
||||||
// default slot
|
// default slot
|
||||||
@@ -817,10 +878,25 @@ function parseComponent(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
slots = slots || {};
|
slots = slots || {};
|
||||||
// t-set-slot="default" has priority over content
|
// t-set-slot="default" has priority over content
|
||||||
if (defaultContent && !slots.default) {
|
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 +910,17 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
const name = node.getAttribute("t-slot")!;
|
const name = node.getAttribute("t-slot")!;
|
||||||
node.removeAttribute("t-slot");
|
node.removeAttribute("t-slot");
|
||||||
let attrs: Attrs | null = null;
|
let attrs: Attrs | null = null;
|
||||||
|
let attrsTranslationCtx: Attrs | null = null;
|
||||||
let on: ASTComponent["on"] = null;
|
let on: ASTComponent["on"] = null;
|
||||||
for (let attributeName of node.getAttributeNames()) {
|
for (let attributeName of node.getAttributeNames()) {
|
||||||
const value = node.getAttribute(attributeName)!;
|
const value = node.getAttribute(attributeName)!;
|
||||||
if (attributeName.startsWith("t-on-")) {
|
if (attributeName.startsWith("t-on-")) {
|
||||||
on = on || {};
|
on = on || {};
|
||||||
on[attributeName.slice(5)] = value;
|
on[attributeName.slice(5)] = value;
|
||||||
|
} else if (attributeName.startsWith("t-translation-context-")) {
|
||||||
|
const attrName = attributeName.slice(22);
|
||||||
|
attrsTranslationCtx = attrsTranslationCtx || {};
|
||||||
|
attrsTranslationCtx[attrName] = value;
|
||||||
} else {
|
} else {
|
||||||
attrs = attrs || {};
|
attrs = attrs || {};
|
||||||
attrs[attributeName] = value;
|
attrs[attributeName] = value;
|
||||||
@@ -849,20 +930,65 @@ function parseTSlot(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name,
|
name,
|
||||||
attrs,
|
attrs,
|
||||||
|
attrsTranslationCtx,
|
||||||
on,
|
on,
|
||||||
defaultContent: parseChildNodes(node, ctx),
|
defaultContent: parseChildNodes(node, ctx),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Translation
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function wrapInTTranslationAST(r: AST | null) {
|
||||||
|
const ast: ASTTranslation = { type: ASTType.TTranslation, content: r };
|
||||||
|
if (r?.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
function parseTTranslation(node: Element, ctx: ParsingContext): AST | null {
|
||||||
if (node.getAttribute("t-translation") !== "off") {
|
if (node.getAttribute("t-translation") !== "off") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
node.removeAttribute("t-translation");
|
node.removeAttribute("t-translation");
|
||||||
return {
|
const result = parseNode(node, ctx);
|
||||||
type: ASTType.TTranslation,
|
if (result?.type === ASTType.Multi) {
|
||||||
content: parseNode(node, ctx),
|
const children = result.content.map(wrapInTTranslationAST);
|
||||||
|
return makeASTMulti(children);
|
||||||
|
}
|
||||||
|
return wrapInTTranslationAST(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
// Translation Context
|
||||||
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function wrapInTTranslationContextAST(r: AST | null, translationCtx: string) {
|
||||||
|
const ast: ASTTranslationContext = {
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
content: r,
|
||||||
|
translationCtx,
|
||||||
};
|
};
|
||||||
|
if (r?.hasNoRepresentation) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseTTranslationContext(node: Element, ctx: ParsingContext): AST | null {
|
||||||
|
const translationCtx = node.getAttribute("t-translation-context");
|
||||||
|
if (!translationCtx) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
node.removeAttribute("t-translation-context");
|
||||||
|
const result = parseNode(node, ctx);
|
||||||
|
if (result?.type === ASTType.Multi) {
|
||||||
|
const children = result.content.map((c) => wrapInTTranslationContextAST(c, translationCtx));
|
||||||
|
return makeASTMulti(children);
|
||||||
|
}
|
||||||
|
return wrapInTTranslationContextAST(result, translationCtx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -911,6 +1037,14 @@ function parseChildren(node: Element, ctx: ParsingContext): AST[] {
|
|||||||
return children;
|
return children;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeASTMulti(children: AST[]) {
|
||||||
|
const ast: ASTMulti = { type: ASTType.Multi, content: children };
|
||||||
|
if (children.every((c) => c.hasNoRepresentation)) {
|
||||||
|
ast.hasNoRepresentation = true;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse all the child nodes of a given node and return an ast if possible.
|
* Parse all the child nodes of a given node and return an ast if possible.
|
||||||
* In the case there are multiple children, they are wrapped in a astmulti.
|
* In the case there are multiple children, they are wrapped in a astmulti.
|
||||||
@@ -923,7 +1057,7 @@ function parseChildNodes(node: Element, ctx: ParsingContext): AST | null {
|
|||||||
case 1:
|
case 1:
|
||||||
return children[0];
|
return children[0];
|
||||||
default:
|
default:
|
||||||
return { type: ASTType.Multi, content: children };
|
return makeASTMulti(children);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -29,15 +29,6 @@ export interface AppConfig<P, E> extends TemplateSetConfig, RootConfig<P, E> {
|
|||||||
|
|
||||||
let hasBeenLogged = false;
|
let hasBeenLogged = false;
|
||||||
|
|
||||||
export const DEV_MSG = () => {
|
|
||||||
const hash = (window as any).owl ? (window as any).owl.__info__.hash : "master";
|
|
||||||
|
|
||||||
return `Owl is running in 'dev' mode.
|
|
||||||
|
|
||||||
This is not suitable for production use.
|
|
||||||
See https://github.com/odoo/owl/blob/${hash}/doc/reference/app.md#configuration for more information.`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const apps = new Set<App>();
|
const apps = new Set<App>();
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
@@ -88,7 +79,7 @@ export class App<
|
|||||||
}
|
}
|
||||||
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
this.warnIfNoStaticProps = config.warnIfNoStaticProps || false;
|
||||||
if (this.dev && !config.test && !hasBeenLogged) {
|
if (this.dev && !config.test && !hasBeenLogged) {
|
||||||
console.info(DEV_MSG());
|
console.info(`Owl is running in 'dev' mode.`);
|
||||||
hasBeenLogged = true;
|
hasBeenLogged = true;
|
||||||
}
|
}
|
||||||
const env = config.env || {};
|
const env = config.env || {};
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export type TaskContext = { isCancelled: boolean; cancel: () => void; meta: Record<string, any> };
|
||||||
|
|
||||||
|
export const taskContextStack: TaskContext[] = [];
|
||||||
|
|
||||||
|
export function getTaskContext() {
|
||||||
|
return taskContextStack[taskContextStack.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeTaskContext(): TaskContext {
|
||||||
|
let isCancelled = false;
|
||||||
|
return {
|
||||||
|
get isCancelled() {
|
||||||
|
return isCancelled;
|
||||||
|
},
|
||||||
|
cancel() {
|
||||||
|
isCancelled = true;
|
||||||
|
},
|
||||||
|
meta: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTaskContext(ctx?: TaskContext) {
|
||||||
|
ctx ??= makeTaskContext();
|
||||||
|
taskContextStack.push(ctx);
|
||||||
|
return {
|
||||||
|
ctx,
|
||||||
|
cleanup: () => {
|
||||||
|
taskContextStack.pop();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushTaskContext(context: TaskContext) {
|
||||||
|
taskContextStack.push(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function popTaskContext() {
|
||||||
|
taskContextStack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function taskEffect(fn: Function) {
|
||||||
|
const { ctx, cleanup } = useTaskContext();
|
||||||
|
fn();
|
||||||
|
cleanup();
|
||||||
|
return ctx;
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
|
import { OwlError } from "../common/owl_error";
|
||||||
|
import { Atom, ExecutionContext } from "../common/types";
|
||||||
import type { App, Env } from "./app";
|
import type { App, Env } from "./app";
|
||||||
import { BDom, VNode } from "./blockdom";
|
import { BDom, VNode } from "./blockdom";
|
||||||
|
import { makeTaskContext, TaskContext } from "./cancellableContext";
|
||||||
import { Component, ComponentConstructor, Props } from "./component";
|
import { Component, ComponentConstructor, Props } from "./component";
|
||||||
import { fibersInError } from "./error_handling";
|
import { fibersInError } from "./error_handling";
|
||||||
import { OwlError } from "../common/owl_error";
|
|
||||||
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
|
import { Fiber, makeChildFiber, makeRootFiber, MountFiber, MountOptions } from "./fibers";
|
||||||
import { clearReactivesForCallback, getSubscriptions, reactive, targets } from "./reactivity";
|
import { addAtomToContext, reactive, targets, withoutReactivity } from "./reactivity";
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
import { batched, Callback } from "./utils";
|
|
||||||
|
|
||||||
let currentNode: ComponentNode | null = null;
|
let currentNode: ComponentNode | null = null;
|
||||||
|
|
||||||
@@ -42,7 +43,7 @@ function applyDefaultProps<P extends object>(props: P, defaultProps: Partial<P>)
|
|||||||
// Integration with reactivity system (useState)
|
// Integration with reactivity system (useState)
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
|
|
||||||
const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
// const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
||||||
/**
|
/**
|
||||||
* Creates a reactive object that will be observed by the current component.
|
* Creates a reactive object that will be observed by the current component.
|
||||||
* Reading data from the returned object (eg during rendering) will cause the
|
* Reading data from the returned object (eg during rendering) will cause the
|
||||||
@@ -54,15 +55,7 @@ const batchedRenderFunctions = new WeakMap<ComponentNode, Callback>();
|
|||||||
* @see reactive
|
* @see reactive
|
||||||
*/
|
*/
|
||||||
export function useState<T extends object>(state: T): T {
|
export function useState<T extends object>(state: T): T {
|
||||||
const node = getCurrent();
|
return reactive(state);
|
||||||
let render = batchedRenderFunctions.get(node)!;
|
|
||||||
if (!render) {
|
|
||||||
render = batched(node.render.bind(node, false));
|
|
||||||
batchedRenderFunctions.set(node, render);
|
|
||||||
// manual implementation of onWillDestroy to break cyclic dependency
|
|
||||||
node.willDestroy.push(clearReactivesForCallback.bind(null, render));
|
|
||||||
}
|
|
||||||
return reactive(state, render);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------------------------------------------
|
// -----------------------------------------------------------------------------
|
||||||
@@ -96,6 +89,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
|||||||
willPatch: LifecycleHook[] = [];
|
willPatch: LifecycleHook[] = [];
|
||||||
patched: LifecycleHook[] = [];
|
patched: LifecycleHook[] = [];
|
||||||
willDestroy: LifecycleHook[] = [];
|
willDestroy: LifecycleHook[] = [];
|
||||||
|
taskContext: TaskContext;
|
||||||
|
executionContext: ExecutionContext;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
C: ComponentConstructor<P, E>,
|
C: ComponentConstructor<P, E>,
|
||||||
@@ -109,6 +104,15 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
|||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
this.props = props;
|
this.props = props;
|
||||||
this.parentKey = parentKey;
|
this.parentKey = parentKey;
|
||||||
|
this.taskContext = makeTaskContext();
|
||||||
|
this.executionContext = {
|
||||||
|
meta: this,
|
||||||
|
update: () => {
|
||||||
|
this.render(false);
|
||||||
|
},
|
||||||
|
onReadAtom: (atom: Atom) => addAtomToContext(atom, this.executionContext),
|
||||||
|
atoms: new Set<Atom>(),
|
||||||
|
};
|
||||||
const defaultProps = C.defaultProps;
|
const defaultProps = C.defaultProps;
|
||||||
props = Object.assign({}, props);
|
props = Object.assign({}, props);
|
||||||
if (defaultProps) {
|
if (defaultProps) {
|
||||||
@@ -116,16 +120,18 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
|||||||
}
|
}
|
||||||
const env = (parent && parent.childEnv) || app.env;
|
const env = (parent && parent.childEnv) || app.env;
|
||||||
this.childEnv = env;
|
this.childEnv = env;
|
||||||
for (const key in props) {
|
// for (const key in props) {
|
||||||
const prop = props[key];
|
// const prop = props[key];
|
||||||
if (prop && typeof prop === "object" && targets.has(prop)) {
|
// if (prop && typeof prop === "object" && targets.has(prop)) {
|
||||||
props[key] = useState(prop);
|
// props[key] = useState(prop);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
this.component = new C(props, env, this);
|
this.component = new C(props, env, this);
|
||||||
const ctx = Object.assign(Object.create(this.component), { this: this.component });
|
const ctx = Object.assign(Object.create(this.component), { this: this.component });
|
||||||
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
|
this.renderFn = app.getTemplate(C.template).bind(this.component, ctx, this);
|
||||||
this.component.setup();
|
withoutReactivity(() => {
|
||||||
|
this.component.setup();
|
||||||
|
});
|
||||||
currentNode = null;
|
currentNode = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,7 +148,11 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
|||||||
}
|
}
|
||||||
const component = this.component;
|
const component = this.component;
|
||||||
try {
|
try {
|
||||||
await Promise.all(this.willStart.map((f) => f.call(component)));
|
let prom: Promise<any[]>;
|
||||||
|
withoutReactivity(() => {
|
||||||
|
prom = Promise.all(this.willStart.map((f) => f.call(component)));
|
||||||
|
});
|
||||||
|
await prom!;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
this.app.handleError({ node: this, error: e });
|
this.app.handleError({ node: this, error: e });
|
||||||
return;
|
return;
|
||||||
@@ -258,15 +268,18 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
|||||||
}
|
}
|
||||||
|
|
||||||
currentNode = this;
|
currentNode = this;
|
||||||
for (const key in props) {
|
// for (const key in props) {
|
||||||
const prop = props[key];
|
// const prop = props[key];
|
||||||
if (prop && typeof prop === "object" && targets.has(prop)) {
|
// if (prop && typeof prop === "object" && targets.has(prop)) {
|
||||||
props[key] = useState(prop);
|
// props[key] = useState(prop);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
currentNode = null;
|
currentNode = null;
|
||||||
const prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
|
let prom: Promise<any[]>;
|
||||||
await prom;
|
withoutReactivity(() => {
|
||||||
|
prom = Promise.all(this.willUpdateProps.map((f) => f.call(component, props)));
|
||||||
|
});
|
||||||
|
await prom!;
|
||||||
if (fiber !== this.fiber) {
|
if (fiber !== this.fiber) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -384,8 +397,8 @@ export class ComponentNode<P extends Props = any, E = any> implements VNode<Comp
|
|||||||
return this.component.constructor.name;
|
return this.component.constructor.name;
|
||||||
}
|
}
|
||||||
|
|
||||||
get subscriptions(): ReturnType<typeof getSubscriptions> {
|
// get subscriptions(): ReturnType<typeof getSubscriptions> {
|
||||||
const render = batchedRenderFunctions.get(this);
|
// const render = batchedRenderFunctions.get(this);
|
||||||
return render ? getSubscriptions(render) : [];
|
// return render ? getSubscriptions(render) : [];
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { ExecutionContext } from "../common/types";
|
||||||
|
|
||||||
|
export const executionContexts: ExecutionContext[] = [];
|
||||||
|
(window as any).executionContexts = executionContexts;
|
||||||
|
// export const scheduledContexts: Set<ExecutionContext> = new Set();
|
||||||
|
|
||||||
|
export function getExecutionContext() {
|
||||||
|
return executionContexts[executionContexts.length - 1];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pushExecutionContext(context: ExecutionContext) {
|
||||||
|
executionContexts.push(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function popExecutionContext() {
|
||||||
|
executionContexts.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
// export function makeExecutionContext({ update, meta }: { update: () => void; meta?: any }) {
|
||||||
|
// const executionContext: ExecutionContext = {
|
||||||
|
// update,
|
||||||
|
// atoms: new Set(),
|
||||||
|
// meta: meta || {},
|
||||||
|
// };
|
||||||
|
// return executionContext;
|
||||||
|
// }
|
||||||
@@ -3,6 +3,8 @@ import type { ComponentNode } from "./component_node";
|
|||||||
import { fibersInError } from "./error_handling";
|
import { fibersInError } from "./error_handling";
|
||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "../common/owl_error";
|
||||||
import { STATUS } from "./status";
|
import { STATUS } from "./status";
|
||||||
|
import { popTaskContext, pushTaskContext } from "./cancellableContext";
|
||||||
|
import { popExecutionContext, pushExecutionContext } from "./executionContext";
|
||||||
|
|
||||||
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
export function makeChildFiber(node: ComponentNode, parent: Fiber): Fiber {
|
||||||
let current = node.fiber;
|
let current = node.fiber;
|
||||||
@@ -30,6 +32,13 @@ export function makeRootFiber(node: ComponentNode): Fiber {
|
|||||||
fibersInError.delete(current);
|
fibersInError.delete(current);
|
||||||
fibersInError.delete(root);
|
fibersInError.delete(root);
|
||||||
current.appliedToDom = false;
|
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;
|
return current;
|
||||||
}
|
}
|
||||||
@@ -126,12 +135,16 @@ export class Fiber {
|
|||||||
const node = this.node;
|
const node = this.node;
|
||||||
const root = this.root;
|
const root = this.root;
|
||||||
if (root) {
|
if (root) {
|
||||||
|
pushTaskContext(node.taskContext);
|
||||||
|
pushExecutionContext(node.executionContext);
|
||||||
try {
|
try {
|
||||||
(this.bdom as any) = true;
|
(this.bdom as any) = true;
|
||||||
this.bdom = node.renderFn();
|
this.bdom = node.renderFn();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
node.app.handleError({ node, error: e });
|
node.app.handleError({ node, error: e });
|
||||||
}
|
}
|
||||||
|
popExecutionContext();
|
||||||
|
popTaskContext();
|
||||||
root.setCounter(root.counter - 1);
|
root.setCounter(root.counter - 1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,6 +165,7 @@ export class RootFiber extends Fiber {
|
|||||||
const node = this.node;
|
const node = this.node;
|
||||||
this.locked = true;
|
this.locked = true;
|
||||||
let current: Fiber | undefined = undefined;
|
let current: Fiber | undefined = undefined;
|
||||||
|
let mountedFibers = this.mounted;
|
||||||
try {
|
try {
|
||||||
// Step 1: calling all willPatch lifecycle hooks
|
// Step 1: calling all willPatch lifecycle hooks
|
||||||
for (current of this.willPatch) {
|
for (current of this.willPatch) {
|
||||||
@@ -173,7 +187,6 @@ export class RootFiber extends Fiber {
|
|||||||
this.locked = false;
|
this.locked = false;
|
||||||
|
|
||||||
// Step 4: calling all mounted lifecycle hooks
|
// Step 4: calling all mounted lifecycle hooks
|
||||||
let mountedFibers = this.mounted;
|
|
||||||
while ((current = mountedFibers.pop())) {
|
while ((current = mountedFibers.pop())) {
|
||||||
current = current;
|
current = current;
|
||||||
if (current.appliedToDom) {
|
if (current.appliedToDom) {
|
||||||
@@ -194,6 +207,15 @@ export class RootFiber extends Fiber {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} 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;
|
this.locked = false;
|
||||||
node.app.handleError({ fiber: current || this, error: e });
|
node.app.handleError({ fiber: current || this, error: e });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Env } from "./app";
|
import type { Env } from "./app";
|
||||||
import { getCurrent } from "./component_node";
|
import { getCurrent } from "./component_node";
|
||||||
|
import { popExecutionContext, pushExecutionContext } from "./executionContext";
|
||||||
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
|
import { onMounted, onPatched, onWillUnmount } from "./lifecycle_hooks";
|
||||||
import { inOwnerDocument } from "./utils";
|
import { inOwnerDocument } from "./utils";
|
||||||
|
|
||||||
@@ -86,22 +87,43 @@ export function useEffect<T extends unknown[]>(
|
|||||||
effect: Effect<T>,
|
effect: Effect<T>,
|
||||||
computeDependencies: () => [...T] = () => [NaN] as never
|
computeDependencies: () => [...T] = () => [NaN] as never
|
||||||
) {
|
) {
|
||||||
|
const context = getCurrent().component.__owl__.executionContext;
|
||||||
let cleanup: (() => void) | void;
|
let cleanup: (() => void) | void;
|
||||||
let dependencies: T;
|
let dependencies: T;
|
||||||
|
|
||||||
|
const runEffect = () => {
|
||||||
|
pushExecutionContext(context);
|
||||||
|
try {
|
||||||
|
cleanup = effect(...dependencies);
|
||||||
|
} finally {
|
||||||
|
popExecutionContext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const computeDependenciesWithContext = () => {
|
||||||
|
pushExecutionContext(context);
|
||||||
|
let r: any;
|
||||||
|
try {
|
||||||
|
r = computeDependencies();
|
||||||
|
} finally {
|
||||||
|
popExecutionContext();
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
dependencies = computeDependencies();
|
dependencies = computeDependenciesWithContext();
|
||||||
cleanup = effect(...dependencies);
|
runEffect();
|
||||||
});
|
});
|
||||||
|
|
||||||
onPatched(() => {
|
onPatched(() => {
|
||||||
const newDeps = computeDependencies();
|
const newDeps = computeDependenciesWithContext();
|
||||||
const shouldReapply = newDeps.some((val, i) => val !== dependencies[i]);
|
const shouldReapply = newDeps.some((val: any, i: number) => val !== dependencies[i]);
|
||||||
if (shouldReapply) {
|
if (shouldReapply) {
|
||||||
dependencies = newDeps;
|
dependencies = newDeps;
|
||||||
if (cleanup) {
|
if (cleanup) {
|
||||||
cleanup();
|
cleanup();
|
||||||
}
|
}
|
||||||
cleanup = effect(...dependencies);
|
runEffect();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -39,9 +39,9 @@ export { Component } from "./component";
|
|||||||
export type { ComponentConstructor } from "./component";
|
export type { ComponentConstructor } from "./component";
|
||||||
export { useComponent, useState } from "./component_node";
|
export { useComponent, useState } from "./component_node";
|
||||||
export { status } from "./status";
|
export { status } from "./status";
|
||||||
export { reactive, markRaw, toRaw } from "./reactivity";
|
export { reactive, markRaw, toRaw, effect, withoutReactivity } from "./reactivity";
|
||||||
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
export { useEffect, useEnv, useExternalListener, useRef, useChildSubEnv, useSubEnv } from "./hooks";
|
||||||
export { batched, EventBus, whenReady, loadFile, markup } from "./utils";
|
export { batched, EventBus, htmlEscape, whenReady, loadFile, markup } from "./utils";
|
||||||
export {
|
export {
|
||||||
onWillStart,
|
onWillStart,
|
||||||
onMounted,
|
onMounted,
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import type { Callback } from "./utils";
|
|
||||||
import { OwlError } from "../common/owl_error";
|
import { OwlError } from "../common/owl_error";
|
||||||
|
import { ExecutionContext, Atom, DerivedAtom, OldValue } from "../common/types";
|
||||||
|
import { getExecutionContext, popExecutionContext, pushExecutionContext } from "./executionContext";
|
||||||
|
|
||||||
// Special key to subscribe to, to be notified of key creation/deletion
|
// Special key to subscribe to, to be notified of key creation/deletion
|
||||||
const KEYCHANGES = Symbol("Key changes");
|
const KEYCHANGES = Symbol("Key changes");
|
||||||
// Used to specify the absence of a callback, can be used as WeakMap key but
|
|
||||||
// should only be used as a sentinel value and never called.
|
|
||||||
const NO_CALLBACK = () => {
|
|
||||||
throw new Error("Called NO_CALLBACK. Owl is broken, please report this to the maintainers.");
|
|
||||||
};
|
|
||||||
|
|
||||||
// The following types only exist to signify places where objects are expected
|
// The following types only exist to signify places where objects are expected
|
||||||
// to be reactive or not, they provide no type checking benefit over "object"
|
// to be reactive or not, they provide no type checking benefit over "object"
|
||||||
@@ -55,8 +51,8 @@ function canBeMadeReactive(value: any): boolean {
|
|||||||
* @param value the value make reactive
|
* @param value the value make reactive
|
||||||
* @returns a reactive for the given object when possible, the original otherwise
|
* @returns a reactive for the given object when possible, the original otherwise
|
||||||
*/
|
*/
|
||||||
function possiblyReactive(val: any, cb: Callback) {
|
function possiblyReactive(val: any) {
|
||||||
return canBeMadeReactive(val) ? reactive(val, cb) : val;
|
return canBeMadeReactive(val) ? reactive(val) : val;
|
||||||
}
|
}
|
||||||
|
|
||||||
const skipped = new WeakSet<Target>();
|
const skipped = new WeakSet<Target>();
|
||||||
@@ -81,7 +77,37 @@ export function toRaw<T extends Target, U extends Reactive<T>>(value: U | T): T
|
|||||||
return targets.has(value) ? (targets.get(value) as T) : value;
|
return targets.has(value) ? (targets.get(value) as T) : value;
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callback>>>();
|
const targetToKeysToAtomItem = new WeakMap<Target, Map<PropertyKey, Atom>>();
|
||||||
|
const scheduledAtoms = new Set<Atom>();
|
||||||
|
|
||||||
|
function makeAtom(getValue: () => any): Atom {
|
||||||
|
const atom: Atom = {
|
||||||
|
executionContexts: new Set<ExecutionContext>(),
|
||||||
|
dependents: new Set<Atom>(),
|
||||||
|
// getValue,
|
||||||
|
};
|
||||||
|
return atom;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTargetKeyAtom(target: Target, key: PropertyKey): Atom {
|
||||||
|
let keyToAtomItem: Map<PropertyKey, Atom> = targetToKeysToAtomItem.get(target)!;
|
||||||
|
if (!keyToAtomItem) {
|
||||||
|
keyToAtomItem = new Map();
|
||||||
|
targetToKeysToAtomItem.set(target, keyToAtomItem);
|
||||||
|
}
|
||||||
|
let atom = keyToAtomItem.get(key)!;
|
||||||
|
if (!atom) {
|
||||||
|
atom = makeAtom(() => Reflect.get(target, key));
|
||||||
|
keyToAtomItem.set(key, atom);
|
||||||
|
}
|
||||||
|
return atom;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addAtomToContext(atom: Atom, executionContext: ExecutionContext) {
|
||||||
|
executionContext.atoms.add(atom);
|
||||||
|
atom.executionContexts.add(executionContext);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observes a given key on a target with an callback. The callback will be
|
* Observes a given key on a target with an callback. The callback will be
|
||||||
* called when the given key changes on the target.
|
* called when the given key changes on the target.
|
||||||
@@ -91,23 +117,73 @@ const targetToKeysToCallbacks = new WeakMap<Target, Map<PropertyKey, Set<Callbac
|
|||||||
* or deletion)
|
* or deletion)
|
||||||
* @param callback the function to call when the key changes
|
* @param callback the function to call when the key changes
|
||||||
*/
|
*/
|
||||||
function observeTargetKey(target: Target, key: PropertyKey, callback: Callback): void {
|
function onReadTargetKey(target: Target, key: PropertyKey, receiver: any): void {
|
||||||
if (callback === NO_CALLBACK) {
|
const executionContext = getExecutionContext();
|
||||||
return;
|
executionContext?.onReadAtom(getTargetKeyAtom(target, key));
|
||||||
}
|
|
||||||
if (!targetToKeysToCallbacks.get(target)) {
|
|
||||||
targetToKeysToCallbacks.set(target, new Map());
|
|
||||||
}
|
|
||||||
const keyToCallbacks = targetToKeysToCallbacks.get(target)!;
|
|
||||||
if (!keyToCallbacks.get(key)) {
|
|
||||||
keyToCallbacks.set(key, new Set());
|
|
||||||
}
|
|
||||||
keyToCallbacks.get(key)!.add(callback);
|
|
||||||
if (!callbacksToTargets.has(callback)) {
|
|
||||||
callbacksToTargets.set(callback, new Set());
|
|
||||||
}
|
|
||||||
callbacksToTargets.get(callback)!.add(target);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let scheduled = false;
|
||||||
|
function scheduleAtom(atom: Atom) {
|
||||||
|
scheduledAtoms.add(atom);
|
||||||
|
// batched(processAtoms)();
|
||||||
|
if (scheduled) return;
|
||||||
|
scheduled = true;
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
scheduled = false;
|
||||||
|
processAtoms();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function processDerivedAtoms() {
|
||||||
|
const processedAtoms = new Set<Atom>();
|
||||||
|
for (const atom of scheduledAtoms) {
|
||||||
|
for (const dep of atom.dependents) {
|
||||||
|
if (processedAtoms.has(dep)) continue;
|
||||||
|
dep.computed = false;
|
||||||
|
processedAtoms.add(dep);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processAtoms() {
|
||||||
|
processDerivedAtoms();
|
||||||
|
|
||||||
|
const scheduledContexts = new Set(
|
||||||
|
[...scheduledAtoms.values()].map((s) => [...s.executionContexts]).flat()
|
||||||
|
);
|
||||||
|
|
||||||
|
// schedule before context.update in case there is write operations during update
|
||||||
|
// todo: add a test in case there is write operations during update the test
|
||||||
|
// will break is scheduledAtoms.clear(); is called after context.update();
|
||||||
|
// that writes
|
||||||
|
scheduledAtoms.clear();
|
||||||
|
for (const ctx of [...scheduledContexts]) {
|
||||||
|
removeAtomsFromContext(ctx);
|
||||||
|
// custom unsubscribe depending on the context.
|
||||||
|
// scheduledContexts might be updated while we're iterating over it.
|
||||||
|
ctx.unsubcribe?.(scheduledContexts);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const context of scheduledContexts) {
|
||||||
|
pushExecutionContext(context);
|
||||||
|
try {
|
||||||
|
context.update?.();
|
||||||
|
} finally {
|
||||||
|
popExecutionContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Notify Reactives that are observing a given target that a key has changed on
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
for (const context of executionContexts) {
|
||||||
|
context.update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notify Reactives that are observing a given target that a key has changed on
|
* Notify Reactives that are observing a given target that a key has changed on
|
||||||
* the target.
|
* the target.
|
||||||
@@ -117,66 +193,21 @@ function observeTargetKey(target: Target, key: PropertyKey, callback: Callback):
|
|||||||
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
* @param key the key that changed (or Symbol `KEYCHANGES` if a key was created
|
||||||
* or deleted)
|
* or deleted)
|
||||||
*/
|
*/
|
||||||
function notifyReactives(target: Target, key: PropertyKey): void {
|
function onWriteTargetKey(target: Target, key: PropertyKey): void {
|
||||||
const keyToCallbacks = targetToKeysToCallbacks.get(target);
|
const keyToAtomItem = targetToKeysToAtomItem.get(target)!;
|
||||||
if (!keyToCallbacks) {
|
if (!keyToAtomItem) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const callbacks = keyToCallbacks.get(key);
|
const atom = keyToAtomItem.get(key);
|
||||||
if (!callbacks) {
|
if (!atom) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Loop on copy because clearReactivesForCallback will modify the set in place
|
scheduleAtom(atom);
|
||||||
for (const callback of [...callbacks]) {
|
|
||||||
clearReactivesForCallback(callback);
|
|
||||||
callback();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const callbacksToTargets = new WeakMap<Callback, Set<Target>>();
|
|
||||||
/**
|
|
||||||
* Clears all subscriptions of the Reactives associated with a given callback.
|
|
||||||
*
|
|
||||||
* @param callback the callback for which the reactives need to be cleared
|
|
||||||
*/
|
|
||||||
export function clearReactivesForCallback(callback: Callback): void {
|
|
||||||
const targetsToClear = callbacksToTargets.get(callback);
|
|
||||||
if (!targetsToClear) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const target of targetsToClear) {
|
|
||||||
const observedKeys = targetToKeysToCallbacks.get(target);
|
|
||||||
if (!observedKeys) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
for (const [key, callbacks] of observedKeys.entries()) {
|
|
||||||
callbacks.delete(callback);
|
|
||||||
if (!callbacks.size) {
|
|
||||||
observedKeys.delete(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
targetsToClear.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSubscriptions(callback: Callback) {
|
|
||||||
const targets = callbacksToTargets.get(callback) || [];
|
|
||||||
return [...targets].map((target) => {
|
|
||||||
const keysToCallbacks = targetToKeysToCallbacks.get(target);
|
|
||||||
let keys = [];
|
|
||||||
if (keysToCallbacks) {
|
|
||||||
for (const [key, cbs] of keysToCallbacks) {
|
|
||||||
if (cbs.has(callback)) {
|
|
||||||
keys.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return { target, keys };
|
|
||||||
});
|
|
||||||
}
|
|
||||||
// Maps reactive objects to the underlying target
|
// Maps reactive objects to the underlying target
|
||||||
export const targets = new WeakMap<Reactive<Target>, Target>();
|
export const targets = new WeakMap<Reactive<Target>, Target>();
|
||||||
const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>();
|
const reactiveCache = new WeakMap<Target, Reactive<Target>>();
|
||||||
/**
|
/**
|
||||||
* Creates a reactive proxy for an object. Reading data on the reactive object
|
* Creates a reactive proxy for an object. Reading data on the reactive object
|
||||||
* subscribes to changes to the data. Writing data on the object will cause the
|
* subscribes to changes to the data. Writing data on the object will cause the
|
||||||
@@ -204,7 +235,7 @@ const reactiveCache = new WeakMap<Target, WeakMap<Callback, Reactive<Target>>>()
|
|||||||
* reactive has changed
|
* reactive has changed
|
||||||
* @returns a proxy that tracks changes to it
|
* @returns a proxy that tracks changes to it
|
||||||
*/
|
*/
|
||||||
export function reactive<T extends Target>(target: T, callback: Callback = NO_CALLBACK): T {
|
export function reactive<T extends Target>(target: T): T {
|
||||||
if (!canBeMadeReactive(target)) {
|
if (!canBeMadeReactive(target)) {
|
||||||
throw new OwlError(`Cannot make the given value reactive`);
|
throw new OwlError(`Cannot make the given value reactive`);
|
||||||
}
|
}
|
||||||
@@ -213,30 +244,130 @@ export function reactive<T extends Target>(target: T, callback: Callback = NO_CA
|
|||||||
}
|
}
|
||||||
if (targets.has(target)) {
|
if (targets.has(target)) {
|
||||||
// target is reactive, create a reactive on the underlying object instead
|
// target is reactive, create a reactive on the underlying object instead
|
||||||
return reactive(targets.get(target) as T, callback);
|
// return reactive(targets.get(target) as T);
|
||||||
|
return target;
|
||||||
}
|
}
|
||||||
if (!reactiveCache.has(target)) {
|
const reactive = reactiveCache.get(target)!;
|
||||||
reactiveCache.set(target, new WeakMap());
|
if (reactive) return reactive as T;
|
||||||
}
|
|
||||||
const reactivesForTarget = reactiveCache.get(target)!;
|
const targetRawType = rawType(target);
|
||||||
if (!reactivesForTarget.has(callback)) {
|
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
||||||
const targetRawType = rawType(target);
|
? collectionsProxyHandler(target as Collection, targetRawType as CollectionRawType)
|
||||||
const handler = COLLECTION_RAW_TYPES.includes(targetRawType)
|
: basicProxyHandler<T>();
|
||||||
? collectionsProxyHandler(target as Collection, callback, targetRawType as CollectionRawType)
|
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
||||||
: basicProxyHandler<T>(callback);
|
|
||||||
const proxy = new Proxy(target, handler as ProxyHandler<T>) as Reactive<T>;
|
reactiveCache.set(target, proxy);
|
||||||
reactivesForTarget.set(callback, proxy);
|
targets.set(proxy, target);
|
||||||
targets.set(proxy, target);
|
|
||||||
}
|
return proxy;
|
||||||
return reactivesForTarget.get(callback) as Reactive<T>;
|
|
||||||
}
|
}
|
||||||
|
function removeAtomsFromContext(executionContext: ExecutionContext) {
|
||||||
|
for (const sig of executionContext.atoms) {
|
||||||
|
sig.executionContexts.delete(executionContext);
|
||||||
|
}
|
||||||
|
executionContext.atoms.clear();
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Unsubscribe an execution context and all its children from all atoms
|
||||||
|
* they are subscribed to.
|
||||||
|
*
|
||||||
|
* @param parentExecutionContext the context to unsubscribe
|
||||||
|
*/
|
||||||
|
function unsubscribeChildEffect(
|
||||||
|
parentExecutionContext: ExecutionContext,
|
||||||
|
scheduledContexts: Set<ExecutionContext>
|
||||||
|
) {
|
||||||
|
// executionContext.update = () => {};
|
||||||
|
|
||||||
|
for (const children of parentExecutionContext.meta.children) {
|
||||||
|
children.meta.parent = undefined;
|
||||||
|
removeAtomsFromContext(children);
|
||||||
|
scheduledContexts.delete(children);
|
||||||
|
unsubscribeChildEffect(children, scheduledContexts);
|
||||||
|
}
|
||||||
|
parentExecutionContext.meta.children.length = 0;
|
||||||
|
}
|
||||||
|
export function withoutReactivity<T extends (...args: any[]) => any>(fn: T): ReturnType<T> {
|
||||||
|
pushExecutionContext(undefined!);
|
||||||
|
let r: ReturnType<T>;
|
||||||
|
try {
|
||||||
|
r = fn();
|
||||||
|
} finally {
|
||||||
|
popExecutionContext();
|
||||||
|
}
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function effect(fn: Function) {
|
||||||
|
let parent = getExecutionContext();
|
||||||
|
// todo: is it useful?
|
||||||
|
if (parent && !parent?.meta.children) {
|
||||||
|
parent = undefined!;
|
||||||
|
}
|
||||||
|
const executionContext: ExecutionContext = {
|
||||||
|
unsubcribe: (scheduledContexts: Set<ExecutionContext>) => {
|
||||||
|
unsubscribeChildEffect(executionContext, scheduledContexts);
|
||||||
|
},
|
||||||
|
update: fn,
|
||||||
|
onReadAtom: (atom: Atom) => addAtomToContext(atom, executionContext),
|
||||||
|
atoms: new Set(),
|
||||||
|
meta: {
|
||||||
|
parent: parent,
|
||||||
|
children: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
if (parent) {
|
||||||
|
// todo: is it useful?
|
||||||
|
parent.meta.children?.push?.(executionContext);
|
||||||
|
}
|
||||||
|
pushExecutionContext(executionContext);
|
||||||
|
try {
|
||||||
|
fn();
|
||||||
|
} finally {
|
||||||
|
popExecutionContext();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function derived(fn: Function) {
|
||||||
|
let lastValue: any;
|
||||||
|
|
||||||
|
const derivedAtom: DerivedAtom = {
|
||||||
|
executionContexts: new Set<ExecutionContext>(),
|
||||||
|
dependents: new Set<Atom>(),
|
||||||
|
dependencies: new Map<Atom, OldValue>(),
|
||||||
|
getValue: () => lastValue,
|
||||||
|
computed: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const executionContext = getExecutionContext();
|
||||||
|
executionContext?.onReadAtom(derivedAtom);
|
||||||
|
if (derivedAtom.computed) return lastValue;
|
||||||
|
|
||||||
|
const derivedExecutionContext: ExecutionContext = {
|
||||||
|
onReadAtom: (atom: Atom) => {
|
||||||
|
atom.dependents.add(derivedAtom);
|
||||||
|
// derivedAtom.executionContexts.add(executionContext);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
pushExecutionContext(derivedExecutionContext);
|
||||||
|
try {
|
||||||
|
lastValue = fn();
|
||||||
|
} finally {
|
||||||
|
popExecutionContext();
|
||||||
|
}
|
||||||
|
derivedAtom.computed = true;
|
||||||
|
return lastValue;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a basic proxy handler for regular objects and arrays.
|
* Creates a basic proxy handler for regular objects and arrays.
|
||||||
*
|
*
|
||||||
* @param callback @see reactive
|
* @param callback @see reactive
|
||||||
* @returns a proxy handler object
|
* @returns a proxy handler object
|
||||||
*/
|
*/
|
||||||
function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T> {
|
function basicProxyHandler<T extends Target>(): ProxyHandler<T> {
|
||||||
return {
|
return {
|
||||||
get(target, key, receiver) {
|
get(target, key, receiver) {
|
||||||
// non-writable non-configurable properties cannot be made reactive
|
// non-writable non-configurable properties cannot be made reactive
|
||||||
@@ -244,15 +375,15 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
|
|||||||
if (desc && !desc.writable && !desc.configurable) {
|
if (desc && !desc.writable && !desc.configurable) {
|
||||||
return Reflect.get(target, key, receiver);
|
return Reflect.get(target, key, receiver);
|
||||||
}
|
}
|
||||||
observeTargetKey(target, key, callback);
|
onReadTargetKey(target, key);
|
||||||
return possiblyReactive(Reflect.get(target, key, receiver), callback);
|
return possiblyReactive(Reflect.get(target, key, receiver));
|
||||||
},
|
},
|
||||||
set(target, key, value, receiver) {
|
set(target, key, value, receiver) {
|
||||||
const hadKey = objectHasOwnProperty.call(target, key);
|
const hadKey = objectHasOwnProperty.call(target, key);
|
||||||
const originalValue = Reflect.get(target, key, receiver);
|
const originalValue = Reflect.get(target, key, receiver);
|
||||||
const ret = Reflect.set(target, key, toRaw(value), receiver);
|
const ret = Reflect.set(target, key, toRaw(value), receiver);
|
||||||
if (!hadKey && objectHasOwnProperty.call(target, key)) {
|
if (!hadKey && objectHasOwnProperty.call(target, key)) {
|
||||||
notifyReactives(target, KEYCHANGES);
|
onWriteTargetKey(target, KEYCHANGES);
|
||||||
}
|
}
|
||||||
// While Array length may trigger the set trap, it's not actually set by this
|
// While Array length may trigger the set trap, it's not actually set by this
|
||||||
// method but is updated behind the scenes, and the trap is not called with the
|
// method but is updated behind the scenes, and the trap is not called with the
|
||||||
@@ -261,26 +392,26 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
|
|||||||
originalValue !== Reflect.get(target, key, receiver) ||
|
originalValue !== Reflect.get(target, key, receiver) ||
|
||||||
(key === "length" && Array.isArray(target))
|
(key === "length" && Array.isArray(target))
|
||||||
) {
|
) {
|
||||||
notifyReactives(target, key);
|
onWriteTargetKey(target, key);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
deleteProperty(target, key) {
|
deleteProperty(target, key) {
|
||||||
const ret = Reflect.deleteProperty(target, key);
|
const ret = Reflect.deleteProperty(target, key);
|
||||||
// TODO: only notify when something was actually deleted
|
// TODO: only notify when something was actually deleted
|
||||||
notifyReactives(target, KEYCHANGES);
|
onWriteTargetKey(target, KEYCHANGES);
|
||||||
notifyReactives(target, key);
|
onWriteTargetKey(target, key);
|
||||||
return ret;
|
return ret;
|
||||||
},
|
},
|
||||||
ownKeys(target) {
|
ownKeys(target) {
|
||||||
observeTargetKey(target, KEYCHANGES, callback);
|
onReadTargetKey(target, KEYCHANGES);
|
||||||
return Reflect.ownKeys(target);
|
return Reflect.ownKeys(target);
|
||||||
},
|
},
|
||||||
has(target, key) {
|
has(target, key) {
|
||||||
// TODO: this observes all key changes instead of only the presence of the argument key
|
// TODO: this observes all key changes instead of only the presence of the argument key
|
||||||
// observing the key itself would observe value changes instead of presence changes
|
// observing the key itself would observe value changes instead of presence changes
|
||||||
// so we may need a finer grained system to distinguish observing value vs presence.
|
// so we may need a finer grained system to distinguish observing value vs presence.
|
||||||
observeTargetKey(target, KEYCHANGES, callback);
|
onReadTargetKey(target, KEYCHANGES);
|
||||||
return Reflect.has(target, key);
|
return Reflect.has(target, key);
|
||||||
},
|
},
|
||||||
} as ProxyHandler<T>;
|
} as ProxyHandler<T>;
|
||||||
@@ -293,11 +424,11 @@ function basicProxyHandler<T extends Target>(callback: Callback): ProxyHandler<T
|
|||||||
* @param target @see reactive
|
* @param target @see reactive
|
||||||
* @param callback @see reactive
|
* @param callback @see reactive
|
||||||
*/
|
*/
|
||||||
function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callback) {
|
function makeKeyObserver(methodName: "has" | "get", target: any) {
|
||||||
return (key: any) => {
|
return (key: any) => {
|
||||||
key = toRaw(key);
|
key = toRaw(key);
|
||||||
observeTargetKey(target, key, callback);
|
onReadTargetKey(target, key);
|
||||||
return possiblyReactive(target[methodName](key), callback);
|
return possiblyReactive(target[methodName](key));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
@@ -310,16 +441,15 @@ function makeKeyObserver(methodName: "has" | "get", target: any, callback: Callb
|
|||||||
*/
|
*/
|
||||||
function makeIteratorObserver(
|
function makeIteratorObserver(
|
||||||
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
|
methodName: "keys" | "values" | "entries" | typeof Symbol.iterator,
|
||||||
target: any,
|
target: any
|
||||||
callback: Callback
|
|
||||||
) {
|
) {
|
||||||
return function* () {
|
return function* () {
|
||||||
observeTargetKey(target, KEYCHANGES, callback);
|
onReadTargetKey(target, KEYCHANGES);
|
||||||
const keys = target.keys();
|
const keys = target.keys();
|
||||||
for (const item of target[methodName]()) {
|
for (const item of target[methodName]()) {
|
||||||
const key = keys.next().value;
|
const key = keys.next().value;
|
||||||
observeTargetKey(target, key, callback);
|
onReadTargetKey(target, key);
|
||||||
yield possiblyReactive(item, callback);
|
yield possiblyReactive(item);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -331,16 +461,16 @@ function makeIteratorObserver(
|
|||||||
* @param target @see reactive
|
* @param target @see reactive
|
||||||
* @param callback @see reactive
|
* @param callback @see reactive
|
||||||
*/
|
*/
|
||||||
function makeForEachObserver(target: any, callback: Callback) {
|
function makeForEachObserver(target: any) {
|
||||||
return function forEach(forEachCb: (val: any, key: any, target: any) => void, thisArg: any) {
|
return function forEach(forEachCb: (val: any, key: any, target: any) => void, thisArg: any) {
|
||||||
observeTargetKey(target, KEYCHANGES, callback);
|
onReadTargetKey(target, KEYCHANGES);
|
||||||
target.forEach(function (val: any, key: any, targetObj: any) {
|
target.forEach(function (val: any, key: any, targetObj: any) {
|
||||||
observeTargetKey(target, key, callback);
|
onReadTargetKey(target, key);
|
||||||
forEachCb.call(
|
forEachCb.call(
|
||||||
thisArg,
|
thisArg,
|
||||||
possiblyReactive(val, callback),
|
possiblyReactive(val),
|
||||||
possiblyReactive(key, callback),
|
possiblyReactive(key),
|
||||||
possiblyReactive(targetObj, callback)
|
possiblyReactive(targetObj)
|
||||||
);
|
);
|
||||||
}, thisArg);
|
}, thisArg);
|
||||||
};
|
};
|
||||||
@@ -367,10 +497,10 @@ function delegateAndNotify(
|
|||||||
const ret = target[setterName](key, value);
|
const ret = target[setterName](key, value);
|
||||||
const hasKey = target.has(key);
|
const hasKey = target.has(key);
|
||||||
if (hadKey !== hasKey) {
|
if (hadKey !== hasKey) {
|
||||||
notifyReactives(target, KEYCHANGES);
|
onWriteTargetKey(target, KEYCHANGES);
|
||||||
}
|
}
|
||||||
if (originalValue !== target[getterName](key)) {
|
if (originalValue !== target[getterName](key)) {
|
||||||
notifyReactives(target, key);
|
onWriteTargetKey(target, key);
|
||||||
}
|
}
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
@@ -385,9 +515,9 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
|
|||||||
return () => {
|
return () => {
|
||||||
const allKeys = [...target.keys()];
|
const allKeys = [...target.keys()];
|
||||||
target.clear();
|
target.clear();
|
||||||
notifyReactives(target, KEYCHANGES);
|
onWriteTargetKey(target, KEYCHANGES);
|
||||||
for (const key of allKeys) {
|
for (const key of allKeys) {
|
||||||
notifyReactives(target, key);
|
onWriteTargetKey(target, key);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -399,40 +529,40 @@ function makeClearNotifier(target: Map<any, any> | Set<any>) {
|
|||||||
* reactives that the key which is being added or deleted has been modified.
|
* reactives that the key which is being added or deleted has been modified.
|
||||||
*/
|
*/
|
||||||
const rawTypeToFuncHandlers = {
|
const rawTypeToFuncHandlers = {
|
||||||
Set: (target: any, callback: Callback) => ({
|
Set: (target: any) => ({
|
||||||
has: makeKeyObserver("has", target, callback),
|
has: makeKeyObserver("has", target),
|
||||||
add: delegateAndNotify("add", "has", target),
|
add: delegateAndNotify("add", "has", target),
|
||||||
delete: delegateAndNotify("delete", "has", target),
|
delete: delegateAndNotify("delete", "has", target),
|
||||||
keys: makeIteratorObserver("keys", target, callback),
|
keys: makeIteratorObserver("keys", target),
|
||||||
values: makeIteratorObserver("values", target, callback),
|
values: makeIteratorObserver("values", target),
|
||||||
entries: makeIteratorObserver("entries", target, callback),
|
entries: makeIteratorObserver("entries", target),
|
||||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
|
||||||
forEach: makeForEachObserver(target, callback),
|
forEach: makeForEachObserver(target),
|
||||||
clear: makeClearNotifier(target),
|
clear: makeClearNotifier(target),
|
||||||
get size() {
|
get size() {
|
||||||
observeTargetKey(target, KEYCHANGES, callback);
|
onReadTargetKey(target, KEYCHANGES);
|
||||||
return target.size;
|
return target.size;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
Map: (target: any, callback: Callback) => ({
|
Map: (target: any) => ({
|
||||||
has: makeKeyObserver("has", target, callback),
|
has: makeKeyObserver("has", target),
|
||||||
get: makeKeyObserver("get", target, callback),
|
get: makeKeyObserver("get", target),
|
||||||
set: delegateAndNotify("set", "get", target),
|
set: delegateAndNotify("set", "get", target),
|
||||||
delete: delegateAndNotify("delete", "has", target),
|
delete: delegateAndNotify("delete", "has", target),
|
||||||
keys: makeIteratorObserver("keys", target, callback),
|
keys: makeIteratorObserver("keys", target),
|
||||||
values: makeIteratorObserver("values", target, callback),
|
values: makeIteratorObserver("values", target),
|
||||||
entries: makeIteratorObserver("entries", target, callback),
|
entries: makeIteratorObserver("entries", target),
|
||||||
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target, callback),
|
[Symbol.iterator]: makeIteratorObserver(Symbol.iterator, target),
|
||||||
forEach: makeForEachObserver(target, callback),
|
forEach: makeForEachObserver(target),
|
||||||
clear: makeClearNotifier(target),
|
clear: makeClearNotifier(target),
|
||||||
get size() {
|
get size() {
|
||||||
observeTargetKey(target, KEYCHANGES, callback);
|
onReadTargetKey(target, KEYCHANGES);
|
||||||
return target.size;
|
return target.size;
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
WeakMap: (target: any, callback: Callback) => ({
|
WeakMap: (target: any) => ({
|
||||||
has: makeKeyObserver("has", target, callback),
|
has: makeKeyObserver("has", target),
|
||||||
get: makeKeyObserver("get", target, callback),
|
get: makeKeyObserver("get", target),
|
||||||
set: delegateAndNotify("set", "get", target),
|
set: delegateAndNotify("set", "get", target),
|
||||||
delete: delegateAndNotify("delete", "has", target),
|
delete: delegateAndNotify("delete", "has", target),
|
||||||
}),
|
}),
|
||||||
@@ -446,20 +576,19 @@ const rawTypeToFuncHandlers = {
|
|||||||
*/
|
*/
|
||||||
function collectionsProxyHandler<T extends Collection>(
|
function collectionsProxyHandler<T extends Collection>(
|
||||||
target: T,
|
target: T,
|
||||||
callback: Callback,
|
|
||||||
targetRawType: CollectionRawType
|
targetRawType: CollectionRawType
|
||||||
): ProxyHandler<T> {
|
): ProxyHandler<T> {
|
||||||
// TODO: if performance is an issue we can create the special handlers lazily when each
|
// TODO: if performance is an issue we can create the special handlers lazily when each
|
||||||
// property is read.
|
// property is read.
|
||||||
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target, callback);
|
const specialHandlers = rawTypeToFuncHandlers[targetRawType](target);
|
||||||
return Object.assign(basicProxyHandler(callback), {
|
return Object.assign(basicProxyHandler(), {
|
||||||
// FIXME: probably broken when part of prototype chain since we ignore the receiver
|
// FIXME: probably broken when part of prototype chain since we ignore the receiver
|
||||||
get(target: any, key: PropertyKey) {
|
get(target: any, key: PropertyKey) {
|
||||||
if (objectHasOwnProperty.call(specialHandlers, key)) {
|
if (objectHasOwnProperty.call(specialHandlers, key)) {
|
||||||
return (specialHandlers as any)[key];
|
return (specialHandlers as any)[key];
|
||||||
}
|
}
|
||||||
observeTargetKey(target, key, callback);
|
onReadTargetKey(target, key);
|
||||||
return possiblyReactive(target[key], callback);
|
return possiblyReactive(target[key]);
|
||||||
},
|
},
|
||||||
}) as ProxyHandler<T>;
|
}) as ProxyHandler<T>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import { getTaskContext, TaskContext, useTaskContext } from "./cancellableContext";
|
||||||
|
|
||||||
|
export class Task<T = any> {
|
||||||
|
_promise: Promise<T>;
|
||||||
|
_ctx?: TaskContext = getTaskContext();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
executor: (resolve: (value: T | PromiseLike<T>) => void, reject: (reason: any) => void) => void,
|
||||||
|
public _onCancelled?: Function
|
||||||
|
) {
|
||||||
|
if (!this._ctx) {
|
||||||
|
this._promise = new Promise(executor);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._promise = new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
executor(
|
||||||
|
(value: T | PromiseLike<T>) => {
|
||||||
|
if (!this._ctx?.isCancelled) resolve(value);
|
||||||
|
},
|
||||||
|
(error: any) => {
|
||||||
|
if (!this._ctx?.isCancelled) reject(error);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
if (!this._ctx?.isCancelled) reject(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
then(onFulfilled: (value: any) => any, onRejected: (error: any) => any) {
|
||||||
|
if (!this._ctx) return this._promise.then(onFulfilled, onRejected);
|
||||||
|
return this._promise.then((v) => {
|
||||||
|
if (this._ctx!.isCancelled) return;
|
||||||
|
let cleanup: Function;
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
const ctx = useTaskContext(this._ctx);
|
||||||
|
cleanup = ctx.cleanup;
|
||||||
|
});
|
||||||
|
const result = onFulfilled(v);
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
cleanup();
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}, onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch(onRejected: (error: any) => any) {
|
||||||
|
return this._promise.catch(onRejected);
|
||||||
|
}
|
||||||
|
|
||||||
|
finally(onFinally: () => any) {
|
||||||
|
return this._promise.finally(onFinally);
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
if (this._onCancelled) {
|
||||||
|
this._onCancelled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get [Symbol.toStringTag]() {
|
||||||
|
return "Promise";
|
||||||
|
}
|
||||||
|
|
||||||
|
// static all(tasks) {
|
||||||
|
// return new Task((resolve, reject) => {
|
||||||
|
// Promise.all(tasks.map((t) => (t instanceof Task ? t._promise : t))).then(resolve, reject);
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ const bdom = { text, createBlock, list, multi, html, toggler, comment };
|
|||||||
export interface TemplateSetConfig {
|
export interface TemplateSetConfig {
|
||||||
dev?: boolean;
|
dev?: boolean;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
translateFn?: (s: string) => string;
|
translateFn?: (s: string, translationCtx: string) => string;
|
||||||
templates?: string | Document | Record<string, string>;
|
templates?: string | Document | Record<string, string>;
|
||||||
getTemplate?: (s: string) => Element | Function | string | void;
|
getTemplate?: (s: string) => Element | Function | string | void;
|
||||||
customDirectives?: customDirectives;
|
customDirectives?: customDirectives;
|
||||||
@@ -27,7 +27,7 @@ export class TemplateSet {
|
|||||||
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
rawTemplates: typeof globalTemplates = Object.create(globalTemplates);
|
||||||
templates: { [name: string]: Template } = {};
|
templates: { [name: string]: Template } = {};
|
||||||
getRawTemplate?: (s: string) => Element | Function | string | void;
|
getRawTemplate?: (s: string) => Element | Function | string | void;
|
||||||
translateFn?: (s: string) => string;
|
translateFn?: (s: string, translationCtx: string) => string;
|
||||||
translatableAttributes?: string[];
|
translatableAttributes?: string[];
|
||||||
Portal = Portal;
|
Portal = Portal;
|
||||||
customDirectives: customDirectives;
|
customDirectives: customDirectives;
|
||||||
|
|||||||
@@ -35,13 +35,43 @@ export function inOwnerDocument(el?: HTMLElement) {
|
|||||||
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
return rootNode instanceof ShadowRoot && el.ownerDocument.contains(rootNode.host);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the given element is contained in a specific root documnet:
|
||||||
|
* either directly or with a shadow root in between or in an iframe.
|
||||||
|
*/
|
||||||
|
function isAttachedToDocument(
|
||||||
|
element: HTMLElement | ShadowRoot,
|
||||||
|
documentElement: Document
|
||||||
|
): boolean {
|
||||||
|
let current: Node = element;
|
||||||
|
const shadowRoot = documentElement.defaultView!.ShadowRoot;
|
||||||
|
while (current) {
|
||||||
|
if (current === documentElement) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (current.parentNode) {
|
||||||
|
current = current.parentNode;
|
||||||
|
} else if (current instanceof shadowRoot && current.host) {
|
||||||
|
current = current.host;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
export function validateTarget(target: HTMLElement | ShadowRoot) {
|
||||||
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
// Get the document and HTMLElement corresponding to the target to allow mounting in iframes
|
||||||
const document = target && target.ownerDocument;
|
const document = target && target.ownerDocument;
|
||||||
if (document) {
|
if (document) {
|
||||||
const HTMLElement = document.defaultView!.HTMLElement;
|
if (!document.defaultView) {
|
||||||
|
throw new OwlError(
|
||||||
|
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const HTMLElement = document.defaultView.HTMLElement;
|
||||||
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
if (target instanceof HTMLElement || target instanceof ShadowRoot) {
|
||||||
if (!document.body.contains(target instanceof HTMLElement ? target : target.host)) {
|
if (!isAttachedToDocument(target, document)) {
|
||||||
throw new OwlError("Cannot mount a component on a detached dom node");
|
throw new OwlError("Cannot mount a component on a detached dom node");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
@@ -81,10 +111,50 @@ export async function loadFile(url: string): Promise<string> {
|
|||||||
*/
|
*/
|
||||||
export class Markup extends String {}
|
export class Markup extends String {}
|
||||||
|
|
||||||
|
export function htmlEscape(str: any): Markup {
|
||||||
|
if (str instanceof Markup) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
if (str === undefined) {
|
||||||
|
return markup("");
|
||||||
|
}
|
||||||
|
if (typeof str === "number") {
|
||||||
|
return markup(String(str));
|
||||||
|
}
|
||||||
|
[
|
||||||
|
["&", "&"],
|
||||||
|
["<", "<"],
|
||||||
|
[">", ">"],
|
||||||
|
["'", "'"],
|
||||||
|
['"', """],
|
||||||
|
["`", "`"],
|
||||||
|
].forEach((pairs) => {
|
||||||
|
str = String(str).replace(new RegExp(pairs[0], "g"), pairs[1]);
|
||||||
|
});
|
||||||
|
return markup(str);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
* Marks a value as safe, that is, a value that can be injected as HTML directly.
|
||||||
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
* It should be used to wrap the value passed to a t-out directive to allow a raw rendering.
|
||||||
|
*
|
||||||
|
* If called as a tag function, the interpolated strings are escaped.
|
||||||
*/
|
*/
|
||||||
export function markup(value: any) {
|
export function markup(strings: TemplateStringsArray, ...placeholders: unknown[]): Markup;
|
||||||
return new Markup(value);
|
export function markup(value: string): Markup;
|
||||||
|
export function markup(
|
||||||
|
valueOrStrings: string | TemplateStringsArray,
|
||||||
|
...placeholders: unknown[]
|
||||||
|
): Markup {
|
||||||
|
if (!Array.isArray(valueOrStrings)) {
|
||||||
|
return new Markup(valueOrStrings);
|
||||||
|
}
|
||||||
|
const strings = valueOrStrings;
|
||||||
|
let acc = "";
|
||||||
|
let i = 0;
|
||||||
|
for (; i < placeholders.length; ++i) {
|
||||||
|
acc += strings[i] + htmlEscape(placeholders[i]);
|
||||||
|
}
|
||||||
|
acc += strings[i];
|
||||||
|
return new Markup(acc);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
// do not modify manually. This file is generated by the release script.
|
// do not modify manually. This file is generated by the release script.
|
||||||
export const version = "2.5.1";
|
export const version = "2.8.1";
|
||||||
|
|||||||
@@ -1,51 +1,5 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Reactivity: useState concurrent renderings 1`] = `
|
|
||||||
"function anonymous(bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
|
||||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<span><block-text-0/><block-text-1/></span>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let d1 = ctx['context'][ctx['props'].key].n;
|
|
||||||
let d2 = ctx['state'].x;
|
|
||||||
return block1([d1, d2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Reactivity: useState concurrent renderings 2`] = `
|
|
||||||
"function anonymous(bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
|
||||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<p><block-child-0/></p>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let b2 = component(\`ComponentC\`, {key: ctx['props'].key}, key + \`__1\`, node, ctx);
|
|
||||||
return block1([], [b2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Reactivity: useState concurrent renderings 3`] = `
|
|
||||||
"function anonymous(bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
|
||||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/></div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let b2 = component(\`ComponentB\`, {key: ctx['context'].key}, key + \`__1\`, node, ctx);
|
|
||||||
return block1([], [b2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
|
exports[`Reactivity: useState destroyed component before being mounted is inactive 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -155,69 +109,6 @@ exports[`Reactivity: useState parent and children subscribed to same context 2`]
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Reactivity: useState several nodes on different level use same context 1`] = `
|
|
||||||
"function anonymous(bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
|
||||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-text-0/> <block-text-1/></div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let d1 = ctx['contextObj'].a;
|
|
||||||
let d2 = ctx['contextObj'].b;
|
|
||||||
return block1([d1, d2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Reactivity: useState several nodes on different level use same context 2`] = `
|
|
||||||
"function anonymous(bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
|
||||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let d1 = ctx['contextObj'].b;
|
|
||||||
return block1([d1]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Reactivity: useState several nodes on different level use same context 3`] = `
|
|
||||||
"function anonymous(bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
|
||||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let d1 = ctx['contextObj'].a;
|
|
||||||
let b2 = component(\`L3A\`, {}, key + \`__1\`, node, ctx);
|
|
||||||
return block1([d1], [b2]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Reactivity: useState several nodes on different level use same context 4`] = `
|
|
||||||
"function anonymous(bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, component } = bdom;
|
|
||||||
let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, isBoundary, shallowEqual, setContextValue, toNumber, safeOutput } = helpers;
|
|
||||||
|
|
||||||
let block1 = createBlock(\`<div><block-child-0/><block-child-1/></div>\`);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
let b2 = component(\`L2A\`, {}, key + \`__1\`, node, ctx);
|
|
||||||
let b3 = component(\`L2B\`, {}, key + \`__2\`, node, ctx);
|
|
||||||
return block1([], [b2, b3]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`Reactivity: useState two components are updated in parallel 1`] = `
|
exports[`Reactivity: useState two components are updated in parallel 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -49,6 +49,27 @@ exports[`debugging t-debug on sub template 2`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`debugging t-debug: interaction with t-set 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
debugger;
|
||||||
|
setContextValue(ctx, \\"foo\\", 42);
|
||||||
|
debugger;
|
||||||
|
setContextValue(ctx, \\"bar\\", 49);
|
||||||
|
let txt1 = ctx['foo']+ctx['bar'];
|
||||||
|
return block1([txt1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`debugging t-log 1`] = `
|
exports[`debugging t-log 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -66,3 +87,24 @@ exports[`debugging t-log 1`] = `
|
|||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`debugging t-log: interaction with t-set 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<span><block-text-0/></span>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
console.log(ctx['foo']);
|
||||||
|
setContextValue(ctx, \\"foo\\", 42);
|
||||||
|
console.log(ctx['bar']);
|
||||||
|
setContextValue(ctx, \\"bar\\", 49);
|
||||||
|
let txt1 = ctx['foo']+ctx['bar'];
|
||||||
|
return block1([txt1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|||||||
@@ -103,3 +103,18 @@ exports[`t-key t-key on sub dom node pushes a child block in its parent 2`] = `
|
|||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`t-key t-key: interaction with t-esc 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<p><block-text-0/></p>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
const tKey_1 = ctx['key'];
|
||||||
|
let txt1 = ctx['text'];
|
||||||
|
return toggler(tKey_1, block1([txt1]));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`t-slot compile t-props correctly multiple time 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { callSlot } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return callSlot(ctx, node, key, 'default', false, Object.assign({}, {a:1}));
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
@@ -1,5 +1,144 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// 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\`);
|
||||||
|
return text(ctx['label']);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
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 t-translation-context with several children 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div><div/><div/><block-child-0/></div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let b2;
|
||||||
|
if (true) {
|
||||||
|
b2 = text(\`\`);
|
||||||
|
}
|
||||||
|
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`] = `
|
exports[`translation support body of t-sets are translated 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -30,6 +169,21 @@ exports[`translation support body of t-sets inside translation=off are not trans
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`translation support body of t-sets inside translation=off are not translated 2 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
let { isBoundary, withDefault, setContextValue } = helpers;
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
ctx = Object.create(ctx);
|
||||||
|
ctx[isBoundary] = 1
|
||||||
|
setContextValue(ctx, \\"label\\", \`untranslated\`);
|
||||||
|
return text(ctx['label']);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`translation support body of t-sets with html content are translated 1`] = `
|
exports[`translation support body of t-sets with html content are translated 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -141,6 +295,23 @@ exports[`translation support t-set and falsy t-value: t-body are translated 1`]
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`translation support t-translation with several children 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div><div/><div/><block-child-0/></div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let b2;
|
||||||
|
if (true) {
|
||||||
|
b2 = text(\`\`);
|
||||||
|
}
|
||||||
|
return block1([], [b2]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
|
exports[`translation support translation is done on the trimmed text, with extra spaces readded after 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ describe("qweb parser", () => {
|
|||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
content: [],
|
content: [],
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -70,6 +71,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -84,6 +86,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -98,6 +101,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -109,6 +113,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -128,6 +133,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -139,6 +145,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -156,6 +163,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -181,6 +189,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -201,6 +210,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -223,6 +233,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -246,6 +257,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -262,6 +274,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: { class: "abc" },
|
attrs: { class: "abc" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -280,6 +293,7 @@ describe("qweb parser", () => {
|
|||||||
height: "90px",
|
height: "90px",
|
||||||
width: "100px",
|
width: "100px",
|
||||||
},
|
},
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -290,6 +304,7 @@ describe("qweb parser", () => {
|
|||||||
stroke: "green",
|
stroke: "green",
|
||||||
"stroke-width": "1",
|
"stroke-width": "1",
|
||||||
},
|
},
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
model: 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>`)
|
parse(`<g><circle cx="50" cy="50" r="4" stroke="green" stroke-width="1" fill="yellow"/></g>`)
|
||||||
).toEqual({
|
).toEqual({
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
attrs: {
|
attrs: {
|
||||||
@@ -322,6 +338,7 @@ describe("qweb parser", () => {
|
|||||||
stroke: "green",
|
stroke: "green",
|
||||||
"stroke-width": "1",
|
"stroke-width": "1",
|
||||||
},
|
},
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -348,6 +365,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [
|
content: [
|
||||||
@@ -356,6 +374,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "pre",
|
tag: "pre",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [],
|
content: [],
|
||||||
@@ -391,6 +410,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -413,6 +433,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -455,6 +476,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -469,6 +491,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -489,6 +512,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -530,6 +554,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -607,6 +632,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -626,6 +652,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "h1",
|
tag: "h1",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -639,6 +666,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "h2",
|
tag: "h2",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -664,6 +692,7 @@ describe("qweb parser", () => {
|
|||||||
value: "value",
|
value: "value",
|
||||||
defaultValue: null,
|
defaultValue: null,
|
||||||
body: null,
|
body: null,
|
||||||
|
hasNoRepresentation: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -674,6 +703,7 @@ describe("qweb parser", () => {
|
|||||||
defaultValue: "ok",
|
defaultValue: "ok",
|
||||||
value: null,
|
value: null,
|
||||||
body: null,
|
body: null,
|
||||||
|
hasNoRepresentation: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parse(`<t t-set="v"><div>ok</div></t>`)).toEqual({
|
expect(parse(`<t t-set="v"><div>ok</div></t>`)).toEqual({
|
||||||
@@ -685,6 +715,7 @@ describe("qweb parser", () => {
|
|||||||
{
|
{
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
@@ -694,6 +725,7 @@ describe("qweb parser", () => {
|
|||||||
content: [{ type: ASTType.Text, value: "ok" }],
|
content: [{ type: ASTType.Text, value: "ok" }],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
hasNoRepresentation: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(parse(`<t t-set="v"><div>ok</div>abc</t>`)).toEqual({
|
expect(parse(`<t t-set="v"><div>ok</div>abc</t>`)).toEqual({
|
||||||
@@ -705,6 +737,7 @@ describe("qweb parser", () => {
|
|||||||
{
|
{
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
@@ -715,6 +748,7 @@ describe("qweb parser", () => {
|
|||||||
},
|
},
|
||||||
{ type: ASTType.Text, value: "abc" },
|
{ type: ASTType.Text, value: "abc" },
|
||||||
],
|
],
|
||||||
|
hasNoRepresentation: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -728,6 +762,7 @@ describe("qweb parser", () => {
|
|||||||
defaultValue: "ok",
|
defaultValue: "ok",
|
||||||
value: null,
|
value: null,
|
||||||
body: null,
|
body: null,
|
||||||
|
hasNoRepresentation: true,
|
||||||
},
|
},
|
||||||
tElif: null,
|
tElif: null,
|
||||||
tElse: null,
|
tElse: null,
|
||||||
@@ -742,6 +777,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -752,7 +788,14 @@ describe("qweb parser", () => {
|
|||||||
condition: "flag",
|
condition: "flag",
|
||||||
content: { type: ASTType.Text, value: "1" },
|
content: { type: ASTType.Text, value: "1" },
|
||||||
tElif: null,
|
tElif: null,
|
||||||
tElse: { type: ASTType.TSet, name: "ourvar", value: "0", defaultValue: null, body: null },
|
tElse: {
|
||||||
|
type: ASTType.TSet,
|
||||||
|
name: "ourvar",
|
||||||
|
value: "0",
|
||||||
|
defaultValue: null,
|
||||||
|
body: null,
|
||||||
|
hasNoRepresentation: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -811,6 +854,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -853,6 +897,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -887,6 +932,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -920,6 +966,7 @@ describe("qweb parser", () => {
|
|||||||
"t-att-selected": "category.id==options.active_category_id",
|
"t-att-selected": "category.id==options.active_category_id",
|
||||||
"t-att-value": "category.id",
|
"t-att-value": "category.id",
|
||||||
},
|
},
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -940,6 +987,7 @@ describe("qweb parser", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -987,6 +1035,7 @@ describe("qweb parser", () => {
|
|||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
ns: null,
|
ns: null,
|
||||||
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
|
content: [{ type: ASTType.TEsc, expr: "item", defaultValue: "" }],
|
||||||
},
|
},
|
||||||
@@ -1010,6 +1059,7 @@ describe("qweb parser", () => {
|
|||||||
name: "Comp",
|
name: "Comp",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
on: null,
|
on: null,
|
||||||
},
|
},
|
||||||
@@ -1099,6 +1149,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1139,6 +1190,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "button",
|
tag: "button",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: { click: "add" },
|
on: { click: "add" },
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1175,6 +1227,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "select",
|
tag: "select",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [
|
content: [
|
||||||
@@ -1183,6 +1236,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "option",
|
tag: "option",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: { value: "1" },
|
attrs: { value: "1" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [],
|
content: [],
|
||||||
@@ -1212,6 +1266,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "select",
|
tag: "select",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [
|
content: [
|
||||||
@@ -1220,6 +1275,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "option",
|
tag: "option",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: { "t-att-value": "valueVar" },
|
attrs: { "t-att-value": "valueVar" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
content: [],
|
content: [],
|
||||||
@@ -1251,6 +1307,7 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
@@ -1263,6 +1320,7 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: { a: "1", b: "'b'" },
|
props: { a: "1", b: "'b'" },
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1275,6 +1333,7 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: "state",
|
dynamicProps: "state",
|
||||||
props: { a: "1" },
|
props: { a: "1" },
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1287,6 +1346,7 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: { click: "someMethod" },
|
on: { click: "someMethod" },
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1329,12 +1389,14 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
content: { type: ASTType.Text, value: "foo" },
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1350,12 +1412,14 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
content: { type: ASTType.Text, value: "foo" },
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
attrs: { param: "param" },
|
attrs: { param: "param" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1370,6 +1434,7 @@ describe("qweb parser", () => {
|
|||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
@@ -1381,6 +1446,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "span",
|
tag: "span",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1392,6 +1458,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1401,6 +1468,7 @@ describe("qweb parser", () => {
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1415,9 +1483,11 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
on: null,
|
on: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
slots: {
|
slots: {
|
||||||
mySlot: {
|
mySlot: {
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: null,
|
content: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
@@ -1434,9 +1504,16 @@ describe("qweb parser", () => {
|
|||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
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 +1525,13 @@ describe("qweb parser", () => {
|
|||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
name: {
|
name: {
|
||||||
content: { type: ASTType.Text, value: "foo" },
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
attrs: { param: "param" },
|
attrs: { param: "param" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1469,12 +1548,14 @@ describe("qweb parser", () => {
|
|||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
name: {
|
name: {
|
||||||
content: { type: ASTType.Text, value: "foo" },
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
on: { click: "doStuff" },
|
on: { click: "doStuff" },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1493,16 +1574,24 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
content: { type: ASTType.Text, value: " " },
|
content: { type: ASTType.Text, value: " " },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
on: null,
|
||||||
|
scope: null,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
content: { type: ASTType.Text, value: "foo" },
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
name: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1518,11 +1607,24 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
a: { content: { type: ASTType.Text, value: "foo" }, attrs: null, on: null, scope: null },
|
a: {
|
||||||
b: { content: { type: ASTType.Text, value: "bar" }, attrs: null, on: null, scope: null },
|
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 +1635,7 @@ describe("qweb parser", () => {
|
|||||||
name: "myComponent",
|
name: "myComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1545,6 +1648,7 @@ describe("qweb parser", () => {
|
|||||||
name: "mycomponent",
|
name: "mycomponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: { a: "1", b: "'b'" },
|
props: { a: "1", b: "'b'" },
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1557,6 +1661,7 @@ describe("qweb parser", () => {
|
|||||||
name: "mycomponent",
|
name: "mycomponent",
|
||||||
dynamicProps: "state",
|
dynamicProps: "state",
|
||||||
props: { a: "1" },
|
props: { a: "1" },
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: true,
|
isDynamic: true,
|
||||||
on: null,
|
on: null,
|
||||||
slots: null,
|
slots: null,
|
||||||
@@ -1587,12 +1692,14 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null },
|
content: { body: null, name: "subTemplate", type: ASTType.TCall, context: null },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
on: null,
|
on: null,
|
||||||
},
|
},
|
||||||
@@ -1613,11 +1720,13 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
content: {
|
content: {
|
||||||
@@ -1626,11 +1735,13 @@ describe("qweb parser", () => {
|
|||||||
name: "Child",
|
name: "Child",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
brol: {
|
brol: {
|
||||||
content: { type: ASTType.Text, value: "coucou" },
|
content: { type: ASTType.Text, value: "coucou" },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
on: null,
|
on: null,
|
||||||
},
|
},
|
||||||
@@ -1654,11 +1765,13 @@ describe("qweb parser", () => {
|
|||||||
name: "MyComponent",
|
name: "MyComponent",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
default: {
|
default: {
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
content: {
|
content: {
|
||||||
@@ -1667,11 +1780,13 @@ describe("qweb parser", () => {
|
|||||||
name: "Child",
|
name: "Child",
|
||||||
dynamicProps: null,
|
dynamicProps: null,
|
||||||
props: null,
|
props: null,
|
||||||
|
propsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
slots: {
|
slots: {
|
||||||
brol: {
|
brol: {
|
||||||
content: { type: ASTType.Text, value: "coucou" },
|
content: { type: ASTType.Text, value: "coucou" },
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
scope: null,
|
scope: null,
|
||||||
},
|
},
|
||||||
@@ -1691,6 +1806,7 @@ describe("qweb parser", () => {
|
|||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name: "default",
|
name: "default",
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
defaultContent: null,
|
defaultContent: null,
|
||||||
});
|
});
|
||||||
@@ -1701,6 +1817,7 @@ describe("qweb parser", () => {
|
|||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name: "header",
|
name: "header",
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
defaultContent: { type: ASTType.Text, value: "default content" },
|
defaultContent: { type: ASTType.Text, value: "default content" },
|
||||||
});
|
});
|
||||||
@@ -1711,6 +1828,7 @@ describe("qweb parser", () => {
|
|||||||
type: ASTType.TSlot,
|
type: ASTType.TSlot,
|
||||||
name: "default",
|
name: "default",
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: { "click.prevent": "doSomething" },
|
on: { "click.prevent": "doSomething" },
|
||||||
defaultContent: null,
|
defaultContent: null,
|
||||||
});
|
});
|
||||||
@@ -1728,6 +1846,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1746,6 +1865,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1765,6 +1885,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: "name",
|
ref: "name",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1779,6 +1900,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: "name",
|
ref: "name",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1795,6 +1917,7 @@ describe("qweb parser", () => {
|
|||||||
tag: "div",
|
tag: "div",
|
||||||
dynamicTag: null,
|
dynamicTag: null,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
on: null,
|
on: null,
|
||||||
ref: "name",
|
ref: "name",
|
||||||
model: null,
|
model: null,
|
||||||
@@ -1831,6 +1954,7 @@ describe("qweb parser", () => {
|
|||||||
body: {
|
body: {
|
||||||
content: {
|
content: {
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
type: ASTType.Text,
|
type: ASTType.Text,
|
||||||
@@ -1859,6 +1983,190 @@ describe("qweb parser", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('t-translation="off": interaction with t-esc', async () => {
|
||||||
|
expect(parse(`<span t-esc="a" t-translation="off"/>`)).toEqual({
|
||||||
|
type: ASTType.TTranslation,
|
||||||
|
content: {
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
defaultValue: "",
|
||||||
|
expr: "a",
|
||||||
|
type: ASTType.TEsc,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "span",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('t-translation="off": interaction with t-out', async () => {
|
||||||
|
expect(parse(`<span t-out="a" t-translation="off"/>`)).toEqual({
|
||||||
|
type: ASTType.TTranslation,
|
||||||
|
content: {
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
body: null,
|
||||||
|
expr: "a",
|
||||||
|
type: ASTType.TOut,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "span",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-translation-context: interaction with t-esc", async () => {
|
||||||
|
expect(parse(`<span t-esc="a" t-translation-context="fr"/>`)).toEqual({
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
content: {
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
defaultValue: "",
|
||||||
|
expr: "a",
|
||||||
|
type: ASTType.TEsc,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "span",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
translationCtx: "fr",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-translation-context: interaction with t-out", async () => {
|
||||||
|
expect(parse(`<span t-out="a" t-translation-context="fr"/>`)).toEqual({
|
||||||
|
type: ASTType.TTranslationContext,
|
||||||
|
content: {
|
||||||
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
body: null,
|
||||||
|
expr: "a",
|
||||||
|
type: ASTType.TOut,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
dynamicTag: null,
|
||||||
|
model: null,
|
||||||
|
ns: null,
|
||||||
|
on: null,
|
||||||
|
ref: null,
|
||||||
|
tag: "span",
|
||||||
|
type: ASTType.DomNode,
|
||||||
|
},
|
||||||
|
translationCtx: "fr",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// 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
|
// t-model
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -1866,6 +2174,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input t-model="state.stuff" />`)).toEqual({
|
expect(parse(`<input t-model="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1886,6 +2195,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input t-model="state['stuff']" />`)).toEqual({
|
expect(parse(`<input t-model="state['stuff']" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1906,6 +2216,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
expect(parse(`<input t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1927,6 +2238,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<textarea t-model="state.stuff" />`)).toEqual({
|
expect(parse(`<textarea t-model="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1947,6 +2259,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input type="checkbox" t-model="state.stuff" />`)).toEqual({
|
expect(parse(`<input type="checkbox" t-model="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: { type: "checkbox" },
|
attrs: { type: "checkbox" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1967,6 +2280,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input type="radio" t-model="state.stuff" />`)).toEqual({
|
expect(parse(`<input type="radio" t-model="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: { type: "radio" },
|
attrs: { type: "radio" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -1987,6 +2301,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<input type="radio" t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
expect(parse(`<input type="radio" t-model.lazy.trim.number="state.stuff" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: { type: "radio" },
|
attrs: { type: "radio" },
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
@@ -2012,6 +2327,7 @@ describe("qweb parser", () => {
|
|||||||
expect(parse(`<div t-tag="theTag" />`)).toEqual({
|
expect(parse(`<div t-tag="theTag" />`)).toEqual({
|
||||||
type: ASTType.DomNode,
|
type: ASTType.DomNode,
|
||||||
attrs: null,
|
attrs: null,
|
||||||
|
attrsTranslationCtx: null,
|
||||||
content: [],
|
content: [],
|
||||||
on: null,
|
on: null,
|
||||||
ref: null,
|
ref: null,
|
||||||
|
|||||||
@@ -38,4 +38,34 @@ describe("debugging", () => {
|
|||||||
expect(console.log).toHaveBeenCalledWith(45);
|
expect(console.log).toHaveBeenCalledWith(45);
|
||||||
console.log = consoleLog;
|
console.log = consoleLog;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-log: interaction with t-set", () => {
|
||||||
|
const consoleLog = console.log;
|
||||||
|
console.log = jest.fn();
|
||||||
|
|
||||||
|
const template = `
|
||||||
|
<t>
|
||||||
|
<t t-log="foo" t-set="foo" t-value="42"/>
|
||||||
|
<t t-log="bar" t-set="bar" t-value="49"/>
|
||||||
|
<span t-esc="foo + bar"/>
|
||||||
|
</t>
|
||||||
|
`;
|
||||||
|
snapshotTemplate(template);
|
||||||
|
renderToString(template);
|
||||||
|
expect(console.log).toHaveBeenCalledWith(undefined);
|
||||||
|
expect(console.log).toHaveBeenCalledWith(undefined);
|
||||||
|
console.log = consoleLog;
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-debug: interaction with t-set", () => {
|
||||||
|
const template = `
|
||||||
|
<t>
|
||||||
|
<t t-debug="" t-set="foo" t-value="42"/>
|
||||||
|
<t t-debug="" t-set="bar" t-value="49"/>
|
||||||
|
<span t-esc="foo + bar"/>
|
||||||
|
</t>
|
||||||
|
`;
|
||||||
|
snapshotTemplate(template);
|
||||||
|
renderToString(template);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,4 +63,10 @@ describe("t-key", () => {
|
|||||||
|
|
||||||
expect(renderToString(template2, { key: "1" })).toBe("<div><h1></h1></div>");
|
expect(renderToString(template2, { key: "1" })).toBe("<div><h1></h1></div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-key: interaction with t-esc", async () => {
|
||||||
|
const template = `<p t-key="key" t-esc="text"/>`;
|
||||||
|
|
||||||
|
expect(renderToString(template, { key: "1", text: "abc" })).toBe("<p>abc</p>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
import { parseXML } from "../../src/common/utils";
|
||||||
|
import { compile } from "../../src/compiler";
|
||||||
|
|
||||||
|
describe("t-slot", () => {
|
||||||
|
test("compile t-props correctly multiple time", () => {
|
||||||
|
const template = `<t t-slot="default" t-props="{ a: 1 }"/>`;
|
||||||
|
const parsedTemplate = parseXML(template).firstChild as Element;
|
||||||
|
|
||||||
|
const fn1 = compile(parsedTemplate);
|
||||||
|
expect(fn1.toString()).toMatchSnapshot();
|
||||||
|
|
||||||
|
const fn2 = compile(parsedTemplate);
|
||||||
|
expect(fn2.toString()).toBe(fn1.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -86,7 +86,7 @@ describe("translation support", () => {
|
|||||||
|
|
||||||
await mount(SomeComponent, fixture, { translateFn });
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
expect(fixture.innerHTML).toBe("<div> mot </div>");
|
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 () => {
|
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));
|
const translateFn = jest.fn((expr: string) => (expr === "some word" ? "un mot" : expr));
|
||||||
|
|
||||||
await mount(SomeComponent, fixture, { translateFn });
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
expect(translateFn).toHaveBeenCalledWith("some word");
|
expect(translateFn).toHaveBeenCalledWith("some word", "");
|
||||||
expect(fixture.innerHTML).toBe("<div>un mot</div>");
|
expect(fixture.innerHTML).toBe("<div>un mot</div>");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -129,6 +129,21 @@ describe("translation support", () => {
|
|||||||
expect(fixture.innerHTML).toBe("untranslated");
|
expect(fixture.innerHTML).toBe("untranslated");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("body of t-sets inside translation=off are not translated 2", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<t>
|
||||||
|
<t t-translation="off" t-set="label">untranslated</t>
|
||||||
|
<t t-esc="label"/>
|
||||||
|
</t>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const translateFn = () => "translated";
|
||||||
|
|
||||||
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
|
expect(fixture.innerHTML).toBe("untranslated");
|
||||||
|
});
|
||||||
|
|
||||||
test("body of t-sets with html content are translated", async () => {
|
test("body of t-sets with html content are translated", async () => {
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
static template = xml`
|
static template = xml`
|
||||||
@@ -170,4 +185,159 @@ describe("translation support", () => {
|
|||||||
await mount(SomeComponent, fixture, { translateFn });
|
await mount(SomeComponent, fixture, { translateFn });
|
||||||
expect(fixture.innerHTML).toBe("translated");
|
expect(fixture.innerHTML).toBe("translated");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("t-translation with several children", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-translation="off">
|
||||||
|
<div/>
|
||||||
|
<div/>
|
||||||
|
</t>
|
||||||
|
<t t-if="true"/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
await mount(SomeComponent, fixture);
|
||||||
|
expect(fixture.outerHTML).toBe("<div><div><div></div><div></div></div></div>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("t-translation-context with several children", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<div>
|
||||||
|
<t t-translation-context="ctx">
|
||||||
|
<div/>
|
||||||
|
<div/>
|
||||||
|
</t>
|
||||||
|
<t t-if="true"/>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
await mount(SomeComponent, fixture);
|
||||||
|
expect(fixture.outerHTML).toBe("<div><div><div></div><div></div></div></div>");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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`] = `
|
exports[`can catch errors onError in class inheritance is called if rethrown 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -52,6 +52,36 @@ exports[`reactivity in lifecycle Component is automatically subscribed to reacti
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`reactivity in lifecycle an external reactive object should be tracked 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
const comp1 = app.createComponent(\`TestSubComponent\`, true, false, false, []);
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div><block-text-0/><block-child-0/></div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let txt1 = ctx['obj1'].value;
|
||||||
|
const b2 = comp1({}, key + \`__1\`, node, this, null);
|
||||||
|
return block1([txt1], [b2]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`reactivity in lifecycle an external reactive object should be tracked 2`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div><block-text-0/></div>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
let txt1 = ctx['obj2'].value;
|
||||||
|
return block1([txt1]);
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`reactivity in lifecycle can use a state hook 1`] = `
|
exports[`reactivity in lifecycle can use a state hook 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
@@ -140,39 +170,3 @@ exports[`reactivity in lifecycle state changes in willUnmount do not trigger rer
|
|||||||
}
|
}
|
||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`subscriptions subscriptions returns the keys and targets observed by the component 1`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
return text(ctx['state'].a);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`subscriptions subscriptions returns the keys observed by the component 1`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
const comp1 = app.createComponent(\`Child\`, true, false, false, [\\"state\\"]);
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
const b2 = text(ctx['state'].a);
|
|
||||||
const b3 = comp1({state: ctx['state']}, key + \`__1\`, node, this, null);
|
|
||||||
return multi([b2, b3]);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|
||||||
exports[`subscriptions subscriptions returns the keys observed by the component 2`] = `
|
|
||||||
"function anonymous(app, bdom, helpers
|
|
||||||
) {
|
|
||||||
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
|
||||||
|
|
||||||
return function template(ctx, node, key = \\"\\") {
|
|
||||||
return text(ctx['props'].state.b);
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
`;
|
|
||||||
|
|||||||
@@ -1,27 +1,28 @@
|
|||||||
import { App, Component, mount, onWillDestroy } from "../../src";
|
import { App, Component, mount, onWillDestroy } from "../../src";
|
||||||
|
import { OwlError } from "../../src/common/owl_error";
|
||||||
import {
|
import {
|
||||||
onError,
|
onError,
|
||||||
onMounted,
|
onMounted,
|
||||||
onPatched,
|
onPatched,
|
||||||
onWillPatch,
|
|
||||||
onWillStart,
|
|
||||||
onWillRender,
|
|
||||||
onRendered,
|
onRendered,
|
||||||
|
onWillPatch,
|
||||||
|
onWillRender,
|
||||||
|
onWillStart,
|
||||||
onWillUnmount,
|
onWillUnmount,
|
||||||
useState,
|
useState,
|
||||||
xml,
|
xml,
|
||||||
} from "../../src/index";
|
} from "../../src/index";
|
||||||
|
import { getCurrent } from "../../src/runtime/component_node";
|
||||||
import {
|
import {
|
||||||
logStep,
|
logStep,
|
||||||
makeTestFixture,
|
makeTestFixture,
|
||||||
nextTick,
|
|
||||||
nextMicroTick,
|
|
||||||
snapshotEverything,
|
|
||||||
useLogLifecycle,
|
|
||||||
nextAppError,
|
nextAppError,
|
||||||
|
nextMicroTick,
|
||||||
|
nextTick,
|
||||||
|
snapshotEverything,
|
||||||
steps,
|
steps,
|
||||||
|
useLogLifecycle,
|
||||||
} from "../helpers";
|
} from "../helpers";
|
||||||
import { OwlError } from "../../src/common/owl_error";
|
|
||||||
|
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
|
|
||||||
@@ -647,7 +648,7 @@ describe("can catch errors", () => {
|
|||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
onWillStart(() => {
|
onWillStart(() => {
|
||||||
this.state = useState({ value: 2 });
|
getCurrent();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1678,4 +1679,198 @@ describe("can catch errors", () => {
|
|||||||
`);
|
`);
|
||||||
expect(fixture.innerHTML).toBe("2");
|
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 expect(async () => {
|
||||||
await mount(Parent, fixture);
|
await mount(Parent, fixture);
|
||||||
}).rejects.toThrowError("Invalid prop suffix");
|
}).rejects.toThrowError("Invalid prop suffix: somesuffix");
|
||||||
});
|
});
|
||||||
|
|
||||||
test(".alike suffix in a simple case", async () => {
|
test(".alike suffix in a simple case", async () => {
|
||||||
@@ -450,14 +450,10 @@ test(".alike suffix in a list", async () => {
|
|||||||
expect(fixture.innerHTML).toBe("<button>1V</button><button>2V</button>");
|
expect(fixture.innerHTML).toBe("<button>1V</button><button>2V</button>");
|
||||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
"Parent:willRender",
|
|
||||||
"Parent:rendered",
|
|
||||||
"Todo:willRender",
|
"Todo:willRender",
|
||||||
"Todo:rendered",
|
"Todo:rendered",
|
||||||
"Todo:willPatch",
|
"Todo:willPatch",
|
||||||
"Todo:patched",
|
"Todo:patched",
|
||||||
"Parent:willPatch",
|
|
||||||
"Parent:patched",
|
|
||||||
]
|
]
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
import { makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
||||||
import { Component, onError, xml, mount, OwlError, useState } from "../../src";
|
import { Component, onError, xml, mount, OwlError, useState } from "../../src";
|
||||||
import { App, DEV_MSG } from "../../src/runtime/app";
|
import { App } from "../../src/runtime/app";
|
||||||
import { validateProps } from "../../src/runtime/template_helpers";
|
import { validateProps } from "../../src/runtime/template_helpers";
|
||||||
import { Schema } from "../../src/runtime/validation";
|
import { Schema } from "../../src/runtime/validation";
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ let mockConsoleWarn: any;
|
|||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
console.info = (message: any) => {
|
console.info = (message: any) => {
|
||||||
if (message === DEV_MSG()) {
|
if (message === `Owl is running in 'dev' mode.`) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info(message);
|
info(message);
|
||||||
@@ -702,7 +702,7 @@ describe("props validation", () => {
|
|||||||
const app = new App(Parent, { test: true });
|
const app = new App(Parent, { test: true });
|
||||||
await app.mount(fixture);
|
await app.mount(fixture);
|
||||||
expect(fixture.innerHTML).toBe("12");
|
expect(fixture.innerHTML).toBe("12");
|
||||||
expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
|
// expect(app.root!.subscriptions).toEqual([{ keys: ["otherValue"], target: obj }]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("props are validated whenever component is updated", async () => {
|
test("props are validated whenever component is updated", async () => {
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import {
|
|||||||
Component,
|
Component,
|
||||||
mount,
|
mount,
|
||||||
onPatched,
|
onPatched,
|
||||||
onWillRender,
|
|
||||||
onWillPatch,
|
onWillPatch,
|
||||||
|
onWillRender,
|
||||||
onWillUnmount,
|
onWillUnmount,
|
||||||
useState,
|
reactive,
|
||||||
xml,
|
xml,
|
||||||
toRaw,
|
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
|
import { makeTestFixture, nextTick, snapshotEverything, steps, useLogLifecycle } from "../helpers";
|
||||||
|
|
||||||
@@ -20,10 +19,36 @@ beforeEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("reactivity in lifecycle", () => {
|
describe("reactivity in lifecycle", () => {
|
||||||
|
test("an external reactive object should be tracked", async () => {
|
||||||
|
const obj1 = reactive({ value: 1 });
|
||||||
|
const obj2 = reactive({ value: 100 });
|
||||||
|
class TestSubComponent extends Component {
|
||||||
|
obj2 = obj2;
|
||||||
|
|
||||||
|
static template = xml`<div>
|
||||||
|
<t t-esc="obj2.value"/>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
class TestComponent extends Component {
|
||||||
|
obj1 = obj1;
|
||||||
|
static template = xml`<div>
|
||||||
|
<t t-esc="obj1.value"/>
|
||||||
|
<TestSubComponent/>
|
||||||
|
</div>`;
|
||||||
|
static components = { TestSubComponent };
|
||||||
|
}
|
||||||
|
await mount(TestComponent, fixture);
|
||||||
|
expect(fixture.innerHTML).toBe("<div>1<div>100</div></div>");
|
||||||
|
obj1.value = 2;
|
||||||
|
obj2.value = 200;
|
||||||
|
await nextTick();
|
||||||
|
|
||||||
|
expect(fixture.innerHTML).toBe("<div>2<div>200</div></div>");
|
||||||
|
});
|
||||||
test("can use a state hook", async () => {
|
test("can use a state hook", async () => {
|
||||||
class Counter extends Component {
|
class Counter extends Component {
|
||||||
static template = xml`<div><t t-esc="counter.value"/></div>`;
|
static template = xml`<div><t t-esc="counter.value"/></div>`;
|
||||||
counter = useState({ value: 42 });
|
counter = reactive({ value: 42 });
|
||||||
}
|
}
|
||||||
const counter = await mount(Counter, fixture);
|
const counter = await mount(Counter, fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div>42</div>");
|
expect(fixture.innerHTML).toBe("<div>42</div>");
|
||||||
@@ -36,7 +61,7 @@ describe("reactivity in lifecycle", () => {
|
|||||||
let n = 0;
|
let n = 0;
|
||||||
class Comp extends Component {
|
class Comp extends Component {
|
||||||
static template = xml`<div><t t-esc="state.a"/></div>`;
|
static template = xml`<div><t t-esc="state.a"/></div>`;
|
||||||
state = useState({ a: 5, b: 7 });
|
state = reactive({ a: 5, b: 7 });
|
||||||
setup() {
|
setup() {
|
||||||
onWillRender(() => n++);
|
onWillRender(() => n++);
|
||||||
}
|
}
|
||||||
@@ -57,7 +82,7 @@ describe("reactivity in lifecycle", () => {
|
|||||||
test("can use a state hook on Map", async () => {
|
test("can use a state hook on Map", async () => {
|
||||||
class Counter extends Component {
|
class Counter extends Component {
|
||||||
static template = xml`<div><t t-esc="counter.get('value')"/></div>`;
|
static template = xml`<div><t t-esc="counter.get('value')"/></div>`;
|
||||||
counter = useState(new Map([["value", 42]]));
|
counter = reactive(new Map([["value", 42]]));
|
||||||
}
|
}
|
||||||
const counter = await mount(Counter, fixture);
|
const counter = await mount(Counter, fixture);
|
||||||
expect(fixture.innerHTML).toBe("<div>42</div>");
|
expect(fixture.innerHTML).toBe("<div>42</div>");
|
||||||
@@ -72,7 +97,7 @@ describe("reactivity in lifecycle", () => {
|
|||||||
static template = xml`
|
static template = xml`
|
||||||
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
|
<span><t t-esc="props.val"/><t t-esc="state.n"/></span>
|
||||||
`;
|
`;
|
||||||
state = useState({ n: 2 });
|
state = reactive({ n: 2 });
|
||||||
setup() {
|
setup() {
|
||||||
onWillRender(() => {
|
onWillRender(() => {
|
||||||
steps.push("render");
|
steps.push("render");
|
||||||
@@ -96,7 +121,7 @@ describe("reactivity in lifecycle", () => {
|
|||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
state = useState({ val: 1, flag: true });
|
state = reactive({ val: 1, flag: true });
|
||||||
}
|
}
|
||||||
const parent = await mount(Parent, fixture);
|
const parent = await mount(Parent, fixture);
|
||||||
expect(steps).toEqual(["render"]);
|
expect(steps).toEqual(["render"]);
|
||||||
@@ -142,7 +167,7 @@ describe("reactivity in lifecycle", () => {
|
|||||||
static template = xml`
|
static template = xml`
|
||||||
<div><t t-esc="state.val"/></div>
|
<div><t t-esc="state.val"/></div>
|
||||||
`;
|
`;
|
||||||
state = useState({ val: 1 });
|
state = reactive({ val: 1 });
|
||||||
setup() {
|
setup() {
|
||||||
STATE = this.state;
|
STATE = this.state;
|
||||||
onWillRender(() => {
|
onWillRender(() => {
|
||||||
@@ -167,7 +192,7 @@ describe("reactivity in lifecycle", () => {
|
|||||||
class Parent extends Component {
|
class Parent extends Component {
|
||||||
static template = xml`<Child t-if="state.renderChild" state="state"/>`;
|
static template = xml`<Child t-if="state.renderChild" state="state"/>`;
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
state: any = useState({ renderChild: true, content: { a: 2 } });
|
state: any = reactive({ renderChild: true, content: { a: 2 } });
|
||||||
setup() {
|
setup() {
|
||||||
useLogLifecycle();
|
useLogLifecycle();
|
||||||
}
|
}
|
||||||
@@ -205,7 +230,8 @@ describe("reactivity in lifecycle", () => {
|
|||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Component is automatically subscribed to reactive object received as prop", async () => {
|
// todo: unskip it
|
||||||
|
test.skip("Component is automatically subscribed to reactive object received as prop", async () => {
|
||||||
let childRenderCount = 0;
|
let childRenderCount = 0;
|
||||||
let parentRenderCount = 0;
|
let parentRenderCount = 0;
|
||||||
class Child extends Component {
|
class Child extends Component {
|
||||||
@@ -218,7 +244,7 @@ describe("reactivity in lifecycle", () => {
|
|||||||
static template = xml`<Child obj="obj" reactiveObj="reactiveObj"/>`;
|
static template = xml`<Child obj="obj" reactiveObj="reactiveObj"/>`;
|
||||||
static components = { Child };
|
static components = { Child };
|
||||||
obj = { a: 1 };
|
obj = { a: 1 };
|
||||||
reactiveObj = useState({ b: 2 });
|
reactiveObj = reactive({ b: 2 });
|
||||||
setup() {
|
setup() {
|
||||||
onWillRender(() => parentRenderCount++);
|
onWillRender(() => parentRenderCount++);
|
||||||
}
|
}
|
||||||
@@ -237,34 +263,3 @@ describe("reactivity in lifecycle", () => {
|
|||||||
expect(fixture.innerHTML).toBe("34");
|
expect(fixture.innerHTML).toBe("34");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("subscriptions", () => {
|
|
||||||
test("subscriptions returns the keys and targets observed by the component", async () => {
|
|
||||||
class Comp extends Component {
|
|
||||||
static template = xml`<t t-esc="state.a"/>`;
|
|
||||||
state = useState({ a: 1, b: 2 });
|
|
||||||
}
|
|
||||||
const comp = await mount(Comp, fixture);
|
|
||||||
expect(fixture.innerHTML).toBe("1");
|
|
||||||
expect(comp.__owl__.subscriptions).toEqual([{ keys: ["a"], target: toRaw(comp.state) }]);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("subscriptions returns the keys observed by the component", async () => {
|
|
||||||
class Child extends Component {
|
|
||||||
static template = xml`<t t-esc="props.state.b"/>`;
|
|
||||||
setup() {
|
|
||||||
child = this;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
let child: Child;
|
|
||||||
class Parent extends Component {
|
|
||||||
static template = xml`<t t-esc="state.a"/><Child state="state"/>`;
|
|
||||||
static components = { Child };
|
|
||||||
state = useState({ a: 1, b: 2 });
|
|
||||||
}
|
|
||||||
const parent = await mount(Parent, fixture);
|
|
||||||
expect(fixture.innerHTML).toBe("12");
|
|
||||||
expect(parent.__owl__.subscriptions).toEqual([{ keys: ["a"], target: toRaw(parent.state) }]);
|
|
||||||
expect(child!.__owl__.subscriptions).toEqual([{ keys: ["b"], target: toRaw(parent.state) }]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|||||||
@@ -330,12 +330,8 @@ describe("rendering semantics", () => {
|
|||||||
expect(fixture.innerHTML).toBe("444");
|
expect(fixture.innerHTML).toBe("444");
|
||||||
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
expect(steps.splice(0)).toMatchInlineSnapshot(`
|
||||||
Array [
|
Array [
|
||||||
"Parent:willRender",
|
|
||||||
"Parent:rendered",
|
|
||||||
"Child:willRender",
|
"Child:willRender",
|
||||||
"Child:rendered",
|
"Child:rendered",
|
||||||
"Parent:willPatch",
|
|
||||||
"Parent:patched",
|
|
||||||
"Child:willPatch",
|
"Child:willPatch",
|
||||||
"Child:patched",
|
"Child:patched",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -0,0 +1,109 @@
|
|||||||
|
import { taskEffect } from "../../src/runtime/cancellableContext";
|
||||||
|
import { Task } from "../../src/runtime/task";
|
||||||
|
|
||||||
|
export type Deffered = Promise<any> & {
|
||||||
|
resolve: (value: any) => void;
|
||||||
|
reject: (reason: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface TaskWithResolvers<T> {
|
||||||
|
promise: Promise<T>;
|
||||||
|
resolve: (value: T | PromiseLike<T>) => void;
|
||||||
|
reject: (reason?: any) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
let resolvers: Record<string, TaskWithResolvers<string>> = {};
|
||||||
|
function getTask(id: string) {
|
||||||
|
const resolver: {
|
||||||
|
task?: Task<string>;
|
||||||
|
resolve?: (value: string | PromiseLike<string>) => void;
|
||||||
|
reject?: (reason?: any) => void;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
const promise = new Task<string>((res, rej) => {
|
||||||
|
resolver.resolve = res;
|
||||||
|
resolver.reject = rej;
|
||||||
|
});
|
||||||
|
resolver.task = promise;
|
||||||
|
|
||||||
|
resolvers[id] = resolver as TaskWithResolvers<string>;
|
||||||
|
return promise;
|
||||||
|
}
|
||||||
|
function tick() {
|
||||||
|
return new Promise((r) => setTimeout(r, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
// const timeoutTask = (ms: number) => new Task((resolve) => setTimeout(() => resolve(ms), ms));
|
||||||
|
|
||||||
|
const steps: string[] = [];
|
||||||
|
function step(msg: string) {
|
||||||
|
steps.push(msg);
|
||||||
|
}
|
||||||
|
function verifySteps(expected: string[]) {
|
||||||
|
expect(steps).toEqual(expected);
|
||||||
|
steps.length = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
resolvers = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("task", () => {
|
||||||
|
test("should run a task properly", async () => {
|
||||||
|
taskEffect(async () => {
|
||||||
|
let result;
|
||||||
|
step(`a:begin`);
|
||||||
|
result = await getTask("a");
|
||||||
|
step(`a:${result}`);
|
||||||
|
result = await getTask("b");
|
||||||
|
step(`b:${result}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
verifySteps(["a:begin"]);
|
||||||
|
resolvers["a"].resolve("a");
|
||||||
|
await tick();
|
||||||
|
verifySteps(["a:a"]);
|
||||||
|
resolvers["b"].resolve("b");
|
||||||
|
await tick();
|
||||||
|
verifySteps(["b:b"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.only("should cancel a task properly", async () => {
|
||||||
|
const ctx = taskEffect(async () => {
|
||||||
|
let result;
|
||||||
|
step(`a:begin`);
|
||||||
|
result = await getTask("a");
|
||||||
|
step(`a:${result}`);
|
||||||
|
result = await getTask("b");
|
||||||
|
step(`b:${result}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
verifySteps(["a:begin"]);
|
||||||
|
resolvers["a"].resolve("a");
|
||||||
|
await tick();
|
||||||
|
verifySteps(["a:a"]);
|
||||||
|
ctx.cancel();
|
||||||
|
resolvers["b"].resolve("b");
|
||||||
|
await tick();
|
||||||
|
verifySteps([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should run a task with subtasks properly", async () => {
|
||||||
|
taskEffect(async () => {
|
||||||
|
let result;
|
||||||
|
step(`a:begin`);
|
||||||
|
result = await getTask("a");
|
||||||
|
step(`a:${result}`);
|
||||||
|
result = await getTask("b");
|
||||||
|
step(`b:${result}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
verifySteps(["a:begin"]);
|
||||||
|
resolvers["a"].resolve("a");
|
||||||
|
await tick();
|
||||||
|
verifySteps(["a:a"]);
|
||||||
|
resolvers["b"].resolve("b");
|
||||||
|
await tick();
|
||||||
|
verifySteps(["b:b"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,7 +11,6 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from "../../src";
|
} from "../../src";
|
||||||
import { xml } from "../../src/";
|
import { xml } from "../../src/";
|
||||||
import { DEV_MSG } from "../../src/runtime/app";
|
|
||||||
import { elem, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
import { elem, makeTestFixture, nextAppError, nextTick, snapshotEverything } from "../helpers";
|
||||||
|
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
@@ -30,7 +29,7 @@ snapshotEverything();
|
|||||||
|
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
console.info = (message: any) => {
|
console.info = (message: any) => {
|
||||||
if (message === DEV_MSG()) {
|
if (message === `Owl is running in 'dev' mode.`) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
info(message);
|
info(message);
|
||||||
@@ -459,10 +458,8 @@ describe("Portal", () => {
|
|||||||
"parent:willPatch",
|
"parent:willPatch",
|
||||||
"child:mounted",
|
"child:mounted",
|
||||||
"parent:patched",
|
"parent:patched",
|
||||||
"parent:willPatch",
|
|
||||||
"child:willPatch",
|
"child:willPatch",
|
||||||
"child:patched",
|
"child:patched",
|
||||||
"parent:patched",
|
|
||||||
]);
|
]);
|
||||||
expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>');
|
expect(fixture.innerHTML).toBe('<div id="outside"><span>2</span></div><div></div>');
|
||||||
|
|
||||||
@@ -473,10 +470,8 @@ describe("Portal", () => {
|
|||||||
"parent:willPatch",
|
"parent:willPatch",
|
||||||
"child:mounted",
|
"child:mounted",
|
||||||
"parent:patched",
|
"parent:patched",
|
||||||
"parent:willPatch",
|
|
||||||
"child:willPatch",
|
"child:willPatch",
|
||||||
"child:patched",
|
"child:patched",
|
||||||
"parent:patched",
|
|
||||||
"parent:willPatch",
|
"parent:willPatch",
|
||||||
"child:willUnmount",
|
"child:willUnmount",
|
||||||
"parent:patched",
|
"parent:patched",
|
||||||
|
|||||||
@@ -27,6 +27,58 @@ exports[`shadow_dom can mount app 1`] = `
|
|||||||
}"
|
}"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app in closed shadow dom 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside a separate HTML document 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside a shadow child element 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
|
exports[`shadow_dom can mount app inside an element in a shadow root inside an iframe 1`] = `
|
||||||
|
"function anonymous(app, bdom, helpers
|
||||||
|
) {
|
||||||
|
let { text, createBlock, list, multi, html, toggler, comment } = bdom;
|
||||||
|
|
||||||
|
let block1 = createBlock(\`<div class=\\"my-div\\"/>\`);
|
||||||
|
|
||||||
|
return function template(ctx, node, key = \\"\\") {
|
||||||
|
return block1();
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`shadow_dom useRef hook 1`] = `
|
exports[`shadow_dom useRef hook 1`] = `
|
||||||
"function anonymous(app, bdom, helpers
|
"function anonymous(app, bdom, helpers
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -29,6 +29,24 @@ describe("shadow_dom", () => {
|
|||||||
expect(status(comp)).toBe("destroyed");
|
expect(status(comp)).toBe("destroyed");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can mount app in closed shadow dom", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const container = document.createElement("div");
|
||||||
|
fixture.appendChild(container);
|
||||||
|
const shadow = container.attachShadow({ mode: "closed" });
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadow);
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
test("can bind event handler", async () => {
|
test("can bind event handler", async () => {
|
||||||
let a = 1;
|
let a = 1;
|
||||||
class SomeComponent extends Component {
|
class SomeComponent extends Component {
|
||||||
@@ -64,4 +82,73 @@ describe("shadow_dom", () => {
|
|||||||
await mountedProm;
|
await mountedProm;
|
||||||
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
expect(comp!.div.el).toBe(shadow.querySelector(".my-div"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("can mount app inside a shadow child element", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
const shadow = fixture.attachShadow({ mode: "open" });
|
||||||
|
const shadowDiv = document.createElement("div");
|
||||||
|
shadow.append(shadowDiv);
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadowDiv);
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can mount app inside a separate HTML document", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const separateDoc = document.implementation.createHTMLDocument();
|
||||||
|
const container = separateDoc.createElement("div");
|
||||||
|
separateDoc.body.appendChild(container);
|
||||||
|
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
let error: Error;
|
||||||
|
try {
|
||||||
|
await app.mount(container);
|
||||||
|
} catch (e) {
|
||||||
|
error = e as Error;
|
||||||
|
}
|
||||||
|
expect(error!).toBeDefined();
|
||||||
|
expect(error!.message).toBe(
|
||||||
|
"Cannot mount a component: the target document is not attached to a window (defaultView is missing)"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can mount app inside an element in a shadow root inside an iframe", async () => {
|
||||||
|
class SomeComponent extends Component {
|
||||||
|
static template = xml`<div class="my-div"/>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
fixture.appendChild(iframe);
|
||||||
|
|
||||||
|
const iframeDoc = iframe.contentDocument!;
|
||||||
|
const container = iframeDoc.createElement("div");
|
||||||
|
iframeDoc.body.appendChild(container);
|
||||||
|
|
||||||
|
const shadow = container.attachShadow({ mode: "open" });
|
||||||
|
|
||||||
|
const shadowTarget = iframeDoc.createElement("div");
|
||||||
|
shadow.appendChild(shadowTarget);
|
||||||
|
|
||||||
|
const app = new App(SomeComponent);
|
||||||
|
const comp = await app.mount(shadowTarget);
|
||||||
|
|
||||||
|
const div = shadow.querySelector(".my-div");
|
||||||
|
expect(div).not.toBe(null);
|
||||||
|
expect(shadow.contains(div)).toBe(true);
|
||||||
|
expect(iframeDoc.body.contains(container)).toBe(true);
|
||||||
|
|
||||||
|
app.destroy();
|
||||||
|
expect(shadow.contains(div)).toBe(false);
|
||||||
|
expect(status(comp)).toBe("destroyed");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { batched, EventBus } from "../src/runtime/utils";
|
import { batched, EventBus, htmlEscape, markup } from "../src/runtime/utils";
|
||||||
import { nextMicroTick } from "./helpers";
|
import { nextMicroTick } from "./helpers";
|
||||||
|
|
||||||
describe("event bus behaviour", () => {
|
describe("event bus behaviour", () => {
|
||||||
@@ -71,3 +71,94 @@ describe("batched", () => {
|
|||||||
expect(n).toBe(2);
|
expect(n).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const Markup = markup("").constructor;
|
||||||
|
describe("markup", () => {
|
||||||
|
test("string is flagged as safe", () => {
|
||||||
|
const html = markup("<blink>Hello</blink>");
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
describe("htmlEscape", () => {
|
||||||
|
test("htmlEscape escapes text", () => {
|
||||||
|
const res = htmlEscape("<p>test</p>");
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape keeps html markup", () => {
|
||||||
|
const res = htmlEscape(markup("<p>test</p>"));
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape produces empty string on undefined", () => {
|
||||||
|
const res = htmlEscape(undefined);
|
||||||
|
expect(res.toString()).toBe("");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape produces string from number", () => {
|
||||||
|
const res = htmlEscape(10);
|
||||||
|
expect(res.toString()).toBe("10");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape produces string from boolean", () => {
|
||||||
|
const res = htmlEscape(false);
|
||||||
|
expect(res.toString()).toBe("false");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("htmlEscape correctly escapes various links", () => {
|
||||||
|
expect(htmlEscape("<a>this is a link</a>").toString()).toBe(
|
||||||
|
"<a>this is a link</a>"
|
||||||
|
);
|
||||||
|
expect(htmlEscape(`<a href="https://www.odoo.com">odoo<a>`).toString()).toBe(
|
||||||
|
`<a href="https://www.odoo.com">odoo<a>`
|
||||||
|
);
|
||||||
|
expect(htmlEscape(`<a href='https://www.odoo.com'>odoo<a>`).toString()).toBe(
|
||||||
|
`<a href='https://www.odoo.com'>odoo<a>`
|
||||||
|
);
|
||||||
|
expect(htmlEscape("<a href='https://www.odoo.com'>Odoo`s website<a>").toString()).toBe(
|
||||||
|
`<a href='https://www.odoo.com'>Odoo`s website<a>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
test("htmlEscape doesn't escape already escaped content", () => {
|
||||||
|
const res = htmlEscape("<p>test</p>");
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
const res2 = htmlEscape(res);
|
||||||
|
expect(res2.toString()).toBe("<p>test</p>");
|
||||||
|
expect(res2).toBeInstanceOf(Markup);
|
||||||
|
expect(res2).toBe(res);
|
||||||
|
});
|
||||||
|
test("htmlEscape returns markup even for only-safe text", () => {
|
||||||
|
const res = htmlEscape("safe");
|
||||||
|
expect(res.toString()).toBe("safe");
|
||||||
|
expect(res).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("tag function", () => {
|
||||||
|
test("interpolated values are escaped", () => {
|
||||||
|
const maliciousInput = "<script>alert('💥💥')</script>";
|
||||||
|
const html = markup`<b>${maliciousInput}</b>`;
|
||||||
|
expect(html.toString()).toBe("<b><script>alert('💥💥')</script></b>");
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("interpolated markups aren't escaped", () => {
|
||||||
|
const shouldBeEscaped = "<script>alert('should be escaped')</script>";
|
||||||
|
const shouldnt = markup("<b>this is safe</b>");
|
||||||
|
const html = markup`<div>${shouldBeEscaped} ${shouldnt}</div>`;
|
||||||
|
expect(html.toString()).toBe(
|
||||||
|
"<div><script>alert('should be escaped')</script> <b>this is safe</b></div>"
|
||||||
|
);
|
||||||
|
expect(html).toBeInstanceOf(Markup);
|
||||||
|
});
|
||||||
|
test("quotes in interpolated values are escaped", () => {
|
||||||
|
const imgUrl = `lol" onerror="alert('xss')`;
|
||||||
|
const html = markup`<img src="${imgUrl}">`;
|
||||||
|
expect(html.toString()).toBe(`<img src="lol" onerror="alert('xss')">`);
|
||||||
|
});
|
||||||
|
test("already escaped content is not escaped again", () => {
|
||||||
|
const res = htmlEscape("<p>test</p>");
|
||||||
|
expect(res.toString()).toBe("<p>test</p>");
|
||||||
|
const html = markup`${res}`;
|
||||||
|
expect(html.toString()).toBe("<p>test</p>");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -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",
|
"name": "Owl devtools",
|
||||||
"version": "1.2.2",
|
"version": "1.3.0",
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"description": "Chrome devtools extension for Odoo Owl framework",
|
"description": "Chrome devtools extension for Odoo Owl framework",
|
||||||
"icons": {
|
"icons": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "Owl devtools",
|
"name": "Owl devtools",
|
||||||
"version": "1.0.0",
|
"version": "1.1.0",
|
||||||
"description": "Firefox devtools extension for Odoo Owl framework",
|
"description": "Firefox devtools extension for Odoo Owl framework",
|
||||||
"manifest_version": 2,
|
"manifest_version": 2,
|
||||||
"browser_specific_settings": {
|
"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 { useStore } from "../../../store/store";
|
||||||
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
import { ObjectTreeElement } from "./object_tree_element/object_tree_element";
|
||||||
|
|
||||||
@@ -7,23 +7,64 @@ export class DetailsWindow extends Component {
|
|||||||
static components = { ObjectTreeElement };
|
static components = { ObjectTreeElement };
|
||||||
setup() {
|
setup() {
|
||||||
this.store = useStore();
|
this.store = useStore();
|
||||||
this.contextMenu = useRef("contextmenu");
|
}
|
||||||
this.contextMenuId = this.store.contextMenu.id++;
|
|
||||||
this.contextMenuEvent;
|
get contextMenuItems() {
|
||||||
// Open the context menu when the ids match
|
return [
|
||||||
useEffect(
|
{
|
||||||
(menuId) => {
|
title: "Inspect source code",
|
||||||
if (menuId === this.contextMenuId) {
|
show: true,
|
||||||
this.store.contextMenu.open(this.contextMenuEvent, this.contextMenu.el);
|
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) {
|
openMenu(ev) {
|
||||||
this.contextMenuEvent = ev;
|
this.store.openContextMenu(ev, this.contextMenuItems);
|
||||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleCategory(ev, category) {
|
toggleCategory(ev, category) {
|
||||||
|
|||||||
@@ -20,6 +20,17 @@
|
|||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
<div class="details-container">
|
<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 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="d-flex mb-2">
|
||||||
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'env')">
|
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'env')">
|
||||||
@@ -59,7 +70,7 @@
|
|||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
|
<div t-if="store.activeComponent.instance.children.length > 0" id="instance" class="details-panel ps-2 py-1">
|
||||||
<div class="d-flex mb-2">
|
<div class="d-flex mb-2">
|
||||||
<div class="w-100 text-truncate" t-on-click.stop="(ev) => this.toggleCategory(ev, 'instance')">
|
<div class="w-100 text-truncate" t-on-click.stop="(ev) => this.toggleCategory(ev, 'instance')">
|
||||||
<i class="fa mx-1 pointer-icon"
|
<i class="fa mx-1 pointer-icon"
|
||||||
@@ -75,22 +86,19 @@
|
|||||||
<ObjectTreeElement object="instance"/>
|
<ObjectTreeElement object="instance"/>
|
||||||
</t>
|
</t>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div t-if="store.activeComponent.hooks?.children.length > 0" id="hooks" class="details-panel ps-2 py-1">
|
||||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
<div class="d-flex mb-2">
|
||||||
<ul class="my-1">
|
<div class="w-100" t-on-click.stop="(ev) => this.toggleCategory(ev, 'hooks')">
|
||||||
<li t-on-click.stop="() => this.store.inspectComponent('source', store.activeComponent.path)" class="custom-menu-item py-1 px-4">Inspect source code</li>
|
<i class="fa mx-1 pointer-icon"
|
||||||
<t t-if="store.activeComponent.path.length !== 1">
|
t-att-class="{'fa-caret-right': !store.activeComponent.hooks.toggled, 'fa-caret-down': store.activeComponent.hooks.toggled}"
|
||||||
<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>
|
/><b>hooks</b>
|
||||||
<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>
|
</div>
|
||||||
<li t-on-click.stop="() => this.store.refreshComponent(store.activeComponent.path)" class="custom-menu-item py-1 px-4">Force rerender</li>
|
<i title="Remove breakpoints" class="fa fa-times utility-icon p-1" t-on-click.stop="() => this.store.removeBreakpoints()"></i>
|
||||||
<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>
|
</div>
|
||||||
<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>
|
<t t-if="store.activeComponent.hooks.toggled" t-foreach="store.activeComponent.hooks.children" t-as="hook" t-key="hook_index">
|
||||||
<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>
|
<ObjectTreeElement object="hook"/>
|
||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
</div>
|
||||||
<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>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
|
|||||||
@@ -13,19 +13,8 @@ export class ObjectTreeElement extends Component {
|
|||||||
menuTop: 0,
|
menuTop: 0,
|
||||||
menuLeft: 0,
|
menuLeft: 0,
|
||||||
});
|
});
|
||||||
this.contextMenu = useRef("contextmenu");
|
|
||||||
const inputRef = useRef("input");
|
const inputRef = useRef("input");
|
||||||
this.store = useStore();
|
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(
|
useEffect(
|
||||||
(editMode) => {
|
(editMode) => {
|
||||||
// Focus on the input when it is created
|
// 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;
|
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) {
|
openMenu(ev) {
|
||||||
this.contextMenuEvent = ev;
|
this.store.openContextMenu(ev, this.contextMenuItems);
|
||||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
setupEditMode() {
|
setupEditMode() {
|
||||||
if (!this.state.editMode) {
|
if (
|
||||||
if (!this.props.object.hasChildren) {
|
!this.state.editMode &&
|
||||||
this.state.editMode = true;
|
!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"
|
<div class="m-0 p-0 text-nowrap w-100 object-line"
|
||||||
t-att-class="props.class + (props.object.hasChildren ? ' bg-feedback' : '')"
|
t-att-class="props.class + (props.object.hasChildren ? ' bg-feedback' : '')"
|
||||||
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
t-on-click.stop="() => this.store.toggleObjectTreeElementsDisplay(this.props.object)"
|
||||||
t-on-contextmenu.prevent="openMenu"
|
t-on-contextmenu.prevent="openMenu"
|
||||||
>
|
>
|
||||||
<div t-attf-style="padding-left: {{objectPadding}}rem">
|
<div t-attf-style="padding-left: {{objectPadding}}rem">
|
||||||
<i class="fa px-1 pointer-icon caret"
|
<i class="fa px-1 pointer-icon caret"
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
|
t-attf-style="visibility: {{props.object.hasChildren ? '' : 'hidden'}};"
|
||||||
/>
|
/>
|
||||||
<t t-esc="props.object.name"/>
|
<t t-esc="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'">
|
<t t-if="props.object.contentType == 'getter'">
|
||||||
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
|
<span class="getter-content object-content" t-att-class="objectLineClass" t-on-click.stop="() => this.store.loadGetterContent(this.props.object)">
|
||||||
<t t-esc="props.object.content"/>
|
<t t-esc="props.object.content"/>
|
||||||
@@ -20,25 +20,17 @@
|
|||||||
</t>
|
</t>
|
||||||
<t t-else="">
|
<t t-else="">
|
||||||
<span class="object-content" t-att-class="objectLineClass" t-on-dblclick.stop="setupEditMode">
|
<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"/>
|
<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 t-else="">
|
<t t-else="">
|
||||||
<t t-esc="props.object.content"/>
|
<t t-esc="props.object.content"/>
|
||||||
</t>
|
</t>
|
||||||
</span>
|
</span>
|
||||||
</t>
|
</t>
|
||||||
<span t-if="keyChanges" class="key-changes ms-1 badge p-1" title="Key additions/deletions are observed">+/-</span>
|
<span t-if="keyChanges" class="key-changes ms-1 badge p-1" title="Key additions/deletions are observed">+/-</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
|
||||||
<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-if="props.object.toggled" t-key="contextMenuId">
|
||||||
<t t-foreach="props.object.children" t-as="child" t-key="child_index">
|
<t t-foreach="props.object.children" t-as="child" t-key="child_index">
|
||||||
<ObjectTreeElement object="child" class="this.classFor(child)"/>
|
<ObjectTreeElement object="child" class="this.classFor(child)"/>
|
||||||
|
|||||||
@@ -16,10 +16,7 @@ export class TreeElement extends Component {
|
|||||||
searched: false,
|
searched: false,
|
||||||
});
|
});
|
||||||
this.store = useStore();
|
this.store = useStore();
|
||||||
this.contextMenu = useRef("contextmenu");
|
|
||||||
this.element = useRef("element");
|
this.element = useRef("element");
|
||||||
this.contextMenuId = this.store.contextMenu.id++;
|
|
||||||
this.contextMenuEvent;
|
|
||||||
this.stringifiedPath = JSON.stringify(this.props.component.path);
|
this.stringifiedPath = JSON.stringify(this.props.component.path);
|
||||||
// Scroll to the selected element when it changes
|
// Scroll to the selected element when it changes
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -38,15 +35,6 @@ export class TreeElement extends Component {
|
|||||||
},
|
},
|
||||||
() => [this.props.component.selected]
|
() => [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
|
// Effect to apply a short highlight effect to the component when it is rendered
|
||||||
useEffect(
|
useEffect(
|
||||||
() => {
|
() => {
|
||||||
@@ -86,9 +74,86 @@ export class TreeElement extends Component {
|
|||||||
return minimizeKey(this.props.component.key);
|
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) {
|
openMenu(ev) {
|
||||||
this.contextMenuEvent = ev;
|
this.store.openContextMenu(ev, this.contextMenuItems);
|
||||||
this.store.contextMenu.activeMenu = this.contextMenuId;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Expand/fold the component node
|
// Expand/fold the component node
|
||||||
|
|||||||
@@ -2,15 +2,15 @@
|
|||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
<t t-name="devtools.TreeElement" owl="1">
|
<t t-name="devtools.TreeElement" owl="1">
|
||||||
<div t-ref="element"
|
<div t-ref="element"
|
||||||
t-att-class="{'component-selected': props.component.selected,'component-highlighted': props.component.highlighted}"
|
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"
|
class="tree-component m-0 p-0 w-100 text-nowrap user-select-none"
|
||||||
t-on-contextmenu.prevent="openMenu"
|
t-on-contextmenu.prevent="openMenu"
|
||||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.component.path)"
|
t-on-mouseover.stop="() => this.store.highlightComponent(props.component.path)"
|
||||||
t-on-click.stop="toggleComponent"
|
t-on-click.stop="toggleComponent"
|
||||||
>
|
>
|
||||||
<div class="component-wrapper" t-attf-style="padding-left: {{componentPadding}}rem">
|
<div class="component-wrapper" t-attf-style="padding-left: {{componentPadding}}rem">
|
||||||
<i class="fa px-1 pointer-icon caret"
|
<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-on-click.stop="toggleDisplay"
|
||||||
t-attf-style="{{props.component.children.length > 0 ? '' : 'visibility: hidden;'}}"
|
t-attf-style="{{props.component.children.length > 0 ? '' : 'visibility: hidden;'}}"
|
||||||
/>
|
/>
|
||||||
@@ -26,29 +26,6 @@
|
|||||||
<span t-if="props.component.depth">></span>
|
<span t-if="props.component.depth">></span>
|
||||||
<span class="version" t-else="">owl=<t t-esc="props.component.version"/></span>
|
<span class="version" t-else="">owl=<t t-esc="props.component.version"/></span>
|
||||||
</div>
|
</div>
|
||||||
<div t-if="store.contextMenu.activeMenu === contextMenuId" class="custom-menu" t-ref="contextmenu">
|
|
||||||
<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>
|
</div>
|
||||||
<t t-if="props.component.toggled">
|
<t t-if="props.component.toggled">
|
||||||
<t t-foreach="props.component.children" t-as="child" t-key="child.key">
|
<t t-foreach="props.component.children" t-as="child" t-key="child.key">
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
const { Component } = owl;
|
const { Component } = owl;
|
||||||
import { ComponentsTab } from "./components_tab/components_tab";
|
import { ContextMenu } from "../context_menu/context_menu";
|
||||||
import { Tab } from "./tab/tab";
|
|
||||||
import { ProfilerTab } from "./profiler_tab/profiler_tab";
|
|
||||||
import { useStore } from "../store/store";
|
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 {
|
export class DevtoolsWindow extends Component {
|
||||||
static props = [];
|
static props = [];
|
||||||
static template = "devtools.DevtoolsWindow";
|
static template = "devtools.DevtoolsWindow";
|
||||||
static components = { ComponentsTab, Tab, ProfilerTab };
|
static components = { ComponentsTab, Tab, ProfilerTab, ContextMenu };
|
||||||
setup() {
|
setup() {
|
||||||
this.store = useStore();
|
this.store = useStore();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
Owl is not loaded on this page.
|
Owl is not loaded on this page.
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
|
<ContextMenu t-if="store.contextMenu" items="store.contextMenu.items"/>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
|
|||||||
@@ -1,24 +1,13 @@
|
|||||||
import { minimizeKey } from "../../../../utils";
|
import { minimizeKey } from "../../../../utils";
|
||||||
import { useStore } from "../../../store/store";
|
import { useStore } from "../../../store/store";
|
||||||
|
|
||||||
const { Component, useEffect, useRef } = owl;
|
const { Component } = owl;
|
||||||
|
|
||||||
export class Event extends Component {
|
export class Event extends Component {
|
||||||
static template = "devtools.Event";
|
static template = "devtools.Event";
|
||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.store = useStore();
|
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
|
// 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") {
|
if (this.props.event.type === "destroy") {
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.contextMenuEvent = ev;
|
this.store.openContextMenu(ev, this.contextMenuItems);
|
||||||
this.store.contextMenu.activeMenu = this.componentContextMenuId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,11 +8,11 @@
|
|||||||
t-att-class="{'fa-caret-right': !props.event.toggled, 'fa-caret-down': props.event.toggled}"
|
t-att-class="{'fa-caret-right': !props.event.toggled, 'fa-caret-down': props.event.toggled}"
|
||||||
t-attf-style="visibility: {{props.event.origin ? '' : 'hidden'}};"
|
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);"
|
<<span style="cursor:pointer; color: var(--component-color);"
|
||||||
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||||
t-on-contextmenu="openComponentMenu"
|
t-on-contextmenu="openMenu"
|
||||||
t-esc="props.event.component"
|
t-esc="props.event.component"
|
||||||
/>
|
/>
|
||||||
<t t-if="minimizedKey.length > 0">
|
<t t-if="minimizedKey.length > 0">
|
||||||
@@ -29,10 +29,10 @@
|
|||||||
<div class="my-0 pt-1 object-line">
|
<div class="my-0 pt-1 object-line">
|
||||||
<i class="fa fa-caret-right mx-1 pe-2" style="visibility: hidden;"></i>
|
<i class="fa fa-caret-right mx-1 pe-2" style="visibility: hidden;"></i>
|
||||||
<span>
|
<span>
|
||||||
origin:
|
origin:
|
||||||
<<span style="cursor:pointer; color: var(--component-color);"
|
<<span style="cursor:pointer; color: var(--component-color);"
|
||||||
t-on-click.stop="() => this.store.selectComponent(props.event.origin.path)"
|
t-on-click.stop="() => this.store.selectComponent(props.event.origin.path)"
|
||||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.origin.path)"
|
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.origin.path)"
|
||||||
t-esc="props.event.origin.component"
|
t-esc="props.event.origin.component"
|
||||||
/>
|
/>
|
||||||
<t t-if="originMinimizedKey.length > 0">
|
<t t-if="originMinimizedKey.length > 0">
|
||||||
@@ -43,22 +43,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</t>
|
</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>
|
</div>
|
||||||
</t>
|
</t>
|
||||||
</templates>
|
</templates>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { minimizeKey } from "../../../../utils";
|
import { minimizeKey } from "../../../../utils";
|
||||||
import { useStore } from "../../../store/store";
|
import { useStore } from "../../../store/store";
|
||||||
|
|
||||||
const { Component, useRef, useEffect } = owl;
|
const { Component } = owl;
|
||||||
|
|
||||||
export class EventNode extends Component {
|
export class EventNode extends Component {
|
||||||
static template = "devtools.EventNode";
|
static template = "devtools.EventNode";
|
||||||
@@ -10,33 +10,89 @@ export class EventNode extends Component {
|
|||||||
|
|
||||||
setup() {
|
setup() {
|
||||||
this.store = useStore();
|
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() {
|
get eventPadding() {
|
||||||
return this.props.event.depth * 0.8 + 0.3;
|
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) {
|
openNodeMenu(ev) {
|
||||||
if (this.props.event.children.length) {
|
if (this.props.event.children.length) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.contextMenuEvent = ev;
|
this.store.openContextMenu(ev, this.nodeContextMenuItems);
|
||||||
this.store.contextMenu.activeMenu = this.nodeContextMenuId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,8 +101,7 @@ export class EventNode extends Component {
|
|||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.contextMenuEvent = ev;
|
this.store.openContextMenu(ev, this.componentContextMenuItems);
|
||||||
this.store.contextMenu.activeMenu = this.componentContextMenuId;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
<templates xml:space="preserve">
|
<templates xml:space="preserve">
|
||||||
<t t-name="devtools.EventNode" owl="1">
|
<t t-name="devtools.EventNode" owl="1">
|
||||||
<div class="my-0 p-0 object-line"
|
<div class="my-0 p-0 object-line"
|
||||||
t-on-click.stop="toggleDisplay"
|
t-on-click.stop="toggleDisplay"
|
||||||
t-on-contextmenu="openNodeMenu"
|
t-on-contextmenu="openNodeMenu"
|
||||||
>
|
>
|
||||||
<div class="text-nowrap" t-attf-style="padding-left: {{eventPadding}}rem">
|
<div class="text-nowrap" t-attf-style="padding-left: {{eventPadding}}rem">
|
||||||
<i class="fa px-1 pointer-icon caret"
|
<i class="fa px-1 pointer-icon caret"
|
||||||
@@ -11,11 +11,11 @@
|
|||||||
t-attf-style="visibility: {{props.event.children.length > 0 ? '' : 'hidden'}};"
|
t-attf-style="visibility: {{props.event.children.length > 0 ? '' : 'hidden'}};"
|
||||||
/>
|
/>
|
||||||
<span>
|
<span>
|
||||||
<t t-esc="props.event.type"/>:
|
<t t-esc="props.event.type"/>:
|
||||||
<<span style="cursor:pointer; color: var(--component-color);"
|
<<span style="cursor:pointer; color: var(--component-color);"
|
||||||
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
t-on-click.stop="() => this.store.selectComponent(props.event.path)"
|
||||||
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
t-on-mouseover.stop="() => this.store.highlightComponent(props.event.path)"
|
||||||
t-on-contextmenu.stop="openComponentMenu"
|
t-on-contextmenu.stop="openComponentMenu"
|
||||||
t-esc="props.event.component"/>
|
t-esc="props.event.component"/>
|
||||||
<t t-if="minimizedKey.length > 0">
|
<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);">
|
<span t-if="minimizedKey.length > 0" style="color: var(--key-name);"> key</span>=<span style="color: var(--key-content);">
|
||||||
@@ -28,29 +28,6 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</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-if="props.event.toggled">
|
||||||
<t t-foreach="props.event.children" t-as="child" t-key="child.id">
|
<t t-foreach="props.event.children" t-as="child" t-key="child.id">
|
||||||
<EventNode event="child"/>
|
<EventNode event="child"/>
|
||||||
|
|||||||
@@ -11,30 +11,7 @@ export const store = reactive({
|
|||||||
darkmode: false,
|
darkmode: false,
|
||||||
componentsToggleBlacklist: new Set(),
|
componentsToggleBlacklist: new Set(),
|
||||||
},
|
},
|
||||||
contextMenu: {
|
contextMenu: null,
|
||||||
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;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
isFirefox: IS_FIREFOX,
|
isFirefox: IS_FIREFOX,
|
||||||
frameUrls: ["top"],
|
frameUrls: ["top"],
|
||||||
activeFrame: "top",
|
activeFrame: "top",
|
||||||
@@ -56,9 +33,11 @@ export const store = reactive({
|
|||||||
props: { toggled: true, children: [] },
|
props: { toggled: true, children: [] },
|
||||||
env: { toggled: false, children: [] },
|
env: { toggled: false, children: [] },
|
||||||
instance: { toggled: true, children: [] },
|
instance: { toggled: true, children: [] },
|
||||||
|
hooks: { toggled: true, children: [] },
|
||||||
version: "1.0",
|
version: "1.0",
|
||||||
},
|
},
|
||||||
selectedElement: null,
|
selectedElement: null,
|
||||||
|
observedVariables: [],
|
||||||
componentSearch: {
|
componentSearch: {
|
||||||
search: "",
|
search: "",
|
||||||
searchResults: [],
|
searchResults: [],
|
||||||
@@ -93,6 +72,16 @@ export const store = reactive({
|
|||||||
evalFunctionInWindow("disableHTMLSelector", [], this.activeFrame);
|
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
|
// 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
|
// Use fromOld to specify if we want to keep most of the toggled/selected data of the old tree
|
||||||
// when generating the new one
|
// 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
|
// Trigger the highlight on the component in the page
|
||||||
highlightComponent(path) {
|
highlightComponent(path) {
|
||||||
evalFunctionInWindow("highlightComponent", [path], this.activeFrame);
|
evalFunctionInWindow("highlightComponent", [path], this.activeFrame);
|
||||||
@@ -646,8 +671,9 @@ async function init() {
|
|||||||
store.updateIFrameList();
|
store.updateIFrameList();
|
||||||
|
|
||||||
// Global listeners to close the currently shown context menu when the user clicks or opens another
|
// 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("click", () => (store.contextMenu = null), { capture: true });
|
||||||
document.addEventListener("contextmenu", () => store.contextMenu.close(), { 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
|
// Make sure the events recorder is at its initial state in every frame
|
||||||
for (const frame of store.frameUrls) {
|
for (const frame of store.frameUrls) {
|
||||||
@@ -666,6 +692,16 @@ async function init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, 500);
|
}, 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;
|
let rootRendersTimeout = false;
|
||||||
@@ -760,6 +796,16 @@ async function loadSettings() {
|
|||||||
storage.owlDevtoolsComponentsToggleBlacklist
|
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
|
// Function to handle and store a batch of events coming from the page
|
||||||
|
|||||||
@@ -51,6 +51,8 @@
|
|||||||
this.requestedFrame = false;
|
this.requestedFrame = false;
|
||||||
this.enabledSelector = false;
|
this.enabledSelector = false;
|
||||||
this.eventsBatch = [];
|
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
|
// Object which defines how different types of data should be displayed when passed to the devtools
|
||||||
this.serializer = {
|
this.serializer = {
|
||||||
// Defines how leaf object nodes should be displayed in the extension when inside a bigger structure
|
// 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
|
// 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)
|
// be their constructor name (useful for prototype, map and set display)
|
||||||
serializeItem(value, asConstructorName = false) {
|
serializeItem(value, asConstructorName = false) {
|
||||||
if (typeof value === "array") {
|
if (typeof value === "object") {
|
||||||
return "Array(" + value.length + ")";
|
|
||||||
} else if (typeof value === "object") {
|
|
||||||
if (value == null) {
|
if (value == null) {
|
||||||
return "null";
|
return "null";
|
||||||
}
|
}
|
||||||
if (asConstructorName) {
|
if (asConstructorName) {
|
||||||
return value.constructor.name;
|
return value.constructor.name;
|
||||||
}
|
}
|
||||||
|
if (value instanceof Array) {
|
||||||
|
return "Array(" + value.length + ")";
|
||||||
|
}
|
||||||
return "{...}";
|
return "{...}";
|
||||||
} else if (typeof value === "undefined") {
|
} else if (typeof value === "undefined") {
|
||||||
return "undefined";
|
return "undefined";
|
||||||
@@ -116,12 +119,17 @@
|
|||||||
} else if (obj instanceof Number) {
|
} else if (obj instanceof Number) {
|
||||||
result[0] = obj.toString();
|
result[0] = obj.toString();
|
||||||
} else {
|
} else {
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const key of Reflect.ownKeys(obj)) {
|
||||||
if (length > 25) {
|
if (length > 25) {
|
||||||
result.push("...");
|
result.push("...");
|
||||||
break;
|
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;
|
length += element.length;
|
||||||
result.push(element);
|
result.push(element);
|
||||||
}
|
}
|
||||||
@@ -135,7 +143,7 @@
|
|||||||
result.push(element);
|
result.push(element);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (obj.constructor.name !== "Object") {
|
if (obj.constructor && obj.constructor.name !== "Object") {
|
||||||
return obj.constructor.name + " {" + result.join(", ") + "}";
|
return obj.constructor.name + " {" + result.join(", ") + "}";
|
||||||
}
|
}
|
||||||
return "{" + result.join(", ") + "}";
|
return "{" + result.join(", ") + "}";
|
||||||
@@ -695,7 +703,7 @@
|
|||||||
try {
|
try {
|
||||||
obj = Object.getOwnPropertyDescriptor(obj, key.value).get.call(obj);
|
obj = Object.getOwnPropertyDescriptor(obj, key.value).get.call(obj);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
obj = e.toString();
|
obj = "Exception: " + e.toString();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "prototype getter":
|
case "prototype getter":
|
||||||
@@ -713,13 +721,13 @@
|
|||||||
try {
|
try {
|
||||||
obj = Object.getOwnPropertyDescriptor(obj, key.value).get;
|
obj = Object.getOwnPropertyDescriptor(obj, key.value).get;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
obj = e.toString();
|
obj = "Exception: " + e.toString();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
obj = obj[key.value];
|
obj = obj[key.value];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
obj = e.toString();
|
obj = "Exception: " + e.toString();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -732,15 +740,20 @@
|
|||||||
|
|
||||||
// Returns the asked property given its global path
|
// Returns the asked property given its global path
|
||||||
getObjectProperty(path) {
|
getObjectProperty(path) {
|
||||||
// Just return the corresponding app if path is of length 1
|
// Just return the corresponding app if path is of length 1 and not simplified
|
||||||
if (path.length === 1) {
|
if (path.length === 1 && !isNaN(path[0])) {
|
||||||
return [...this.apps][path[0]];
|
return [...this.apps][path[0]];
|
||||||
}
|
}
|
||||||
// Path to the component node is only strings, becomes objects for properties
|
// Path to the component node is only strings, becomes objects for properties
|
||||||
const index = path.findIndex((key) => typeof key !== "string");
|
const index = path.findIndex((key) => typeof key !== "string");
|
||||||
const componentNode = this.getComponentNode(path.slice(0, index));
|
if (index === -1) {
|
||||||
const obj = this.getObject(componentNode, path.slice(index));
|
return this.getComponentNode(path);
|
||||||
return obj;
|
}
|
||||||
|
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
|
// Returns a modified version of an object node that has compatible format with the devtools ObjectTreeElement component
|
||||||
@@ -788,6 +801,11 @@
|
|||||||
child.name = "value";
|
child.name = "value";
|
||||||
obj = parentObj[1];
|
obj = parentObj[1];
|
||||||
break;
|
break;
|
||||||
|
case "getter":
|
||||||
|
child.name = key.value;
|
||||||
|
obj = parentObj[key.value];
|
||||||
|
break;
|
||||||
|
case "prototype getter":
|
||||||
case "set entry":
|
case "set entry":
|
||||||
case "map entry":
|
case "map entry":
|
||||||
case "item":
|
case "item":
|
||||||
@@ -803,6 +821,8 @@
|
|||||||
parentObj
|
parentObj
|
||||||
).findIndex((sym) => sym === key.value);
|
).findIndex((sym) => sym === key.value);
|
||||||
child.path[child.path.length - 1].value = key.value.toString();
|
child.path[child.path.length - 1].value = key.value.toString();
|
||||||
|
} else if (key.hasOwnProperty("symbolIndex")) {
|
||||||
|
obj = parentObj[Object.getOwnPropertySymbols(parentObj)[key.symbolIndex]];
|
||||||
} else {
|
} else {
|
||||||
obj = parentObj[key.value];
|
obj = parentObj[key.value];
|
||||||
}
|
}
|
||||||
@@ -834,7 +854,7 @@
|
|||||||
child.contentType = "set";
|
child.contentType = "set";
|
||||||
child.hasChildren = true;
|
child.hasChildren = true;
|
||||||
break;
|
break;
|
||||||
case obj.constructor.name === "Array":
|
case obj.constructor?.name === "Array":
|
||||||
child.contentType = "array";
|
child.contentType = "array";
|
||||||
child.hasChildren = obj.length > 0;
|
child.hasChildren = obj.length > 0;
|
||||||
break;
|
break;
|
||||||
@@ -842,11 +862,11 @@
|
|||||||
child.contentType = "function";
|
child.contentType = "function";
|
||||||
child.hasChildren = true;
|
child.hasChildren = true;
|
||||||
break;
|
break;
|
||||||
case obj instanceof Object:
|
case typeof obj === "object":
|
||||||
child.contentType = "object";
|
child.contentType = "object";
|
||||||
child.hasChildren =
|
child.hasChildren =
|
||||||
Object.keys(obj).length ||
|
Object.keys(obj).length > 0 ||
|
||||||
Object.getOwnPropertySymbols(obj).length ||
|
Object.getOwnPropertySymbols(obj).length > 0 ||
|
||||||
obj.constructor.name !== "Object";
|
obj.constructor.name !== "Object";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -890,12 +910,15 @@
|
|||||||
path.shift();
|
path.shift();
|
||||||
}
|
}
|
||||||
// the value is either "props" or "env" here
|
// the value is either "props" or "env" here
|
||||||
if (objType !== "instance") {
|
if (objType !== "instance" && objType !== "hook") {
|
||||||
obj = oldTree[path[0].value].children;
|
obj = oldTree[path[0].value].children;
|
||||||
path.shift();
|
path.shift();
|
||||||
// there is nothing otherwise but extension side it is in instance
|
// there is nothing otherwise but extension side it is in instance
|
||||||
} else {
|
} else if (objType === "instance") {
|
||||||
obj = oldTree.instance.children;
|
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
|
// the first element here is directly in an array instead of a children array
|
||||||
obj = obj[path[0].childIndex];
|
obj = obj[path[0].childIndex];
|
||||||
@@ -1026,36 +1049,40 @@
|
|||||||
break;
|
break;
|
||||||
case "object":
|
case "object":
|
||||||
case "function":
|
case "function":
|
||||||
Reflect.ownKeys(obj).forEach((key) => {
|
Reflect.ownKeys(obj)
|
||||||
if (
|
.sort(compareKeys)
|
||||||
key !== "__proto__" &&
|
.forEach((key) => {
|
||||||
Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
|
if (
|
||||||
) {
|
key !== "__proto__" &&
|
||||||
let child = {
|
Object.getOwnPropertyDescriptor(obj, key).hasOwnProperty("get")
|
||||||
name: key,
|
) {
|
||||||
depth: depth,
|
let child = {
|
||||||
toggled: false,
|
name: key,
|
||||||
objectType: objType,
|
depth: depth,
|
||||||
path: [...path, { type: "getter", value: key, childIndex: children.length }],
|
toggled: false,
|
||||||
contentType: "getter",
|
objectType: objType,
|
||||||
content: "(...)",
|
path: [...path, { type: "getter", value: key, childIndex: children.length }],
|
||||||
hasChildren: false,
|
contentType: "getter",
|
||||||
children: [],
|
content: "(...)",
|
||||||
};
|
hasChildren: false,
|
||||||
children.push(child);
|
children: [],
|
||||||
}
|
};
|
||||||
const child = this.serializeObjectChild(
|
children.push(child);
|
||||||
obj,
|
}
|
||||||
{ type: "item", value: key, childIndex: children.length },
|
const child = this.serializeObjectChild(
|
||||||
depth,
|
obj,
|
||||||
objType,
|
{ type: "item", value: key, childIndex: children.length },
|
||||||
path,
|
depth,
|
||||||
oldBranch?.children[index],
|
objType,
|
||||||
oldTree
|
path,
|
||||||
);
|
oldBranch?.children[index],
|
||||||
if (child) children.push(child);
|
oldTree
|
||||||
index++;
|
);
|
||||||
});
|
if (child) {
|
||||||
|
children.push(child);
|
||||||
|
}
|
||||||
|
index++;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let proto = Object.getPrototypeOf(obj);
|
let proto = Object.getPrototypeOf(obj);
|
||||||
while (proto) {
|
while (proto) {
|
||||||
@@ -1099,25 +1126,44 @@
|
|||||||
}
|
}
|
||||||
// Returns the Component node given its path and the root component node
|
// Returns the Component node given its path and the root component node
|
||||||
getComponentNode(path) {
|
getComponentNode(path) {
|
||||||
// The node is an app and not a component
|
// All paths that consists in an array containing a single stringified number lead to an app
|
||||||
if (path.length === 1) {
|
if (path.length === 1 && !isNaN(path[0])) {
|
||||||
return [...this.apps][path[0]];
|
return [...this.apps][path[0]];
|
||||||
}
|
}
|
||||||
// The second element in the path will always be the root of the app
|
let node;
|
||||||
let node = [...this.apps][path[0]]?.root;
|
// If the path is longer and its first item is indeed an app number, it is a regular path
|
||||||
if (!node) {
|
if (!isNaN(path[0])) {
|
||||||
return null;
|
// The second element in the path will always be the root of the app
|
||||||
}
|
node = [...this.apps][path[0]]?.root;
|
||||||
for (let i = 2; i < path.length; i++) {
|
if (!node) {
|
||||||
// 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;
|
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;
|
return node;
|
||||||
}
|
}
|
||||||
@@ -1155,42 +1201,46 @@
|
|||||||
const propsPath = isApp
|
const propsPath = isApp
|
||||||
? [...path, { type: "item", value: "props" }]
|
? [...path, { type: "item", value: "props" }]
|
||||||
: [...path, { type: "item", value: "component" }, { type: "item", value: "props" }];
|
: [...path, { type: "item", value: "component" }, { type: "item", value: "props" }];
|
||||||
Reflect.ownKeys(props).forEach((key) => {
|
Reflect.ownKeys(props)
|
||||||
let oldBranch = oldTree?.props.children[component.props.children.length];
|
.sort(compareKeys)
|
||||||
const property = this.serializeObjectChild(
|
.forEach((key) => {
|
||||||
props,
|
let oldBranch = oldTree?.props.children[component.props.children.length];
|
||||||
{ type: "item", value: key, childIndex: component.props.children.length },
|
const property = this.serializeObjectChild(
|
||||||
0,
|
props,
|
||||||
"props",
|
{ type: "item", value: key, childIndex: component.props.children.length },
|
||||||
propsPath,
|
0,
|
||||||
oldBranch,
|
"props",
|
||||||
oldTree
|
propsPath,
|
||||||
);
|
oldBranch,
|
||||||
if (property) {
|
oldTree
|
||||||
component.props.children.push(property);
|
);
|
||||||
}
|
if (property) {
|
||||||
});
|
component.props.children.push(property);
|
||||||
|
}
|
||||||
|
});
|
||||||
// Load env of the component
|
// Load env of the component
|
||||||
const env = isApp ? node.env : node.component.env;
|
const env = isApp ? node.env : node.component.env;
|
||||||
component.env = { toggled: oldTree ? oldTree.env.toggled : false, children: [] };
|
component.env = { toggled: oldTree ? oldTree.env.toggled : false, children: [] };
|
||||||
const envPath = isApp
|
const envPath = isApp
|
||||||
? [...path, { type: "item", value: "env" }]
|
? [...path, { type: "item", value: "env" }]
|
||||||
: [...path, { type: "item", value: "component" }, { type: "item", value: "env" }];
|
: [...path, { type: "item", value: "component" }, { type: "item", value: "env" }];
|
||||||
Reflect.ownKeys(env).forEach((key) => {
|
Reflect.ownKeys(env)
|
||||||
let oldBranch = oldTree?.env.children[component.env.children.length];
|
.sort(compareKeys)
|
||||||
const envElement = this.serializeObjectChild(
|
.forEach((key) => {
|
||||||
env,
|
let oldBranch = oldTree?.env.children[component.env.children.length];
|
||||||
{ type: "item", value: key, childIndex: component.env.children.length },
|
const envElement = this.serializeObjectChild(
|
||||||
0,
|
env,
|
||||||
"env",
|
{ type: "item", value: key, childIndex: component.env.children.length },
|
||||||
envPath,
|
0,
|
||||||
oldBranch,
|
"env",
|
||||||
oldTree
|
envPath,
|
||||||
);
|
oldBranch,
|
||||||
if (envElement) {
|
oldTree
|
||||||
component.env.children.push(envElement);
|
);
|
||||||
}
|
if (envElement) {
|
||||||
});
|
component.env.children.push(envElement);
|
||||||
|
}
|
||||||
|
});
|
||||||
// Load env getters
|
// Load env getters
|
||||||
let obj = Object.getPrototypeOf(env);
|
let obj = Object.getPrototypeOf(env);
|
||||||
Reflect.ownKeys(obj).forEach((key) => {
|
Reflect.ownKeys(obj).forEach((key) => {
|
||||||
@@ -1229,23 +1279,25 @@
|
|||||||
const instance = isApp ? node : node.component;
|
const instance = isApp ? node : node.component;
|
||||||
component.instance = { toggled: oldTree ? oldTree.instance.toggled : true, children: [] };
|
component.instance = { toggled: oldTree ? oldTree.instance.toggled : true, children: [] };
|
||||||
const instancePath = isApp ? path : [...path, { type: "item", value: "component" }];
|
const instancePath = isApp ? path : [...path, { type: "item", value: "component" }];
|
||||||
Reflect.ownKeys(instance).forEach((key) => {
|
Reflect.ownKeys(instance)
|
||||||
if (!["env", "props"].includes(key)) {
|
.sort(compareKeys)
|
||||||
let oldBranch = oldTree?.instance.children[component.instance.children.length];
|
.forEach((key) => {
|
||||||
const instanceElement = this.serializeObjectChild(
|
if (!["env", "props"].includes(key)) {
|
||||||
instance,
|
let oldBranch = oldTree?.instance.children[component.instance.children.length];
|
||||||
{ type: "item", value: key, childIndex: component.instance.children.length },
|
const instanceElement = this.serializeObjectChild(
|
||||||
0,
|
instance,
|
||||||
"instance",
|
{ type: "item", value: key, childIndex: component.instance.children.length },
|
||||||
instancePath,
|
0,
|
||||||
oldBranch,
|
"instance",
|
||||||
oldTree
|
instancePath,
|
||||||
);
|
oldBranch,
|
||||||
if (instanceElement) {
|
oldTree
|
||||||
component.instance.children.push(instanceElement);
|
);
|
||||||
|
if (instanceElement) {
|
||||||
|
component.instance.children.push(instanceElement);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
// Load instance getters
|
// Load instance getters
|
||||||
obj = Object.getPrototypeOf(instance);
|
obj = Object.getPrototypeOf(instance);
|
||||||
while (obj) {
|
while (obj) {
|
||||||
@@ -1361,6 +1413,39 @@
|
|||||||
component.subscriptions.children.push(subscription);
|
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;
|
return component;
|
||||||
}
|
}
|
||||||
// Replace the content of a parsed getter object with the result of the corresponding get method
|
// Replace the content of a parsed getter object with the result of the corresponding get method
|
||||||
@@ -1635,6 +1720,37 @@
|
|||||||
}
|
}
|
||||||
return children;
|
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
|
// Returns the path of the given component node
|
||||||
getComponentPath(componentNode) {
|
getComponentPath(componentNode) {
|
||||||
let path = [];
|
let path = [];
|
||||||
@@ -1651,6 +1767,23 @@
|
|||||||
path.unshift(index.toString());
|
path.unshift(index.toString());
|
||||||
return path;
|
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
|
// Store the object into a temp window variable and log it to the console
|
||||||
sendObjectToConsole(path) {
|
sendObjectToConsole(path) {
|
||||||
const obj = this.getObjectProperty(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) {
|
targetName(target, node) {
|
||||||
// check on component
|
// check on component
|
||||||
const { component } = node;
|
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() {
|
function checkOwlStatus() {
|
||||||
let owlStatus = 2;
|
let owlStatus = 2;
|
||||||
if (!window.__OWL__DEVTOOLS_GLOBAL_HOOK__) {
|
if (!window.__OWL__DEVTOOLS_GLOBAL_HOOK__) {
|
||||||
|
|||||||